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 {
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"
}
}

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;
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();
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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());
}
}

View file

@ -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;
}
}
}

View file

@ -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() {

View file

@ -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;
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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());

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("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();

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
*/
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));

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.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;
}
}

View file

@ -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));
}
}

View file

@ -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);

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;
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");