bug solving, inline grid editing

This commit is contained in:
F43nd1r 2017-05-31 18:36:53 +02:00
parent 8474f34528
commit ae70018a9c
20 changed files with 246 additions and 215 deletions

View file

@ -4,6 +4,7 @@ buildscript {
} }
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "https://oss.sonatype.org/content/repositories/vaadin-snapshots/"}
} }
dependencies { dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
@ -40,7 +41,9 @@ dependencies {
compile 'commons-fileupload:commons-fileupload:1.3.2' compile 'commons-fileupload:commons-fileupload:1.3.2'
compile 'org.ocpsoft.prettytime:prettytime:3.2.7.Final' compile 'org.ocpsoft.prettytime:prettytime:3.2.7.Final'
compile 'net.sf.proguard:proguard-retrace:5.3.3' compile 'net.sf.proguard:proguard-retrace:5.3.3'
compile 'org.jfree:jfreechart:1.0.19' compile ('org.jfree:jfreechart:1.0.19') {
exclude group:'javax.servlet'
}
//local //local
compileOnly project(':annotation') compileOnly project(':annotation')
apt project(':annotationprocessor') apt project(':annotationprocessor')
@ -59,7 +62,7 @@ configurations {
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom "com.vaadin:vaadin-bom:8.0.6" mavenBom "com.vaadin:vaadin-bom:8.1-SNAPSHOT"
} }
} }

View file

@ -0,0 +1,14 @@
package com.faendir.acra.mongod.data;
import com.faendir.acra.mongod.model.Bug;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
/**
* @author Lukas
* @since 31.05.2017
*/
interface BugRepository extends MongoRepository<Bug, Bug.Identification> {
List<Bug> findByApp(String app);
}

View file

