From ee7a285d30547e539ef5ba908bf1e4f554a1ab0b Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Sat, 15 Mar 2025 04:57:22 -0700 Subject: [PATCH 1/3] Refactored vega-based visualizations to chart.js. Signed-off-by: AWSHurneyt --- package.json | 4 +- .../pages/Alerts/containers/Alerts/Alerts.tsx | 67 +-- .../Findings/containers/Findings/Findings.tsx | 66 +-- .../Overview/components/Widgets/Summary.tsx | 64 +- .../components/Widgets/TopRulesWidget.tsx | 13 +- .../Overview/containers/Overview/Overview.tsx | 3 - .../utils/__snapshots__/helper.test.ts.snap | 476 --------------- public/pages/Overview/utils/helper.test.ts | 28 - public/pages/Overview/utils/helpers.ts | 301 ---------- public/utils/chartUtils.tsx | 558 ++++++++++++++++++ public/utils/helpers.tsx | 78 --- yarn.lock | 17 + 12 files changed, 665 insertions(+), 1010 deletions(-) delete mode 100644 public/pages/Overview/utils/__snapshots__/helper.test.ts.snap create mode 100644 public/utils/chartUtils.tsx diff --git a/package.json b/package.json index 2f2e25bd5..25512d41e 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,8 @@ }, "dependencies": { "formik": "^2.2.6", - "react-graph-vis": "^1.0.7" + "react-graph-vis": "^1.0.7", + "chart.js": "^4.4.8", + "chartjs-adapter-moment": "^1.0.1" } } diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index ca1e26cc7..ce3f154e5 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -17,7 +17,6 @@ import { EuiToolTip, EuiEmptyPrompt, EuiTableSelectionType, - EuiIcon, EuiTabbedContent, EuiText, } from '@elastic/eui'; @@ -25,12 +24,7 @@ import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components import dateMath from '@elastic/datemath'; import React, { Component } from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; -import { - getAlertsVisualizationSpec, - getChartTimeUnit, - getDomainRange, - TimeUnit, -} from '../../../Overview/utils/helpers'; +import { getChartTimeUnit, TimeUnit } from '../../../Overview/utils/helpers'; import moment from 'moment'; import { ALERT_STATE, @@ -58,13 +52,11 @@ import { errorNotificationToast, getDuration, renderTime, - renderVisualization, setBreadcrumbs, successNotificationToast, } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { match, RouteComponentProps, withRouter } from 'react-router-dom'; -import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { AlertItem, CorrelationAlertTableItem, @@ -77,6 +69,7 @@ import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { DataStore } from '../../../../store/DataStore'; import { ThreatIntelAlertsTable } from '../../components/ThreatIntelAlertsTable/ThreatIntelAlertsTable'; import { PageHeader } from '../../../../components/PageHeader/PageHeader'; +import { createBarChartWrapper } from '../../../../utils/chartUtils'; type FilterAlertParams = | { alerts: AlertItem[]; timeField: 'last_notification_time' } @@ -121,11 +114,13 @@ export interface AlertsState { selectedTabId: AlertTabId; } -const groupByOptions = [ +export const alertsGroupByOptions = [ { text: 'Alert status', value: 'status' }, { text: 'Alert severity', value: 'severity' }, ]; +const ALERTS_VIEW_CHART = 'alerts-view'; + export class Alerts extends Component { private abortControllers: AbortController[] = []; @@ -248,7 +243,7 @@ export class Alerts extends Component { /> ), }); - renderVisualization(this.generateVisualizationSpec(filteredAlerts), 'alerts-view'); + this.getChart(this.getVisData(filteredAlerts)); }; filterCorrelationAlerts = () => { @@ -272,7 +267,7 @@ export class Alerts extends Component { /> ), }); - renderVisualization(this.generateVisualizationSpec(filteredCorrelationAlerts), 'alerts-view'); + this.getChart(this.getVisData(filteredCorrelationAlerts)); }; filterThreatIntelAlerts = () => { @@ -296,28 +291,19 @@ export class Alerts extends Component { /> ), }); - renderVisualization(this.generateVisualizationSpec(filteredAlerts), 'alerts-view'); + this.getChart(this.getVisData(filteredAlerts)); }; private renderVisAsPerTab() { switch (this.state.selectedTabId) { case AlertTabId.DetectionRules: - renderVisualization( - this.generateVisualizationSpec(this.state.filteredAlerts), - 'alerts-view' - ); + this.getChart(this.getVisData(this.state.filteredAlerts)); break; case AlertTabId.Correlations: - renderVisualization( - this.generateVisualizationSpec(this.state.filteredCorrelationAlerts), - 'alerts-view' - ); + this.getChart(this.getVisData(this.state.filteredCorrelationAlerts)); break; case AlertTabId.ThreatIntel: - renderVisualization( - this.generateVisualizationSpec(this.state.filteredThreatIntelAlerts), - 'alerts-view' - ); + this.getChart(this.getVisData(this.state.filteredThreatIntelAlerts)); break; } } @@ -518,39 +504,28 @@ export class Alerts extends Component { this.setState({ flyoutCorrelationData: alertItem ? { alertItem } : undefined }); } - generateVisualizationSpec(alerts: (AlertItem | CorrelationAlertTableItem | ThreatIntelAlert)[]) { - const visData = alerts.map((alert) => { + getChart(data: any) { + createBarChartWrapper(data, this.state.groupBy, ALERTS_VIEW_CHART, this.props.dateTimeFilter); + } + + getVisData(alerts: (AlertItem | CorrelationAlertTableItem | ThreatIntelAlert)[]) { + return alerts.map((alert) => { const time = new Date(alert.start_time); time.setMilliseconds(0); time.setSeconds(0); return { alert: 1, - time, + time: time.getTime(), status: alert.state, 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: chartTimeUnits.timeUnit, - dateFormat: chartTimeUnits.dateFormat, - domain: getDomainRange( - [dateTimeFilter.startTime, dateTimeFilter.endTime], - chartTimeUnits.timeUnit.unit - ), - }); } createGroupByControl(): React.ReactNode { return createSelectComponent( - groupByOptions, + alertsGroupByOptions, this.state.groupBy, 'alert-vis-groupBy', (event) => { @@ -1181,7 +1156,9 @@ export class Alerts extends Component { } /> ) : ( - +
+ +
)} diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index aeebd7d3e..777cde04c 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -32,13 +32,7 @@ import { FindingTabId, MAX_RECENTLY_USED_TIME_RANGES, } from '../../../../utils/constants'; -import { - getChartTimeUnit, - getDomainRange, - getFindingsVisualizationSpec, - getThreatIntelFindingsVisualizationSpec, - TimeUnit, -} from '../../../Overview/utils/helpers'; +import { getChartTimeUnit, TimeUnit } from '../../../Overview/utils/helpers'; import { getNotificationChannels, parseNotificationChannelsToOptions, @@ -46,7 +40,6 @@ import { import { createSelectComponent, errorNotificationToast, - renderVisualization, getDuration, getIsNotificationPluginInstalled, setBreadcrumbs, @@ -54,7 +47,6 @@ import { } from '../../../../utils/helpers'; import { RuleSource } from '../../../../../server/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { DataStore } from '../../../../store/DataStore'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { @@ -69,6 +61,7 @@ import { import { ThreatIntelFindingsTable } from '../../components/FindingsTable/ThreatIntelFindingsTable'; import { PageHeader } from '../../../../components/PageHeader/PageHeader'; import { RuleSeverityValue, RuleSeverityPriority } from '../../../Rules/utils/constants'; +import { createBarChartWrapper } from '../../../../utils/chartUtils'; interface FindingsProps extends RouteComponentProps, DataSourceProps { detectorService: DetectorsService; @@ -133,6 +126,8 @@ export const groupByOptionsByTabId = { [FindingTabId.ThreatIntel]: [{ text: 'Indicator type', value: 'indicatorType' }], }; +const FINDINGS_VIEW_CHART = 'findings-view'; + class Findings extends Component { private abortGetFindingsControllers: AbortController[] = []; @@ -219,7 +214,8 @@ class Findings extends Component { ) { this.onRefresh(); } else if (this.shouldUpdateVisualization(prevState)) { - renderVisualization(this.generateVisualizationSpec(), 'findings-view'); + const data = this.generateVisualizationData(); + this.createStackedBarChart(data.visData, data.groupBy); } } @@ -239,7 +235,8 @@ class Findings extends Component { } else if (this.state.selectedTabId === FindingTabId.ThreatIntel) { await this.getThreatIntelFindings(); } - renderVisualization(this.generateVisualizationSpec(), 'findings-view'); + const data = this.generateVisualizationData(); + this.createStackedBarChart(data.visData, data.groupBy); }; setStateForTab( @@ -419,7 +416,7 @@ class Findings extends Component { }); }; - generateVisualizationSpec() { + generateVisualizationData() { const visData: (FindingVisualizationData | ThreatIntelFindingVisualizationData)[] = []; const { selectedTabId, findingStateByTabId } = this.state; @@ -428,7 +425,6 @@ class Findings extends Component { ? findingStateByTabId[FindingTabId.DetectionRules] : findingStateByTabId[FindingTabId.ThreatIntel]; const groupBy = findingsState.groupBy; - let specGetter; if (selectedTabId === FindingTabId.DetectionRules) { (findingsState.filteredFindings as FindingItemType[]).forEach((finding: FindingItemType) => { @@ -449,7 +445,6 @@ class Findings extends Component { ruleLevel === 'critical' ? ruleLevel : (finding as any)['ruleSeverity'] || ruleLevel, }); }); - specGetter = getFindingsVisualizationSpec; } else { (findingsState.findings as ThreatIntelFinding[]).forEach((finding) => { const findingTime = new Date(finding.timestamp); @@ -462,24 +457,12 @@ class Findings extends Component { indicatorType: finding.ioc_type, }); }); - specGetter = getThreatIntelFindingsVisualizationSpec; } - const { - dateTimeFilter = { - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, - }, - } = this.props; - const chartTimeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); - - return specGetter(visData, groupBy, { - timeUnit: chartTimeUnits.timeUnit, - dateFormat: chartTimeUnits.dateFormat, - domain: getDomainRange( - [dateTimeFilter.startTime, dateTimeFilter.endTime], - chartTimeUnits.timeUnit.unit - ), - }); + + return { + visData, + groupBy, + }; } createGroupByControl(): React.ReactNode { @@ -506,6 +489,21 @@ class Findings extends Component { }); }; + createStackedBarChart( + data: (FindingVisualizationData | ThreatIntelFindingVisualizationData)[], + groupBy: string + ) { + // Calculate the time difference in milliseconds + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + } = this.props; + + createBarChartWrapper(data, groupBy, FINDINGS_VIEW_CHART, dateTimeFilter); + } + render() { const { loading, @@ -541,7 +539,7 @@ class Findings extends Component { finding['ruleName'] = matchedRules[0]?.title || - (finding.queries.find(({ id }) => isThreatIntelQuery(id)) + (finding.queries.find(({ id }: any) => isThreatIntelQuery(id)) ? 'Threat intel' : DEFAULT_EMPTY_DATA); finding['ruleSeverity'] = @@ -679,7 +677,9 @@ class Findings extends Component { } /> ) : ( - +
+ +
)} diff --git a/public/pages/Overview/components/Widgets/Summary.tsx b/public/pages/Overview/components/Widgets/Summary.tsx index 910aa555b..23e90b8d2 100644 --- a/public/pages/Overview/components/Widgets/Summary.tsx +++ b/public/pages/Overview/components/Widgets/Summary.tsx @@ -16,19 +16,12 @@ import { import React, { useCallback, useEffect, useState } from 'react'; import { WidgetContainer } from './WidgetContainer'; import { summaryGroupByOptions } from '../../utils/constants'; -import { - getChartTimeUnit, - getDomainRange, - getOverviewVisualizationSpec, - getTimeWithMinPrecision, - TimeUnit, -} from '../../utils/helpers'; -import { createSelectComponent, renderVisualization } from '../../../../utils/helpers'; +import { TimeUnit } from '../../utils/helpers'; +import { createSelectComponent } from '../../../../utils/helpers'; import { ROUTES } from '../../../../utils/constants'; -import { ChartContainer } from '../../../../components/Charts/ChartContainer'; -import { getLogTypeLabel } from '../../../LogTypes/utils/helpers'; import { OverviewAlertItem, OverviewFindingItem } from '../../../../../types'; import { getUseUpdatedUx } from '../../../../services/utils/constants'; +import { createBarAndLineChartWrapper } from '../../../../utils/chartUtils'; export interface SummaryProps { findings: OverviewFindingItem[]; @@ -39,12 +32,7 @@ export interface SummaryProps { timeUnit: TimeUnit; } -export interface SummaryData { - time: number; - alert: number; - finding: number; - logType?: string; -} +export const SUMMARY_VIEW_CHART = 'summary-view'; export const Summary: React.FC = ({ alerts, @@ -54,7 +42,8 @@ export const Summary: React.FC = ({ loading = false, }) => { const [groupBy, setGroupBy] = useState(''); - const [summaryData, setSummaryData] = useState([]); + const [alertsVisData, setAlertsVisData] = useState([]); + const [findingsVisData, setFindingsVisData] = useState([]); const [activeAlerts, setActiveAlerts] = useState(undefined); const [totalFindings, setTotalFindings] = useState(undefined); @@ -76,51 +65,44 @@ export const Summary: React.FC = ({ [onGroupByChange] ); - 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[] = []; + const alertsVisData: any[] = []; let activeAlerts = 0; alerts.forEach((alert) => { if (!alert.acknowledged) { activeAlerts++; } - summaryData.push({ - time: getTimeWithMinPrecision(alert.time), + alertsVisData.push({ + time: alert.time, alert: 1, finding: 0, - logType: getLogTypeLabel(alert.logType), + logType: alert.logType, }); }); + const findingsVisData: any[] = []; findings.forEach((finding) => { - summaryData.push({ - time: getTimeWithMinPrecision(finding.time), + findingsVisData.push({ + time: finding.time, alert: 0, finding: 1, - logType: getLogTypeLabel(finding.logType), + logType: finding.logType, }); }); setActiveAlerts(activeAlerts); setTotalFindings(findings.length); - setSummaryData(summaryData); + setAlertsVisData(alertsVisData); + setFindingsVisData(findingsVisData); }, [alerts, findings]); useEffect(() => { - renderVisualization(generateVisualizationSpec(summaryData, groupBy), 'summary-view'); - }, [summaryData, groupBy]); + createBarAndLineChartWrapper(alertsVisData, findingsVisData, groupBy, SUMMARY_VIEW_CHART, { + startTime, + endTime, + }); + }, [alertsVisData, findingsVisData, groupBy]); const createStatComponent = useCallback( (description: string, urlData: { url: string; color: EuiLinkColor }, stat?: number) => ( @@ -197,7 +179,9 @@ export const Summary: React.FC = ({ } /> ) : ( - +
+ +
)} diff --git a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx index b6ef0a512..45de884e2 100644 --- a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx +++ b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx @@ -3,12 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getEuiEmptyPrompt, renderVisualization } from '../../../../utils/helpers'; +import { getEuiEmptyPrompt } from '../../../../utils/helpers'; import React, { useEffect } from 'react'; import { WidgetContainer } from './WidgetContainer'; -import { getTopRulesVisualizationSpec } from '../../utils/helpers'; -import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { OverviewFindingItem } from '../../../../../types'; +import { createDoughnutChartWrapper } from '../../../../utils/chartUtils'; export interface TopRulesWidgetProps { findings: OverviewFindingItem[]; @@ -17,6 +16,8 @@ export interface TopRulesWidgetProps { type RulesCount = { [ruleName: string]: number }; +export const TOP_RULES_VIEW_CHART = 'top-rules-view'; + export const TopRulesWidget: React.FC = ({ findings, loading = false }) => { useEffect(() => { const rulesCount: RulesCount = {}; @@ -29,7 +30,7 @@ export const TopRulesWidget: React.FC = ({ findings, loadin ruleName, count: rulesCount[ruleName], })); - renderVisualization(getTopRulesVisualizationSpec(visualizationData), 'top-rules-view'); + createDoughnutChartWrapper(visualizationData, TOP_RULES_VIEW_CHART); } }, [findings]); @@ -38,7 +39,9 @@ export const TopRulesWidget: React.FC = ({ findings, loadin {findings.length === 0 ? ( getEuiEmptyPrompt('No findings with detection rules.') ) : ( - +
+ +
)} ); diff --git a/public/pages/Overview/containers/Overview/Overview.tsx b/public/pages/Overview/containers/Overview/Overview.tsx index cd98ec273..0a4782303 100644 --- a/public/pages/Overview/containers/Overview/Overview.tsx +++ b/public/pages/Overview/containers/Overview/Overview.tsx @@ -9,9 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiPopover, - EuiSuperDatePicker, - EuiTitle, - EuiSpacer, EuiSmallButton, EuiCard, EuiPanel, diff --git a/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap b/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap deleted file mode 100644 index 36e2531b8..000000000 --- a/public/pages/Overview/utils/__snapshots__/helper.test.ts.snap +++ /dev/null @@ -1,476 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`helper utilities spec tests getAlertsVisualizationSpec function - snapshot test: should match alerts spec 1`] = ` -Object { - "$schema": "https://vega.github.io/schema/vega-lite/v5.json", - "config": Object { - "legend": Object { - "labelFont": "\\"Inter UI\\", -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Helvetica, Arial, sans-serif, \\"Apple Color Emoji\\", \\"Segoe UI Emoji\\", \\"Segoe UI Symbol\\"", - "labelFontSize": 14, - "rowPadding": 6, - "titleFontSize": 14, - "titleFontWeight": 600, - "titleLineHeight": 21, - "titlePadding": 10, - }, - "view": Object { - "stroke": "transparent", - }, - }, - "data": Object { - "values": Array [], - }, - "description": "Alerts data overview", - "layer": Array [ - Object { - "encoding": Object { - "color": Object { - "field": "", - "scale": Object { - "domain": Array [ - "5 (Lowest)", - "4 (Low)", - "3 (Medium)", - "2 (High)", - "1 (Highest)", - ], - "range": Array [ - "#209280", - "#54b399", - "#d6bf57", - "#e7664c", - "#cc5642", - ], - }, - "title": "Alert severity", - }, - "opacity": Object { - "condition": Object { - "selection": "series", - "value": 1, - }, - "value": 0.2, - }, - "tooltip": Array [ - Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "alert", - "title": "Alerts", - "type": "quantitative", - }, - Object { - "field": "time", - "format": "%Y-%m-%d %H:%M:%S", - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "Time", - "type": "temporal", - }, - Object { - "field": "", - "title": "Alert severity", - }, - ], - "x": Object { - "axis": Object { - "format": "%Y-%m-%d %H:%M", - "grid": false, - }, - "field": "time", - "scale": Object { - "domain": undefined, - }, - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "", - "type": "temporal", - }, - "y": Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "alert", - "title": "Count", - "type": "quantitative", - }, - }, - "mark": Object { - "clip": true, - "type": "bar", - }, - "selection": Object { - "series": Object { - "bind": "legend", - "encodings": Array [ - "color", - ], - "on": "click", - "type": "multi", - }, - }, - }, - ], -} -`; - -exports[`helper utilities spec tests getFindingsVisualizationSpec function - snapshot test: should match findings spec 1`] = ` -Object { - "$schema": "https://vega.github.io/schema/vega-lite/v5.json", - "config": Object { - "legend": Object { - "labelFont": "\\"Inter UI\\", -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Helvetica, Arial, sans-serif, \\"Apple Color Emoji\\", \\"Segoe UI Emoji\\", \\"Segoe UI Symbol\\"", - "labelFontSize": 14, - "rowPadding": 6, - "titleFontSize": 14, - "titleFontWeight": 600, - "titleLineHeight": 21, - "titlePadding": 10, - }, - "view": Object { - "stroke": "transparent", - }, - }, - "data": Object { - "values": Array [], - }, - "description": "Findings data overview", - "layer": Array [ - Object { - "encoding": Object { - "color": Object { - "field": "", - "scale": Object { - "domain": Array [ - "info", - "low", - "medium", - "high", - "critical", - ], - "range": Array [ - "#209280", - "#54b399", - "#d6bf57", - "#e7664c", - "#cc5642", - ], - }, - "title": "Rule severity", - }, - "opacity": Object { - "condition": Object { - "selection": "series", - "value": 1, - }, - "value": 0.2, - }, - "tooltip": Array [ - Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "finding", - "title": "Findings", - "type": "quantitative", - }, - Object { - "field": "time", - "format": "%Y-%m-%d %H:%M:%S", - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "Time", - "type": "temporal", - }, - Object { - "field": "", - "title": "Rule severity", - }, - ], - "x": Object { - "axis": Object { - "format": "%Y-%m-%d %H:%M", - "grid": false, - }, - "field": "time", - "scale": Object { - "domain": undefined, - }, - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "", - "type": "temporal", - }, - "y": Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "finding", - "title": "Count", - "type": "quantitative", - }, - }, - "mark": Object { - "clip": true, - "type": "bar", - }, - "selection": Object { - "series": Object { - "bind": "legend", - "encodings": Array [ - "color", - ], - "on": "click", - "type": "multi", - }, - }, - }, - ], -} -`; - -exports[`helper utilities spec tests getOverviewVisualizationSpec function - snapshot test: should match overview spec 1`] = ` -Object { - "$schema": "https://vega.github.io/schema/vega-lite/v5.json", - "config": Object { - "legend": Object { - "labelFont": "\\"Inter UI\\", -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Helvetica, Arial, sans-serif, \\"Apple Color Emoji\\", \\"Segoe UI Emoji\\", \\"Segoe UI Symbol\\"", - "labelFontSize": 14, - "rowPadding": 6, - "titleFontSize": 14, - "titleFontWeight": 600, - "titleLineHeight": 21, - "titlePadding": 10, - }, - "view": Object { - "stroke": "transparent", - }, - }, - "data": Object { - "values": Array [], - }, - "description": "Plot showing average data with raw values in the background.", - "layer": Array [ - Object { - "encoding": Object { - "color": Object { - "field": "fieldType", - "legend": Object { - "values": Array [ - "Active alerts", - "Findings", - ], - }, - "scale": Object { - "domain": Array [ - "Active alerts", - "Findings", - ], - "range": Array [ - "#BD271E", - "#6092C0", - ], - }, - "title": "", - }, - "tooltip": Array [ - Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "finding", - "title": "Findings", - "type": "quantitative", - }, - Object { - "field": "time", - "format": "%Y-%m-%d %H:%M:%S", - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "Time", - "type": "temporal", - }, - ], - "x": Object { - "axis": Object { - "format": "%Y-%m-%d %H:%M", - "grid": false, - }, - "field": "time", - "scale": Object { - "domain": undefined, - }, - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "", - "type": "temporal", - }, - "y": Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "finding", - "scale": Object { - "domain": Array [ - 0, - 0.1, - ], - }, - "title": "Count", - "type": "quantitative", - }, - }, - "mark": Object { - "clip": true, - "type": "bar", - }, - "transform": Array [ - Object { - "as": "fieldType", - "calculate": "datum.alert == 1 ? 'Active alerts' : 'Findings'", - }, - ], - }, - Object { - "encoding": Object { - "tooltip": Array [ - Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "alert", - "title": "Alerts", - "type": "quantitative", - }, - Object { - "field": "time", - "format": "%Y-%m-%d %H:%M:%S", - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "Time", - "type": "temporal", - }, - ], - "x": Object { - "axis": Object { - "format": "%Y-%m-%d %H:%M", - "grid": false, - }, - "band": 0.5, - "field": "time", - "scale": Object { - "domain": undefined, - }, - "timeUnit": Object { - "step": 1, - "unit": "yearmonthdatehoursminutes", - }, - "title": "", - "type": "temporal", - }, - "y": Object { - "aggregate": "sum", - "axis": Object { - "grid": true, - }, - "field": "alert", - "title": "Count", - "type": "quantitative", - }, - }, - "mark": Object { - "clip": true, - "color": "#BD271E", - "interpolate": "monotone", - "point": Object { - "color": "#BD271E", - "fill": "white", - "filled": false, - "size": 50, - }, - "type": "line", - }, - }, - ], -} -`; - -exports[`helper utilities spec tests getVisualizationSpec function - snapshot test: should match visualization spec 1`] = ` -Object { - "$schema": "https://vega.github.io/schema/vega-lite/v5.json", - "config": Object { - "legend": Object { - "labelFont": "\\"Inter UI\\", -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Helvetica, Arial, sans-serif, \\"Apple Color Emoji\\", \\"Segoe UI Emoji\\", \\"Segoe UI Symbol\\"", - "labelFontSize": 14, - "rowPadding": 6, - "titleFontSize": 14, - "titleFontWeight": 600, - "titleLineHeight": 21, - "titlePadding": 10, - }, - "view": Object { - "stroke": "transparent", - }, - }, - "data": Object { - "values": Array [ - Object { - "xField": 1, - "yField": 1, - }, - ], - }, - "description": "Visualization", - "layer": Array [ - Object { - "encoding": Object { - "opacity": Object { - "condition": Object { - "selection": "series", - "value": 1, - }, - "value": 0.2, - }, - }, - "selection": Object { - "series": Object { - "bind": "legend", - "encodings": Array [ - "color", - ], - "on": "click", - "type": "multi", - }, - }, - "x": Object { - "field": "xField", - }, - "y": Object { - "field": "yField", - }, - }, - ], -} -`; diff --git a/public/pages/Overview/utils/helper.test.ts b/public/pages/Overview/utils/helper.test.ts index 88ec7e93a..b0077e5e8 100644 --- a/public/pages/Overview/utils/helper.test.ts +++ b/public/pages/Overview/utils/helper.test.ts @@ -181,34 +181,6 @@ describe('helper utilities spec', () => { }); }); - describe('tests getVisualizationSpec function', () => { - const result = getVisualizationSpec(description, data, [layer]); - it(' - snapshot test', () => { - expect(result).toMatchSnapshot('should match visualization spec'); - }); - }); - - describe('tests getOverviewVisualizationSpec function', () => { - const result = getOverviewVisualizationSpec([], '', dateOpts); - it(' - snapshot test', () => { - expect(result).toMatchSnapshot('should match overview spec'); - }); - }); - - describe('tests getFindingsVisualizationSpec function', () => { - const result = getFindingsVisualizationSpec([], '', dateOpts); - it(' - snapshot test', () => { - expect(result).toMatchSnapshot('should match findings spec'); - }); - }); - - describe('tests getAlertsVisualizationSpec function', () => { - const result = getAlertsVisualizationSpec([], '', dateOpts); - it(' - snapshot test', () => { - expect(result).toMatchSnapshot('should match alerts spec'); - }); - }); - describe('tests getTimeWithMinPrecision function', () => { const result = getTimeWithMinPrecision('2022/12/01 01:01:01'); it(' - test should be with ms and seconds eq to 0', () => { diff --git a/public/pages/Overview/utils/helpers.ts b/public/pages/Overview/utils/helpers.ts index 92caba7ac..7daa8efd8 100644 --- a/public/pages/Overview/utils/helpers.ts +++ b/public/pages/Overview/utils/helpers.ts @@ -3,13 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { euiPaletteColorBlind, euiPaletteForStatus } from '@elastic/eui'; -import { TopLevelSpec } from 'vega-lite'; -import { SummaryData } from '../components/Widgets/Summary'; import dateMath from '@elastic/datemath'; import _ from 'lodash'; import { DEFAULT_DATE_RANGE } from '../../../utils/constants'; -import { severityOptions } from '../../Alerts/utils/constants'; import moment from 'moment'; import { euiThemeVars } from '@osd/ui-shared-deps/theme'; @@ -98,30 +94,6 @@ export const getTimeTooltip = (dateOpts: DateOpts) => ({ format: '%Y-%m-%d %H:%M:%S', }); -export function getVisualizationSpec(description: string, data: any, layers: any[]): TopLevelSpec { - return { - config: { - view: { stroke: 'transparent' }, - legend: { - labelFontSize: 14, - titleFontWeight: 600, - titleLineHeight: 21, - titleFontSize: 14, - titlePadding: 10, - rowPadding: 6, - labelFont: - '"Inter UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', - }, - }, - $schema: 'https://vega.github.io/schema/vega-lite/v5.json', - description: description, - data: { - values: data, - }, - layer: layers, - }; -} - /** * Recalculates vertical domain range to add a bit of space * so that the topmost items in the chart are not clipped @@ -141,279 +113,6 @@ export const getYDomainRange = (data: any[], timeUnit: string): number[] => { return [0, domainMax.length + 0.1]; }; -export function getOverviewVisualizationSpec( - visualizationData: SummaryData[], - groupBy: string, - dateOpts: DateOpts = { - timeUnit: defaultTimeUnit, - dateFormat: defaultDateFormat, - domain: defaultScaleDomain, - } -): TopLevelSpec { - const findingsEncoding: { [x: string]: any } = { - x: getXAxis(dateOpts), - y: getYAxis('finding', 'Count', true, { - scale: { - domain: getYDomainRange(visualizationData, dateOpts.timeUnit.unit), - }, - }), - tooltip: [getYAxis('finding', 'Findings'), getTimeTooltip(dateOpts)], - color: { - field: 'fieldType', - title: '', - legend: { - values: ['Active alerts', 'Findings'], - }, - scale: { - domain: ['Active alerts', 'Findings'], - range: [alertsDefaultColor, euiPaletteColorBlind()[1]], - }, - }, - }; - - let barLayer = { - mark: { - type: 'bar', - clip: true, - }, - transform: [{ calculate: "datum.alert == 1 ? 'Active alerts' : 'Findings'", as: 'fieldType' }], - encoding: findingsEncoding, - }; - - if (groupBy === 'logType') { - findingsEncoding['color'] = { - field: 'logType', - type: 'nominal', - title: 'Log type', - scale: { - range: euiPaletteColorBlind(), - }, - }; - - findingsEncoding['tooltip'].push({ - field: groupBy, - title: groupBy === 'logType' ? 'Log type' : 'Rule severity', - }); - - barLayer = addInteractiveLegends(barLayer); - } - - return getVisualizationSpec( - 'Plot showing average data with raw values in the background.', - visualizationData, - [ - barLayer, - { - mark: { - type: 'line', - clip: true, - interpolate: 'monotone', - color: alertsDefaultColor, - point: { - filled: false, - fill: 'white', - color: alertsDefaultColor, - size: 50, - }, - }, - encoding: { - x: getXAxis(dateOpts, { - band: 0.5, - }), - y: getYAxis('alert', 'Count'), - tooltip: [getYAxis('alert', 'Alerts'), getTimeTooltip(dateOpts)], - }, - }, - ] - ); -} - -export function getFindingsVisualizationSpec( - visualizationData: any[], - groupBy: string, - dateOpts: DateOpts = { - timeUnit: defaultTimeUnit, - dateFormat: defaultDateFormat, - domain: defaultScaleDomain, - } -) { - const severities = ['info', 'low', 'medium', 'high', 'critical']; - const isGroupedByLogType = groupBy === 'logType'; - const logTitle = 'Log type'; - const severityTitle = 'Rule severity'; - const title = isGroupedByLogType ? logTitle : severityTitle; - return getVisualizationSpec('Findings data overview', visualizationData, [ - addInteractiveLegends({ - mark: { - type: 'bar', - clip: true, - }, - encoding: { - tooltip: [ - getYAxis('finding', 'Findings'), - getTimeTooltip(dateOpts), - { - field: groupBy, - title: title, - }, - ], - x: getXAxis(dateOpts), - y: getYAxis('finding', 'Count'), - color: { - field: groupBy, - title: title, - scale: { - domain: isGroupedByLogType ? undefined : severities, - range: groupBy === 'logType' ? euiPaletteColorBlind() : euiPaletteForStatus(5), - }, - }, - }, - }), - ]); -} - -export function getThreatIntelFindingsVisualizationSpec( - visualizationData: any[], - groupBy: string, - dateOpts: DateOpts = { - timeUnit: defaultTimeUnit, - dateFormat: defaultDateFormat, - domain: defaultScaleDomain, - } -) { - const indicatorTypeTitle = 'Indicator type'; - - return getVisualizationSpec('Findings data overview', visualizationData, [ - addInteractiveLegends({ - mark: { - type: 'bar', - clip: true, - }, - encoding: { - tooltip: [ - getYAxis('finding', 'Findings'), - getTimeTooltip(dateOpts), - { - field: groupBy, - title: indicatorTypeTitle, - }, - ], - x: getXAxis(dateOpts), - y: getYAxis('finding', 'Count'), - color: { - field: groupBy, - title: indicatorTypeTitle, - scale: { - range: euiPaletteColorBlind(), - }, - }, - }, - }), - ]); -} - -export function getAlertsVisualizationSpec( - visualizationData: any[], - groupBy: string, - dateOpts: DateOpts = { - timeUnit: defaultTimeUnit, - dateFormat: defaultDateFormat, - domain: defaultScaleDomain, - } -) { - const isGroupedByStatus = groupBy === 'status'; - let severities = severityOptions.map((severity) => severity.text); - severities.reverse().pop(); - - let states = ['ACTIVE', 'ACKNOWLEDGED', 'COMPLETED', 'ERROR']; - const statusColors = { - ACTIVE: '#E7664C', - ACKNOWLEDGED: '#6092C0', - COMPLETED: '#54B399', - ERROR: '#B9A888', - }; - - const statusTitle = 'Alert status'; - const severityTitle = 'Alert severity'; - const title = isGroupedByStatus ? statusTitle : severityTitle; - return getVisualizationSpec('Alerts data overview', visualizationData, [ - addInteractiveLegends({ - mark: { - type: 'bar', - clip: true, - }, - encoding: { - tooltip: [ - getYAxis('alert', 'Alerts'), - getTimeTooltip(dateOpts), - { - field: groupBy, - title: title, - }, - ], - x: getXAxis(dateOpts), - y: getYAxis('alert', 'Count'), - color: { - field: groupBy, - title: title, - scale: { - domain: isGroupedByStatus ? states : severities, - range: isGroupedByStatus ? Object.values(statusColors) : euiPaletteForStatus(5), - }, - }, - }, - }), - ]); -} - -export function getTopRulesVisualizationSpec(visualizationData: any[]) { - return getVisualizationSpec('Most frequent detection rules', visualizationData, [ - { - mark: { type: 'arc', innerRadius: 90 }, - transform: [ - { - joinaggregate: [ - { - op: 'sum', - field: 'count', - as: 'total', - }, - ], - }, - { - calculate: 'datum.count/datum.total', - as: 'percentage', - }, - ], - encoding: { - tooltip: [ - { - field: 'percentage', - title: 'Percentage', - type: 'quantitative', - format: '2.0%', - }, - { - field: 'ruleName', - type: 'nominal', - title: 'Rule', - }, - getYAxis('count', 'Count'), - ], - theta: getYAxis('count', ''), - color: { - field: 'ruleName', - type: 'nominal', - title: 'Rule name', - scale: { - range: euiPaletteColorBlind(), - }, - }, - }, - }, - ]); -} - export function getTimeWithMinPrecision(time: number | string) { const date = new Date(time); date.setSeconds(0); diff --git a/public/utils/chartUtils.tsx b/public/utils/chartUtils.tsx new file mode 100644 index 000000000..e59550284 --- /dev/null +++ b/public/utils/chartUtils.tsx @@ -0,0 +1,558 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { euiPaletteColorBlind, euiPaletteForStatus } from '@elastic/eui'; +import { + BarController, + BarElement, + CategoryScale, + Chart, + Legend, + LinearScale, + TimeScale, + Title, + Tooltip, + LineElement, + PointElement, + LineController, + ArcElement, + DoughnutController, +} from 'chart.js'; +import datemath from '@elastic/datemath'; +import moment from 'moment'; +import 'chartjs-adapter-moment'; +import { DEFAULT_DATE_RANGE, FindingTabId } from './constants'; +import { alertsGroupByOptions } from '../pages/Alerts/containers/Alerts/Alerts'; +import { groupByOptionsByTabId } from '../pages/Findings/containers/Findings/Findings'; +import { summaryGroupByOptions } from '../pages/Overview/utils/constants'; + +export const createBarChartWrapper = ( + data: any[], + groupBy: string, + container: string = 'myBarChart', + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + } +) => { + try { + createBarChart(data, groupBy, container, dateTimeFilter); + } catch (e) { + console.error(`Error while compiling bar chart ${container}`, e); + return <>{/*TODO: hurneyt Error panel of some kind*/}; + } +}; + +const createBarChart = ( + data: any[], + groupBy: string, + container: string = 'myBarChart', + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + } +) => { + // Register the required components + Chart.register( + CategoryScale, + LinearScale, + BarController, + BarElement, + Title, + Tooltip, + Legend, + TimeScale + ); + + const groupByOption = getGroupByText(groupBy); + + const defaultColorPalette = euiPaletteColorBlind(); + const severityColorPalette = euiPaletteForStatus(5); + + // Calculate the time difference in milliseconds + const start = datemath.parse(dateTimeFilter.startTime)!; + const end = datemath.parse(dateTimeFilter.endTime)!; + const diffInHours = end?.diff(start, 'hour') || 0; + + // Determine the appropriate time unit and format based on the range + let timeUnit: 'hour' | 'day' | 'week' | 'month'; + let displayFormat: string; + let tickLimit: number; + let stepSize: number; + + if (diffInHours <= 24) { + // Last 24 hours - show hourly ticks + timeUnit = 'hour'; + displayFormat = 'HH:mm'; + tickLimit = 24; + stepSize = 1; // Show every hour + } else if (diffInHours <= 72) { + // Last 3 days - show every 3 hours + timeUnit = 'hour'; + displayFormat = 'MMM D HH:mm'; + tickLimit = 24; + stepSize = 3; // Show every 3 hours + } else if (diffInHours <= 168) { + // Last week - show daily ticks + timeUnit = 'day'; + displayFormat = 'MMM D'; + tickLimit = 7; + stepSize = 6; // Show every 6 hours + } else { + // More than a week - show weekly ticks + timeUnit = 'week'; + displayFormat = 'MMM D'; + tickLimit = 10; + stepSize = 12; // Show every 12 hours + } + // Destroy existing chart if it exists + const existingChart = Chart.getChart(container); + if (existingChart) { + existingChart.destroy(); + } + + // Group data by the selected field + const uniqueGroups = [...new Set(data.map((item) => (item as any)[groupBy]))]; + + // Group data by time periods + const timeLabels = [...new Set(data.map((item) => item.time))] + .sort((a, b) => a - b) + .map((time) => new Date(time).toLocaleString()); + + // Create datasets for each group + const datasets = uniqueGroups.map((group, idx) => { + const color = groupBy.includes('severity') + ? severityColorPalette[idx % severityColorPalette.length] + : defaultColorPalette[idx % defaultColorPalette.length]; + + const counts: Record = {}; + return { + label: group, + data: data + .filter((item: any) => item[groupBy] === group) + .map((item) => { + counts[item.time] = (counts[item.time] || 0) + 1; + return { + x: moment(item.time).toDate(), // Convert to JavaScript Date object + y: counts[item.time], + }; + }), + backgroundColor: color, + borderColor: color, + borderWidth: 1, + }; + }); + + const ctx = document.getElementById(container) as HTMLCanvasElement; + if (!ctx) { + return; + } + + new Chart(ctx, { + type: 'bar', + data: { + labels: timeLabels, + datasets: datasets, + }, + options: { + responsive: true, + scales: { + x: { + type: 'time', + time: { + unit: timeUnit, // or 'hour', 'day', etc. + displayFormats: { + minute: 'HH:mm', + hour: 'HH:mm', + day: 'MMM d', + week: 'MMM d', + month: 'MMM yyyy', + quarter: 'MMM yyyy', + year: 'yyyy', + }, + }, + bounds: 'ticks', + offset: true, + ticks: { + source: 'auto', + autoSkip: true, + maxRotation: 45, + maxTicksLimit: tickLimit, // Adjust this to show more/fewer labels + stepSize: stepSize, + major: { + enabled: true, + }, + callback: function (value) { + // Format the tick label + return moment(value).format(displayFormat); + }, + }, + min: start.valueOf(), // Add start time + max: end.valueOf(), // Add end time + stacked: true, + title: { + display: true, + text: 'Time', + }, + }, + y: { + stacked: true, + beginAtZero: true, + title: { + display: true, + text: 'Count', + }, + ticks: { + stepSize: 1, + precision: 0, // This prevents decimal places + }, + }, + }, + plugins: { + tooltip: { + callbacks: { + label: function (context: any) { + const label = context.dataset.label || ''; + const value = context.parsed.y; + const timeLabel = context.label; + return `${groupByOption.text}: ${label} + Count: ${value} + Time: ${timeLabel}`; + }, + }, + }, + legend: { + position: 'top', + }, + title: { + display: true, + text: `Grouped by ${groupByOption.text}`, + }, + }, + }, + }); +}; + +export const createBarAndLineChartWrapper = ( + alertData: any[], + findingData: any[], + groupBy: string, + container: string = 'myBarAndLineChart', + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + } +) => { + try { + createBarAndLineChart(alertData, findingData, groupBy, container, dateTimeFilter); + } catch (e) { + console.error(`Error while compiling bar/line chart ${container}`, e); + return <>{/*TODO: hurneyt Error panel of some kind*/}; + } +}; + +const createBarAndLineChart = ( + alertData: any[], + findingData: any[], + groupBy: string, + container: string = 'myChart', + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + } +) => { + // Register the required components + Chart.register( + CategoryScale, + LinearScale, + BarController, + BarElement, + Title, + Tooltip, + Legend, + TimeScale, + LineElement, + PointElement, + LineController + ); + + const groupByOption = getGroupByText(groupBy); + + const defaultColorPalette = euiPaletteColorBlind(); + const severityColorPalette = euiPaletteForStatus(5); + + // Calculate the time difference in milliseconds + const start = datemath.parse(dateTimeFilter.startTime)!; + const end = datemath.parse(dateTimeFilter.endTime)!; + const diffInHours = end?.diff(start, 'hour') || 0; + + // Determine the appropriate time unit and format based on the range + let timeUnit: 'hour' | 'day' | 'week' | 'month'; + let displayFormat: string; + let tickLimit: number; + let stepSize: number; + + if (diffInHours <= 24) { + // Last 24 hours - show hourly ticks + timeUnit = 'hour'; + displayFormat = 'HH:mm'; + tickLimit = 24; + stepSize = 1; // Show every hour + } else if (diffInHours <= 72) { + // Last 3 days - show every 3 hours + timeUnit = 'hour'; + displayFormat = 'MMM D HH:mm'; + tickLimit = 24; + stepSize = 3; // Show every 3 hours + } else if (diffInHours <= 168) { + // Last week - show daily ticks + timeUnit = 'day'; + displayFormat = 'MMM D'; + tickLimit = 7; + stepSize = 6; // Show every 6 hours + } else { + // More than a week - show weekly ticks + timeUnit = 'week'; + displayFormat = 'MMM D'; + tickLimit = 10; + stepSize = 12; // Show every 12 hours + } + // Destroy existing chart if it exists + const existingChart = Chart.getChart(container); + if (existingChart) { + existingChart.destroy(); + } + + // Group data by the selected field + const uniqueGroups = + groupBy === 'logType' + ? [...new Set(findingData.map((item) => (item as any)[groupBy]))] + : ['All findings']; + + // Group data by time periods + const timeLabels = [...new Set(findingData.map((item) => item.time))] + .sort((a, b) => a - b) + .map((time) => new Date(time).toLocaleString()); + + // Create datasets for each group + const datasets = uniqueGroups.map((group, idx) => { + const color = groupBy.includes('severity') + ? severityColorPalette[idx % severityColorPalette.length] + : defaultColorPalette[idx % defaultColorPalette.length]; + + const counts: Record = {}; + return { + type: 'bar', + order: 2, + label: group, + data: findingData + // Overview page only supports grouping by logType, otherwise all findings should be mapped + .filter((item: any) => groupBy !== 'logType' || item[groupBy] === group) + .map((item) => { + counts[item.time] = (counts[item.time] || 0) + 1; + return { + x: moment(item.time).toDate(), // Convert to JavaScript Date object + y: counts[item.time], + }; + }), + backgroundColor: color, + borderColor: color, + borderWidth: 1, + }; + }); + + const alertCounts: Record = {}; + alertData.forEach((item) => { + alertCounts[item.time] = (alertCounts[item.time] || 0) + 1; + }); + const transData = Object.entries(alertCounts).map(([time, count]) => { + return { + x: moment(time).toDate(), // Convert to JavaScript Date object + y: count, + }; + }); + console.info(`hurneyt transData =`, transData); + const alertsDataset = { + type: 'line', + order: 1, + label: 'Alerts', + data: transData, + backgroundColor: 'warning', + borderColor: 'warning', + borderWidth: 1, + }; + + const ctx = document.getElementById(container) as HTMLCanvasElement; + if (!ctx) { + return; + } + + new Chart(ctx, { + type: 'bar', + data: { + labels: timeLabels, + datasets: [...datasets, alertsDataset], + }, + options: { + responsive: true, + scales: { + x: { + type: 'time', + time: { + unit: timeUnit, // or 'hour', 'day', etc. + displayFormats: { + minute: 'HH:mm', + hour: 'HH:mm', + day: 'MMM d', + week: 'MMM d', + month: 'MMM yyyy', + quarter: 'MMM yyyy', + year: 'yyyy', + }, + }, + bounds: 'ticks', + offset: true, + ticks: { + source: 'auto', + autoSkip: true, + maxRotation: 45, + maxTicksLimit: tickLimit, // Adjust this to show more/fewer labels + stepSize: stepSize, + major: { + enabled: true, + }, + callback: function (value) { + // Format the tick label + return moment(value).format(displayFormat); + }, + }, + min: start.valueOf(), // Add start time + max: end.valueOf(), // Add end time + stacked: true, + title: { + display: true, + text: 'Time', + }, + }, + y: { + stacked: true, + beginAtZero: true, + title: { + display: true, + text: 'Count', + }, + ticks: { + stepSize: 1, + precision: 0, // This prevents decimal places + }, + }, + }, + plugins: { + tooltip: { + callbacks: { + label: function (context: any) { + const label = context.dataset.label || ''; + const value = context.parsed.y; + const timeLabel = context.label; + return `${groupByOption.text}: ${label} + Count: ${value} + Time: ${timeLabel}`; + }, + }, + }, + legend: { + position: 'top', + }, + title: { + display: true, + text: `Grouped by ${groupByOption.text}`, + }, + }, + }, + }); +}; + +export const createDoughnutChartWrapper = ( + visData: { + ruleName: string; + count: number; + }[] = [], + container: string = 'myDoughnutChart' +) => { + try { + createDoughnutChart(visData, container); + } catch (e) { + console.error(`Error while compiling doughnut chart ${container}`, e); + return <>{/*TODO: hurneyt Error panel of some kind*/}; + } +}; + +const createDoughnutChart = ( + visData: { + ruleName: string; + count: number; + }[] = [], + container: string = 'myDoughnutChart' +) => { + // Register the required components + Chart.register( + DoughnutController, // for doughnut charts + ArcElement, // for doughnut/pie charts + Tooltip, + Legend + ); + + const ctx = document.getElementById(container) as HTMLCanvasElement; + if (!ctx) { + return; + } + + const labels: string[] = []; + const counts: number[] = []; + visData.forEach((item) => { + labels.push(item.ruleName); + counts.push(item.count); + }); + new Chart(ctx, { + type: 'doughnut', + data: { + labels: labels, + datasets: [ + { + data: counts, + backgroundColor: euiPaletteColorBlind(labels.length), + }, + ], + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + tooltip: { + callbacks: { + label: function (context) { + const label = context.label || ''; + const value = context.parsed; + return `${label}: ${value}`; + }, + }, + }, + }, + }, + }); +}; + +const getGroupByText = (groupByValue: string) => { + const allOptions = [ + ...alertsGroupByOptions, + ...groupByOptionsByTabId[FindingTabId.DetectionRules], + ...groupByOptionsByTabId[FindingTabId.ThreatIntel], + ...summaryGroupByOptions, + ]; + return allOptions.filter((item) => item.value === groupByValue)[0] || { text: '-', value: '-' }; +}; diff --git a/public/utils/helpers.tsx b/public/utils/helpers.tsx index eed5c5ad7..d4009bb3a 100644 --- a/public/utils/helpers.tsx +++ b/public/utils/helpers.tsx @@ -58,10 +58,6 @@ import { LogCategoryOptionView } from '../components/Utility/LogCategoryOption'; import { getLogTypeLabel } from '../pages/LogTypes/utils/helpers'; import { euiThemeVars } from '@osd/ui-shared-deps/theme'; import dateMath from '@elastic/datemath'; -import { parse, View } from 'vega/build-es5/vega.js'; -import { compile } from 'vega-lite'; -import { Handler } from 'vega-tooltip'; -import { expressionInterpreter as vegaExpressionInterpreter } from 'vega-interpreter/build/vega-interpreter'; import { getBreadCrumbsSetter, getBrowserServices, @@ -221,80 +217,6 @@ export function getUpdatedEnabledRuleIds( return newEnabledIds; } -export async function renderVisualization(spec: any, containerId: string) { - let view; - - try { - setDefaultColors(spec); - renderVegaSpec(compile({ ...spec, width: 'container', height: 400 }).spec).catch((err: Error) => - console.error(err) - ); - } catch (error) { - console.error(error); - } - - async function renderVegaSpec(spec: {}) { - let chartColoredItems: any[] = []; - const handler = new Handler({ - formatTooltip: (value, sanitize) => { - let tooltipData = { ...value }; - let values = Object.entries(tooltipData); - if (!values.length) return ''; - const tooltipItem = chartColoredItems.filter((groupItem: any) => - _.isEqual(groupItem.tooltip, tooltipData) - ); - const color = tooltipItem.length - ? tooltipItem[0].fill || tooltipItem[0].stroke - : 'transparent'; - - const firstItem = values.pop() || ['', '']; - - let rowData = ''; - values.forEach((item: any) => { - rowData += ` - - ${sanitize(item[0])} - ${sanitize(item[1])} - - `; - }); - - return ` -
-
- - - - - - -
${sanitize(firstItem[0])}${sanitize(firstItem[1])}
-
-
- ${rowData}
-
-
- `; - }, - }); - view = new View(parse(spec, undefined, { expr: vegaExpressionInterpreter } as any), { - renderer: 'canvas', // renderer (canvas or svg) - container: `#${containerId}`, // parent DOM container - hover: true, // enable hover processing - }); - view.tooltip(handler.call); - return view.runAsync().then((view: any) => { - const items = view.scenegraph().root.items[0].items || []; - const groups = items.filter( - (item: any) => item.name && item.name.match(/^(layer_).*(_marks)$/) - ); - for (let item of groups) { - chartColoredItems = chartColoredItems.concat(item.items); - } - }); - } -} - export function createSelectComponent( options: EuiSelectOption[], value: string, diff --git a/yarn.lock b/yarn.lock index c142fab61..194d5a774 100644 --- a/yarn.lock +++ b/yarn.lock @@ -677,6 +677,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" + integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== + "@napi-rs/wasm-runtime@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.1.1.tgz#ec090e2f46bee2ed0c8486dd9d97ddca836ae30e" @@ -1767,6 +1772,18 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chart.js@^4.4.8: + version "4.4.8" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.8.tgz#54645b638e9d585099bc16b892947b5e6cd2a552" + integrity sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA== + dependencies: + "@kurkle/color" "^0.3.0" + +chartjs-adapter-moment@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz#0f04c30d330b207c14bfb57dfaae9ce332f09102" + integrity sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA== + check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" From d3e350df043abc5767536dc85aed93fa795e6c55 Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Sat, 15 Mar 2025 18:42:43 -0700 Subject: [PATCH 2/3] Removed visualizations as chart.js dependency is also not building into distribution. Signed-off-by: AWSHurneyt --- package.json | 3 +- .../pages/Alerts/containers/Alerts/Alerts.tsx | 32 +- .../Findings/containers/Findings/Findings.tsx | 49 +- .../Overview/components/Widgets/Summary.tsx | 84 +- .../components/Widgets/TopRulesWidget.tsx | 4 +- .../Overview/containers/Overview/Overview.tsx | 2 +- public/utils/chartUtils.tsx | 1130 +++++++++-------- yarn.lock | 9 +- 8 files changed, 687 insertions(+), 626 deletions(-) diff --git a/package.json b/package.json index 25512d41e..97362170d 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "jest-environment-jsdom": "^27.5.1", "lint-staged": "^10.2.0", "string.prototype.replaceall": "1.0.7", - "ts-loader": "^6.2.1" + "ts-loader": "^6.2.1", + "@types/chart.js": "^2.9.37" }, "engines": { "yarn": "^1.21.1" diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index ce3f154e5..1859e96c4 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -69,7 +69,7 @@ import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { DataStore } from '../../../../store/DataStore'; import { ThreatIntelAlertsTable } from '../../components/ThreatIntelAlertsTable/ThreatIntelAlertsTable'; import { PageHeader } from '../../../../components/PageHeader/PageHeader'; -import { createBarChartWrapper } from '../../../../utils/chartUtils'; +// import { createBarChartWrapper } from '../../../../utils/chartUtils'; type FilterAlertParams = | { alerts: AlertItem[]; timeField: 'last_notification_time' } @@ -243,7 +243,7 @@ export class Alerts extends Component { /> ), }); - this.getChart(this.getVisData(filteredAlerts)); + // this.getChart(this.getVisData(filteredAlerts)); }; filterCorrelationAlerts = () => { @@ -267,7 +267,7 @@ export class Alerts extends Component { /> ), }); - this.getChart(this.getVisData(filteredCorrelationAlerts)); + // this.getChart(this.getVisData(filteredCorrelationAlerts)); }; filterThreatIntelAlerts = () => { @@ -291,19 +291,19 @@ export class Alerts extends Component { /> ), }); - this.getChart(this.getVisData(filteredAlerts)); + // this.getChart(this.getVisData(filteredAlerts)); }; private renderVisAsPerTab() { switch (this.state.selectedTabId) { case AlertTabId.DetectionRules: - this.getChart(this.getVisData(this.state.filteredAlerts)); + // this.getChart(this.getVisData(this.state.filteredAlerts)); break; case AlertTabId.Correlations: - this.getChart(this.getVisData(this.state.filteredCorrelationAlerts)); + // this.getChart(this.getVisData(this.state.filteredCorrelationAlerts)); break; case AlertTabId.ThreatIntel: - this.getChart(this.getVisData(this.state.filteredThreatIntelAlerts)); + // this.getChart(this.getVisData(this.state.filteredThreatIntelAlerts)); break; } } @@ -504,9 +504,9 @@ export class Alerts extends Component { this.setState({ flyoutCorrelationData: alertItem ? { alertItem } : undefined }); } - getChart(data: any) { - createBarChartWrapper(data, this.state.groupBy, ALERTS_VIEW_CHART, this.props.dateTimeFilter); - } + // getChart(data: any) { + // createBarChartWrapper(data, this.state.groupBy, ALERTS_VIEW_CHART, this.props.dateTimeFilter); + // } getVisData(alerts: (AlertItem | CorrelationAlertTableItem | ThreatIntelAlert)[]) { return alerts.map((alert) => { @@ -999,8 +999,8 @@ export class Alerts extends Component { content: ( <> - {this.getAlertsGraph(alerts, loading)} - + {/*{this.getAlertsGraph(alerts, loading)}*/} + {/**/} { content: ( <> - {this.getAlertsGraph(alerts, loading)} - + {/*{this.getAlertsGraph(alerts, loading)}*/} + {/**/} { content: ( <> - {this.getAlertsGraph(alerts, loading)} - + {/*{this.getAlertsGraph(alerts, loading)}*/} + {/**/} { this.state.selectedTabId !== prevState.selectedTabId ) { this.onRefresh(); - } else if (this.shouldUpdateVisualization(prevState)) { - const data = this.generateVisualizationData(); - this.createStackedBarChart(data.visData, data.groupBy); } + // else if (this.shouldUpdateVisualization(prevState)) { + // const data = this.generateVisualizationData(); + // this.createStackedBarChart(data.visData, data.groupBy); + // } } componentDidMount = async () => { @@ -235,8 +236,8 @@ class Findings extends Component { } else if (this.state.selectedTabId === FindingTabId.ThreatIntel) { await this.getThreatIntelFindings(); } - const data = this.generateVisualizationData(); - this.createStackedBarChart(data.visData, data.groupBy); + // const data = this.generateVisualizationData(); + // this.createStackedBarChart(data.visData, data.groupBy); }; setStateForTab( @@ -489,20 +490,20 @@ class Findings extends Component { }); }; - createStackedBarChart( - data: (FindingVisualizationData | ThreatIntelFindingVisualizationData)[], - groupBy: string - ) { - // Calculate the time difference in milliseconds - const { - dateTimeFilter = { - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, - }, - } = this.props; - - createBarChartWrapper(data, groupBy, FINDINGS_VIEW_CHART, dateTimeFilter); - } + // createStackedBarChart( + // data: (FindingVisualizationData | ThreatIntelFindingVisualizationData)[], + // groupBy: string + // ) { + // // Calculate the time difference in milliseconds + // const { + // dateTimeFilter = { + // startTime: DEFAULT_DATE_RANGE.start, + // endTime: DEFAULT_DATE_RANGE.end, + // }, + // } = this.props; + // + // createBarChartWrapper(data, groupBy, FINDINGS_VIEW_CHART, dateTimeFilter); + // } render() { const { @@ -563,8 +564,8 @@ class Findings extends Component { content: ( <> - {this.getFindingsGraph(findings, loading)} - + {/*{this.getFindingsGraph(findings, loading)}*/} + {/**/} { content: ( <> - {this.getFindingsGraph(findings, loading)} - + {/*{this.getFindingsGraph(findings, loading)}*/} + {/**/} = ({ setFindingsVisData(findingsVisData); }, [alerts, findings]); - useEffect(() => { - createBarAndLineChartWrapper(alertsVisData, findingsVisData, groupBy, SUMMARY_VIEW_CHART, { - startTime, - endTime, - }); - }, [alertsVisData, findingsVisData, groupBy]); + // useEffect(() => { + // createBarAndLineChartWrapper(alertsVisData, findingsVisData, groupBy, SUMMARY_VIEW_CHART, { + // startTime, + // endTime, + // }); + // }, [alertsVisData, findingsVisData, groupBy]); const createStatComponent = useCallback( (description: string, urlData: { url: string; color: EuiLinkColor }, stat?: number) => ( @@ -149,41 +149,41 @@ export const Summary: React.FC = ({ )} )} - - {activeAlerts === 0 && totalFindings === 0 ? ( - -

No alerts and findings found

- - } - body={ - <> -

- - Adjust the time range to see more results or create a
- detector to generate findings. -
-

- - Create a detector - - - } - /> - ) : ( -
- -
- )} -
+ {/**/} + {/* {activeAlerts === 0 && totalFindings === 0 ? (*/} + {/* */} + {/*

No alerts and findings found

*/} + {/* */} + {/* }*/} + {/* body={*/} + {/* <>*/} + {/*

*/} + {/* */} + {/* Adjust the time range to see more results or create a
*/} + {/* detector to generate findings.*/} + {/*
*/} + {/*

*/} + {/* */} + {/* Create a detector*/} + {/* */} + {/* */} + {/* }*/} + {/* />*/} + {/* ) : (*/} + {/*
*/} + {/* */} + {/*
*/} + {/* )}*/} + {/*
*/} ); diff --git a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx index 45de884e2..1b0407831 100644 --- a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx +++ b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx @@ -7,7 +7,7 @@ import { getEuiEmptyPrompt } from '../../../../utils/helpers'; import React, { useEffect } from 'react'; import { WidgetContainer } from './WidgetContainer'; import { OverviewFindingItem } from '../../../../../types'; -import { createDoughnutChartWrapper } from '../../../../utils/chartUtils'; +// import { createDoughnutChartWrapper } from '../../../../utils/chartUtils'; export interface TopRulesWidgetProps { findings: OverviewFindingItem[]; @@ -30,7 +30,7 @@ export const TopRulesWidget: React.FC = ({ findings, loadin ruleName, count: rulesCount[ruleName], })); - createDoughnutChartWrapper(visualizationData, TOP_RULES_VIEW_CHART); + // createDoughnutChartWrapper(visualizationData, TOP_RULES_VIEW_CHART); } }, [findings]); diff --git a/public/pages/Overview/containers/Overview/Overview.tsx b/public/pages/Overview/containers/Overview/Overview.tsx index 0a4782303..ec1fc07e0 100644 --- a/public/pages/Overview/containers/Overview/Overview.tsx +++ b/public/pages/Overview/containers/Overview/Overview.tsx @@ -313,7 +313,7 @@ export const Overview: React.FC = (props) => { - + {/**/} { - try { - createBarChart(data, groupBy, container, dateTimeFilter); - } catch (e) { - console.error(`Error while compiling bar chart ${container}`, e); - return <>{/*TODO: hurneyt Error panel of some kind*/}; - } -}; - -const createBarChart = ( - data: any[], - groupBy: string, - container: string = 'myBarChart', - dateTimeFilter = { - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, - } -) => { - // Register the required components - Chart.register( - CategoryScale, - LinearScale, - BarController, - BarElement, - Title, - Tooltip, - Legend, - TimeScale - ); - - const groupByOption = getGroupByText(groupBy); - - const defaultColorPalette = euiPaletteColorBlind(); - const severityColorPalette = euiPaletteForStatus(5); - - // Calculate the time difference in milliseconds - const start = datemath.parse(dateTimeFilter.startTime)!; - const end = datemath.parse(dateTimeFilter.endTime)!; - const diffInHours = end?.diff(start, 'hour') || 0; - - // Determine the appropriate time unit and format based on the range - let timeUnit: 'hour' | 'day' | 'week' | 'month'; - let displayFormat: string; - let tickLimit: number; - let stepSize: number; - - if (diffInHours <= 24) { - // Last 24 hours - show hourly ticks - timeUnit = 'hour'; - displayFormat = 'HH:mm'; - tickLimit = 24; - stepSize = 1; // Show every hour - } else if (diffInHours <= 72) { - // Last 3 days - show every 3 hours - timeUnit = 'hour'; - displayFormat = 'MMM D HH:mm'; - tickLimit = 24; - stepSize = 3; // Show every 3 hours - } else if (diffInHours <= 168) { - // Last week - show daily ticks - timeUnit = 'day'; - displayFormat = 'MMM D'; - tickLimit = 7; - stepSize = 6; // Show every 6 hours - } else { - // More than a week - show weekly ticks - timeUnit = 'week'; - displayFormat = 'MMM D'; - tickLimit = 10; - stepSize = 12; // Show every 12 hours - } - // Destroy existing chart if it exists - const existingChart = Chart.getChart(container); - if (existingChart) { - existingChart.destroy(); - } - - // Group data by the selected field - const uniqueGroups = [...new Set(data.map((item) => (item as any)[groupBy]))]; - - // Group data by time periods - const timeLabels = [...new Set(data.map((item) => item.time))] - .sort((a, b) => a - b) - .map((time) => new Date(time).toLocaleString()); - - // Create datasets for each group - const datasets = uniqueGroups.map((group, idx) => { - const color = groupBy.includes('severity') - ? severityColorPalette[idx % severityColorPalette.length] - : defaultColorPalette[idx % defaultColorPalette.length]; - - const counts: Record = {}; - return { - label: group, - data: data - .filter((item: any) => item[groupBy] === group) - .map((item) => { - counts[item.time] = (counts[item.time] || 0) + 1; - return { - x: moment(item.time).toDate(), // Convert to JavaScript Date object - y: counts[item.time], - }; - }), - backgroundColor: color, - borderColor: color, - borderWidth: 1, - }; - }); - - const ctx = document.getElementById(container) as HTMLCanvasElement; - if (!ctx) { - return; - } - - new Chart(ctx, { - type: 'bar', - data: { - labels: timeLabels, - datasets: datasets, - }, - options: { - responsive: true, - scales: { - x: { - type: 'time', - time: { - unit: timeUnit, // or 'hour', 'day', etc. - displayFormats: { - minute: 'HH:mm', - hour: 'HH:mm', - day: 'MMM d', - week: 'MMM d', - month: 'MMM yyyy', - quarter: 'MMM yyyy', - year: 'yyyy', - }, - }, - bounds: 'ticks', - offset: true, - ticks: { - source: 'auto', - autoSkip: true, - maxRotation: 45, - maxTicksLimit: tickLimit, // Adjust this to show more/fewer labels - stepSize: stepSize, - major: { - enabled: true, - }, - callback: function (value) { - // Format the tick label - return moment(value).format(displayFormat); - }, - }, - min: start.valueOf(), // Add start time - max: end.valueOf(), // Add end time - stacked: true, - title: { - display: true, - text: 'Time', - }, - }, - y: { - stacked: true, - beginAtZero: true, - title: { - display: true, - text: 'Count', - }, - ticks: { - stepSize: 1, - precision: 0, // This prevents decimal places - }, - }, - }, - plugins: { - tooltip: { - callbacks: { - label: function (context: any) { - const label = context.dataset.label || ''; - const value = context.parsed.y; - const timeLabel = context.label; - return `${groupByOption.text}: ${label} - Count: ${value} - Time: ${timeLabel}`; - }, - }, - }, - legend: { - position: 'top', - }, - title: { - display: true, - text: `Grouped by ${groupByOption.text}`, - }, - }, - }, - }); -}; - -export const createBarAndLineChartWrapper = ( - alertData: any[], - findingData: any[], - groupBy: string, - container: string = 'myBarAndLineChart', - dateTimeFilter = { - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, - } -) => { - try { - createBarAndLineChart(alertData, findingData, groupBy, container, dateTimeFilter); - } catch (e) { - console.error(`Error while compiling bar/line chart ${container}`, e); - return <>{/*TODO: hurneyt Error panel of some kind*/}; - } -}; - -const createBarAndLineChart = ( - alertData: any[], - findingData: any[], - groupBy: string, - container: string = 'myChart', - dateTimeFilter = { - startTime: DEFAULT_DATE_RANGE.start, - endTime: DEFAULT_DATE_RANGE.end, - } -) => { - // Register the required components - Chart.register( - CategoryScale, - LinearScale, - BarController, - BarElement, - Title, - Tooltip, - Legend, - TimeScale, - LineElement, - PointElement, - LineController - ); - - const groupByOption = getGroupByText(groupBy); - - const defaultColorPalette = euiPaletteColorBlind(); - const severityColorPalette = euiPaletteForStatus(5); - - // Calculate the time difference in milliseconds - const start = datemath.parse(dateTimeFilter.startTime)!; - const end = datemath.parse(dateTimeFilter.endTime)!; - const diffInHours = end?.diff(start, 'hour') || 0; - - // Determine the appropriate time unit and format based on the range - let timeUnit: 'hour' | 'day' | 'week' | 'month'; - let displayFormat: string; - let tickLimit: number; - let stepSize: number; - - if (diffInHours <= 24) { - // Last 24 hours - show hourly ticks - timeUnit = 'hour'; - displayFormat = 'HH:mm'; - tickLimit = 24; - stepSize = 1; // Show every hour - } else if (diffInHours <= 72) { - // Last 3 days - show every 3 hours - timeUnit = 'hour'; - displayFormat = 'MMM D HH:mm'; - tickLimit = 24; - stepSize = 3; // Show every 3 hours - } else if (diffInHours <= 168) { - // Last week - show daily ticks - timeUnit = 'day'; - displayFormat = 'MMM D'; - tickLimit = 7; - stepSize = 6; // Show every 6 hours - } else { - // More than a week - show weekly ticks - timeUnit = 'week'; - displayFormat = 'MMM D'; - tickLimit = 10; - stepSize = 12; // Show every 12 hours - } - // Destroy existing chart if it exists - const existingChart = Chart.getChart(container); - if (existingChart) { - existingChart.destroy(); - } - - // Group data by the selected field - const uniqueGroups = - groupBy === 'logType' - ? [...new Set(findingData.map((item) => (item as any)[groupBy]))] - : ['All findings']; - - // Group data by time periods - const timeLabels = [...new Set(findingData.map((item) => item.time))] - .sort((a, b) => a - b) - .map((time) => new Date(time).toLocaleString()); - - // Create datasets for each group - const datasets = uniqueGroups.map((group, idx) => { - const color = groupBy.includes('severity') - ? severityColorPalette[idx % severityColorPalette.length] - : defaultColorPalette[idx % defaultColorPalette.length]; - - const counts: Record = {}; - return { - type: 'bar', - order: 2, - label: group, - data: findingData - // Overview page only supports grouping by logType, otherwise all findings should be mapped - .filter((item: any) => groupBy !== 'logType' || item[groupBy] === group) - .map((item) => { - counts[item.time] = (counts[item.time] || 0) + 1; - return { - x: moment(item.time).toDate(), // Convert to JavaScript Date object - y: counts[item.time], - }; - }), - backgroundColor: color, - borderColor: color, - borderWidth: 1, - }; - }); - - const alertCounts: Record = {}; - alertData.forEach((item) => { - alertCounts[item.time] = (alertCounts[item.time] || 0) + 1; - }); - const transData = Object.entries(alertCounts).map(([time, count]) => { - return { - x: moment(time).toDate(), // Convert to JavaScript Date object - y: count, - }; - }); - console.info(`hurneyt transData =`, transData); - const alertsDataset = { - type: 'line', - order: 1, - label: 'Alerts', - data: transData, - backgroundColor: 'warning', - borderColor: 'warning', - borderWidth: 1, - }; - - const ctx = document.getElementById(container) as HTMLCanvasElement; - if (!ctx) { - return; - } - - new Chart(ctx, { - type: 'bar', - data: { - labels: timeLabels, - datasets: [...datasets, alertsDataset], - }, - options: { - responsive: true, - scales: { - x: { - type: 'time', - time: { - unit: timeUnit, // or 'hour', 'day', etc. - displayFormats: { - minute: 'HH:mm', - hour: 'HH:mm', - day: 'MMM d', - week: 'MMM d', - month: 'MMM yyyy', - quarter: 'MMM yyyy', - year: 'yyyy', - }, - }, - bounds: 'ticks', - offset: true, - ticks: { - source: 'auto', - autoSkip: true, - maxRotation: 45, - maxTicksLimit: tickLimit, // Adjust this to show more/fewer labels - stepSize: stepSize, - major: { - enabled: true, - }, - callback: function (value) { - // Format the tick label - return moment(value).format(displayFormat); - }, - }, - min: start.valueOf(), // Add start time - max: end.valueOf(), // Add end time - stacked: true, - title: { - display: true, - text: 'Time', - }, - }, - y: { - stacked: true, - beginAtZero: true, - title: { - display: true, - text: 'Count', - }, - ticks: { - stepSize: 1, - precision: 0, // This prevents decimal places - }, - }, - }, - plugins: { - tooltip: { - callbacks: { - label: function (context: any) { - const label = context.dataset.label || ''; - const value = context.parsed.y; - const timeLabel = context.label; - return `${groupByOption.text}: ${label} - Count: ${value} - Time: ${timeLabel}`; - }, - }, - }, - legend: { - position: 'top', - }, - title: { - display: true, - text: `Grouped by ${groupByOption.text}`, - }, - }, - }, - }); -}; - -export const createDoughnutChartWrapper = ( - visData: { - ruleName: string; - count: number; - }[] = [], - container: string = 'myDoughnutChart' -) => { - try { - createDoughnutChart(visData, container); - } catch (e) { - console.error(`Error while compiling doughnut chart ${container}`, e); - return <>{/*TODO: hurneyt Error panel of some kind*/}; - } -}; - -const createDoughnutChart = ( - visData: { - ruleName: string; - count: number; - }[] = [], - container: string = 'myDoughnutChart' -) => { - // Register the required components - Chart.register( - DoughnutController, // for doughnut charts - ArcElement, // for doughnut/pie charts - Tooltip, - Legend - ); - - const ctx = document.getElementById(container) as HTMLCanvasElement; - if (!ctx) { - return; - } - - const labels: string[] = []; - const counts: number[] = []; - visData.forEach((item) => { - labels.push(item.ruleName); - counts.push(item.count); - }); - new Chart(ctx, { - type: 'doughnut', - data: { - labels: labels, - datasets: [ - { - data: counts, - backgroundColor: euiPaletteColorBlind(labels.length), - }, - ], - }, - options: { - responsive: true, - plugins: { - legend: { - position: 'top', - }, - tooltip: { - callbacks: { - label: function (context) { - const label = context.label || ''; - const value = context.parsed; - return `${label}: ${value}`; - }, - }, - }, - }, - }, - }); -}; +// import { getEuiEmptyPrompt } from "./helpers"; +// import { +// BarController, +// BarElement, +// CategoryScale, +// Chart, +// Legend, +// LinearScale, +// TimeScale, +// Title, +// Tooltip, +// LineElement, +// PointElement, +// LineController, +// ArcElement, +// DoughnutController, +// } from 'chart.js'; +// import 'chartjs-adapter-moment'; + +// /** +// * This function is intended to be used to prevent the UI page failing to load when +// * there is a webpack/build issues that makes chart.js unavailable. +// * +// * An empty UI panel will appear in place of the chart. +// * +// * @param callback +// * @param container +// */ +// const initializeChart = (container: string) => { +// try { +// // Try to access Chart.js - this will throw if exports is not defined +// require('chart.js'); +// require('chartjs-adapter-moment'); +// } catch (e) { +// console.error(`Chart initialization error for ${container}:`, e); +// const chartContainer = document.getElementById(container)?.parentElement; +// if (chartContainer) { +// const errorMessage = getEuiEmptyPrompt('Unable to load chart visualization.'); +// chartContainer.innerHTML = ` +//
+// ${errorMessage} +//
+// `; +// } +// } +// }; +// +// export const createBarChartWrapper = ( +// data: any[], +// groupBy: string, +// container: string = 'myBarChart', +// dateTimeFilter = { +// startTime: DEFAULT_DATE_RANGE.start, +// endTime: DEFAULT_DATE_RANGE.end, +// } +// ) => { +// initializeChart(container); +// try { +// createBarChart(data, groupBy, container, dateTimeFilter); +// } catch (e) { +// console.error(`Error while compiling bar chart ${container}`, e); +// const chartContainer = document.getElementById(container)?.parentElement; +// if (chartContainer) { +// chartContainer.innerHTML = ` +//
+// Error while compiling bar chart ${container} +//
+// `; +// } +// } +// }; +// +// const createBarChart = ( +// data: any[], +// groupBy: string, +// container: string = 'myBarChart', +// dateTimeFilter = { +// startTime: DEFAULT_DATE_RANGE.start, +// endTime: DEFAULT_DATE_RANGE.end, +// } +// ) => { +// // Register the required components +// Chart.register( +// CategoryScale, +// LinearScale, +// BarController, +// BarElement, +// Title, +// Tooltip, +// Legend, +// TimeScale +// ); +// +// const groupByOption = getGroupByText(groupBy); +// +// const defaultColorPalette = euiPaletteColorBlind(); +// const severityColorPalette = euiPaletteForStatus(5); +// +// // Calculate the time difference in milliseconds +// const start = datemath.parse(dateTimeFilter.startTime)!; +// const end = datemath.parse(dateTimeFilter.endTime)!; +// const diffInHours = end?.diff(start, 'hour') || 0; +// +// // Determine the appropriate time unit and format based on the range +// let timeUnit: 'hour' | 'day' | 'week' | 'month'; +// let displayFormat: string; +// let tickLimit: number; +// let stepSize: number; +// +// if (diffInHours <= 24) { +// // Last 24 hours - show hourly ticks +// timeUnit = 'hour'; +// displayFormat = 'HH:mm'; +// tickLimit = 24; +// stepSize = 1; // Show every hour +// } else if (diffInHours <= 72) { +// // Last 3 days - show every 3 hours +// timeUnit = 'hour'; +// displayFormat = 'MMM D HH:mm'; +// tickLimit = 24; +// stepSize = 3; // Show every 3 hours +// } else if (diffInHours <= 168) { +// // Last week - show daily ticks +// timeUnit = 'day'; +// displayFormat = 'MMM D'; +// tickLimit = 7; +// stepSize = 6; // Show every 6 hours +// } else { +// // More than a week - show weekly ticks +// timeUnit = 'week'; +// displayFormat = 'MMM D'; +// tickLimit = 10; +// stepSize = 12; // Show every 12 hours +// } +// // Destroy existing chart if it exists +// const existingChart = Chart.getChart(container); +// if (existingChart) { +// existingChart.destroy(); +// } +// +// // Group data by the selected field +// const uniqueGroups = [...new Set(data.map((item) => (item as any)[groupBy]))]; +// +// // Group data by time periods +// const timeLabels = [...new Set(data.map((item) => item.time))] +// .sort((a, b) => a - b) +// .map((time) => new Date(time).toLocaleString()); +// +// // Create datasets for each group +// const datasets = uniqueGroups.map((group, idx) => { +// const color = groupBy.includes('severity') +// ? severityColorPalette[idx % severityColorPalette.length] +// : defaultColorPalette[idx % defaultColorPalette.length]; +// +// const counts: Record = {}; +// return { +// label: group, +// data: data +// .filter((item: any) => item[groupBy] === group) +// .map((item) => { +// counts[item.time] = (counts[item.time] || 0) + 1; +// return { +// x: moment(item.time).toDate(), // Convert to JavaScript Date object +// y: counts[item.time], +// }; +// }), +// backgroundColor: color, +// borderColor: color, +// borderWidth: 1, +// }; +// }); +// +// const ctx = document.getElementById(container) as HTMLCanvasElement; +// if (!ctx) { +// return; +// } +// +// new Chart(ctx, { +// type: 'bar', +// data: { +// labels: timeLabels, +// datasets: datasets, +// }, +// options: { +// responsive: true, +// scales: { +// x: { +// type: 'time', +// time: { +// unit: timeUnit, // or 'hour', 'day', etc. +// displayFormats: { +// minute: 'HH:mm', +// hour: 'HH:mm', +// day: 'MMM d', +// week: 'MMM d', +// month: 'MMM yyyy', +// quarter: 'MMM yyyy', +// year: 'yyyy', +// }, +// }, +// bounds: 'ticks', +// offset: true, +// ticks: { +// source: 'auto', +// autoSkip: true, +// maxRotation: 45, +// maxTicksLimit: tickLimit, // Adjust this to show more/fewer labels +// stepSize: stepSize, +// major: { +// enabled: true, +// }, +// callback: function (value) { +// // Format the tick label +// return moment(value).format(displayFormat); +// }, +// }, +// min: start.valueOf(), // Add start time +// max: end.valueOf(), // Add end time +// stacked: true, +// title: { +// display: true, +// text: 'Time', +// }, +// }, +// y: { +// stacked: true, +// beginAtZero: true, +// title: { +// display: true, +// text: 'Count', +// }, +// ticks: { +// stepSize: 1, +// precision: 0, // This prevents decimal places +// }, +// }, +// }, +// plugins: { +// tooltip: { +// callbacks: { +// label: function (context: any) { +// const label = context.dataset.label || ''; +// const value = context.parsed.y; +// const timeLabel = context.label; +// return `${groupByOption.text}: ${label} +// Count: ${value} +// Time: ${timeLabel}`; +// }, +// }, +// }, +// legend: { +// position: 'top', +// }, +// title: { +// display: true, +// text: `Grouped by ${groupByOption.text}`, +// }, +// }, +// }, +// }); +// }; +// +// export const createBarAndLineChartWrapper = ( +// alertData: any[], +// findingData: any[], +// groupBy: string, +// container: string = 'myBarAndLineChart', +// dateTimeFilter = { +// startTime: DEFAULT_DATE_RANGE.start, +// endTime: DEFAULT_DATE_RANGE.end, +// } +// ) => { +// initializeChart(container); +// try { +// createBarAndLineChart(alertData, findingData, groupBy, container, dateTimeFilter); +// } catch (e) { +// console.error(`Error while compiling bar/line chart ${container}`, e); +// const chartContainer = document.getElementById(container)?.parentElement; +// if (chartContainer) { +// chartContainer.innerHTML = ` +//
+// Error while compiling bar/line chart ${container} +//
+// `; +// } +// } +// }; +// +// const createBarAndLineChart = ( +// alertData: any[], +// findingData: any[], +// groupBy: string, +// container: string = 'myChart', +// dateTimeFilter = { +// startTime: DEFAULT_DATE_RANGE.start, +// endTime: DEFAULT_DATE_RANGE.end, +// } +// ) => { +// // Register the required components +// Chart.register( +// CategoryScale, +// LinearScale, +// BarController, +// BarElement, +// Title, +// Tooltip, +// Legend, +// TimeScale, +// LineElement, +// PointElement, +// LineController +// ); +// +// const groupByOption = getGroupByText(groupBy); +// +// const defaultColorPalette = euiPaletteColorBlind(); +// const severityColorPalette = euiPaletteForStatus(5); +// +// // Calculate the time difference in milliseconds +// const start = datemath.parse(dateTimeFilter.startTime)!; +// const end = datemath.parse(dateTimeFilter.endTime)!; +// const diffInHours = end?.diff(start, 'hour') || 0; +// +// // Determine the appropriate time unit and format based on the range +// let timeUnit: 'hour' | 'day' | 'week' | 'month'; +// let displayFormat: string; +// let tickLimit: number; +// let stepSize: number; +// +// if (diffInHours <= 24) { +// // Last 24 hours - show hourly ticks +// timeUnit = 'hour'; +// displayFormat = 'HH:mm'; +// tickLimit = 24; +// stepSize = 1; // Show every hour +// } else if (diffInHours <= 72) { +// // Last 3 days - show every 3 hours +// timeUnit = 'hour'; +// displayFormat = 'MMM D HH:mm'; +// tickLimit = 24; +// stepSize = 3; // Show every 3 hours +// } else if (diffInHours <= 168) { +// // Last week - show daily ticks +// timeUnit = 'day'; +// displayFormat = 'MMM D'; +// tickLimit = 7; +// stepSize = 6; // Show every 6 hours +// } else { +// // More than a week - show weekly ticks +// timeUnit = 'week'; +// displayFormat = 'MMM D'; +// tickLimit = 10; +// stepSize = 12; // Show every 12 hours +// } +// // Destroy existing chart if it exists +// const existingChart = Chart.getChart(container); +// if (existingChart) { +// existingChart.destroy(); +// } +// +// // Group data by the selected field +// const uniqueGroups = +// groupBy === 'logType' +// ? [...new Set(findingData.map((item) => (item as any)[groupBy]))] +// : ['All findings']; +// +// // Group data by time periods +// const timeLabels = [...new Set(findingData.map((item) => item.time))] +// .sort((a, b) => a - b) +// .map((time) => new Date(time).toLocaleString()); +// +// // Create datasets for each group +// const datasets = uniqueGroups.map((group, idx) => { +// const color = groupBy.includes('severity') +// ? severityColorPalette[idx % severityColorPalette.length] +// : defaultColorPalette[idx % defaultColorPalette.length]; +// +// const counts: Record = {}; +// return { +// type: 'bar', +// order: 2, +// label: group, +// data: findingData +// // Overview page only supports grouping by logType, otherwise all findings should be mapped +// .filter((item: any) => groupBy !== 'logType' || item[groupBy] === group) +// .map((item) => { +// counts[item.time] = (counts[item.time] || 0) + 1; +// return { +// x: moment(item.time).toDate(), // Convert to JavaScript Date object +// y: counts[item.time], +// }; +// }), +// backgroundColor: color, +// borderColor: color, +// borderWidth: 1, +// }; +// }); +// +// const alertCounts: Record = {}; +// alertData.forEach((item) => { +// alertCounts[item.time] = (alertCounts[item.time] || 0) + 1; +// }); +// const transData = Object.entries(alertCounts).map(([time, count]) => { +// return { +// x: moment(time).toDate(), // Convert to JavaScript Date object +// y: count, +// }; +// }); +// console.info(`hurneyt transData =`, transData); +// const alertsDataset = { +// type: 'line', +// order: 1, +// label: 'Alerts', +// data: transData, +// backgroundColor: 'warning', +// borderColor: 'warning', +// borderWidth: 1, +// }; +// +// const ctx = document.getElementById(container) as HTMLCanvasElement; +// if (!ctx) { +// return; +// } +// +// new Chart(ctx, { +// type: 'bar', +// data: { +// labels: timeLabels, +// datasets: [...datasets, alertsDataset], +// }, +// options: { +// responsive: true, +// scales: { +// x: { +// type: 'time', +// time: { +// unit: timeUnit, // or 'hour', 'day', etc. +// displayFormats: { +// minute: 'HH:mm', +// hour: 'HH:mm', +// day: 'MMM d', +// week: 'MMM d', +// month: 'MMM yyyy', +// quarter: 'MMM yyyy', +// year: 'yyyy', +// }, +// }, +// bounds: 'ticks', +// offset: true, +// ticks: { +// source: 'auto', +// autoSkip: true, +// maxRotation: 45, +// maxTicksLimit: tickLimit, // Adjust this to show more/fewer labels +// stepSize: stepSize, +// major: { +// enabled: true, +// }, +// callback: function (value) { +// // Format the tick label +// return moment(value).format(displayFormat); +// }, +// }, +// min: start.valueOf(), // Add start time +// max: end.valueOf(), // Add end time +// stacked: true, +// title: { +// display: true, +// text: 'Time', +// }, +// }, +// y: { +// stacked: true, +// beginAtZero: true, +// title: { +// display: true, +// text: 'Count', +// }, +// ticks: { +// stepSize: 1, +// precision: 0, // This prevents decimal places +// }, +// }, +// }, +// plugins: { +// tooltip: { +// callbacks: { +// label: function (context: any) { +// const label = context.dataset.label || ''; +// const value = context.parsed.y; +// const timeLabel = context.label; +// return `${groupByOption.text}: ${label} +// Count: ${value} +// Time: ${timeLabel}`; +// }, +// }, +// }, +// legend: { +// position: 'top', +// }, +// title: { +// display: true, +// text: `Grouped by ${groupByOption.text}`, +// }, +// }, +// }, +// }); +// }; +// +// export const createDoughnutChartWrapper = ( +// visData: { +// ruleName: string; +// count: number; +// }[] = [], +// container: string = 'myDoughnutChart' +// ) => { +// initializeChart(container); +// try { +// createDoughnutChart(visData, container); +// } catch (e) { +// console.error(`Error while compiling doughnut chart ${container}`, e); +// const chartContainer = document.getElementById(container)?.parentElement; +// if (chartContainer) { +// chartContainer.innerHTML = ` +//
+// Error while compiling doughnut chart ${container} +//
+// `; +// } +// } +// }; +// +// const createDoughnutChart = ( +// visData: { +// ruleName: string; +// count: number; +// }[] = [], +// container: string = 'myDoughnutChart' +// ) => { +// // Register the required components +// Chart.register( +// DoughnutController, // for doughnut charts +// ArcElement, // for doughnut/pie charts +// Tooltip, +// Legend +// ); +// +// const ctx = document.getElementById(container) as HTMLCanvasElement; +// if (!ctx) { +// return; +// } +// +// const labels: string[] = []; +// const counts: number[] = []; +// visData.forEach((item) => { +// labels.push(item.ruleName); +// counts.push(item.count); +// }); +// new Chart(ctx, { +// type: 'doughnut', +// data: { +// labels: labels, +// datasets: [ +// { +// data: counts, +// backgroundColor: euiPaletteColorBlind(labels.length), +// }, +// ], +// }, +// options: { +// responsive: true, +// plugins: { +// legend: { +// position: 'top', +// }, +// tooltip: { +// callbacks: { +// label: function (context) { +// const label = context.label || ''; +// const value = context.parsed; +// return `${label}: ${value}`; +// }, +// }, +// }, +// }, +// }, +// }); +// }; const getGroupByText = (groupByValue: string) => { const allOptions = [ diff --git a/yarn.lock b/yarn.lock index 194d5a774..0c01e34d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -876,6 +876,13 @@ dependencies: "@babel/types" "^7.20.7" +"@types/chart.js@^2.9.37": + version "2.9.41" + resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.41.tgz#4148cdc87d4f98fad44b2883271cd0fa57f05e0d" + integrity sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ== + dependencies: + moment "^2.10.2" + "@types/graceful-fs@^4.1.2": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -4450,7 +4457,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.3: dependencies: minimist "^1.2.6" -moment@>=2.13.0, moment@^2.29.2: +moment@>=2.13.0, moment@^2.10.2, moment@^2.29.2: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== From 64695b4966007f83ba782ee7ababad68d3b3ad8d Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Sat, 15 Mar 2025 18:50:09 -0700 Subject: [PATCH 3/3] Updated snapshots. Signed-off-by: AWSHurneyt --- .../Alerts/__snapshots__/Alerts.test.tsx.snap | 572 ------------------ 1 file changed, 572 deletions(-) diff --git a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap index 8c07a5059..b4651f427 100644 --- a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap +++ b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap @@ -835,79 +835,6 @@ exports[` spec renders the component 1`] = ` initialSelectedTab={ Object { "content": - - - - - - - - - - - - - - Adjust the time range to see more results or create alert triggers in your - - - detectors - - to generate alerts. - -

- } - title={ - -

- No alerts -

-
- } - /> -
-
-
@@ -1039,79 +966,6 @@ exports[` spec renders the component 1`] = ` Array [ Object { "content": - - - - - - - - - - - - - - Adjust the time range to see more results or create alert triggers in your - - - detectors - - to generate alerts. - -

- } - title={ - -

- No alerts -

-
- } - /> -
-
-
@@ -1238,79 +1092,6 @@ exports[` spec renders the component 1`] = ` }, Object { "content": - - - - - - - - - - - - - - Adjust the time range to see more results or create alert triggers in your - - - detectors - - to generate alerts. - -

- } - title={ - -

- No alerts -

-
- } - /> -
-
-
@@ -1340,79 +1121,6 @@ exports[` spec renders the component 1`] = ` }, Object { "content": - - - - - - - - - - - - - - Adjust the time range to see more results or create alert triggers in your - - - detectors - - to generate alerts. - -

- } - title={ - -

- No alerts -

-
- } - /> -
-
-
@@ -1670,286 +1378,6 @@ exports[` spec renders the component 1`] = ` id="some_html_id" role="tabpanel" > - -
- - -
- -
- -
- -
- -
- - - -
- - - -
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - - Adjust the time range to see more results or create alert triggers in your - - - detectors - - to generate alerts. - -

- } - title={ - -

- No alerts -

-
- } - > -
- - -
-

- No alerts -

-
-
-
- - - -
- - -
-

- -

- Adjust the time range to see more results or create alert triggers in your - - - - detectors - - - to generate alerts. -
- -

-
-
- - -
- -
- -
- -
-