From f3996690d71743e27811f504e9263297bb1fcb40 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Fri, 30 Dec 2022 00:21:44 +0530 Subject: [PATCH] Feature/charts should show the entire time range selected in the filter #258 (#265) (#274) * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 Signed-off-by: Jovan Cvetkovic * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 [FEATURE] Add chart data to the tooltips #263 Signed-off-by: Jovan Cvetkovic * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 [FEATURE] Add chart data to the tooltips #263 Signed-off-by: Jovan Cvetkovic * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 [FEATURE] Add chart data to the tooltips #263 Signed-off-by: Jovan Cvetkovic * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 [FEATURE] Add chart data to the tooltips #263 Signed-off-by: Jovan Cvetkovic * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 [FEATURE] Add chart data to the tooltips #263 Signed-off-by: Jovan Cvetkovic * [FEATURE] Set default time range to 24 hrs and share the same setting for all UI pages #253 [FEATURE] Charts should show the entire time range selected in the filter #258 [FEATURE] Add chart data to the tooltips #263 Signed-off-by: Jovan Cvetkovic * PR 265 Code review Signed-off-by: Jovan Cvetkovic * PR 265 Code review Signed-off-by: Jovan Cvetkovic Signed-off-by: Jovan Cvetkovic (cherry picked from commit 2ccadad82baca5c94bbbb1aacb5f5793cffb886b) Co-authored-by: Jovan Cvetkovic Signed-off-by: AWSHurneyt --- .../pages/Alerts/containers/Alerts/Alerts.tsx | 92 +++-- .../Findings/containers/Findings/Findings.tsx | 82 +++-- public/pages/Main/Main.tsx | 21 +- .../Overview/components/Widgets/Summary.tsx | 24 +- .../Overview/containers/Overview/Overview.tsx | 46 ++- .../Overview/models/OverviewViewModel.ts | 6 +- public/pages/Overview/models/interfaces.ts | 7 + public/pages/Overview/utils/constants.ts | 15 - public/pages/Overview/utils/helper.test.ts | 86 +++-- public/pages/Overview/utils/helpers.ts | 337 +++++++++++------- public/utils/constants.ts | 2 +- 11 files changed, 446 insertions(+), 272 deletions(-) diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index e362039e4..12e70b745 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -25,7 +25,8 @@ import { ContentPanel } from '../../../../components/ContentPanel'; import { getAlertsVisualizationSpec, getChartTimeUnit, - getDateFormatByTimeUnit, + getDomainRange, + TimeUnit, } from '../../../Overview/utils/helpers'; import moment from 'moment'; import { @@ -54,6 +55,8 @@ import { } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { match, withRouter } from 'react-router-dom'; +import { DateTimeFilter } from '../../../Overview/models/interfaces'; +import { ChartContainer } from '../../../../components/Charts/ChartContainer'; export interface AlertsProps { alertService: AlertsService; @@ -63,12 +66,12 @@ export interface AlertsProps { opensearchService: OpenSearchService; notifications: NotificationsStart; match: match; + dateTimeFilter?: DateTimeFilter; + setDateTimeFilter?: Function; } export interface AlertsState { groupBy: string; - startTime: string; - endTime: string; recentlyUsedRanges: DurationRange[]; selectedItems: AlertItem[]; alerts: AlertItem[]; @@ -77,7 +80,8 @@ export interface AlertsState { filteredAlerts: AlertItem[]; detectors: { [key: string]: Detector }; loading: boolean; - timeUnit: string; + timeUnit: TimeUnit; + dateFormat: string; } const groupByOptions = [ @@ -90,25 +94,38 @@ class Alerts extends Component { constructor(props: AlertsProps) { super(props); + + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = props; + const timeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); this.state = { + loading: true, groupBy: 'status', - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, recentlyUsedRanges: [DEFAULT_DATE_RANGE], selectedItems: [], alerts: [], alertsFiltered: false, filteredAlerts: [], detectors: {}, - loading: false, - timeUnit: 'yearmonthdatehoursminutes', + timeUnit: timeUnits.timeUnit, + dateFormat: timeUnits.dateFormat, }; } componentDidUpdate(prevProps: Readonly, prevState: Readonly) { + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; const alertsChanged = - prevState.startTime !== this.state.startTime || - prevState.endTime !== this.state.endTime || + prevProps.dateTimeFilter?.startTime !== dateTimeFilter.startTime || + prevProps.dateTimeFilter?.endTime !== dateTimeFilter.endTime || prevState.alerts !== this.state.alerts || prevState.alerts.length !== this.state.alerts.length; @@ -120,9 +137,15 @@ class Alerts extends Component { } filterAlerts = () => { - const { alerts, startTime, endTime } = this.state; - const startMoment = dateMath.parse(startTime); - const endMoment = dateMath.parse(endTime); + const { alerts } = this.state; + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; + const startMoment = dateMath.parse(dateTimeFilter.startTime); + const endMoment = dateMath.parse(dateTimeFilter.endTime); const filteredAlerts = alerts.filter((alert) => moment(alert.last_notification_time).isBetween(moment(startMoment), moment(endMoment)) ); @@ -226,10 +249,20 @@ class Alerts extends Component { severity: parseAlertSeverityToOption(alert.severity)?.label || alert.severity, }; }); - + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; + const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); return getAlertsVisualizationSpec(visData, this.state.groupBy, { - timeUnit: this.state.timeUnit, - dateFormat: getDateFormatByTimeUnit(this.state.startTime, this.state.endTime), + timeUnit: chartTimeUnits.timeUnit, + dateFormat: chartTimeUnits.dateFormat, + domain: getDomainRange( + [dateTimeFilter.startTime, dateTimeFilter.endTime], + chartTimeUnits.timeUnit.unit + ), }); } @@ -313,13 +346,17 @@ class Alerts extends Component { recentlyUsedRanges = recentlyUsedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES); const endTime = start === end ? DEFAULT_DATE_RANGE.end : end; - const timeUnit = getChartTimeUnit(start, endTime); + const timeUnits = getChartTimeUnit(start, endTime); this.setState({ - startTime: start, - endTime: endTime, recentlyUsedRanges: recentlyUsedRanges, - timeUnit, + ...timeUnits, }); + + this.props.setDateTimeFilter && + this.props.setDateTimeFilter({ + startTime: start, + endTime: endTime, + }); }; onRefresh = async () => { @@ -375,11 +412,15 @@ class Alerts extends Component { filteredAlerts, flyoutData, loading, - startTime, - endTime, recentlyUsedRanges, } = this.state; + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; const severities = new Set(); const statuses = new Set(); filteredAlerts.forEach((alert) => { @@ -455,8 +496,8 @@ class Alerts extends Component { { {this.createGroupByControl()} -
+
@@ -491,6 +532,7 @@ class Alerts extends Component { search={search} sorting={sorting} selection={selection} + loading={loading} /> diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index 579f3b4c4..93f75103c 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -28,12 +28,12 @@ import { DEFAULT_DATE_RANGE, MAX_RECENTLY_USED_TIME_RANGES, OS_NOTIFICATION_PLUGIN, - ROUTES, } from '../../../../utils/constants'; import { getChartTimeUnit, - getDateFormatByTimeUnit, + getDomainRange, getFindingsVisualizationSpec, + TimeUnit, } from '../../../Overview/utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; import { Finding } from '../../models/interfaces'; @@ -51,7 +51,8 @@ import { } from '../../../../utils/helpers'; import { DetectorHit, RuleSource } from '../../../../../server/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { ruleSeverity } from '../../../Rules/utils/constants'; +import { DateTimeFilter } from '../../../Overview/models/interfaces'; +import { ChartContainer } from '../../../../components/Charts/ChartContainer'; interface FindingsProps extends RouteComponentProps { detectorService: DetectorsService; @@ -61,6 +62,8 @@ interface FindingsProps extends RouteComponentProps { ruleService: RuleService; notifications: NotificationsStart; match: match; + dateTimeFilter?: DateTimeFilter; + setDateTimeFilter?: Function; } interface FindingsState { @@ -69,13 +72,12 @@ interface FindingsState { findings: FindingItemType[]; notificationChannels: FeatureChannelList[]; rules: { [id: string]: RuleSource }; - startTime: string; - endTime: string; recentlyUsedRanges: DurationRange[]; groupBy: FindingsGroupByType; filteredFindings: FindingItemType[]; plugins: string[]; - timeUnit: string; + timeUnit: TimeUnit; + dateFormat: string; } interface FindingVisualizationData { @@ -99,19 +101,26 @@ class Findings extends Component { constructor(props: FindingsProps) { super(props); + + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = props; + const timeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); this.state = { - loading: false, + loading: true, detectors: [], findings: [], notificationChannels: [], rules: {}, - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, recentlyUsedRanges: [DEFAULT_DATE_RANGE], groupBy: 'logType', filteredFindings: [], plugins: [], - timeUnit: 'yearmonthdatehoursminutes', + timeUnit: timeUnits.timeUnit, + dateFormat: timeUnits.dateFormat, }; } @@ -246,13 +255,17 @@ 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); + const timeUnits = getChartTimeUnit(start, endTime); this.setState({ - startTime: start, - endTime: endTime, recentlyUsedRanges: recentlyUsedRanges, - timeUnit, + ...timeUnits, }); + + this.props.setDateTimeFilter && + this.props.setDateTimeFilter({ + startTime: start, + endTime: endTime, + }); }; generateVisualizationSpec() { @@ -269,10 +282,20 @@ class Findings extends Component { ruleSeverity: this.state.rules[finding.queries[0].id].level, }); }); - + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; + const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); return getFindingsVisualizationSpec(visData, this.state.groupBy, { - timeUnit: this.state.timeUnit, - dateFormat: getDateFormatByTimeUnit(this.state.startTime, this.state.endTime), + timeUnit: chartTimeUnits.timeUnit, + dateFormat: chartTimeUnits.dateFormat, + domain: getDomainRange( + [dateTimeFilter.startTime, dateTimeFilter.endTime], + chartTimeUnits.timeUnit.unit + ), }); } @@ -293,16 +316,15 @@ class Findings extends Component { }; render() { - const { - loading, - notificationChannels, - rules, - startTime, - endTime, - recentlyUsedRanges, - } = this.state; + const { loading, notificationChannels, rules, recentlyUsedRanges } = this.state; let { findings } = this.state; + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; if (Object.keys(rules).length > 0) { findings = findings.map((finding) => { const rule = rules[finding.queries[0].id]; @@ -325,8 +347,8 @@ class Findings extends Component { { {this.createGroupByControl()} -
+
@@ -359,8 +381,8 @@ class Findings extends Component { findings={findings} loading={loading} rules={rules} - startTime={startTime} - endTime={endTime} + startTime={dateTimeFilter.startTime} + endTime={dateTimeFilter.endTime} onRefresh={this.onRefresh} notificationChannels={parseNotificationChannelsToOptions(notificationChannels)} refreshNotificationChannels={this.getNotificationChannels} diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 01fb90318..e00bb05d1 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -22,7 +22,7 @@ import { import { CoreStart } from 'opensearch-dashboards/public'; import { ServicesConsumer } from '../../services'; import { BrowserServices } from '../../models/interfaces'; -import { ROUTES } from '../../utils/constants'; +import { DEFAULT_DATE_RANGE, ROUTES } from '../../utils/constants'; import { CoreServicesConsumer } from '../../components/core_services'; import Findings from '../Findings'; import Detectors from '../Detectors'; @@ -39,6 +39,7 @@ import { CreateRule } from '../Rules/containers/CreateRule/CreateRule'; import { EditRule } from '../Rules/containers/EditRule/EditRule'; import { ImportRule } from '../Rules/containers/ImportRule/ImportRule'; import { DuplicateRule } from '../Rules/containers/DuplicateRule/DuplicateRule'; +import { DateTimeFilter } from '../Overview/models/interfaces'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -67,6 +68,7 @@ interface MainProps extends RouteComponentProps { interface MainState { getStartedDismissedOnce: boolean; selectedNavItemIndex: number; + dateTimeFilter: DateTimeFilter; } const navItemIndexByRoute: { [route: string]: number } = { @@ -83,6 +85,10 @@ export default class Main extends Component { this.state = { getStartedDismissedOnce: false, selectedNavItemIndex: 1, + dateTimeFilter: { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, }; } @@ -102,6 +108,12 @@ export default class Main extends Component { this.updateSelectedNavItem(); } + setDateTimeFilter = (dateTimeFilter: DateTimeFilter) => { + this.setState({ + dateTimeFilter: dateTimeFilter, + }); + }; + /** * Returns current component route index * @return {number} @@ -215,6 +227,7 @@ export default class Main extends Component { ], }, ]; + return ( {(core: CoreStart | null) => @@ -260,6 +273,8 @@ export default class Main extends Component { render={(props: RouteComponentProps) => ( { render={(props: RouteComponentProps) => ( { render={(props: RouteComponentProps) => ( = ({ [onGroupByChange] ); - const generateVisualizationSpec = useCallback((summaryData, groupBy) => { - return getOverviewVisualizationSpec(summaryData, groupBy, { - timeUnit: timeUnit, - dateFormat: getDateFormatByTimeUnit(startTime, endTime), - }); - }, []); + const generateVisualizationSpec = useCallback( + (summaryData, groupBy) => { + const chartTimeUnits = getChartTimeUnit(startTime, endTime); + return getOverviewVisualizationSpec(summaryData, groupBy, { + timeUnit: chartTimeUnits.timeUnit, + dateFormat: chartTimeUnits.dateFormat, + domain: getDomainRange([startTime, endTime], chartTimeUnits.timeUnit.unit), + }); + }, + [startTime, endTime] + ); useEffect(() => { const summaryData: SummaryData[] = []; diff --git a/public/pages/Overview/containers/Overview/Overview.tsx b/public/pages/Overview/containers/Overview/Overview.tsx index ec35abb92..4ba1c5218 100644 --- a/public/pages/Overview/containers/Overview/Overview.tsx +++ b/public/pages/Overview/containers/Overview/Overview.tsx @@ -28,9 +28,16 @@ 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'; +import { getChartTimeUnit, TimeUnit } from '../../utils/helpers'; export const Overview: React.FC = (props) => { + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = props; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [initialLoadingFinished, setInitialLoadingFinished] = useState(false); const [state, setState] = useState({ @@ -41,11 +48,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 timeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); + const [timeUnit, setTimeUnit] = useState(timeUnits.timeUnit); const context = useContext(CoreServicesContext); const services = useContext(ServicesContext); @@ -68,12 +76,12 @@ export const Overview: React.FC = (props) => { overviewViewModelActor.registerRefreshHandler(updateState); const updateModel = async () => { - await overviewViewModelActor.onRefresh(startTime, endTime); + await overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); setInitialLoadingFinished(true); }; updateModel(); - }, []); + }, [dateTimeFilter.startTime, dateTimeFilter.endTime]); useEffect(() => { if ( @@ -94,20 +102,24 @@ export const Overview: React.FC = (props) => { 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); + const timeUnits = getChartTimeUnit(start, endTime); + + props.setDateTimeFilter && + props.setDateTimeFilter({ + startTime: start, + endTime: endTime, + }); + setTimeUnit(timeUnits.timeUnit); setRecentlyUsedRanges(usedRanges); }; useEffect(() => { - overviewViewModelActor.onRefresh(startTime, endTime); - }, [startTime, endTime]); + overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); + }, [dateTimeFilter.startTime, dateTimeFilter.endTime]); const onRefresh = async () => { setLoading(true); - await overviewViewModelActor.onRefresh(startTime, endTime); + await overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); }; const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); @@ -144,8 +156,8 @@ export const Overview: React.FC = (props) => { = (props) => { diff --git a/public/pages/Overview/models/OverviewViewModel.ts b/public/pages/Overview/models/OverviewViewModel.ts index 8c6c1953f..c8378a8da 100644 --- a/public/pages/Overview/models/OverviewViewModel.ts +++ b/public/pages/Overview/models/OverviewViewModel.ts @@ -7,7 +7,7 @@ import { BrowserServices } from '../../../models/interfaces'; import { DetectorHit, RuleSource } from '../../../../server/models/interfaces'; import { AlertItem, FindingItem } from './interfaces'; import { RuleService } from '../../../services'; -import { DEFAULT_EMPTY_DATA } from '../../../utils/constants'; +import { DEFAULT_DATE_RANGE, DEFAULT_EMPTY_DATA } from '../../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../../../utils/helpers'; import dateMath from '@elastic/datemath'; @@ -191,8 +191,8 @@ export class OverviewViewModelActor { this.refreshHandlers.push(handler); } - startTime = 'now-15m'; - endTime = 'now'; + startTime = DEFAULT_DATE_RANGE.start; + endTime = DEFAULT_DATE_RANGE.end; public async onRefresh(startTime: string, endTime: string) { this.startTime = startTime; diff --git a/public/pages/Overview/models/interfaces.ts b/public/pages/Overview/models/interfaces.ts index 9c507888c..2b4a5293f 100644 --- a/public/pages/Overview/models/interfaces.ts +++ b/public/pages/Overview/models/interfaces.ts @@ -7,10 +7,17 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { RouteComponentProps } from 'react-router-dom'; import { OverviewViewModel } from './OverviewViewModel'; +export interface DateTimeFilter { + startTime: string; + endTime: string; +} + export interface OverviewProps extends RouteComponentProps { getStartedDismissedOnce: boolean; onGetStartedDismissed: () => void; notifications?: NotificationsStart; + setDateTimeFilter?: Function; + dateTimeFilter?: DateTimeFilter; } export interface OverviewState { diff --git a/public/pages/Overview/utils/constants.ts b/public/pages/Overview/utils/constants.ts index 12b80c0b1..d8438e667 100644 --- a/public/pages/Overview/utils/constants.ts +++ b/public/pages/Overview/utils/constants.ts @@ -9,18 +9,3 @@ 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 index 2f7d95f61..87735bec5 100644 --- a/public/pages/Overview/utils/helper.test.ts +++ b/public/pages/Overview/utils/helper.test.ts @@ -1,46 +1,53 @@ -import { getChartTimeUnit, getDateFormatByTimeUnit } from './helpers'; -import { TimeUnitsMap } from './constants'; -import moment from 'moment'; +import { getChartTimeUnit, TimeUnits } from './helpers'; 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 hoursAgo = moment().subtract(15, 'hours'); - - const timeFormats: { - [key: string]: string; - } = { - 'now-15m': dayFormat, - 'now-15h': hoursAgo.date() === moment().date() ? dayFormat : 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'; + const defaultTimeUnit = { + timeUnit: { unit: 'yearmonthdatehoursminutes', step: 1 }, + dateFormat: '%Y-%m-%d %H:%M', + }; it(' - function should return default timeUnit if fn params are invalid', () => { - expect(getChartTimeUnit('', '')).toBe(defaultTimeUnit); + const timeUnits = getChartTimeUnit('', ''); + expect(timeUnits.dateFormat).toBe(defaultTimeUnit.dateFormat); + expect(timeUnits.timeUnit.unit).toBe(defaultTimeUnit.timeUnit.unit); + expect(timeUnits.timeUnit.step).toBe(defaultTimeUnit.timeUnit.step); }); it(' - function should return default timeUnit if one is passed as param', () => { - const defaultFormat = 'yearmonthdate'; - expect(getChartTimeUnit('', '', defaultFormat)).toBe(defaultFormat); + const defaultFormat = 'yearmonthdatehoursminutes'; + const timeUnits = getChartTimeUnit('', '', defaultFormat); + expect(timeUnits.timeUnit.unit).toBe(defaultFormat); }); + const timeUnitsExpected: { + [key: string]: TimeUnits; + } = { + minutes: { + dateFormat: '%Y-%m-%d %H:%M', + timeUnit: { step: 1, unit: 'yearmonthdatehoursminutes' }, + }, + hours: { + dateFormat: '%Y-%m-%d %H:%M', + timeUnit: { step: 1, unit: 'yearmonthdatehours' }, + }, + days: { + dateFormat: '%Y-%m-%d', + timeUnit: { step: 1, unit: 'yearmonthdate' }, + }, + weeks: { + dateFormat: '%Y-%m-%d', + timeUnit: { step: 1, unit: 'yearmonthdate' }, + }, + months: { + dateFormat: '%Y-%m-%d', + timeUnit: { step: 1, unit: 'yearmonthdate' }, + }, + years: { + dateFormat: '%Y', + timeUnit: { step: 1, unit: 'year' }, + }, + }; + const timeUnits: { [key: string]: string; } = { @@ -52,9 +59,16 @@ describe('helper utilities spec', () => { years: 'now-15y', }; + const validateTimeUnit = (timeUnit: TimeUnits, expectedTimeUnit: TimeUnits) => { + expect(timeUnit.dateFormat).toBe(expectedTimeUnit.dateFormat); + expect(timeUnit.timeUnit.unit).toBe(expectedTimeUnit.timeUnit.unit); + expect(timeUnit.timeUnit.step).toBe(expectedTimeUnit.timeUnit.step); + }; + 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]); + it(` - filter ${start} should return valid timeUnit object`, () => { + const timeUnitResult = getChartTimeUnit(start, 'now'); + validateTimeUnit(timeUnitResult, timeUnitsExpected[unit]); }); } }); diff --git a/public/pages/Overview/utils/helpers.ts b/public/pages/Overview/utils/helpers.ts index a58be0047..d72c7a8ec 100644 --- a/public/pages/Overview/utils/helpers.ts +++ b/public/pages/Overview/utils/helpers.ts @@ -3,17 +3,27 @@ * 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'; +import { DEFAULT_DATE_RANGE } from '../../../utils/constants'; + +export interface TimeUnit { + unit: string; + step: number; +} + +export interface TimeUnits { + timeUnit: TimeUnit; + dateFormat: string; +} export type DateOpts = { - timeUnit: string; + timeUnit: TimeUnit; dateFormat: string; + domain?: number[]; }; /** @@ -36,16 +46,55 @@ const legendSelectionCfg = { }, }; -/** - * Adds interactive legends to the chart layer - * @param layer - */ -const addInteractiveLegends = (layer: any) => _.defaultsDeep(layer, legendSelectionCfg); +export const defaultTimeUnit = { + unit: 'yearmonthdatehoursminutes', + step: 1, +}; + +export const defaultDateFormat = '%Y-%m-%d %H:%M'; + +export const parseDateString = (dateString: string): number => { + const date = dateMath.parse(dateString); + return date ? date.toDate().getTime() : new Date().getTime(); +}; + +export const defaultScaleDomain = [ + parseDateString(DEFAULT_DATE_RANGE.start), + parseDateString(DEFAULT_DATE_RANGE.end), +]; + +export const getYAxis = (field: string, title: string, axisGrid: boolean = true) => ({ + aggregate: 'sum', + field: field, + type: 'quantitative', + title: title, + axis: { grid: axisGrid }, +}); + +export const getXAxis = (dateOpts: DateOpts, opts: any = {}) => + _.defaultsDeep(opts, { + timeUnit: dateOpts.timeUnit, + field: 'time', + title: '', + type: 'temporal', + axis: { grid: false, format: dateOpts.dateFormat }, + scale: { + domain: dateOpts.domain, + }, + }); + +export const getTimeTooltip = (dateOpts: DateOpts) => ({ + timeUnit: dateOpts.timeUnit, + field: 'time', + type: 'temporal', + title: 'Time', + format: '%Y-%m-%d %H:%M:%S', +}); function getVisualizationSpec(description: string, data: any, layers: any[]): TopLevelSpec { return { config: { - view: { stroke: null }, + view: { stroke: 'transparent' }, legend: { labelColor: '#343741', titleColor: '#1a1c21', @@ -72,26 +121,27 @@ export function getOverviewVisualizationSpec( visualizationData: SummaryData[], groupBy: string, dateOpts: DateOpts = { - timeUnit: 'yearmonthdatehoursminutes', - dateFormat: '%Y-%m-%d %H:%M', + timeUnit: defaultTimeUnit, + dateFormat: defaultDateFormat, + domain: defaultScaleDomain, } ): TopLevelSpec { - const aggregate = 'sum'; const findingsEncoding: { [x: string]: any } = { - x: { - timeUnit: dateOpts.timeUnit, - field: 'time', - title: '', - axis: { grid: false, ticks: false, format: dateOpts.dateFormat }, + x: getXAxis(dateOpts), + y: getYAxis('finding', 'Count'), + tooltip: [getYAxis('finding', 'Findings'), getTimeTooltip(dateOpts)], + color: { + scale: null, + value: euiPaletteColorBlind()[1], }, - y: { - aggregate, - field: 'finding', - type: 'quantitative', - title: 'Count', - axis: { grid: true, ticks: false }, + }; + + let barLayer = { + mark: { + type: 'bar', + clip: true, }, - tooltip: [{ field: 'finding', aggregate: 'sum', type: 'quantitative', title: 'Findings' }], + encoding: findingsEncoding, }; if (groupBy === 'logType') { @@ -103,6 +153,13 @@ export function getOverviewVisualizationSpec( range: euiPaletteColorBlind(), }, }; + + findingsEncoding['tooltip'].push({ + field: groupBy, + title: groupBy === 'logType' ? 'Log type' : 'Rule severity', + }); + + barLayer = addInteractiveLegends(barLayer); } const lineColor = '#ff0000'; @@ -110,108 +167,60 @@ export function getOverviewVisualizationSpec( 'Plot showing average data with raw values in the background.', visualizationData, [ - addInteractiveLegends({ - mark: 'bar', - encoding: findingsEncoding, - }), + barLayer, { mark: { type: 'line', + clip: true, + interpolate: 'monotone', color: lineColor, point: { - filled: true, - fill: lineColor, + filled: false, + fill: 'white', + color: lineColor, + size: 50, }, }, encoding: { - 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' }], + x: getXAxis(dateOpts, { + band: 0.5, + }), + y: getYAxis('alert', 'Count'), + tooltip: [getYAxis('alert', 'Alerts'), getTimeTooltip(dateOpts)], }, }, ] ); } -/** - * 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', + timeUnit: defaultTimeUnit, + dateFormat: defaultDateFormat, + domain: defaultScaleDomain, } ) { return getVisualizationSpec('Findings data overview', visualizationData, [ addInteractiveLegends({ - mark: 'bar', + mark: { + type: 'bar', + clip: true, + }, encoding: { - tooltip: [{ field: 'finding', aggregate: 'sum', type: 'quantitative', title: 'Findings' }], - x: { - timeUnit: dateOpts.timeUnit, - field: 'time', - title: '', - axis: { - grid: false, - format: dateOpts.dateFormat, + tooltip: [ + getYAxis('finding', 'Findings'), + getTimeTooltip(dateOpts), + { + field: groupBy, + title: groupBy === 'logType' ? 'Log type' : 'Rule severity', }, - }, - y: { - aggregate: 'sum', - field: 'finding', - type: 'quantitative', - title: 'Count', - axis: { grid: true, ticks: false }, - }, + ], + x: getXAxis(dateOpts), + y: getYAxis('finding', 'Count'), color: { field: groupBy, - type: 'nominal', title: groupBy === 'logType' ? 'Log type' : 'Rule severity', scale: { range: euiPaletteColorBlind(), @@ -226,35 +235,30 @@ export function getAlertsVisualizationSpec( visualizationData: any[], groupBy: string, dateOpts: DateOpts = { - timeUnit: 'yearmonthdatehoursminutes', - dateFormat: '%Y-%m-%d %H:%M', + timeUnit: defaultTimeUnit, + dateFormat: defaultDateFormat, + domain: defaultScaleDomain, } ) { return getVisualizationSpec('Alerts data overview', visualizationData, [ addInteractiveLegends({ - mark: 'bar', + mark: { + type: 'bar', + clip: true, + }, encoding: { - tooltip: [{ field: 'alert', aggregate: 'sum', title: 'Alerts' }], - x: { - timeUnit: dateOpts.timeUnit, - field: 'time', - title: '', - axis: { - grid: false, - ticks: false, - format: dateOpts.dateFormat, + tooltip: [ + getYAxis('alert', 'Alerts'), + getTimeTooltip(dateOpts), + { + field: groupBy, + title: groupBy === 'status' ? 'Alert status' : 'Alert severity', }, - }, - y: { - aggregate: 'sum', - field: 'alert', - type: 'quantitative', - title: 'Count', - axis: { grid: true, ticks: false }, - }, + ], + x: getXAxis(dateOpts), + y: getYAxis('alert', 'Count'), color: { field: groupBy, - type: 'nominal', title: groupBy === 'status' ? 'Alert status' : 'Alert severity', scale: { range: groupBy === 'status' ? euiPaletteForStatus(5) : euiPaletteColorBlind(), @@ -292,8 +296,14 @@ export function getTopRulesVisualizationSpec(visualizationData: any[]) { type: 'quantitative', format: '2.0%', }, + { + field: 'ruleName', + type: 'nominal', + title: 'Rule', + }, + getYAxis('count', 'Count'), ], - theta: { aggregate: 'sum', field: 'count', type: 'quantitative' }, + theta: getYAxis('count', ''), color: { field: 'ruleName', type: 'nominal', @@ -325,29 +335,82 @@ export function getChartTimeUnit( start: string, end: string, defaultUnit: string = 'yearmonthdatehoursminutes' -): string { +): TimeUnits { const startMoment = dateMath.parse(start); const endMoment = dateMath.parse(end); - let minUnit: string = 'minutes'; - let timeUnit: string = TimeUnitsMap[minUnit]; + let timeUnit: string = defaultUnit; + let dateFormat: string = '%Y-%m-%d %H:%M'; - if (!startMoment || !endMoment) return defaultUnit; + if (!startMoment || !endMoment) + return { + timeUnit: { unit: timeUnit, step: 1 }, + dateFormat, + }; try { - const timeDiff = endMoment.diff(startMoment); - const momentTimeDiff = moment.duration(timeDiff); + const milliseconds = endMoment.diff(startMoment); + + const second = 1001; // set 1ms as a threshold since moment can make a mistake in 1 ms when calculating start and end datetime + const minute = second * 60; + const hour = minute * 60; + const day = hour * 24; + const month = day * 30; + const year = month * 12; - const timeUnits: string[] = ['years', 'months', 'days', 'hours', 'minutes']; - for (const unit of timeUnits) { - // @ts-ignore - if (momentTimeDiff._data[unit]) { - timeUnit = TimeUnitsMap[unit]; - break; - } + if (milliseconds <= minute) { + timeUnit = 'yearmonthdatehoursminutesseconds'; + dateFormat = '%Y-%m-%d %H:%M:%S'; + } else if (milliseconds <= hour) { + timeUnit = 'yearmonthdatehoursminutes'; + dateFormat = '%Y-%m-%d %H:%M'; + } else if (milliseconds <= day * 2) { + timeUnit = 'yearmonthdatehours'; + dateFormat = '%Y-%m-%d %H:%M'; + } else if (milliseconds <= month * 6) { + timeUnit = 'yearmonthdate'; + dateFormat = '%Y-%m-%d'; + } else if (milliseconds <= year * 2) { + timeUnit = 'yearmonth'; + dateFormat = '%Y-%m'; + } else if (milliseconds <= year * 6) { + timeUnit = 'yearquarter'; + dateFormat = '%Y'; + } else { + timeUnit = 'year'; + dateFormat = '%Y'; } } catch (e) { console.error(`Time diff can't be calculated for dates: ${start} and ${end}`); } - - return timeUnit; + return { + timeUnit: _.assign( + { + step: 1, + }, + { unit: timeUnit } + ), + dateFormat, + }; } + +/** + * Adds interactive legends to the chart layer + * @param layer + */ +const addInteractiveLegends = (layer: any) => _.defaultsDeep(layer, legendSelectionCfg); + +export const getDomainRange = ( + range: string[] = [DEFAULT_DATE_RANGE.start, DEFAULT_DATE_RANGE.end], + timeUnit?: string +): number[] => { + const start: number = parseDateString(range[0] || DEFAULT_DATE_RANGE.start); + let rangeEnd = range[1] || DEFAULT_DATE_RANGE.end; + if (timeUnit) { + const timeUnitSize = timeUnit.match(/.*(seconds|minutes|hours|date|month|year)$/); + if (timeUnitSize && timeUnitSize[1]) { + rangeEnd = `now+1${timeUnitSize[1][0]}`; + } + } + const end: number = parseDateString(rangeEnd); + return [start, end]; +}; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 8715f20c7..2d0a58b3b 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -9,7 +9,7 @@ import { DETECTOR_TYPES } from '../pages/Detectors/utils/constants'; export const DATE_MATH_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; export const MAX_RECENTLY_USED_TIME_RANGES = 5; -export const DEFAULT_DATE_RANGE = { start: 'now-15m', end: 'now' }; +export const DEFAULT_DATE_RANGE = { start: 'now-24h', end: 'now' }; export const PLUGIN_NAME = 'opensearch_security_analytics_dashboards'; export const OS_NOTIFICATION_PLUGIN = 'opensearch-notifications';