@ -1,6 +1,7 @@
package com.faendir.acra.mongod.data; package com.faendir.acra.mongod.data;
import com.faendir.acra.mongod.model.App; import com.faendir.acra.mongod.model.App;
import com.faendir.acra.mongod.model.Bug;
import com.faendir.acra.mongod.model.ProguardMapping; import com.faendir.acra.mongod.model.ProguardMapping;
import com.faendir.acra.mongod.model.Report; import com.faendir.acra.mongod.model.Report;
import com.mongodb.BasicDBObjectBuilder; import com.mongodb.BasicDBObjectBuilder;
@ -9,12 +10,9 @@ import org.json.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsTemplate; import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -33,16 +31,18 @@ import java.util.List;
public class DataManager { public class DataManager {
private final MappingRepository mappingRepository; private final MappingRepository mappingRepository;
private final ReportRepository reportRepository; private final ReportRepository reportRepository;
private final AppRepository appRepository;
private final BugRepository bugRepository;
private final List<ReportChangeListener> listeners; private final List<ReportChangeListener> listeners;
private final GridFsTemplate gridFsTemplate; private final GridFsTemplate gridFsTemplate;
private final Logger logger; private final Logger logger;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final AppRepository appRepository;
@Autowired @Autowired
public DataManager(SecureRandom secureRandom, AppRepository appRepository, GridFsTemplate gridFsTemplate, MappingRepository mappingRepository, ReportRepository reportRepository) { public DataManager(SecureRandom secureRandom, AppRepository appRepository, GridFsTemplate gridFsTemplate, MappingRepository mappingRepository, ReportRepository reportRepository, BugRepository bugRepository) {
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.appRepository = appRepository; this.appRepository = appRepository;
this.bugRepository = bugRepository;
logger = LoggerFactory.getLogger(DataManager.class); logger = LoggerFactory.getLogger(DataManager.class);
this.gridFsTemplate = gridFsTemplate; this.gridFsTemplate = gridFsTemplate;
this.mappingRepository = mappingRepository; this.mappingRepository = mappingRepository;
@ -50,45 +50,31 @@ public class DataManager {
this.listeners = new ArrayList<>(); this.listeners = new ArrayList<>();
} }
public void createNewApp(String name){ public synchronized void createNewApp(String name) {
byte[] bytes = new byte[12]; byte[] bytes = new byte[12];
secureRandom.nextBytes(bytes); secureRandom.nextBytes(bytes);
appRepository.save(new App(name, Base64Utils.encodeToString(bytes))); appRepository.save(new App(name, Base64Utils.encodeToString(bytes)));
} }
public List<App> getApps(){ public List<App> getApps() {
return appRepository.findAll(); return appRepository.findAll();
} }
public App getApp(String id){ public App getApp(String id) {
return appRepository.findOne(id); return appRepository.findOne(id);
} }
public void deleteApp(String id){ public synchronized void deleteApp(String id) {
appRepository.delete(id); appRepository.delete(id);
getReports(id).forEach(this::remove); getReportsForApp(id).forEach(this::deleteReport);
mappingRepository.delete(getMappings(id)); mappingRepository.delete(getMappings(id));
} }
public void saveAttachments(String report, List<MultipartFile> attachments) {
for (MultipartFile a : attachments) {
try {
gridFsTemplate.store(a.getInputStream(), a.getOriginalFilename(), a.getContentType(), new BasicDBObjectBuilder().add("reportId", report).get());
} catch (IOException e) {
logger.warn("Failed to load attachment", e);
}
}
}
public List<GridFSDBFile> getAttachments(String report) { public List<GridFSDBFile> getAttachments(String report) {
return gridFsTemplate.find(new Query(Criteria.where("metadata.reportId").is(report))); return gridFsTemplate.find(new Query(Criteria.where("metadata.reportId").is(report)));
} }
public void removeAttachments(String report) { public synchronized void addMapping(String app, int version, String mappings) {
gridFsTemplate.delete(new Query(Criteria.where("metadata.reportId").is(report)));
}
public void addMapping(String app, int version, String mappings) {
mappingRepository.save(new ProguardMapping(app, version, mappings)); mappingRepository.save(new ProguardMapping(app, version, mappings));
} }
@ -97,33 +83,74 @@ public class DataManager {
} }
public List<ProguardMapping> getMappings(String app) { public List<ProguardMapping> getMappings(String app) {
return mappingRepository.findAll(Example.of(new ProguardMapping(app, -1, null), ExampleMatcher.matchingAny())); return mappingRepository.findByApp(app);
} }
public void newReport(JSONObject content) { public void newReport(String app, JSONObject content) {
newReport(content, Collections.emptyList()); newReport(app, content, Collections.emptyList());
} }
public void newReport(JSONObject content, List<MultipartFile> attachments) { public synchronized void newReport(String app, JSONObject content, List<MultipartFile> attachments) {
Report report = reportRepository.save(new Report(content, SecurityContextHolder.getContext().getAuthentication().getName())); Report report = reportRepository.save(new Report(content, app));
saveAttachments(report.getId(), attachments); for (MultipartFile a : attachments) {
try {
gridFsTemplate.store(a.getInputStream(), a.getOriginalFilename(), a.getContentType(), new BasicDBObjectBuilder().add("reportId", report.getId()).get());
} catch (IOException e) {
logger.warn("Failed to load attachment", e);
}
}
Bug bug = bugRepository.findOne(new Bug.Identification(report.getStacktrace().hashCode(), report.getVersionCode()));
if (bug == null) {
bugRepository.save(new Bug(app, report.getStacktrace(), report.getVersionCode()));
}
listeners.forEach(ReportChangeListener::onChange); listeners.forEach(ReportChangeListener::onChange);
} }
public List<Report> getReports(String app) { public List<Report> getReportsForApp(String app) {
return reportRepository.findAll(Example.of(new Report(null, app))); return reportRepository.findByApp(app);
} }
public Report getReport(String id) { public Report getReport(String id) {
return reportRepository.findOne(id); return reportRepository.findOne(id);
} }
public void remove(Report report){ public synchronized void deleteReport(Report report) {
reportRepository.delete(report); reportRepository.delete(report);
removeAttachments(report.getId()); gridFsTemplate.delete(new Query(Criteria.where("metadata.reportId").is(report.getId())));
if(reportRepository.countByBug(report.getStacktrace(), report.getVersionCode()) == 0){
bugRepository.delete(bugRepository.findOne(new Bug.Identification(report.getStacktrace().hashCode(), report.getVersionCode())));
}
listeners.forEach(ReportChangeListener::onChange); listeners.forEach(ReportChangeListener::onChange);
} }
public List<Bug> getBugs(String app) {
return bugRepository.findByApp(app);
}
public List<Report> getReportsForBug(Bug bug) {
return reportRepository.findByBug(bug.getStacktrace(), bug.getVersionCode());
}
public int countReportsForBug(Bug bug){
return reportRepository.countByBug(bug.getStacktrace(), bug.getVersionCode());
}
public String retrace(Report report){
ProguardMapping mapping = getMapping(report.getApp(), report.getVersionCode());
if (mapping != null) {
try {
return ReportUtils.retrace(report.getStacktrace(), mapping);
} catch (IOException ignored) {
}
}
return report.getStacktrace();
}
public void setBugSolved(Bug bug, boolean solved){
bug.setSolved(solved);
bugRepository.save(bug);
}
public boolean addListener(ReportChangeListener reportChangeListener) { public boolean addListener(ReportChangeListener reportChangeListener) {
return listeners.add(reportChangeListener); return listeners.add(reportChangeListener);
} }
@ -135,4 +162,5 @@ public class DataManager {
public interface ReportChangeListener { public interface ReportChangeListener {
void onChange(); void onChange();
} }
} }

View file

@ -2,10 +2,15 @@ package com.faendir.acra.mongod.data;
import com.faendir.acra.mongod.model.ProguardMapping; import com.faendir.acra.mongod.model.ProguardMapping;
import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
/** /**
* @author Lukas * @author Lukas
* @since 19.05.2017 * @since 19.05.2017
*/ */
interface MappingRepository extends MongoRepository<ProguardMapping, ProguardMapping.MetaData> { interface MappingRepository extends MongoRepository<ProguardMapping, ProguardMapping.MetaData> {
@Query("{id.app:?0}")
List<ProguardMapping> findByApp(String app);
} }

View file

@ -1,11 +1,22 @@
package com.faendir.acra.mongod.data; package com.faendir.acra.mongod.data;
import com.faendir.acra.mongod.model.Report; import com.faendir.acra.mongod.model.Report;
import org.springframework.data.mongodb.repository.CountQuery;
import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
/** /**
* @author Lukas * @author Lukas
* @since 22.03.2017 * @since 22.03.2017
*/ */
interface ReportRepository extends MongoRepository<Report, String> { interface ReportRepository extends MongoRepository<Report, String> {
List<Report> findByApp(String app);
@Query("{content.map.STACK_TRACE:?0,content.map.APP_VERSION_CODE:?1}")
List<Report> findByBug(String stacktrace, int versionCode);
@CountQuery("{content.map.STACK_TRACE:?0,content.map.APP_VERSION_CODE:?1}")
int countByBug(String stacktrace, int versionCode);
} }

View file

@ -1,6 +1,5 @@
package com.faendir.acra.mongod.data; package com.faendir.acra.mongod.data;
import com.faendir.acra.mongod.model.Bug;
import com.faendir.acra.mongod.model.ProguardMapping; import com.faendir.acra.mongod.model.ProguardMapping;
import com.faendir.acra.mongod.model.Report; import com.faendir.acra.mongod.model.Report;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -14,7 +13,6 @@ import java.io.StringReader;
import java.io.StringWriter; import java.io.StringWriter;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -26,18 +24,6 @@ import java.util.Locale;
public final class ReportUtils { public final class ReportUtils {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ENGLISH); private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ENGLISH);
public static List<Bug> getBugs(List<Report> reports) {
List<Bug> bugs = new ArrayList<>();
for (Report report : reports) {
bugs.stream().filter(bug -> bug.is(report)).findAny().orElseGet(() -> {
Bug bug = new Bug(report);
bugs.add(bug);
return bug;
}).getReports().add(report);
}
return bugs;
}
public static Date getDateFromString(String s) { public static Date getDateFromString(String s) {
try { try {
return dateFormat.parse(s); return dateFormat.parse(s);
@ -56,4 +42,8 @@ public final class ReportUtils {
return writer.toString(); return writer.toString();
} }
public static Date getLastReportDate(List<Report> reports) {
return reports.stream().map(Report::getDate).reduce((d1, d2) -> d1.after(d2) ? d1 : d2).orElse(new Date());
}
} }

View file

@ -1,41 +1,58 @@
package com.faendir.acra.mongod.model; package com.faendir.acra.mongod.model;
import java.util.ArrayList; import org.springframework.data.mongodb.core.index.Indexed;
import java.util.Date; import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List;
import java.io.Serializable;
/** /**
* @author Lukas * @author Lukas
* @since 13.05.2017 * @since 13.05.2017
*/ */
@Document
public class Bug { public class Bug {
private List<Report> reports; private Identification id;
private String trace; @Indexed
private int versionCode; private String app;
private boolean solved;
private String stacktrace;
public Bug(Report report){ public Bug(){
reports = new ArrayList<>(); }
this.trace = report.getStacktrace();
this.versionCode = report.getVersionCode(); public Bug(String app, String stacktrace, int versionCode){
this.id = new Identification(stacktrace.hashCode(), versionCode);
this.app = app;
this.stacktrace = stacktrace;
} }
public boolean is(Report report){ public boolean is(Report report){
return report.getStacktrace().equals(trace) && report.getVersionCode() == versionCode; return report.getStacktrace().hashCode() == id.stacktraceHash && report.getVersionCode() == id.versionCode;
} }
public List<Report> getReports() { public boolean isSolved() {
return reports; return solved;
} }
public Date getLastDate(){ public void setSolved(boolean solved) {
return reports.stream().map(Report::getDate).reduce((d1, d2) -> d1.after(d2) ? d1 : d2).orElse(new Date()); this.solved = solved;
} }
public String getTrace() { public String getStacktrace() {
return trace; return stacktrace;
} }
public int getVersionCode() { public int getVersionCode() {
return versionCode; return id.versionCode;
}
public static class Identification implements Serializable {
private int stacktraceHash;
private int versionCode;
public Identification(int stacktraceHash, int versionCode) {
this.stacktraceHash = stacktraceHash;
this.versionCode = versionCode;
}
} }
} }

View file

@ -1,13 +1,11 @@
package com.faendir.acra.mongod.model; package com.faendir.acra.mongod.model;
import com.faendir.acra.mongod.data.DataManager;
import com.faendir.acra.mongod.data.ReportUtils; import com.faendir.acra.mongod.data.ReportUtils;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import java.io.IOException;
import java.util.Date; import java.util.Date;
import java.util.function.Function; import java.util.function.Function;
@ -21,7 +19,6 @@ public class Report {
@Indexed @Indexed
private String app; private String app;
private JSONObject content; private JSONObject content;
private String deObfuscatedTrace;
public Report() { public Report() {
} }
@ -30,7 +27,6 @@ public class Report {
this.content = content; this.content = content;
this.app = app; this.app = app;
id = content == null ? null : getValueSafe("REPORT_ID", content::getString, ""); id = content == null ? null : getValueSafe("REPORT_ID", content::getString, "");
deObfuscatedTrace = null;
} }
public JSONObject getContent() { public JSONObject getContent() {
@ -45,23 +41,12 @@ public class Report {
return id; return id;
} }
public String getStacktrace() { public String getApp() {
return getValueSafe("STACK_TRACE", content::getString, ""); return app;
} }
public String getDeObfuscatedStacktrace(DataManager dataManager){ public String getStacktrace() {
if (deObfuscatedTrace != null) { return getValueSafe("STACK_TRACE", content::getString, "");
return deObfuscatedTrace;
}
ProguardMapping mapping = dataManager.getMapping(app, getVersionCode());
if (mapping != null) {
try {
deObfuscatedTrace = ReportUtils.retrace(getStacktrace(), mapping);
return deObfuscatedTrace;
} catch (IOException ignored) {
}
}
return getStacktrace();
} }
public int getVersionCode() { public int getVersionCode() {

View file

@ -47,9 +47,11 @@ public class UserManager {
public User getUser(String username) { public User getUser(String username) {
User user = userRepository.findOne(username); User user = userRepository.findOne(username);
if (user == null && defaultUser.equals(username)) { if (user == null && defaultUser.equals(username)) {
return getDefaultUser(); user = getDefaultUser();
}
if(user != null) {
ensureValidPermissions(user);
} }
ensureValidPermissions(user);
return user; return user;
} }

View file

@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -35,14 +36,14 @@ public class ReportService {
@PreAuthorize("hasRole('REPORTER')") @PreAuthorize("hasRole('REPORTER')")
@RequestMapping(value = "/report", consumes = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/report", consumes = MediaType.APPLICATION_JSON_VALUE)
public void report(@RequestBody String content) throws IOException { public void report(@RequestBody String content, Principal principal) throws IOException {
JSONObject jsonObject = new JSONObject(content); JSONObject jsonObject = new JSONObject(content);
dataManager.newReport(jsonObject); dataManager.newReport(principal.getName(), jsonObject);
} }
@PreAuthorize("hasRole('REPORTER')") @PreAuthorize("hasRole('REPORTER')")
@RequestMapping(value = "/report", consumes = "multipart/mixed") @RequestMapping(value = "/report", consumes = "multipart/mixed")
public ResponseEntity report(MultipartHttpServletRequest request) throws IOException, ServletException { public ResponseEntity report(MultipartHttpServletRequest request, Principal principal) throws IOException, ServletException {
MultiValueMap<String, MultipartFile> fileMap = request.getMultiFileMap(); MultiValueMap<String, MultipartFile> fileMap = request.getMultiFileMap();
List<MultipartFile> files = fileMap.get(null); List<MultipartFile> files = fileMap.get(null);
JSONObject jsonObject = null; JSONObject jsonObject = null;
@ -56,7 +57,7 @@ public class ReportService {
} }
} }
if(jsonObject != null) { if(jsonObject != null) {
dataManager.newReport(jsonObject, attachments); dataManager.newReport(principal.getName(), jsonObject, attachments);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
}else { }else {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();

View file

@ -14,6 +14,7 @@ import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button; import com.vaadin.ui.Button;
import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.LoginForm; import com.vaadin.ui.LoginForm;
import com.vaadin.ui.Notification;
import com.vaadin.ui.UI; import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.VerticalLayout;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -54,15 +55,14 @@ public class BackendUI extends UI {
} }
} }
private boolean login(String username, String password) { private void login(String username, String password) {
try { try {
Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username.toLowerCase(), password)); Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username.toLowerCase(), password));
VaadinService.reinitializeSession(VaadinService.getCurrentRequest()); VaadinService.reinitializeSession(VaadinService.getCurrentRequest());
SecurityContextHolder.getContext().setAuthentication(token); SecurityContextHolder.getContext().setAuthentication(token);
showMain(); showMain();
return true;
} catch (AuthenticationException ex) { } catch (AuthenticationException ex) {
return false; Notification.show("Unknown username/password combination", Notification.Type.ERROR_MESSAGE);
} }
} }

View file

@ -64,7 +64,7 @@ public class Overview extends NamedView {
grid.setSizeFull(); grid.setSizeFull();
grid.setSelectionMode(Grid.SelectionMode.NONE); grid.setSelectionMode(Grid.SelectionMode.NONE);
grid.addColumn(App::getName, "Name"); grid.addColumn(App::getName, "Name");
grid.addColumn(app -> dataManager.getReports(app.getId()).size(), "Reports"); grid.addColumn(app -> dataManager.getReportsForApp(app.getId()).size(), "Reports");
VerticalLayout layout = new VerticalLayout(grid); VerticalLayout layout = new VerticalLayout(grid);
if(SecurityUtils.hasRole(UserManager.ROLE_ADMIN)){ if(SecurityUtils.hasRole(UserManager.ROLE_ADMIN)){
Button add = new Button("New App", e -> addApp()); Button add = new Button("New App", e -> addApp());

View file

@ -87,7 +87,7 @@ public class ReportView extends NamedView {
summaryGrid.addComponents(new Label("Version", ContentMode.PREFORMATTED), new Label(report.getVersionName(), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("Version", ContentMode.PREFORMATTED), new Label(report.getVersionName(), ContentMode.PREFORMATTED));
summaryGrid.addComponents(new Label("Email", ContentMode.PREFORMATTED), new Label(report.getUserEmail(), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("Email", ContentMode.PREFORMATTED), new Label(report.getUserEmail(), ContentMode.PREFORMATTED));
summaryGrid.addComponents(new Label("Comment", ContentMode.PREFORMATTED), new Label(report.getUserComment(), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("Comment", ContentMode.PREFORMATTED), new Label(report.getUserComment(), ContentMode.PREFORMATTED));
summaryGrid.addComponents(new Label("De-obfuscated Stacktrace", ContentMode.PREFORMATTED), new Label(report.getDeObfuscatedStacktrace(dataManager), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("De-obfuscated Stacktrace", ContentMode.PREFORMATTED), new Label(dataManager.retrace(report), ContentMode.PREFORMATTED));
summaryGrid.addComponents(new Label("Attachments", ContentMode.PREFORMATTED), attachments); summaryGrid.addComponents(new Label("Attachments", ContentMode.PREFORMATTED), attachments);
summaryGrid.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT); summaryGrid.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT);
summaryGrid.setSizeFull(); summaryGrid.setSizeFull();

View file

@ -0,0 +1,19 @@
package com.faendir.acra.ui.view.base;
import com.vaadin.ui.CheckBox;
/**
* @author Lukas
* @since 31.05.2017
*/
public class MyCheckBox extends CheckBox {
public MyCheckBox(boolean value, ValueChangeListener<Boolean> changeListener) {
this(value, true, changeListener);
}
public MyCheckBox(boolean value, boolean enabled, ValueChangeListener<Boolean> changeListener) {
setValue(value);
setEnabled(enabled);
addValueChangeListener(changeListener);
}
}

View file

@ -18,10 +18,12 @@ import java.util.function.Supplier;
* @since 14.05.2017 * @since 14.05.2017
*/ */
public class ReportList extends MyGrid<Report> implements DataManager.ReportChangeListener { public class ReportList extends MyGrid<Report> implements DataManager.ReportChangeListener {
public static final String CAPTION = "Reports";
private final Supplier<List<Report>> reportSupplier; private final Supplier<List<Report>> reportSupplier;
public ReportList(String app, NavigationManager navigationManager, DataManager dataManager, Supplier<List<Report>> reportSupplier) { public ReportList(String app, NavigationManager navigationManager, DataManager dataManager, Supplier<List<Report>> reportSupplier) {
super("Reports", reportSupplier.get()); super(CAPTION, reportSupplier.get());
setId(CAPTION);
this.reportSupplier = reportSupplier; this.reportSupplier = reportSupplier;
setSizeFull(); setSizeFull();
setSelectionMode(SelectionMode.NONE); setSelectionMode(SelectionMode.NONE);
@ -31,7 +33,7 @@ public class ReportList extends MyGrid<Report> implements DataManager.ReportChan
addColumn(Report::getPhoneModel, "Device"); addColumn(Report::getPhoneModel, "Device");
addColumn(report -> report.getStacktrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1); addColumn(report -> report.getStacktrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1);
if (SecurityUtils.hasPermission(app, Permission.Level.EDIT)) { if (SecurityUtils.hasPermission(app, Permission.Level.EDIT)) {
addColumn(report -> "Delete", new ButtonRenderer<>(e -> dataManager.remove(e.getItem()))); addColumn(report -> "Delete", new ButtonRenderer<>(e -> dataManager.deleteReport(e.getItem())));
} }
addItemClickListener(e -> navigationManager.navigateTo(ReportView.class, e.getItem().getId())); addItemClickListener(e -> navigationManager.navigateTo(ReportView.class, e.getItem().getId()));
addAttachListener(e -> dataManager.addListener(this)); addAttachListener(e -> dataManager.addListener(this));

View file

@ -3,15 +3,24 @@ package com.faendir.acra.ui.view.tabs;
import com.faendir.acra.mongod.data.DataManager; import com.faendir.acra.mongod.data.DataManager;
import com.faendir.acra.mongod.data.ReportUtils; import com.faendir.acra.mongod.data.ReportUtils;
import com.faendir.acra.mongod.model.Bug; import com.faendir.acra.mongod.model.Bug;
import com.faendir.acra.mongod.model.Permission;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.ui.NavigationManager; import com.faendir.acra.ui.NavigationManager;
import com.faendir.acra.ui.view.base.MyCheckBox;
import com.faendir.acra.ui.view.base.MyGrid; import com.faendir.acra.ui.view.base.MyGrid;
import com.faendir.acra.ui.view.base.ReportList; import com.faendir.acra.ui.view.base.ReportList;
import com.faendir.acra.util.Style; import com.faendir.acra.util.Style;
import com.faendir.acra.util.TimeSpanRenderer; import com.faendir.acra.util.TimeSpanRenderer;
import com.vaadin.event.selection.SelectionEvent; import com.vaadin.event.selection.SelectionEvent;
import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.VerticalLayout;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* @author Lukas * @author Lukas
* @since 17.05.2017 * @since 17.05.2017
@ -22,19 +31,29 @@ public class BugTab extends VerticalLayout implements DataManager.ReportChangeLi
private final NavigationManager navigationManager; private final NavigationManager navigationManager;
private final DataManager dataManager; private final DataManager dataManager;
private final MyGrid<Bug> bugs; private final MyGrid<Bug> bugs;
private final CheckBox hideSolved;
public BugTab(String app, NavigationManager navigationManager, DataManager dataManager) { public BugTab(String app, NavigationManager navigationManager, DataManager dataManager) {
this.app = app; this.app = app;
this.navigationManager = navigationManager; this.navigationManager = navigationManager;
this.dataManager = dataManager; this.dataManager = dataManager;
bugs = new MyGrid<>(null, ReportUtils.getBugs(dataManager.getReports(app))); hideSolved = new CheckBox("Hide solved", true);
hideSolved.addValueChangeListener(e -> onChange());
addComponent(hideSolved);
setComponentAlignment(hideSolved, Alignment.MIDDLE_RIGHT);
bugs = new MyGrid<>(null, getBugs());
bugs.setSizeFull(); bugs.setSizeFull();
bugs.addColumn(bug -> bug.getReports().size(), "Reports"); bugs.addColumn(dataManager::countReportsForBug, "Reports");
bugs.sort(bugs.addColumn(Bug::getLastDate, new TimeSpanRenderer(), "Latest Report"), SortDirection.DESCENDING); bugs.sort(bugs.addColumn(bug -> ReportUtils.getLastReportDate(dataManager.getReportsForBug(bug)), new TimeSpanRenderer(), "Latest Report"), SortDirection.DESCENDING);
bugs.addColumn(Bug::getVersionCode, "Version"); bugs.addColumn(Bug::getVersionCode, "Version");
bugs.addColumn(bug -> bug.getTrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1); bugs.addColumn(bug -> bug.getStacktrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1);
bugs.addSelectionListener(this::handleBugSelection); bugs.addSelectionListener(this::handleBugSelection);
bugs.addComponentColumn(bug -> new MyCheckBox(bug.isSolved(), SecurityUtils.hasPermission(app, Permission.Level.EDIT), e -> {
dataManager.setBugSolved(bug, e.getValue());
onChange();
})).setCaption("Solved");
addComponent(bugs); addComponent(bugs);
setExpandRatio(bugs, 1);
Style.NO_PADDING.apply(this); Style.NO_PADDING.apply(this);
setSizeFull(); setSizeFull();
setCaption(CAPTION); setCaption(CAPTION);
@ -43,14 +62,28 @@ public class BugTab extends VerticalLayout implements DataManager.ReportChangeLi
} }
private void handleBugSelection(SelectionEvent<Bug> e) { private void handleBugSelection(SelectionEvent<Bug> e) {
if (getComponentCount() == 2) { for (Component component : this) {
removeComponent(getComponent(1)); if (ReportList.CAPTION.equals(component.getId())) {
removeComponent(component);
}
} }
e.getFirstSelectedItem().ifPresent(bug -> addComponent(new ReportList(app, navigationManager, dataManager, bug::getReports))); e.getFirstSelectedItem().ifPresent(bug -> {
ReportList reportList = new ReportList(app, navigationManager, dataManager, () -> dataManager.getReportsForBug(bug));
addComponent(reportList);
setExpandRatio(reportList, 1);
});
} }
@Override @Override
public void onChange() { public void onChange() {
bugs.setItems(ReportUtils.getBugs(dataManager.getReports(app))); bugs.setItems(getBugs());
}
private List<Bug> getBugs() {
List<Bug> bugs = dataManager.getBugs(app);
if (hideSolved.getValue()) {
return bugs.stream().filter(bug -> !bug.isSolved()).collect(Collectors.toList());
}
return bugs;
} }
} }

View file

@ -10,6 +10,6 @@ import com.faendir.acra.ui.view.base.ReportList;
*/ */
public class ReportTab extends ReportList { public class ReportTab extends ReportList {
public ReportTab(String app, NavigationManager navigationManager, DataManager dataManager) { public ReportTab(String app, NavigationManager navigationManager, DataManager dataManager) {
super(app, navigationManager, dataManager, () -> dataManager.getReports(app)); super(app, navigationManager, dataManager, () -> dataManager.getReportsForApp(app));
} }
} }

View file

@ -48,7 +48,7 @@ public class StatisticsTab extends HorizontalLayout {
numberField.setValue(30); numberField.setValue(30);
numberField.setMinValue(5); numberField.setMinValue(5);
numberField.addValueChangeListener(e -> setTimeChart(e.getValue())); numberField.addValueChangeListener(e -> setTimeChart(e.getValue()));
reports = dataManager.getReports(app); reports = dataManager.getReportsForApp(app);
timeLayout = new VerticalLayout(numberField); timeLayout = new VerticalLayout(numberField);
Style.NO_PADDING.apply(timeLayout); Style.NO_PADDING.apply(timeLayout);
addComponent(timeLayout); addComponent(timeLayout);

View file

@ -1,86 +0,0 @@
package com.faendir.acra.ui.view.user;
import com.faendir.acra.mongod.data.DataManager;
import com.faendir.acra.mongod.model.Permission;
import com.faendir.acra.ui.view.base.MyGrid;
import com.vaadin.data.HasValue;
import com.vaadin.shared.Registration;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.Grid;
import org.vaadin.hene.popupbutton.PopupButton;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
/**
* @author Lukas
* @since 20.05.2017
*/
public class PermissionEditor extends PopupButton implements HasValue<Collection<Permission>> {
private Collection<Permission> items;
private MyGrid<Permission> grid;
public PermissionEditor(DataManager dataManager) {
super("Editor");
items = new ArrayList<>();
grid = new MyGrid<>(null, items);
grid.addColumn(permission -> dataManager.getApp(permission.getApp()).getName(), "App");
ComboBox<Permission.Level> levelComboBox = new ComboBox<>("", Arrays.asList(Permission.Level.values()));
grid.addColumn(permission -> permission.getLevel().name(), "Level")
.setEditorBinding(grid.getEditor().getBinder().bind(levelComboBox, Permission::getLevel, (permission, level) -> {
Collection<Permission> newValue = items.stream().map(p -> new Permission(p.getApp(), p.getLevel())).collect(Collectors.toList());
newValue.stream().filter(p -> p.getApp().equals(permission.getApp())).findAny().ifPresent(p -> p.setLevel(level));
setValue(newValue, true);
}));
grid.getEditor().setEnabled(true).setBuffered(false);
grid.setSelectionMode(Grid.SelectionMode.NONE);
setContent(grid);
setPopupVisible(true);
}
@Override
public void setValue(Collection<Permission> value) {
setValue(value, false);
}
private void setValue(Collection<Permission> value, boolean userOriginated) {
Collection<Permission> oldValue = items;
items = value;
grid.setItems(items);
grid.setHeightByRows(items.size());
fireEvent(new ValueChangeEvent<>(this, oldValue, userOriginated));
}
@Override
public Collection<Permission> getValue() {
return items;
}
@Override
public void setRequiredIndicatorVisible(boolean requiredIndicatorVisible) {
}
@Override
public boolean isRequiredIndicatorVisible() {
return false;
}
@Override
public Registration addValueChangeListener(ValueChangeListener<Collection<Permission>> listener) {
return addListener(ValueChangeEvent.class, listener,
ValueChangeListener.VALUE_CHANGE_METHOD);
}
@Override
public void setReadOnly(boolean readOnly) {
}
@Override
public boolean isReadOnly() {
return false;
}
}

View file

@ -1,18 +1,20 @@
package com.faendir.acra.ui.view.user; package com.faendir.acra.ui.view.user;
import com.faendir.acra.mongod.data.DataManager; import com.faendir.acra.mongod.data.DataManager;
import com.faendir.acra.mongod.model.App;
import com.faendir.acra.mongod.model.Permission;
import com.faendir.acra.mongod.model.User; import com.faendir.acra.mongod.model.User;
import com.faendir.acra.mongod.user.UserManager; import com.faendir.acra.mongod.user.UserManager;
import com.faendir.acra.security.SecurityUtils; import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.ui.view.base.MyCheckBox;
import com.faendir.acra.ui.view.base.MyGrid; import com.faendir.acra.ui.view.base.MyGrid;
import com.faendir.acra.ui.view.base.NamedView; import com.faendir.acra.ui.view.base.NamedView;
import com.faendir.acra.util.Style; import com.faendir.acra.util.Style;
import com.vaadin.data.Binder;
import com.vaadin.navigator.ViewChangeListener; import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.server.UserError; import com.vaadin.server.UserError;
import com.vaadin.spring.annotation.UIScope; import com.vaadin.spring.annotation.UIScope;
import com.vaadin.ui.Button; import com.vaadin.ui.Button;
import com.vaadin.ui.CheckBox; import com.vaadin.ui.ComboBox;
import com.vaadin.ui.Grid; import com.vaadin.ui.Grid;
import com.vaadin.ui.PasswordField; import com.vaadin.ui.PasswordField;
import com.vaadin.ui.TextField; import com.vaadin.ui.TextField;
@ -22,7 +24,7 @@ import com.vaadin.ui.Window;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.stream.Collectors; import java.util.Arrays;
/** /**
* @author Lukas * @author Lukas
@ -54,17 +56,28 @@ public class UserManagerView extends NamedView {
@Override @Override
public void enter(ViewChangeListener.ViewChangeEvent event) { public void enter(ViewChangeListener.ViewChangeEvent event) {
userGrid = new MyGrid<>("Users", userManager.getUsers()); userGrid = new MyGrid<>("Users", userManager.getUsers());
userGrid.getEditor().setEnabled(true).setBuffered(false);
userGrid.setSelectionMode(Grid.SelectionMode.NONE); userGrid.setSelectionMode(Grid.SelectionMode.NONE);
userGrid.addColumn(User::getUsername, "Username"); userGrid.addColumn(User::getUsername, "Username");
Binder<User> binder = userGrid.getEditor().getBinder(); userGrid.addComponentColumn(user -> new MyCheckBox(user.getRoles().contains(UserManager.ROLE_ADMIN), e -> {
userGrid.addColumn(user -> user.getRoles().contains(UserManager.ROLE_ADMIN) ? "Yes" : "No").setCaption("Admin") if (!e.getValue() && user.getUsername().equals(SecurityUtils.getUsername())) {
.setEditorBinding(binder.forField(new CheckBox()) MyCheckBox checkBox = ((MyCheckBox) e.getComponent());
.withValidator(bool -> bool || !binder.getBean().getUsername().equals(SecurityUtils.getUsername()), "Cannot revoke own admin privileges") checkBox.setComponentError(new UserError("Cannot revoke own admin privileges"));
.bind(user -> user.getRoles().contains(UserManager.ROLE_ADMIN), userManager::setAdmin)); checkBox.setValue(true);
userGrid.addColumn(this::getPermissionString, "App Permissions") } else {
.setEditorBinding(userGrid.getEditor().getBinder().bind(new PermissionEditor(dataManager), User::getPermissions, userManager.setAdmin(user, e.getValue());
((user, permissions) -> permissions.forEach(p -> userManager.setPermission(user, p.getApp(), p.getLevel()))))); }
})).setCaption("Admin");
for (App app : dataManager.getApps()) {
userGrid.addComponentColumn(user -> {
Permission permission = user.getPermissions().stream().filter(p -> p.getApp().equals(app.getId())).findAny().orElseThrow(IllegalStateException::new);
ComboBox<Permission.Level> levelComboBox = new ComboBox<>(null, Arrays.asList(Permission.Level.values()));
levelComboBox.setEmptySelectionAllowed(false);
levelComboBox.setValue(permission.getLevel());
levelComboBox.addValueChangeListener(e -> userManager.setPermission(user, permission.getApp(), e.getValue()));
return levelComboBox;
}).setCaption("Access Permission for " + app.getName());
}
userGrid.setRowHeight(42);
Button newUser = new Button("New User", e -> newUser()); Button newUser = new Button("New User", e -> newUser());
VerticalLayout layout = new VerticalLayout(userGrid, newUser); VerticalLayout layout = new VerticalLayout(userGrid, newUser);
layout.setExpandRatio(userGrid, 1); layout.setExpandRatio(userGrid, 1);
@ -76,12 +89,6 @@ public class UserManagerView extends NamedView {
Style.apply(this, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM); Style.apply(this, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM);
} }
private String getPermissionString(User user) {
return user.getPermissions().stream()
.map(permission -> dataManager.getApp(permission.getApp()).getName() + ": " + permission.getLevel().name())
.collect(Collectors.joining(", "));
}
private void newUser() { private void newUser() {
Window window = new Window("New User"); Window window = new Window("New User");
TextField name = new TextField("Username"); TextField name = new TextField("Username");