push new reports, layout improvements
This commit is contained in:
parent
152e82d5f9
commit
c02cfe19bf
20 changed files with 326 additions and 124 deletions
|
@ -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
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
apply plugin: 'war'
|
||||
|
||||
war {
|
||||
archiveName = "acra.war"
|
||||
version = version
|
||||
}
|
||||
|
||||
dependencies{
|
||||
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
|
||||
}
|
|
@ -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,39 +123,66 @@ 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);
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -162,16 +202,36 @@ public class DataManager {
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.faendir.acra.mongod.model;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 16.06.2017
|
||||
*/
|
||||
public interface AppScoped {
|
||||
String getApp();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
@ -120,6 +121,19 @@ public class StatisticsTab extends HorizontalLayout {
|
|||
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)));
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue