From 011bf82cad28facd8abf8ef507d816acb454e745 Mon Sep 17 00:00:00 2001 From: Greg DiCristofaro Date: Tue, 10 Nov 2020 07:59:46 -0500 Subject: [PATCH] added timeline panel --- .../datamodel/TimelineSummary.java | 7 +- .../datasourcesummary/ui/Bundle.properties | 3 +- .../ui/DataSourceSummaryTabbedPane.java | 4 +- .../datasourcesummary/ui/PastCasesPanel.java | 29 +------ .../datasourcesummary/ui/TimelinePanel.form | 73 ++++++++-------- .../datasourcesummary/ui/TimelinePanel.java | 86 ++++++++++++++----- .../uiutils/DataFetchResult.java | 30 ++++++- 7 files changed, 134 insertions(+), 98 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java index a5981de6ac..af056387dd 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; -import com.google.common.collect.Sets; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -195,10 +194,10 @@ public class TimelineSummary implements DefaultUpdateGovernor { private final Date maxDate; private final List histogramActivity; - TimelineSummaryData(Date minDate, Date maxDate, List histogramActivity) { + TimelineSummaryData(Date minDate, Date maxDate, List recentDaysActivity) { this.minDate = minDate; this.maxDate = maxDate; - this.histogramActivity = (histogramActivity == null) ? Collections.emptyList() : Collections.unmodifiableList(histogramActivity); + this.histogramActivity = (recentDaysActivity == null) ? Collections.emptyList() : Collections.unmodifiableList(recentDaysActivity); } public Date getMinDate() { @@ -209,7 +208,7 @@ public class TimelineSummary implements DefaultUpdateGovernor { return maxDate; } - public List getHistogramActivity() { + public List getMostRecentDaysActivity() { return histogramActivity; } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties index 33292cab98..5d9e6adf0c 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties @@ -42,5 +42,4 @@ RecentFilesPanel.attachmentLabel.text=Recent Attachments PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs DataSourceSummaryTabbedPane.noDataSourceLabel.text=No data source has been selected. -TimelinePanel.sameIdLabel.text=Past Cases with the Same Device IDs -TimelinePanel.activityRangeLabel.text=Activity Range: +TimelinePanel.activityRangeLabel.text=Activity Range diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 55320f7dbb..f758f9716f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -38,7 +38,8 @@ import org.sleuthkit.datamodel.DataSource; "DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History", "DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files", "DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases", - "DataSourceSummaryTabbedPane_analysisTab_title=Analysis" + "DataSourceSummaryTabbedPane_analysisTab_title=Analysis", + "DataSourceSummaryTabbedPane_timelineTab_title=Timeline" }) public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { @@ -123,6 +124,7 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_analysisTab_title(), new AnalysisPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_recentFileTab_title(), new RecentFilesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_pastCasesTab_title(), new PastCasesPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_timelineTab_title(), new TimelinePanel()), // do nothing on closing new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel, ingestHistoryPanel::setDataSource, () -> { }), diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java index 76d36f4785..d99f4a1778 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.util.Arrays; import java.util.List; -import java.util.function.Function; import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; @@ -28,7 +27,6 @@ import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary.PastCasesResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; @@ -103,32 +101,11 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { * @param result The result. */ private void handleResult(DataFetchResult result) { - showResultWithModuleCheck(notableFileTable, getSubResult(result, (res) -> res.getTaggedNotable()), CR_FACTORY, CR_NAME); - showResultWithModuleCheck(sameIdTable, getSubResult(result, (res) -> res.getSameIdsResults()), CR_FACTORY, CR_NAME); + showResultWithModuleCheck(notableFileTable, DataFetchResult.getSubResult(result, (res) -> res.getTaggedNotable()), CR_FACTORY, CR_NAME); + showResultWithModuleCheck(sameIdTable, DataFetchResult.getSubResult(result, (res) -> res.getSameIdsResults()), CR_FACTORY, CR_NAME); } - /** - * Given an input data fetch result, creates an error result if the original - * is an error. Otherwise, uses the getSubResult function on the underlying - * data to create a new DataFetchResult. - * - * @param inputResult The input result. - * @param getSubComponent The means of getting the data given the original - * data. - * - * @return The new result with the error of the original or the processed - * data. - */ - private DataFetchResult getSubResult(DataFetchResult inputResult, Function getSubResult) { - if (inputResult == null) { - return null; - } else if (inputResult.getResultType() == ResultType.SUCCESS) { - O innerData = (inputResult.getData() == null) ? null : getSubResult.apply(inputResult.getData()); - return DataFetchResult.getSuccessResult(innerData); - } else { - return DataFetchResult.getErrorResult(inputResult.getException()); - } - } + @Override protected void fetchInformation(DataSource dataSource) { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form index 6b68566967..e824d632f6 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form @@ -74,6 +74,9 @@ + + + @@ -107,21 +110,42 @@ - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + @@ -147,51 +171,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + - + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java index b88954c317..906de8eef1 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java @@ -18,12 +18,22 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; +import java.awt.Color; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; +import java.util.Locale; +import org.apache.commons.collections.CollectionUtils; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.DailyActivityAmount; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.TimelineSummaryData; import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartItem; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartSeries; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; @@ -44,6 +54,9 @@ import org.sleuthkit.datamodel.DataSource; public class TimelinePanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; + private static final DateFormat EARLIEST_LATEST_FORMAT = new SimpleDateFormat("MMM d, yyyy", Locale.getDefault()); + private static final DateFormat CHART_FORMAT = new SimpleDateFormat("MMM d", Locale.getDefault()); + private static final Color CHART_COLOR = Color.BLUE; private final LoadableLabel earliestLabel = new LoadableLabel(Bundle.TimelinePanel_earliestLabel_title()); private final LoadableLabel latestLabel = new LoadableLabel(Bundle.TimelinePanel_latestLabel_title()); @@ -70,6 +83,34 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { initComponents(); } + + + private static String parseEarliestLatest(Date date) { + return date == null ? null : EARLIEST_LATEST_FORMAT.format(date); + } + + private BarChartSeries parseChartData(List recentDaysActivity) { + if (CollectionUtils.isEmpty(recentDaysActivity)) { + return null; + } + + List items = new ArrayList<>(); + for (int i = 0; i < recentDaysActivity.size(); i++) { + DailyActivityAmount curItem = recentDaysActivity.get(i); + long amount = curItem.getArtifactActivityCount() * 1000 + curItem.getFileActivityCount(); + + if (i == 0 || i == recentDaysActivity.size() - 1) { + String formattedDate = curItem.getDay() == null ? "" : CHART_FORMAT.format(curItem.getDay()); + items.add(new BarChartItem(formattedDate, amount)); + } else { + items.add(new BarChartItem("", amount)); + } + } + + return new BarChartSeries(CHART_COLOR, items); + } + + /** * Handles displaying the result for each table by breaking apart subdata @@ -78,11 +119,9 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { * @param result The result. */ private void handleResult(DataFetchResult result) { - if (result == null) { - // TODO - } else { - // TODO - } + earliestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseEarliestLatest(r.getMinDate()))); + latestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseEarliestLatest(r.getMaxDate()))); + last30DaysChart.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseChartData(r.getMostRecentDaysActivity()))); } @Override @@ -115,11 +154,10 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; javax.swing.JLabel activityRangeLabel = new javax.swing.JLabel(); javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); - javax.swing.JPanel notableFilePanel = notableFileTable; + javax.swing.JPanel earliestLabelPanel = earliestLabel; + javax.swing.JPanel latestLabelPanel = latestLabel; javax.swing.Box.Filler filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20)); - javax.swing.JLabel sameIdLabel = new javax.swing.JLabel(); - javax.swing.Box.Filler filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); - javax.swing.JPanel sameIdPanel = sameIdTable; + javax.swing.JPanel sameIdPanel = last30DaysChart; javax.swing.Box.Filler filler5 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); mainContentPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); @@ -131,6 +169,7 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); mainContentPanel.add(ingestRunningPanel); + activityRangeLabel.setFont(new java.awt.Font("Segoe UI", 1, 12)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(activityRangeLabel, org.openide.util.NbBundle.getMessage(TimelinePanel.class, "TimelinePanel.activityRangeLabel.text")); // NOI18N mainContentPanel.add(activityRangeLabel); activityRangeLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(TimelinePanel.class, "PastCasesPanel.notableFileLabel.text")); // NOI18N @@ -138,25 +177,26 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { filler1.setAlignmentX(0.0F); mainContentPanel.add(filler1); - notableFilePanel.setAlignmentX(0.0F); - notableFilePanel.setMaximumSize(new java.awt.Dimension(32767, 106)); - notableFilePanel.setMinimumSize(new java.awt.Dimension(100, 106)); - notableFilePanel.setPreferredSize(new java.awt.Dimension(100, 106)); - mainContentPanel.add(notableFilePanel); + earliestLabelPanel.setAlignmentX(0.0F); + earliestLabelPanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + earliestLabelPanel.setMinimumSize(new java.awt.Dimension(100, 20)); + earliestLabelPanel.setPreferredSize(new java.awt.Dimension(32767, 20)); + mainContentPanel.add(earliestLabelPanel); + + latestLabelPanel.setAlignmentX(0.0F); + latestLabelPanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + latestLabelPanel.setMinimumSize(new java.awt.Dimension(100, 20)); + latestLabelPanel.setPreferredSize(new java.awt.Dimension(32767, 20)); + mainContentPanel.add(latestLabelPanel); filler2.setAlignmentX(0.0F); mainContentPanel.add(filler2); - org.openide.awt.Mnemonics.setLocalizedText(sameIdLabel, org.openide.util.NbBundle.getMessage(TimelinePanel.class, "TimelinePanel.sameIdLabel.text")); // NOI18N - mainContentPanel.add(sameIdLabel); - - filler3.setAlignmentX(0.0F); - mainContentPanel.add(filler3); - sameIdPanel.setAlignmentX(0.0F); - sameIdPanel.setMaximumSize(new java.awt.Dimension(32767, 106)); - sameIdPanel.setMinimumSize(new java.awt.Dimension(100, 106)); - sameIdPanel.setPreferredSize(new java.awt.Dimension(100, 106)); + sameIdPanel.setMaximumSize(new java.awt.Dimension(600, 300)); + sameIdPanel.setMinimumSize(new java.awt.Dimension(600, 300)); + sameIdPanel.setPreferredSize(new java.awt.Dimension(600, 300)); + sameIdPanel.setVerifyInputWhenFocusTarget(false); mainContentPanel.add(sameIdPanel); filler5.setAlignmentX(0.0F); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java index 93cc24f5fe..6e2d8f4991 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.datasourcesummary.uiutils; +import java.util.function.Function; + /** * The result of a loading process. */ @@ -30,6 +32,29 @@ public final class DataFetchResult { SUCCESS, ERROR } + /** + * A utility method that, given an input data fetch result, creates an error + * result if the original is an error. Otherwise, uses the getSubResult + * function on the underlying data to create a new DataFetchResult. + * + * @param inputResult The input result. + * @param getSubComponent The means of getting the data given the original + * data. + * + * @return The new result with the error of the original or the processed + * data. + */ + public static DataFetchResult getSubResult(DataFetchResult inputResult, Function getSubResult) { + if (inputResult == null) { + return null; + } else if (inputResult.getResultType() == ResultType.SUCCESS) { + O innerData = (inputResult.getData() == null) ? null : getSubResult.apply(inputResult.getData()); + return DataFetchResult.getSuccessResult(innerData); + } else { + return DataFetchResult.getErrorResult(inputResult.getException()); + } + } + /** * Creates a DataFetchResult of loaded data including the data. * @@ -59,9 +84,8 @@ public final class DataFetchResult { /** * Main constructor for the DataLoadingResult. * - * @param state The state of the result. - * @param data If the result is SUCCESS, the data related to this - * result. + * @param state The state of the result. + * @param data If the result is SUCCESS, the data related to this result. * @param exception If the result is ERROR, the related exception. */ private DataFetchResult(ResultType state, R data, Throwable exception) {