From 7e87fa4b8b3afc4fe93d8f2ef4a36ac0a8e623d1 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 10:28:06 -0800 Subject: [PATCH] [FEATURE] Charts | Change charts time-unit to bigger timespan #164 (#204) (#205) * [FEATURE] Charts | Change charts time-unit to bigger timespan #164 Signed-off-by: Jovan Cvetkovic * [FEATURE] Charts | Change charts time-unit to bigger timespan #164 Signed-off-by: Jovan Cvetkovic Signed-off-by: Jovan Cvetkovic (cherry picked from commit a915f855a624a12d27f4ba3bae5d4b2f4fdd7288) Co-authored-by: Jovan Cvetkovic Signed-off-by: AWSHurneyt --- .../pages/Alerts/containers/Alerts/Alerts.tsx | 16 +- .../Findings/containers/Findings/Findings.tsx | 18 +- public/pages/Overview/utils/constants.ts | 15 ++ public/pages/Overview/utils/helper.test.ts | 59 +++++++ public/pages/Overview/utils/helpers.ts | 155 +++++++++++++++--- 5 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 public/pages/Overview/utils/helper.test.ts diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index 6e3f6c12f..76127d98e 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -22,7 +22,11 @@ import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components import dateMath from '@elastic/datemath'; import React, { Component } from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; -import { getAlertsVisualizationSpec } from '../../../Overview/utils/helpers'; +import { + getAlertsVisualizationSpec, + getChartTimeUnit, + getDateFormatByTimeUnit, +} from '../../../Overview/utils/helpers'; import moment from 'moment'; import { ALERT_STATE, @@ -70,6 +74,7 @@ export interface AlertsState { filteredAlerts: AlertItem[]; detectors: { [key: string]: Detector }; loading: boolean; + timeUnit: string; } const groupByOptions = [ @@ -93,6 +98,7 @@ export default class Alerts extends Component { filteredAlerts: [], detectors: {}, loading: false, + timeUnit: 'yearmonthdatehoursminutes', }; } @@ -218,7 +224,10 @@ export default class Alerts extends Component { }; }); - return getAlertsVisualizationSpec(visData, this.state.groupBy); + return getAlertsVisualizationSpec(visData, this.state.groupBy, { + timeUnit: this.state.timeUnit, + dateFormat: getDateFormatByTimeUnit(this.state.startTime, this.state.endTime), + }); } createGroupByControl(): React.ReactNode { @@ -297,10 +306,13 @@ export default class Alerts extends Component { if (recentlyUsedRanges.length > MAX_RECENTLY_USED_TIME_RANGES) recentlyUsedRanges = recentlyUsedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES); const endTime = start === end ? DEFAULT_DATE_RANGE.end : end; + + const timeUnit = getChartTimeUnit(start, endTime); this.setState({ startTime: start, endTime: endTime, recentlyUsedRanges: recentlyUsedRanges, + timeUnit, }); }; diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index 2faceb18d..211e50ee6 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -29,11 +29,15 @@ import { MAX_RECENTLY_USED_TIME_RANGES, OS_NOTIFICATION_PLUGIN, } from '../../../../utils/constants'; -import { getFindingsVisualizationSpec } from '../../../Overview/utils/helpers'; +import { + getChartTimeUnit, + getDateFormatByTimeUnit, + getFindingsVisualizationSpec, +} from '../../../Overview/utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; import { Finding } from '../../models/interfaces'; import { Detector } from '../../../../../models/interfaces'; -import { FeatureChannelList } from '../../../../../server/models/interfaces/Notifications'; +import { FeatureChannelList } from '../../../../../server/models/interfaces'; import { getNotificationChannels, parseNotificationChannelsToOptions, @@ -46,6 +50,7 @@ import { } from '../../../../utils/helpers'; import { DetectorHit, RuleSource } from '../../../../../server/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import { ruleSeverity } from '../../../Rules/utils/constants'; interface FindingsProps extends RouteComponentProps { detectorService: DetectorsService; @@ -68,6 +73,7 @@ interface FindingsState { groupBy: FindingsGroupByType; filteredFindings: FindingItemType[]; plugins: string[]; + timeUnit: string; } interface FindingVisualizationData { @@ -103,6 +109,7 @@ export default class Findings extends Component { groupBy: 'logType', filteredFindings: [], plugins: [], + timeUnit: 'yearmonthdatehoursminutes', }; } @@ -234,10 +241,12 @@ export default class Findings extends Component { if (recentlyUsedRanges.length > MAX_RECENTLY_USED_TIME_RANGES) recentlyUsedRanges = recentlyUsedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES); const endTime = start === end ? DEFAULT_DATE_RANGE.end : end; + const timeUnit = getChartTimeUnit(start, endTime); this.setState({ startTime: start, endTime: endTime, recentlyUsedRanges: recentlyUsedRanges, + timeUnit, }); }; @@ -256,7 +265,10 @@ export default class Findings extends Component { }); }); - return getFindingsVisualizationSpec(visData, this.state.groupBy); + return getFindingsVisualizationSpec(visData, this.state.groupBy, { + timeUnit: this.state.timeUnit, + dateFormat: getDateFormatByTimeUnit(this.state.startTime, this.state.endTime), + }); } createGroupByControl(): React.ReactNode { diff --git a/public/pages/Overview/utils/constants.ts b/public/pages/Overview/utils/constants.ts index d8438e667..12b80c0b1 100644 --- a/public/pages/Overview/utils/constants.ts +++ b/public/pages/Overview/utils/constants.ts @@ -9,3 +9,18 @@ export const summaryGroupByOptions = [ ]; export const moreLink = 'https://opensearch.org/docs/latest/security-analytics/'; + +/** + * Time axis' timeUnit map + * for each time search unit there is a mapped chart timeUnit + */ +export const TimeUnitsMap: { + [key: string]: string; +} = { + minutes: 'yearmonthdatehoursminutes', + hours: 'yearmonthdatehoursminutes', + days: 'yearmonthdatehours', + weeks: 'yearmonthdatehours', + months: 'yearmonthdatehours', + years: 'yearmonthdate', +}; diff --git a/public/pages/Overview/utils/helper.test.ts b/public/pages/Overview/utils/helper.test.ts new file mode 100644 index 000000000..5d3dc31f3 --- /dev/null +++ b/public/pages/Overview/utils/helper.test.ts @@ -0,0 +1,59 @@ +import { getChartTimeUnit, getDateFormatByTimeUnit } from './helpers'; +import { TimeUnitsMap } from './constants'; + +describe('helper utilities spec', () => { + describe('tests getDateFormatByTimeUnit function', () => { + const yearFormat = '%Y-%m-%d'; + const dayFormat = '%H:%M:%S'; + const fullFormat = '%Y-%m-%d %H:%M'; + + const timeFormats: { + [key: string]: string; + } = { + 'now-15m': dayFormat, + 'now-15h': fullFormat, + 'now-15d': fullFormat, + 'now-2M': yearFormat, + 'now-2y': fullFormat, + }; + + it(` - function should return default format ${fullFormat} if dates are not valid`, () => { + expect(getDateFormatByTimeUnit('', '')).toBe(fullFormat); + }); + + for (const [start, format] of Object.entries(timeFormats)) { + it(` - function should return ${format} if start date is ${start}`, () => { + expect(getDateFormatByTimeUnit(start, 'now')).toBe(format); + }); + } + }); + + describe('tests getChartTimeUnit function', () => { + const defaultTimeUnit = 'yearmonthdatehoursminutes'; + it(' - function should return default timeUnit if fn params are invalid', () => { + expect(getChartTimeUnit('', '')).toBe(defaultTimeUnit); + }); + + it(' - function should return default timeUnit if one is passed as param', () => { + const defaultFormat = 'yearmonthdate'; + expect(getChartTimeUnit('', '', defaultFormat)).toBe(defaultFormat); + }); + + const timeUnits: { + [key: string]: string; + } = { + minutes: 'now-15m', + hours: 'now-15h', + days: 'now-15d', + weeks: 'now-15w', + months: 'now-5M', + years: 'now-15y', + }; + + for (const [unit, start] of Object.entries(timeUnits)) { + it(` - function should return ${TimeUnitsMap[unit]} if unit is ${unit}`, () => { + expect(getChartTimeUnit(start, 'now')).toBe(TimeUnitsMap[unit]); + }); + } + }); +}); diff --git a/public/pages/Overview/utils/helpers.ts b/public/pages/Overview/utils/helpers.ts index 934f10e47..e26d52d52 100644 --- a/public/pages/Overview/utils/helpers.ts +++ b/public/pages/Overview/utils/helpers.ts @@ -3,9 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import moment from 'moment'; import { euiPaletteColorBlind, euiPaletteForStatus } from '@elastic/eui'; import { TopLevelSpec } from 'vega-lite'; import { SummaryData } from '../components/Widgets/Summary'; +import dateMath from '@elastic/datemath'; +import { TimeUnitsMap } from './constants'; +import _ from 'lodash'; + +export type DateOpts = { + timeUnit: string; + dateFormat: string; +}; function getVisualizationSpec(description: string, data: any, layers: any[]): TopLevelSpec { return { @@ -35,11 +44,25 @@ function getVisualizationSpec(description: string, data: any, layers: any[]): To export function getOverviewVisualizationSpec( visualizationData: SummaryData[], - groupBy: string + groupBy: string, + dynamicTimeUnit: string = 'yearmonthdatehoursminutes' ): TopLevelSpec { - const timeUnit = 'yearmonthdatehoursminutes'; + const timeUnit = dynamicTimeUnit; const aggregate = 'sum'; - const groupByLogType = groupBy === 'logType'; + const findingsEncoding: { [x: string]: any } = { + x: { timeUnit, field: 'time', title: '', axis: { grid: false, ticks: false } }, + y: { + aggregate, + field: 'finding', + type: 'quantitative', + title: 'Count', + axis: { grid: true, ticks: false }, + }, + }; + + if (groupBy === 'log_type') { + findingsEncoding['color'] = { field: 'logType', type: 'nominal', title: 'Log type' }; + } return getVisualizationSpec( 'Plot showing average data with raw values in the background.', @@ -47,24 +70,7 @@ export function getOverviewVisualizationSpec( [ { mark: 'bar', - encoding: { - x: { timeUnit, field: 'time', title: '', axis: { grid: false, ticks: false } }, - y: { - aggregate, - field: 'finding', - type: 'quantitative', - title: 'Count', - axis: { grid: true, ticks: false }, - }, - color: { - field: groupByLogType ? 'logType' : 'finding', - type: 'nominal', - title: groupByLogType ? 'Log type' : 'All findings', - scale: { - range: euiPaletteColorBlind(), - }, - }, - }, + encoding: findingsEncoding, }, { mark: { @@ -80,16 +86,63 @@ export function getOverviewVisualizationSpec( ); } -export function getFindingsVisualizationSpec(visualizationData: any[], groupBy: string) { +/** + * Returns chart x-axis date format based on time span + * @param start + * @param end + */ +export function getDateFormatByTimeUnit(start: string, end: string) { + const startMoment = dateMath.parse(start); + const endMoment = dateMath.parse(end); + let dateFormat = '%Y-%m-%d %H:%M'; + + if (startMoment && endMoment) { + const startData = startMoment.toObject(); + const endData = endMoment.toObject(); + const dateDiff = endMoment.diff(startMoment); + const momentDiff = moment.duration(dateDiff); + const daysDiff = _.get(momentDiff, '_data.days', 0); + + if (startData.years === endData.years) { + if (startData.months !== endData.months) { + dateFormat = '%Y-%m-%d'; + + if (daysDiff < 30 && daysDiff > 1) { + dateFormat = '%Y-%m-%d %H:%M'; + } + } else { + dateFormat = '%Y-%m-%d %H:%M'; + + if (startData.date === endData.date) { + dateFormat = '%H:%M:%S'; + } + } + } + } + + return dateFormat; +} + +export function getFindingsVisualizationSpec( + visualizationData: any[], + groupBy: string, + dateOpts: DateOpts = { + timeUnit: 'yearmonthdatehoursminutes', + dateFormat: '%Y-%m-%d %H:%M', + } +) { return getVisualizationSpec('Findings data overview', visualizationData, [ { mark: 'bar', encoding: { x: { - timeUnit: 'yearmonthdatehoursminutes', + timeUnit: dateOpts.timeUnit, field: 'time', title: '', - axis: { grid: false, ticks: false }, + axis: { + grid: false, + format: dateOpts.dateFormat, + }, }, y: { aggregate: 'sum', @@ -111,16 +164,27 @@ export function getFindingsVisualizationSpec(visualizationData: any[], groupBy: ]); } -export function getAlertsVisualizationSpec(visualizationData: any[], groupBy: string) { +export function getAlertsVisualizationSpec( + visualizationData: any[], + groupBy: string, + dateOpts: DateOpts = { + timeUnit: 'yearmonthdatehoursminutes', + dateFormat: '%Y-%m-%d %H:%M', + } +) { return getVisualizationSpec('Alerts data overview', visualizationData, [ { mark: 'bar', encoding: { x: { - timeUnit: 'yearmonthdatehoursminutes', + timeUnit: dateOpts.timeUnit, field: 'time', title: '', - axis: { grid: false, ticks: false }, + axis: { + grid: false, + ticks: false, + format: dateOpts.dateFormat, + }, }, y: { aggregate: 'sum', @@ -168,3 +232,40 @@ export function getTimeWithMinPrecision(time: number | string) { return date.getTime(); } + +/** + * Returns timeUnit based on how big time diff is between start and end dates + * @param start Chart start time + * @param end Chart end time + * @param [defaultUnit = 'yearmonthdatehoursminutes'] Default timeUnit + */ +export function getChartTimeUnit( + start: string, + end: string, + defaultUnit: string = 'yearmonthdatehoursminutes' +): string { + const startMoment = dateMath.parse(start); + const endMoment = dateMath.parse(end); + let minUnit: string = 'minutes'; + let timeUnit: string = TimeUnitsMap[minUnit]; + + if (!startMoment || !endMoment) return defaultUnit; + + try { + const timeDiff = endMoment.diff(startMoment); + const momentTimeDiff = moment.duration(timeDiff); + + const timeUnits: string[] = ['years', 'months', 'days', 'hours', 'minutes']; + for (const unit of timeUnits) { + // @ts-ignore + if (momentTimeDiff._data[unit]) { + timeUnit = TimeUnitsMap[unit]; + break; + } + } + } catch (e) { + console.error(`Time diff can't be calculated for dates: ${start} and ${end}`); + } + + return timeUnit; +}