bug solving, inline grid editing
This commit is contained in:
parent
8474f34528
commit
ae70018a9c
20 changed files with 246 additions and 215 deletions
|
@ -4,6 +4,7 @@ buildscript {
|
|||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://oss.sonatype.org/content/repositories/vaadin-snapshots/"}
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
|
||||
|
@ -40,7 +41,9 @@ dependencies {
|
|||
compile 'commons-fileupload:commons-fileupload:1.3.2'
|
||||
compile 'org.ocpsoft.prettytime:prettytime:3.2.7.Final'
|
||||
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
|
||||
compileOnly project(':annotation')
|
||||
apt project(':annotationprocessor')
|
||||
|
@ -59,7 +62,7 @@ configurations {
|
|||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.vaadin:vaadin-bom:8.0.6"
|
||||
mavenBom "com.vaadin:vaadin-bom:8.1-SNAPSHOT"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.faendir.acra.mongod.data;
|
||||
|
||||
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.Report;
|
||||
import com.mongodb.BasicDBObjectBuilder;
|
||||
|
@ -9,12 +10,9 @@ import org.json.JSONObject;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.Query;
|
||||
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Base64Utils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
@ -33,16 +31,18 @@ import java.util.List;
|
|||
public class DataManager {
|
||||
private final MappingRepository mappingRepository;
|
||||
private final ReportRepository reportRepository;
|
||||
private final AppRepository appRepository;
|
||||
private final BugRepository bugRepository;
|
||||
private final List<ReportChangeListener> listeners;
|
||||
private final GridFsTemplate gridFsTemplate;
|
||||
private final Logger logger;
|
||||
private final SecureRandom secureRandom;
|
||||
private final AppRepository appRepository;
|
||||
|
||||
@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.appRepository = appRepository;
|
||||
this.bugRepository = bugRepository;
|
||||
logger = LoggerFactory.getLogger(DataManager.class);
|
||||
this.gridFsTemplate = gridFsTemplate;
|
||||
this.mappingRepository = mappingRepository;
|
||||
|
@ -50,45 +50,31 @@ public class DataManager {
|
|||
this.listeners = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void createNewApp(String name){
|
||||
public synchronized void createNewApp(String name) {
|
||||
byte[] bytes = new byte[12];
|
||||
secureRandom.nextBytes(bytes);
|
||||
appRepository.save(new App(name, Base64Utils.encodeToString(bytes)));
|
||||
}
|
||||
|
||||
public List<App> getApps(){
|
||||
public List<App> getApps() {
|
||||
return appRepository.findAll();
|
||||
}
|
||||
|
||||
public App getApp(String id){
|
||||
public App getApp(String id) {
|
||||
return appRepository.findOne(id);
|
||||
}
|
||||
|
||||
public void deleteApp(String id){
|
||||
public synchronized void deleteApp(String id) {
|
||||
appRepository.delete(id);
|
||||
getReports(id).forEach(this::remove);
|
||||
getReportsForApp(id).forEach(this::deleteReport);
|
||||
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) {
|
||||
return gridFsTemplate.find(new Query(Criteria.where("metadata.reportId").is(report)));
|
||||
}
|
||||
|
||||
public void removeAttachments(String report) {
|
||||
gridFsTemplate.delete(new Query(Criteria.where("metadata.reportId").is(report)));
|
||||
}
|
||||
|
||||
public void addMapping(String app, int version, String mappings) {
|
||||
public synchronized void addMapping(String app, int version, String mappings) {
|
||||
mappingRepository.save(new ProguardMapping(app, version, mappings));
|
||||
}
|
||||
|
||||
|
@ -97,33 +83,74 @@ public class DataManager {
|
|||
}
|
||||
|
||||
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) {
|
||||
newReport(content, Collections.emptyList());
|
||||
public void newReport(String app, JSONObject content) {
|
||||
newReport(app, content, Collections.emptyList());
|
||||
}
|
||||
|
||||
public void newReport(JSONObject content, List<MultipartFile> attachments) {
|
||||
Report report = reportRepository.save(new Report(content, SecurityContextHolder.getContext().getAuthentication().getName()));
|
||||
saveAttachments(report.getId(), attachments);
|
||||
public synchronized void newReport(String app, JSONObject content, List<MultipartFile> attachments) {
|
||||
Report report = reportRepository.save(new Report(content, app));
|
||||
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);
|
||||
}
|
||||
|
||||
public List<Report> getReports(String app) {
|
||||
return reportRepository.findAll(Example.of(new Report(null, app)));
|
||||
public List<Report> getReportsForApp(String app) {
|
||||
return reportRepository.findByApp(app);
|
||||
}
|
||||
|
||||
public Report getReport(String id) {
|
||||
return reportRepository.findOne(id);
|
||||
}
|
||||
|
||||
public void remove(Report report){
|
||||
public synchronized void deleteReport(Report 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
return listeners.add(reportChangeListener);
|
||||
}
|
||||
|
@ -135,4 +162,5 @@ public class DataManager {
|
|||
public interface ReportChangeListener {
|
||||
void onChange();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,10 +2,15 @@ package com.faendir.acra.mongod.data;
|
|||
|
||||
import com.faendir.acra.mongod.model.ProguardMapping;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.data.mongodb.repository.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 19.05.2017
|
||||
*/
|
||||
interface MappingRepository extends MongoRepository<ProguardMapping, ProguardMapping.MetaData> {
|
||||
@Query("{id.app:?0}")
|
||||
List<ProguardMapping> findByApp(String app);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
package com.faendir.acra.mongod.data;
|
||||
|
||||
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.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 22.03.2017
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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.Report;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
@ -14,7 +13,6 @@ import java.io.StringReader;
|
|||
import java.io.StringWriter;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -26,18 +24,6 @@ import java.util.Locale;
|
|||
public final class ReportUtils {
|
||||
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) {
|
||||
try {
|
||||
return dateFormat.parse(s);
|
||||
|
@ -56,4 +42,8 @@ public final class ReportUtils {
|
|||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,41 +1,58 @@
|
|||
package com.faendir.acra.mongod.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 13.05.2017
|
||||
*/
|
||||
@Document
|
||||
public class Bug {
|
||||
private List<Report> reports;
|
||||
private String trace;
|
||||
private int versionCode;
|
||||
private Identification id;
|
||||
@Indexed
|
||||
private String app;
|
||||
private boolean solved;
|
||||
private String stacktrace;
|
||||
|
||||
public Bug(Report report){
|
||||
reports = new ArrayList<>();
|
||||
this.trace = report.getStacktrace();
|
||||
this.versionCode = report.getVersionCode();
|
||||
public Bug(){
|
||||
}
|
||||
|
||||
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){
|
||||
return report.getStacktrace().equals(trace) && report.getVersionCode() == versionCode;
|
||||
return report.getStacktrace().hashCode() == id.stacktraceHash && report.getVersionCode() == id.versionCode;
|
||||
}
|
||||
|
||||
public List<Report> getReports() {
|
||||
return reports;
|
||||
public boolean isSolved() {
|
||||
return solved;
|
||||
}
|
||||
|
||||
public Date getLastDate(){
|
||||
return reports.stream().map(Report::getDate).reduce((d1, d2) -> d1.after(d2) ? d1 : d2).orElse(new Date());
|
||||
public void setSolved(boolean solved) {
|
||||
this.solved = solved;
|
||||
}
|
||||
|
||||
public String getTrace() {
|
||||
return trace;
|
||||
public String getStacktrace() {
|
||||
return stacktrace;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package com.faendir.acra.mongod.model;
|
||||
|
||||
import com.faendir.acra.mongod.data.DataManager;
|
||||
import com.faendir.acra.mongod.data.ReportUtils;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
@ -21,7 +19,6 @@ public class Report {
|
|||
@Indexed
|
||||
private String app;
|
||||
private JSONObject content;
|
||||
private String deObfuscatedTrace;
|
||||
|
||||
public Report() {
|
||||
}
|
||||
|
@ -30,7 +27,6 @@ public class Report {
|
|||
this.content = content;
|
||||
this.app = app;
|
||||
id = content == null ? null : getValueSafe("REPORT_ID", content::getString, "");
|
||||
deObfuscatedTrace = null;
|
||||
}
|
||||
|
||||
public JSONObject getContent() {
|
||||
|
@ -45,23 +41,12 @@ public class Report {
|
|||
return id;
|
||||
}
|
||||
|
||||
public String getStacktrace() {
|
||||
return getValueSafe("STACK_TRACE", content::getString, "");
|
||||
public String getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public String getDeObfuscatedStacktrace(DataManager dataManager){
|
||||
if (deObfuscatedTrace != null) {
|
||||
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 String getStacktrace() {
|
||||
return getValueSafe("STACK_TRACE", content::getString, "");
|
||||
}
|
||||
|
||||
public int getVersionCode() {
|
||||
|
|
|
@ -47,9 +47,11 @@ public class UserManager {
|
|||
public User getUser(String username) {
|
||||
User user = userRepository.findOne(username);
|
||||
if (user == null && defaultUser.equals(username)) {
|
||||
return getDefaultUser();
|
||||
user = getDefaultUser();
|
||||
}
|
||||
if(user != null) {
|
||||
ensureValidPermissions(user);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartHttpServletRequest;
|
|||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -35,14 +36,14 @@ public class ReportService {
|
|||
|
||||
@PreAuthorize("hasRole('REPORTER')")
|
||||
@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);
|
||||
dataManager.newReport(jsonObject);
|
||||
dataManager.newReport(principal.getName(), jsonObject);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('REPORTER')")
|
||||
@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();
|
||||
List<MultipartFile> files = fileMap.get(null);
|
||||
JSONObject jsonObject = null;
|
||||
|
@ -56,7 +57,7 @@ public class ReportService {
|
|||
}
|
||||
}
|
||||
if(jsonObject != null) {
|
||||
dataManager.newReport(jsonObject, attachments);
|
||||
dataManager.newReport(principal.getName(), jsonObject, attachments);
|
||||
return ResponseEntity.ok().build();
|
||||
}else {
|
||||
return ResponseEntity.badRequest().build();
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.vaadin.ui.Alignment;
|
|||
import com.vaadin.ui.Button;
|
||||
import com.vaadin.ui.HorizontalLayout;
|
||||
import com.vaadin.ui.LoginForm;
|
||||
import com.vaadin.ui.Notification;
|
||||
import com.vaadin.ui.UI;
|
||||
import com.vaadin.ui.VerticalLayout;
|
||||
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 {
|
||||
Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username.toLowerCase(), password));
|
||||
VaadinService.reinitializeSession(VaadinService.getCurrentRequest());
|
||||
SecurityContextHolder.getContext().setAuthentication(token);
|
||||
showMain();
|
||||
return true;
|
||||
} catch (AuthenticationException ex) {
|
||||
return false;
|
||||
Notification.show("Unknown username/password combination", Notification.Type.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ public class Overview extends NamedView {
|
|||
grid.setSizeFull();
|
||||
grid.setSelectionMode(Grid.SelectionMode.NONE);
|
||||
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);
|
||||
if(SecurityUtils.hasRole(UserManager.ROLE_ADMIN)){
|
||||
Button add = new Button("New App", e -> addApp());
|
||||
|
|
|
@ -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("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("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.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT);
|
||||
summaryGrid.setSizeFull();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -18,10 +18,12 @@ import java.util.function.Supplier;
|
|||
* @since 14.05.2017
|
||||
*/
|
||||
public class ReportList extends MyGrid<Report> implements DataManager.ReportChangeListener {
|
||||
public static final String CAPTION = "Reports";
|
||||
private final 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;
|
||||
setSizeFull();
|
||||
setSelectionMode(SelectionMode.NONE);
|
||||
|
@ -31,7 +33,7 @@ public class ReportList extends MyGrid<Report> implements DataManager.ReportChan
|
|||
addColumn(Report::getPhoneModel, "Device");
|
||||
addColumn(report -> report.getStacktrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1);
|
||||
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()));
|
||||
addAttachListener(e -> dataManager.addListener(this));
|
||||
|
|
|
@ -3,15 +3,24 @@ package com.faendir.acra.ui.view.tabs;
|
|||
import com.faendir.acra.mongod.data.DataManager;
|
||||
import com.faendir.acra.mongod.data.ReportUtils;
|
||||
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.view.base.MyCheckBox;
|
||||
import com.faendir.acra.ui.view.base.MyGrid;
|
||||
import com.faendir.acra.ui.view.base.ReportList;
|
||||
import com.faendir.acra.util.Style;
|
||||
import com.faendir.acra.util.TimeSpanRenderer;
|
||||
import com.vaadin.event.selection.SelectionEvent;
|
||||
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 java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 17.05.2017
|
||||
|
@ -22,19 +31,29 @@ public class BugTab extends VerticalLayout implements DataManager.ReportChangeLi
|
|||
private final NavigationManager navigationManager;
|
||||
private final DataManager dataManager;
|
||||
private final MyGrid<Bug> bugs;
|
||||
private final CheckBox hideSolved;
|
||||
|
||||
public BugTab(String app, NavigationManager navigationManager, DataManager dataManager) {
|
||||
this.app = app;
|
||||
this.navigationManager = navigationManager;
|
||||
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.addColumn(bug -> bug.getReports().size(), "Reports");
|
||||
bugs.sort(bugs.addColumn(Bug::getLastDate, new TimeSpanRenderer(), "Latest Report"), SortDirection.DESCENDING);
|
||||
bugs.addColumn(dataManager::countReportsForBug, "Reports");
|
||||
bugs.sort(bugs.addColumn(bug -> ReportUtils.getLastReportDate(dataManager.getReportsForBug(bug)), new TimeSpanRenderer(), "Latest Report"), SortDirection.DESCENDING);
|
||||
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.addComponentColumn(bug -> new MyCheckBox(bug.isSolved(), SecurityUtils.hasPermission(app, Permission.Level.EDIT), e -> {
|
||||
dataManager.setBugSolved(bug, e.getValue());
|
||||
onChange();
|
||||
})).setCaption("Solved");
|
||||
addComponent(bugs);
|
||||
setExpandRatio(bugs, 1);
|
||||
Style.NO_PADDING.apply(this);
|
||||
setSizeFull();
|
||||
setCaption(CAPTION);
|
||||
|
@ -43,14 +62,28 @@ public class BugTab extends VerticalLayout implements DataManager.ReportChangeLi
|
|||
}
|
||||
|
||||
private void handleBugSelection(SelectionEvent<Bug> e) {
|
||||
if (getComponentCount() == 2) {
|
||||
removeComponent(getComponent(1));
|
||||
for (Component component : this) {
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@ import com.faendir.acra.ui.view.base.ReportList;
|
|||
*/
|
||||
public class ReportTab extends ReportList {
|
||||
public ReportTab(String app, NavigationManager navigationManager, DataManager dataManager) {
|
||||
super(app, navigationManager, dataManager, () -> dataManager.getReports(app));
|
||||
super(app, navigationManager, dataManager, () -> dataManager.getReportsForApp(app));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ public class StatisticsTab extends HorizontalLayout {
|
|||
numberField.setValue(30);
|
||||
numberField.setMinValue(5);
|
||||
numberField.addValueChangeListener(e -> setTimeChart(e.getValue()));
|
||||
reports = dataManager.getReports(app);
|
||||
reports = dataManager.getReportsForApp(app);
|
||||
timeLayout = new VerticalLayout(numberField);
|
||||
Style.NO_PADDING.apply(timeLayout);
|
||||
addComponent(timeLayout);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,18 +1,20 @@
|
|||
package com.faendir.acra.ui.view.user;
|
||||
|
||||
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.user.UserManager;
|
||||
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.NamedView;
|
||||
import com.faendir.acra.util.Style;
|
||||
import com.vaadin.data.Binder;
|
||||
import com.vaadin.navigator.ViewChangeListener;
|
||||
import com.vaadin.server.UserError;
|
||||
import com.vaadin.spring.annotation.UIScope;
|
||||
import com.vaadin.ui.Button;
|
||||
import com.vaadin.ui.CheckBox;
|
||||
import com.vaadin.ui.ComboBox;
|
||||
import com.vaadin.ui.Grid;
|
||||
import com.vaadin.ui.PasswordField;
|
||||
import com.vaadin.ui.TextField;
|
||||
|
@ -22,7 +24,7 @@ import com.vaadin.ui.Window;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
|
@ -54,17 +56,28 @@ public class UserManagerView extends NamedView {
|
|||
@Override
|
||||
public void enter(ViewChangeListener.ViewChangeEvent event) {
|
||||
userGrid = new MyGrid<>("Users", userManager.getUsers());
|
||||
userGrid.getEditor().setEnabled(true).setBuffered(false);
|
||||
userGrid.setSelectionMode(Grid.SelectionMode.NONE);
|
||||
userGrid.addColumn(User::getUsername, "Username");
|
||||
Binder<User> binder = userGrid.getEditor().getBinder();
|
||||
userGrid.addColumn(user -> user.getRoles().contains(UserManager.ROLE_ADMIN) ? "Yes" : "No").setCaption("Admin")
|
||||
.setEditorBinding(binder.forField(new CheckBox())
|
||||
.withValidator(bool -> bool || !binder.getBean().getUsername().equals(SecurityUtils.getUsername()), "Cannot revoke own admin privileges")
|
||||
.bind(user -> user.getRoles().contains(UserManager.ROLE_ADMIN), userManager::setAdmin));
|
||||
userGrid.addColumn(this::getPermissionString, "App Permissions")
|
||||
.setEditorBinding(userGrid.getEditor().getBinder().bind(new PermissionEditor(dataManager), User::getPermissions,
|
||||
((user, permissions) -> permissions.forEach(p -> userManager.setPermission(user, p.getApp(), p.getLevel())))));
|
||||
userGrid.addComponentColumn(user -> new MyCheckBox(user.getRoles().contains(UserManager.ROLE_ADMIN), e -> {
|
||||
if (!e.getValue() && user.getUsername().equals(SecurityUtils.getUsername())) {
|
||||
MyCheckBox checkBox = ((MyCheckBox) e.getComponent());
|
||||
checkBox.setComponentError(new UserError("Cannot revoke own admin privileges"));
|
||||
checkBox.setValue(true);
|
||||
} else {
|
||||
userManager.setAdmin(user, e.getValue());
|
||||
}
|
||||
})).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());
|
||||
VerticalLayout layout = new VerticalLayout(userGrid, newUser);
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
Window window = new Window("New User");
|
||||
TextField name = new TextField("Username");
|
||||
|
|
Loading…
Reference in a new issue