From 190d20c749b32bc73a2631673796de95b470fee7 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 19 Dec 2022 14:12:19 -0800 Subject: [PATCH] [FEATURE] Implement date/time picker on the overview page (#232) (#240) * [FEATURE] Implement date/time picker on the overview page #133 Signed-off-by: Jovan Cvetkovic * [FEATURE] Implement date/time picker on the overview page #133 Signed-off-by: Jovan Cvetkovic Signed-off-by: Jovan Cvetkovic (cherry picked from commit 3dc90dd2db7dc6622381c2aa07da9634bd945ccf) Co-authored-by: Jovan Cvetkovic --- .../Overview/components/Widgets/Summary.tsx | 20 +++++-- .../Overview/containers/Overview/Overview.tsx | 52 ++++++++++++++++--- .../Overview/models/OverviewViewModel.ts | 27 ++++++++-- public/pages/Overview/utils/helpers.ts | 27 ++++++++-- 4 files changed, 107 insertions(+), 19 deletions(-) diff --git a/public/pages/Overview/components/Widgets/Summary.tsx b/public/pages/Overview/components/Widgets/Summary.tsx index 0ecfc9d0e..6776db96c 100644 --- a/public/pages/Overview/components/Widgets/Summary.tsx +++ b/public/pages/Overview/components/Widgets/Summary.tsx @@ -7,7 +7,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiStat } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { WidgetContainer } from './WidgetContainer'; import { summaryGroupByOptions } from '../../utils/constants'; -import { getOverviewVisualizationSpec, getTimeWithMinPrecision } from '../../utils/helpers'; +import { + getDateFormatByTimeUnit, + getOverviewVisualizationSpec, + getTimeWithMinPrecision, +} from '../../utils/helpers'; import { AlertItem, FindingItem } from '../../models/interfaces'; import { createSelectComponent, renderVisualization } from '../../../../utils/helpers'; import { ROUTES } from '../../../../utils/constants'; @@ -26,7 +30,14 @@ export interface SummaryData { logType?: string; } -export const Summary: React.FC = ({ alerts, findings, loading = false }) => { +export const Summary: React.FC = ({ + alerts, + findings, + startTime, + endTime, + timeUnit, + loading = false, +}) => { const [groupBy, setGroupBy] = useState(''); const [summaryData, setSummaryData] = useState([]); const [activeAlerts, setActiveAlerts] = useState(0); @@ -51,7 +62,10 @@ export const Summary: React.FC = ({ alerts, findings, loading = fa ); const generateVisualizationSpec = useCallback((summaryData, groupBy) => { - return getOverviewVisualizationSpec(summaryData, groupBy); + return getOverviewVisualizationSpec(summaryData, groupBy, { + timeUnit: timeUnit, + dateFormat: getDateFormatByTimeUnit(startTime, endTime), + }); }, []); useEffect(() => { diff --git a/public/pages/Overview/containers/Overview/Overview.tsx b/public/pages/Overview/containers/Overview/Overview.tsx index d84d93233..9106f7033 100644 --- a/public/pages/Overview/containers/Overview/Overview.tsx +++ b/public/pages/Overview/containers/Overview/Overview.tsx @@ -4,16 +4,20 @@ */ import { - EuiButton, EuiButtonEmpty, EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiPopover, + EuiSuperDatePicker, EuiTitle, } from '@elastic/eui'; import React, { useContext, useEffect, useMemo, useState } from 'react'; -import { BREADCRUMBS } from '../../../../utils/constants'; +import { + BREADCRUMBS, + DEFAULT_DATE_RANGE, + MAX_RECENTLY_USED_TIME_RANGES, +} from '../../../../utils/constants'; import { OverviewProps, OverviewState } from '../../models/interfaces'; import { CoreServicesContext } from '../../../../../public/components/core_services'; import { RecentAlertsWidget } from '../../components/Widgets/RecentAlertsWidget'; @@ -24,6 +28,7 @@ import { ServicesContext } from '../../../../services'; import { Summary } from '../../components/Widgets/Summary'; import { TopRulesWidget } from '../../components/Widgets/TopRulesWidget'; import { GettingStartedPopup } from '../../components/GettingStarted/GettingStartedPopup'; +import { getChartTimeUnit } from '../../utils/helpers'; export const Overview: React.FC = (props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -36,7 +41,12 @@ export const Overview: React.FC = (props) => { alerts: [], }, }); + const [startTime, setStartTime] = useState(DEFAULT_DATE_RANGE.start); + const [endTime, setEndTime] = useState(DEFAULT_DATE_RANGE.end); + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([DEFAULT_DATE_RANGE]); const [loading, setLoading] = useState(true); + const [timeUnit, setTimeUnit] = useState('yearmonthdatehoursminutes'); + const context = useContext(CoreServicesContext); const services = useContext(ServicesContext); @@ -58,7 +68,7 @@ export const Overview: React.FC = (props) => { overviewViewModelActor.registerRefreshHandler(updateState); const updateModel = async () => { - await overviewViewModelActor.onRefresh(); + await overviewViewModelActor.onRefresh(startTime, endTime); setInitialLoadingFinished(true); }; @@ -75,16 +85,33 @@ export const Overview: React.FC = (props) => { } }, [initialLoadingFinished, state.overviewViewModel, props.getStartedDismissedOnce]); - const onTimeChange = ({ start, end }: { start: string; end: string }) => { - // TODO: NYI + const onTimeChange = async ({ start, end }: { start: string; end: string }) => { + let usedRanges = recentlyUsedRanges.filter( + (range) => !(range.start === start && range.end === end) + ); + usedRanges.unshift({ start: start, end: end }); + if (usedRanges.length > MAX_RECENTLY_USED_TIME_RANGES) + usedRanges = usedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES); + + const endTime = start === end ? DEFAULT_DATE_RANGE.end : end; + const timeUnit = getChartTimeUnit(start, endTime); + setStartTime(start); + setEndTime(endTime); + setTimeUnit(timeUnit); + setRecentlyUsedRanges(usedRanges); }; + useEffect(() => { + overviewViewModelActor.onRefresh(startTime, endTime); + }, [startTime, endTime]); + const onRefresh = async () => { setLoading(true); - overviewViewModelActor.onRefresh(); + await overviewViewModelActor.onRefresh(startTime, endTime); }; const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + const closePopover = () => { setIsPopoverOpen(false); props.onGetStartedDismissed(); @@ -116,7 +143,15 @@ export const Overview: React.FC = (props) => { - Refresh + @@ -124,6 +159,9 @@ export const Overview: React.FC = (props) => { diff --git a/public/pages/Overview/models/OverviewViewModel.ts b/public/pages/Overview/models/OverviewViewModel.ts index e138115a8..8c6c1953f 100644 --- a/public/pages/Overview/models/OverviewViewModel.ts +++ b/public/pages/Overview/models/OverviewViewModel.ts @@ -10,6 +10,8 @@ import { RuleService } from '../../../services'; import { DEFAULT_EMPTY_DATA } from '../../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../../../utils/helpers'; +import dateMath from '@elastic/datemath'; +import moment from 'moment'; export interface OverviewViewModel { detectors: DetectorHit[]; @@ -114,11 +116,14 @@ export class OverviewViewModelActor { const ids = finding.queries.map((query) => query.id); ids.forEach((id) => ruleIds.add(id)); + const findingTime = new Date(finding.timestamp); + findingTime.setMilliseconds(0); + findingTime.setSeconds(0); return { detector: detectorName, findingName: finding.id, id: finding.id, - time: finding.timestamp, + time: findingTime, logType: logType || '', ruleId: finding.queries[0].id, ruleName: '', @@ -141,7 +146,7 @@ export class OverviewViewModelActor { ruleSeverity: rulesRes[item.ruleId]?.level || DEFAULT_EMPTY_DATA, })); - this.overviewViewModel.findings = findingItems; + this.overviewViewModel.findings = this.filterChartDataByTime(findingItems); } private async updateAlerts() { @@ -175,7 +180,7 @@ export class OverviewViewModelActor { errorNotificationToast(this.notifications, 'retrieve', 'alerts', e); } - this.overviewViewModel.alerts = alertItems; + this.overviewViewModel.alerts = this.filterChartDataByTime(alertItems); } public getOverviewViewModel() { @@ -186,7 +191,13 @@ export class OverviewViewModelActor { this.refreshHandlers.push(handler); } - public async onRefresh() { + startTime = 'now-15m'; + endTime = 'now'; + + public async onRefresh(startTime: string, endTime: string) { + this.startTime = startTime; + this.endTime = endTime; + if (this.refreshState === 'InProgress') { return; } @@ -202,4 +213,12 @@ export class OverviewViewModelActor { this.refreshState = 'Complete'; } + + private filterChartDataByTime = (chartData) => { + const startMoment = dateMath.parse(this.startTime); + const endMoment = dateMath.parse(this.endTime); + return chartData.filter((dataItem) => { + return moment(dataItem.time).isBetween(moment(startMoment), moment(endMoment)); + }); + }; } diff --git a/public/pages/Overview/utils/helpers.ts b/public/pages/Overview/utils/helpers.ts index 9b2b1c0e9..9427ba02a 100644 --- a/public/pages/Overview/utils/helpers.ts +++ b/public/pages/Overview/utils/helpers.ts @@ -45,12 +45,19 @@ function getVisualizationSpec(description: string, data: any, layers: any[]): To export function getOverviewVisualizationSpec( visualizationData: SummaryData[], groupBy: string, - dynamicTimeUnit: string = 'yearmonthdatehoursminutes' + dateOpts: DateOpts = { + timeUnit: 'yearmonthdatehoursminutes', + dateFormat: '%Y-%m-%d %H:%M', + } ): TopLevelSpec { - const timeUnit = dynamicTimeUnit; const aggregate = 'sum'; const findingsEncoding: { [x: string]: any } = { - x: { timeUnit, field: 'time', title: '', axis: { grid: false, ticks: false } }, + x: { + timeUnit: dateOpts.timeUnit, + field: 'time', + title: '', + axis: { grid: false, ticks: false, format: dateOpts.dateFormat }, + }, y: { aggregate, field: 'finding', @@ -91,8 +98,18 @@ export function getOverviewVisualizationSpec( }, }, encoding: { - x: { timeUnit, field: 'time', title: '', axis: { grid: false, ticks: false } }, - y: { aggregate, field: 'alert', title: 'Count', axis: { grid: true, ticks: false } }, + x: { + timeUnit: dateOpts.timeUnit, + field: 'time', + title: '', + axis: { grid: false, ticks: false, format: dateOpts.dateFormat }, + }, + y: { + aggregate: 'sum', + field: 'alert', + title: 'Count', + axis: { grid: true, ticks: false }, + }, tooltip: [{ field: 'alert', aggregate: 'sum', title: 'Alerts' }], }, },