diff --git a/backend/build.gradle b/backend/build.gradle index dbd93f6..87aaa3f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -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" } } diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/BugRepository.java b/backend/src/main/java/com/faendir/acra/mongod/data/BugRepository.java new file mode 100644 index 0000000..ff5808e --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/data/BugRepository.java @@ -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 { + List findByApp(String app); +} diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java b/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java index df60a04..446cc4d 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java +++ b/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java @@ -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 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 getApps(){ + public List 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 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 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 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 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 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 getReports(String app) { - return reportRepository.findAll(Example.of(new Report(null, app))); + public List 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 getBugs(String app) { + return bugRepository.findByApp(app); + } + + public List 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(); } + } diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java b/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java index 92ed790..a560587 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java +++ b/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java @@ -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 { + @Query("{id.app:?0}") + List findByApp(String app); } diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java b/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java index 43ea3fe..1096e5e 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java +++ b/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java @@ -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 { + List findByApp(String app); + + @Query("{content.map.STACK_TRACE:?0,content.map.APP_VERSION_CODE:?1}") + List findByBug(String stacktrace, int versionCode); + + @CountQuery("{content.map.STACK_TRACE:?0,content.map.APP_VERSION_CODE:?1}") + int countByBug(String stacktrace, int versionCode); } diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java b/backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java index 5f8b93f..27b278e 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java +++ b/backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java @@ -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 getBugs(List reports) { - List 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 reports) { + return reports.stream().map(Report::getDate).reduce((d1, d2) -> d1.after(d2) ? d1 : d2).orElse(new Date()); + } + } diff --git a/backend/src/main/java/com/faendir/acra/mongod/model/Bug.java b/backend/src/main/java/com/faendir/acra/mongod/model/Bug.java index 0771485..d28a354 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/model/Bug.java +++ b/backend/src/main/java/com/faendir/acra/mongod/model/Bug.java @@ -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 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 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; + } } } diff --git a/backend/src/main/java/com/faendir/acra/mongod/model/Report.java b/backend/src/main/java/com/faendir/acra/mongod/model/Report.java index 2879cad..d0e62fc 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/model/Report.java +++ b/backend/src/main/java/com/faendir/acra/mongod/model/Report.java @@ -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() { diff --git a/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java b/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java index d22cef9..ffcd0ec 100644 --- a/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java +++ b/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java @@ -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); } - ensureValidPermissions(user); return user; } diff --git a/backend/src/main/java/com/faendir/acra/service/ReportService.java b/backend/src/main/java/com/faendir/acra/service/ReportService.java index 96fde04..34877a3 100644 --- a/backend/src/main/java/com/faendir/acra/service/ReportService.java +++ b/backend/src/main/java/com/faendir/acra/service/ReportService.java @@ -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 fileMap = request.getMultiFileMap(); List 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(); diff --git a/backend/src/main/java/com/faendir/acra/ui/BackendUI.java b/backend/src/main/java/com/faendir/acra/ui/BackendUI.java index 8393023..5ad24f3 100644 --- a/backend/src/main/java/com/faendir/acra/ui/BackendUI.java +++ b/backend/src/main/java/com/faendir/acra/ui/BackendUI.java @@ -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); } } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/Overview.java b/backend/src/main/java/com/faendir/acra/ui/view/Overview.java index 14ce32c..652736b 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/Overview.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/Overview.java @@ -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()); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java b/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java index 179b06d..705999a 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java @@ -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(); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/base/MyCheckBox.java b/backend/src/main/java/com/faendir/acra/ui/view/base/MyCheckBox.java new file mode 100644 index 0000000..a304344 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/base/MyCheckBox.java @@ -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 changeListener) { + this(value, true, changeListener); + } + + public MyCheckBox(boolean value, boolean enabled, ValueChangeListener changeListener) { + setValue(value); + setEnabled(enabled); + addValueChangeListener(changeListener); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java b/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java index 2da0571..5814a5b 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java @@ -18,10 +18,12 @@ import java.util.function.Supplier; * @since 14.05.2017 */ public class ReportList extends MyGrid implements DataManager.ReportChangeListener { + public static final String CAPTION = "Reports"; private final Supplier> reportSupplier; public ReportList(String app, NavigationManager navigationManager, DataManager dataManager, Supplier> 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 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)); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java b/backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java index e809e62..f70d5ef 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java @@ -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 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 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 getBugs() { + List bugs = dataManager.getBugs(app); + if (hideSolved.getValue()) { + return bugs.stream().filter(bug -> !bug.isSolved()).collect(Collectors.toList()); + } + return bugs; } } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/tabs/ReportTab.java b/backend/src/main/java/com/faendir/acra/ui/view/tabs/ReportTab.java index 0a4d971..dde2e8e 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/tabs/ReportTab.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/tabs/ReportTab.java @@ -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)); } } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/tabs/StatisticsTab.java b/backend/src/main/java/com/faendir/acra/ui/view/tabs/StatisticsTab.java index efde160..2d4e76c 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/tabs/StatisticsTab.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/tabs/StatisticsTab.java @@ -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); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java b/backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java deleted file mode 100644 index d284a1e..0000000 --- a/backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java +++ /dev/null @@ -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> { - - private Collection items; - private MyGrid 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 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 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 value) { - setValue(value, false); - } - - private void setValue(Collection value, boolean userOriginated) { - Collection oldValue = items; - items = value; - grid.setItems(items); - grid.setHeightByRows(items.size()); - fireEvent(new ValueChangeEvent<>(this, oldValue, userOriginated)); - } - - @Override - public Collection getValue() { - return items; - } - - @Override - public void setRequiredIndicatorVisible(boolean requiredIndicatorVisible) { - } - - @Override - public boolean isRequiredIndicatorVisible() { - return false; - } - - @Override - public Registration addValueChangeListener(ValueChangeListener> listener) { - return addListener(ValueChangeEvent.class, listener, - ValueChangeListener.VALUE_CHANGE_METHOD); - } - - @Override - public void setReadOnly(boolean readOnly) { - - } - - @Override - public boolean isReadOnly() { - return false; - } -} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java b/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java index 70d6243..439d4cb 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java @@ -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 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 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");