push new reports, layout improvements

This commit is contained in:
F43nd1r 2017-06-16 13:44:38 +02:00
parent 152e82d5f9
commit c02cfe19bf
20 changed files with 326 additions and 124 deletions

View file

@ -16,7 +16,6 @@ plugins {
id 'fi.jasoft.plugin.vaadin' version '1.1.11'
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
group 'com.faendir'
@ -24,10 +23,14 @@ version = '0.1.0-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8
ext {
debug = true
aspectjVersion = '1.8.10'
}
vaadin {
version '8.1-SNAPSHOT'
push true
}
configurations {
ajc
aspects
@ -57,6 +60,7 @@ dependencies {
compile 'org.springframework.boot:spring-boot-starter-security'
compile "org.springframework.boot:spring-boot-starter-cache"
compile 'com.vaadin:vaadin-spring-boot-starter'
compile 'com.vaadin:vaadin-push'
aspects "org.springframework:spring-aspects"
ajc "org.aspectj:aspectjtools:$aspectjVersion"
compile "org.aspectj:aspectjrt:$aspectjVersion"
@ -91,12 +95,13 @@ configurations {
dependencyManagement {
imports {
mavenBom "com.vaadin:vaadin-bom:8.1-SNAPSHOT"
mavenBom "com.vaadin:vaadin-bom:${vaadin.version}"
}
}
project.convention.getPlugin(WarPluginConvention).webAppDirName = "src/main/resources"
if (!ext.debug) {
apply from: 'releasepackaging.gradle'
war {
archiveName = "acra.war"
version = version
}

View file

@ -1,10 +0,0 @@
apply plugin: 'war'
war {
archiveName = "acra.war"
version = version
}
dependencies{
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
}

View file

@ -4,6 +4,7 @@ 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.faendir.acra.mongod.model.ReportInfo;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.gridfs.GridFSDBFile;
import org.json.JSONObject;
@ -12,6 +13,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
@ -25,6 +27,10 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Lukas
@ -34,35 +40,39 @@ import java.util.Optional;
public class DataManager {
private static final String APP_REPORT_CACHE = "appReport";
private static final String APP_CACHE = "app";
private static final String BUG_REPORT_CACHE = "bugReport";
private static final String APP_BUG_CACHE = "appBug";
private final MappingRepository mappingRepository;
private final ReportRepository reportRepository;
private final AppRepository appRepository;
private final BugRepository bugRepository;
private final List<ReportChangeListener> listeners;
private final List<ListenerHolder<?>> listeners;
private final GridFsTemplate gridFsTemplate;
private final Logger logger;
private final SecureRandom secureRandom;
private final ExecutorService listenerExecutor;
@Autowired
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;
this.reportRepository = reportRepository;
this.logger = LoggerFactory.getLogger(DataManager.class);
this.listeners = new ArrayList<>();
this.listenerExecutor = Executors.newSingleThreadExecutor();
}
@CacheEvict(APP_CACHE)
@CacheEvict(value = APP_CACHE, allEntries = true)
public synchronized void createNewApp(String name) {
byte[] bytes = new byte[12];
secureRandom.nextBytes(bytes);
appRepository.save(new App(name, Base64Utils.encodeToString(bytes)));
}
@Cacheable(APP_CACHE)
@Cacheable(value = APP_CACHE)
public List<App> getApps() {
return appRepository.findAll();
}
@ -71,7 +81,7 @@ public class DataManager {
return appRepository.findOne(id);
}
@CacheEvict(APP_CACHE)
@CacheEvict(value = APP_CACHE, allEntries = true)
public synchronized void deleteApp(String id) {
appRepository.delete(id);
getReportsForApp(id).forEach(this::deleteReport);
@ -86,7 +96,7 @@ public class DataManager {
mappingRepository.save(new ProguardMapping(app, version, mappings));
}
public ProguardMapping getMapping(String app, int version) {
private ProguardMapping getMapping(String app, int version) {
return mappingRepository.findOne(new ProguardMapping.MetaData(app, version));
}
@ -98,8 +108,11 @@ public class DataManager {
newReport(app, content, Collections.emptyList());
}
@CacheEvict(value = APP_REPORT_CACHE, key = "#a0")
public synchronized void newReport(String app, JSONObject content, List<MultipartFile> attachments) {
@SuppressWarnings("UnusedReturnValue")
@Caching(evict = {
@CacheEvict(value = APP_REPORT_CACHE, key = "#a0"),
@CacheEvict(value = BUG_REPORT_CACHE, key = "#result.stacktrace.hashCode() + \" \" + #result.versionCode")})
public synchronized Report newReport(String app, JSONObject content, List<MultipartFile> attachments) {
Report report = reportRepository.save(new Report(content, app));
for (MultipartFile a : attachments) {
try {
@ -110,43 +123,70 @@ public class DataManager {
}
Bug bug = bugRepository.findOne(new Bug.Identification(report.getStacktrace().hashCode(), report.getVersionCode()));
if (bug == null) {
bugRepository.save(new Bug(app, report.getStacktrace(), report.getVersionCode()));
addBug(new Bug(app, report.getStacktrace(), report.getVersionCode()));
}
listeners.forEach(ReportChangeListener::onChange);
notifyListeners(new ReportInfo(report));
return report;
}
@Cacheable(APP_REPORT_CACHE)
public List<Report> getReportsForApp(String app) {
return reportRepository.findByApp(app);
public List<ReportInfo> getReportsForApp(String app) {
try (Stream<Report> stream = reportRepository.findByApp(app)) {
return stream.map(ReportInfo::new).collect(Collectors.toList());
}
}
public long reportCountForApp(String app) {
return reportRepository.countByApp(app);
}
public Report getReport(String id) {
return reportRepository.findOne(id);
}
@CacheEvict(value = APP_REPORT_CACHE, key = "#a0.app")
public synchronized void deleteReport(Report report) {
reportRepository.delete(report);
@Caching(evict = {
@CacheEvict(value = APP_REPORT_CACHE, key = "#a0.app"),
@CacheEvict(value = BUG_REPORT_CACHE, key = "#a0.stacktrace.hashCode() + \" \" + #a0.versionCode")})
public synchronized void deleteReport(ReportInfo report) {
reportRepository.delete(report.getId());
gridFsTemplate.delete(new Query(Criteria.where("metadata.reportId").is(report.getId())));
if(reportRepository.countByBug(report.getStacktrace(), report.getVersionCode()) == 0){
Optional.ofNullable(bugRepository.findOne(new Bug.Identification(report.getStacktrace().hashCode(), report.getVersionCode()))).ifPresent(bugRepository::delete);
if (reportRepository.countByBug(report.getStacktrace(), report.getVersionCode()) == 0) {
Optional.ofNullable(bugRepository.findOne(new Bug.Identification(report.getStacktrace().hashCode(), report.getVersionCode()))).ifPresent(this::deleteBug);
}
listeners.forEach(ReportChangeListener::onChange);
notifyListeners(report);
}
@SuppressWarnings("WeakerAccess")
@CacheEvict(value = APP_BUG_CACHE, key = "#a0.app")
public void deleteBug(Bug bug) {
bugRepository.delete(bug);
notifyListeners(bug);
}
@SuppressWarnings("WeakerAccess")
@CacheEvict(value = APP_BUG_CACHE, key = "#a0.app")
public void addBug(Bug bug) {
bugRepository.save(bug);
notifyListeners(bug);
}
@Cacheable(value = APP_BUG_CACHE)
public List<Bug> getBugs(String app) {
return bugRepository.findByApp(app);
}
public List<Report> getReportsForBug(Bug bug) {
return reportRepository.findByBug(bug.getStacktrace(), bug.getVersionCode());
@Cacheable(value = BUG_REPORT_CACHE, key = "#a0.stacktrace.hashCode() + \" \" + #a0.versionCode")
public List<ReportInfo> getReportsForBug(Bug bug) {
try (Stream<Report> stream = reportRepository.findByBug(bug.getStacktrace(), bug.getVersionCode())) {
return stream.map(ReportInfo::new).collect(Collectors.toList());
}
}
public int countReportsForBug(Bug bug){
public long reportCountForBug(Bug bug) {
return reportRepository.countByBug(bug.getStacktrace(), bug.getVersionCode());
}
public String retrace(Report report){
public String retrace(Report report) {
ProguardMapping mapping = getMapping(report.getApp(), report.getVersionCode());
if (mapping != null) {
try {
@ -157,21 +197,41 @@ public class DataManager {
return report.getStacktrace();
}
public void setBugSolved(Bug bug, boolean solved){
public void setBugSolved(Bug bug, boolean solved) {
bug.setSolved(solved);
bugRepository.save(bug);
}
public boolean addListener(ReportChangeListener reportChangeListener) {
return listeners.add(reportChangeListener);
public <T> void addListener(Listener<T> listener, Class<T> clazz) {
listeners.add(new ListenerHolder<>(listener, clazz));
}
public boolean removeListener(ReportChangeListener reportChangeListener) {
return listeners.remove(reportChangeListener);
public void removeListener(Listener<?> listener) {
listeners.removeIf(h -> h.listener.equals(listener));
}
public interface ReportChangeListener {
void onChange();
private void notifyListeners(Object change) {
listeners.forEach(h -> listenerExecutor.submit(() -> h.callIfAssignable(change)));
}
public interface Listener<T> {
void onChange(T t);
}
private static class ListenerHolder<T> {
private final Listener<T> listener;
private final Class<T> clazz;
private ListenerHolder(Listener<T> listener, Class<T> clazz) {
this.listener = listener;
this.clazz = clazz;
}
private void callIfAssignable(Object change) {
if (clazz.isInstance(change)) {
listener.onChange(clazz.cast(change));
}
}
}
}

View file

@ -5,18 +5,23 @@ 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;
import java.util.stream.Stream;
/**
* @author Lukas
* @since 22.03.2017
*/
interface ReportRepository extends MongoRepository<Report, String> {
List<Report> findByApp(String app);
@Query("{app:?0}")
Stream<Report> findByApp(String app);
long countByApp(String app);
@Query("{content.map.STACK_TRACE:?0,content.map.APP_VERSION_CODE:?1}")
List<Report> findByBug(String stacktrace, int versionCode);
Stream<Report> findByBug(String stacktrace, int versionCode);
@CountQuery("{content.map.STACK_TRACE:?0,content.map.APP_VERSION_CODE:?1}")
int countByBug(String stacktrace, int versionCode);
long countByBug(String stacktrace, int versionCode);
}

View file

@ -1,7 +1,7 @@
package com.faendir.acra.mongod.data;
import com.faendir.acra.mongod.model.ProguardMapping;
import com.faendir.acra.mongod.model.Report;
import com.faendir.acra.mongod.model.ReportInfo;
import org.apache.commons.io.FileUtils;
import proguard.retrace.ReTrace;
@ -42,8 +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());
public static Date getLastReportDate(List<ReportInfo> reports) {
return reports.stream().map(ReportInfo::getDate).reduce((d1, d2) -> d1.after(d2) ? d1 : d2).orElse(new Date());
}
}

View file

@ -0,0 +1,9 @@
package com.faendir.acra.mongod.model;
/**
* @author Lukas
* @since 16.06.2017
*/
public interface AppScoped {
String getApp();
}

View file

@ -10,7 +10,7 @@ import java.io.Serializable;
* @since 13.05.2017
*/
@Document
public class Bug {
public class Bug implements AppScoped{
private Identification id;
@Indexed
private String app;
@ -26,7 +26,7 @@ public class Bug {
this.stacktrace = stacktrace;
}
public boolean is(Report report){
public boolean is(ReportInfo report){
return report.getStacktrace().hashCode() == id.stacktraceHash && report.getVersionCode() == id.versionCode;
}
@ -46,6 +46,11 @@ public class Bug {
return id.versionCode;
}
@Override
public String getApp() {
return app;
}
public static class Identification implements Serializable {
private int stacktraceHash;
private int versionCode;
@ -54,5 +59,22 @@ public class Bug {
this.stacktraceHash = stacktraceHash;
this.versionCode = versionCode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Identification that = (Identification) o;
return stacktraceHash == that.stacktraceHash && versionCode == that.versionCode;
}
@Override
public int hashCode() {
int result = stacktraceHash;
result = 31 * result + versionCode;
return result;
}
}
}

View file

@ -14,7 +14,7 @@ import java.util.function.Function;
* @since 22.03.2017
*/
@Document
public class Report {
public class Report implements AppScoped {
private String id;
@Indexed
private String app;
@ -41,6 +41,7 @@ public class Report {
return id;
}
@Override
public String getApp() {
return app;
}
@ -53,23 +54,23 @@ public class Report {
return getValueSafe("APP_VERSION_CODE", content::getInt, -1);
}
public String getVersionName(){
public String getVersionName() {
return getValueSafe("APP_VERSION_NAME", content::getString, "");
}
public String getUserEmail(){
public String getUserEmail() {
return getValueSafe("USER_EMAIL", content::getString, "");
}
public String getUserComment(){
public String getUserComment() {
return getValueSafe("USER_COMMENT", content::getString, "");
}
public String getAndroidVersion(){
public String getAndroidVersion() {
return getValueSafe("ANDROID_VERSION", content::getString, "");
}
public String getPhoneModel(){
public String getPhoneModel() {
return getValueSafe("PHONE_MODEL", content::getString, "");
}

View file

@ -0,0 +1,62 @@
package com.faendir.acra.mongod.model;
import java.util.Date;
/**
* @author Lukas
* @since 16.06.2017
*/
public class ReportInfo implements AppScoped {
private final Date date;
private final String id;
private final String app;
private final String stacktrace;
private final int versionCode;
private final String versionName;
private final String androidVersion;
private final String phoneModel;
public ReportInfo(Report report) {
this.date = report.getDate();
this.id = report.getId();
this.app = report.getApp();
this.stacktrace = report.getStacktrace();
this.versionCode = report.getVersionCode();
this.versionName = report.getVersionName();
this.androidVersion = report.getAndroidVersion();
this.phoneModel = report.getPhoneModel();
}
public Date getDate() {
return date;
}
public String getId() {
return id;
}
@Override
public String getApp() {
return app;
}
public String getStacktrace() {
return stacktrace;
}
public int getVersionCode() {
return versionCode;
}
public String getVersionName() {
return versionName;
}
public String getAndroidVersion() {
return androidVersion;
}
public String getPhoneModel() {
return phoneModel;
}
}

View file

@ -8,6 +8,7 @@ import com.faendir.acra.util.Style;
import com.vaadin.annotations.Theme;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.shared.communication.PushMode;
import com.vaadin.spring.annotation.SpringUI;
import com.vaadin.spring.annotation.UIScope;
import com.vaadin.ui.Alignment;
@ -61,12 +62,14 @@ public class BackendUI extends UI {
VaadinService.reinitializeSession(VaadinService.getCurrentRequest());
SecurityContextHolder.getContext().setAuthentication(token);
showMain();
getPushConfiguration().setPushMode(PushMode.AUTOMATIC);
} catch (AuthenticationException ex) {
Notification.show("Unknown username/password combination", Notification.Type.ERROR_MESSAGE);
}
}
public void logout() {
getPushConfiguration().setPushMode(PushMode.DISABLED);
SecurityContextHolder.clearContext();
getPage().reload();
getSession().close();

View file

@ -38,7 +38,7 @@ public class NavigationManager implements ViewAccessControl {
navigator.setErrorView(ErrorView.class);
String target = Optional.ofNullable(ui.getPage().getLocation().getFragment()).orElse("").replace("!", "");
backStack.add(target);
navigator.navigateTo(target);
ui.access(() -> navigator.navigateTo(target));
}
public void navigateTo(Class<? extends NamedView> namedView, String contentId) {

View file

@ -59,10 +59,11 @@ public class Overview extends NamedView {
@Override
public void enter(ViewChangeListener.ViewChangeEvent event) {
grid = new MyGrid<>("Apps", getApps());
grid.setSizeFull();
grid.setWidth(100, Unit.PERCENTAGE);
grid.setSelectionMode(Grid.SelectionMode.NONE);
grid.addColumn(App::getName, "Name");
grid.addColumn(app -> dataManager.getReportsForApp(app.getId()).size(), "Reports");
grid.addColumn(app -> dataManager.reportCountForApp(app.getId()), "Reports");
grid.addItemClickListener(e -> getNavigationManager().navigateTo(AppView.class, e.getItem().getId()));
VerticalLayout layout = new VerticalLayout(grid);
if(SecurityUtils.hasRole(UserManager.ROLE_ADMIN)){
Button add = new Button("New App", e -> addApp());
@ -70,6 +71,5 @@ public class Overview extends NamedView {
}
Style.apply(layout, Style.NO_PADDING, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM);
setCompositionRoot(layout);
grid.addItemClickListener(e -> getNavigationManager().navigateTo(AppView.class, e.getItem().getId()));
}
}

View file

@ -18,6 +18,7 @@ public class MyGrid<T> extends Grid<T> {
public MyGrid(String caption, Collection<T> items) {
super(caption, items);
setHeightByRows(items.size());
}
public <R> Grid.Column<T, R> addColumn(ValueProvider<T, R> valueProvider, String caption) {
@ -27,4 +28,15 @@ public class MyGrid<T> extends Grid<T> {
public <R> Grid.Column<T, R> addColumn(ValueProvider<T, R> valueProvider, AbstractRenderer<? super T, ? super R> renderer, String caption) {
return addColumn(valueProvider, renderer).setId(caption).setCaption(caption);
}
@Override
public void setItems(Collection<T> items) {
super.setItems(items);
setHeightByRows(items.size());
}
@Override
public void setHeightByRows(double rows) {
super.setHeightByRows(rows >= 1 ? rows : 1);
}
}

View file

@ -2,7 +2,7 @@ package com.faendir.acra.ui.view.base;
import com.faendir.acra.mongod.data.DataManager;
import com.faendir.acra.mongod.model.Permission;
import com.faendir.acra.mongod.model.Report;
import com.faendir.acra.mongod.model.ReportInfo;
import com.faendir.acra.security.SecurityUtils;
import com.faendir.acra.ui.NavigationManager;
import com.faendir.acra.ui.view.ReportView;
@ -11,37 +11,46 @@ import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.ui.renderers.ButtonRenderer;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @author Lukas
* @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 class ReportList extends MyGrid<ReportInfo> implements DataManager.Listener<ReportInfo> {
private static final String CAPTION = "Reports";
private final Supplier<List<ReportInfo>> reportSupplier;
private final Function<ReportInfo, Boolean> relevanceFunction;
public ReportList(String app, NavigationManager navigationManager, DataManager dataManager, Supplier<List<Report>> reportSupplier) {
public ReportList(String app, NavigationManager navigationManager, DataManager dataManager, Supplier<List<ReportInfo>> reportSupplier, Function<ReportInfo, Boolean> relevanceFunction) {
super(CAPTION, reportSupplier.get());
setId(CAPTION);
this.reportSupplier = reportSupplier;
setSizeFull();
this.relevanceFunction = relevanceFunction;
setWidth(100, Unit.PERCENTAGE);
setSelectionMode(SelectionMode.NONE);
sort(addColumn(Report::getDate, new TimeSpanRenderer(), "Date"), SortDirection.DESCENDING);
addColumn(Report::getVersionCode, "App Version");
addColumn(Report::getAndroidVersion, "Android Version");
addColumn(Report::getPhoneModel, "Device");
sort(addColumn(ReportInfo::getDate, new TimeSpanRenderer(), "Date"), SortDirection.DESCENDING);
addColumn(ReportInfo::getVersionCode, "App Version");
addColumn(ReportInfo::getAndroidVersion, "Android Version");
addColumn(ReportInfo::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.deleteReport(e.getItem())));
}
addItemClickListener(e -> navigationManager.navigateTo(ReportView.class, e.getItem().getId()));
addAttachListener(e -> dataManager.addListener(this));
addAttachListener(e -> dataManager.addListener(this, ReportInfo.class));
addDetachListener(e -> dataManager.removeListener(this));
}
@Override
public void onChange() {
setItems(reportSupplier.get());
public void onChange(ReportInfo info) {
if(relevanceFunction.apply(info)) {
setItems();
}
}
private void setItems(){
getUI().access(() -> setItems(reportSupplier.get()));
}
}

View file

@ -2,6 +2,7 @@ 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.AppScoped;
import com.faendir.acra.mongod.model.Bug;
import com.faendir.acra.mongod.model.Permission;
import com.faendir.acra.security.SecurityUtils;
@ -15,68 +16,77 @@ 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.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Lukas
* @since 17.05.2017
*/
public class BugTab extends VerticalLayout implements DataManager.ReportChangeListener {
public static final String CAPTION = "Bugs";
public class BugTab extends VerticalLayout implements DataManager.Listener<AppScoped> {
private static final String CAPTION = "Bugs";
private final String app;
private final NavigationManager navigationManager;
private final DataManager dataManager;
private final MyGrid<Bug> bugs;
private final CheckBox hideSolved;
private ReportList reportList;
public BugTab(String app, NavigationManager navigationManager, DataManager dataManager) {
this.app = app;
this.navigationManager = navigationManager;
this.dataManager = dataManager;
hideSolved = new CheckBox("Hide solved", true);
hideSolved.addValueChangeListener(e -> onChange());
hideSolved.addValueChangeListener(e -> setItems());
addComponent(hideSolved);
setComponentAlignment(hideSolved, Alignment.MIDDLE_RIGHT);
bugs = new MyGrid<>(null, getBugs());
bugs.setSizeFull();
bugs.addColumn(dataManager::countReportsForBug, "Reports");
bugs.setWidth(100, Unit.PERCENTAGE);
bugs.addColumn(dataManager::reportCountForBug, "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.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();
setItems();
})).setCaption("Solved");
addComponent(bugs);
setExpandRatio(bugs, 1);
Style.NO_PADDING.apply(this);
setSizeFull();
setCaption(CAPTION);
addAttachListener(e -> dataManager.addListener(this));
addAttachListener(e -> dataManager.addListener(this, AppScoped.class));
addDetachListener(e -> dataManager.removeListener(this));
}
private void handleBugSelection(SelectionEvent<Bug> e) {
for (Component component : this) {
if (ReportList.CAPTION.equals(component.getId())) {
removeComponent(component);
}
Optional<Bug> selection = e.getFirstSelectedItem();
ReportList reportList = null;
if (selection.isPresent()) {
reportList = new ReportList(app, navigationManager, dataManager, () -> dataManager.getReportsForBug(selection.get()), selection.get()::is);
replaceComponent(this.reportList, reportList);
} else {
removeComponent(this.reportList);
}
e.getFirstSelectedItem().ifPresent(bug -> {
ReportList reportList = new ReportList(app, navigationManager, dataManager, () -> dataManager.getReportsForBug(bug));
addComponent(reportList);
setExpandRatio(reportList, 1);
});
this.reportList = reportList;
}
@Override
public void onChange() {
bugs.setItems(getBugs());
public void onChange(AppScoped appScoped) {
if (appScoped.getApp().equals(app)) {
setItems();
}
}
private void setItems() {
getUI().access(() -> {
Set<Bug> selection = bugs.getSelectedItems();
bugs.setItems(getBugs());
selection.forEach(bugs::select);
});
}
private List<Bug> getBugs() {

View file

@ -34,7 +34,7 @@ public class DeObfuscationTab extends VerticalLayout {
this.app = app;
this.dataManager = dataManager;
grid.addColumn(ProguardMapping::getVersion, "Version");
grid.setSizeFull();
grid.setWidth(100, Unit.PERCENTAGE);
addComponent(grid);
setSizeFull();
Style.NO_PADDING.apply(this);

View file

@ -39,7 +39,7 @@ public class PropertiesTab extends VerticalLayout {
IntStepper age = new IntStepper();
age.setValue(30);
age.setMinValue(0);
HorizontalLayout purgeAge = new HorizontalLayout(new Button("Purge", e->{
HorizontalLayout purgeAge = new HorizontalLayout(new Button("Purge", e -> {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -age.getValue());
Date keepAfter = calendar.getTime();

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.getReportsForApp(app));
super(app, navigationManager, dataManager, () -> dataManager.getReportsForApp(app), reportInfo -> reportInfo.getApp().equals(app));
}
}

View file

@ -1,9 +1,8 @@
package com.faendir.acra.ui.view.tabs;
import com.faendir.acra.mongod.data.DataManager;
import com.faendir.acra.mongod.model.Report;
import com.faendir.acra.mongod.model.ReportInfo;
import com.faendir.acra.util.Style;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.VerticalLayout;
import org.jfree.chart.ChartFactory;
@ -35,41 +34,43 @@ import java.util.Optional;
* @author Lukas
* @since 22.05.2017
*/
public class StatisticsTab extends HorizontalLayout {
private static final String TIME_CHART_ID = "timeChart";
public class StatisticsTab extends HorizontalLayout implements DataManager.Listener<ReportInfo> {
private static final Color BACKGROUND_GRAY = new Color(0xfafafa); //vaadin gray
private static final Color BLUE = new Color(0x197de1); //vaadin blue
private final List<Report> reports;
private final VerticalLayout timeLayout;
private final IntStepper numberField;
private final String app;
private final DataManager dataManager;
private JFreeChartWrapper timeChart;
private JFreeChartWrapper versionChart;
public StatisticsTab(String app, DataManager dataManager) {
this.app = app;
this.dataManager = dataManager;
setCaption("Statistics");
IntStepper numberField = new IntStepper("Days");
numberField = new IntStepper("Days");
numberField.setValue(30);
numberField.setMinValue(5);
numberField.addValueChangeListener(e -> setTimeChart(e.getValue()));
reports = dataManager.getReportsForApp(app);
numberField.addValueChangeListener(e -> setTimeChart(e.getValue(), dataManager.getReportsForApp(app)));
timeLayout = new VerticalLayout(numberField);
Style.NO_PADDING.apply(timeLayout);
addComponent(timeLayout);
setTimeChart(30);
setVersionChart();
List<ReportInfo> reportInfos = dataManager.getReportsForApp(app);
setTimeChart(30, reportInfos);
setVersionChart(reportInfos);
setSizeFull();
addAttachListener(e -> dataManager.addListener(this, ReportInfo.class));
addDetachListener(e -> dataManager.removeListener(this));
}
private void setTimeChart(int age) {
for (Component component : timeLayout) {
if (TIME_CHART_ID.equals(component.getId())) {
timeLayout.removeComponent(component);
}
}
private void setTimeChart(int age, List<ReportInfo> reports) {
TimeSeries series = new TimeSeries("Date");
series.setMaximumItemAge(age);
series.add(new Day(new Date()), 0);
Calendar start = Calendar.getInstance();
start.add(Calendar.DAY_OF_MONTH, -age);
series.add(new Day(start.getTime()), 0);
for (Report report : reports) {
for (ReportInfo report : reports) {
Date date = report.getDate();
Day day = new Day(date);
int count = Optional.ofNullable(series.getDataItem(day)).map(TimeSeriesDataItem::getValue).map(Number::intValue).orElse(0);
@ -90,13 +91,13 @@ public class StatisticsTab extends HorizontalLayout {
barRenderer.setBarAlignmentFactor(0.5);
barRenderer.setMargin(0.2);
JFreeChartWrapper wrapper = new JFreeChartWrapper(chart);
wrapper.setId(TIME_CHART_ID);
timeLayout.addComponent(wrapper);
timeLayout.replaceComponent(timeChart, wrapper);
timeChart = wrapper;
}
private void setVersionChart() {
private void setVersionChart(List<ReportInfo> reports) {
DefaultPieDataset dataset = new DefaultPieDataset();
for (Report report : reports) {
for (ReportInfo report : reports) {
String version = report.getAndroidVersion();
int count;
if (dataset.getKeys().contains(version)) {
@ -119,7 +120,20 @@ public class StatisticsTab extends HorizontalLayout {
plot.setLabelLinkStyle(PieLabelLinkStyle.QUAD_CURVE);
plot.setLabelGenerator(new StandardPieSectionLabelGenerator("{0} ({2})"));
//noinspection unchecked
((List<String>)dataset.getKeys()).forEach(key->plot.setExplodePercent(key, 0.01));
addComponent(new VerticalLayout(new JFreeChartWrapper(chart)));
((List<String>) dataset.getKeys()).forEach(key -> plot.setExplodePercent(key, 0.01));
JFreeChartWrapper wrapper = new JFreeChartWrapper(chart);
replaceComponent(versionChart, wrapper);
versionChart = wrapper;
}
@Override
public void onChange(ReportInfo reportInfo) {
if (reportInfo.getApp().equals(app)) {
getUI().access(() -> {
List<ReportInfo> reportInfos = dataManager.getReportsForApp(app);
setTimeChart(numberField.getValue(), reportInfos);
setVersionChart(reportInfos);
});
}
}
}

View file

@ -67,7 +67,7 @@ public class UserManagerView extends NamedView {
layout.setSizeFull();
Style.NO_PADDING.apply(layout);
setCompositionRoot(layout);
userGrid.setSizeFull();
userGrid.setWidth(100, Unit.PERCENTAGE);
setSizeFull();
Style.apply(this, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM);
}