diff --git a/package.json b/package.json index 7c94813f2..e1b9f5e13 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "plugin-helpers": "node ../../scripts/plugin_helpers", "test:jest": "../../node_modules/.bin/jest --config ./test/jest.config.js", "test:jest:dev": "../../node_modules/.bin/jest --watch --config ./test/jest.config.js", + "test:jest:update-snapshots": "yarn run test:jest -u", "build": "yarn plugin-helpers build", "postbuild": "echo Renaming build artifact to [$npm_package_config_zip_name-$npm_package_version.zip] && mv build/$npm_package_config_id*.zip build/$npm_package_config_zip_name-$npm_package_version.zip" }, diff --git a/public/app.scss b/public/app.scss index 1682d852e..ffbc4e3a0 100644 --- a/public/app.scss +++ b/public/app.scss @@ -116,3 +116,7 @@ $euiTextColor: $euiColorDarkestShade !default; } } } + +.sa-overview-widget-empty tbody > .euiTableRow > .euiTableRowCell { + border-bottom: none; +} diff --git a/public/pages/Overview/components/Widgets/DetectorsWidget.tsx b/public/pages/Overview/components/Widgets/DetectorsWidget.tsx index 03c8f9ecc..262cfb448 100644 --- a/public/pages/Overview/components/Widgets/DetectorsWidget.tsx +++ b/public/pages/Overview/components/Widgets/DetectorsWidget.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiButton, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { ROUTES } from '../../../../utils/constants'; import React, { useCallback } from 'react'; import { DetectorItem } from '../../models/interfaces'; @@ -71,6 +71,23 @@ export const DetectorsWidget: React.FC = ({ }); }, []); + const widgetEmptyMessage = + detectors.length === 0 ? ( + + No security detectors.Create a detector to + generate findings. +

+ } + actions={[ + + Create detector + , + ]} + /> + ) : undefined; + const actions = React.useMemo( () => [ View all detectors, @@ -85,6 +102,8 @@ export const DetectorsWidget: React.FC = ({ columns={getColumns(detectorIdToHit, showDetectorDetails)} items={detectors} loading={loading} + message={widgetEmptyMessage} + className={widgetEmptyMessage ? 'sa-overview-widget-empty' : undefined} /> ); diff --git a/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx b/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx index 398291a3c..62979c10a 100644 --- a/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx +++ b/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBasicTableColumn, EuiButton } from '@elastic/eui'; -import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants'; +import { EuiBasicTableColumn, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { DEFAULT_EMPTY_DATA, ROUTES, SortDirection } from '../../../../utils/constants'; import React, { useEffect, useState } from 'react'; import { AlertItem } from '../../models/interfaces'; import { TableWidget } from './TableWidget'; @@ -45,6 +45,9 @@ export const RecentAlertsWidget: React.FC = ({ loading = false, }) => { const [alertItems, setAlertItems] = useState([]); + const [widgetEmptyMessage, setwidgetEmptyMessage] = useState( + undefined + ); useEffect(() => { items.sort((a, b) => { @@ -53,6 +56,18 @@ export const RecentAlertsWidget: React.FC = ({ return timeA - timeB; }); setAlertItems(items.slice(0, 20)); + setwidgetEmptyMessage( + items.length > 0 ? undefined : ( + + No recent alerts.Adjust the time range to + see more results. +

+ } + /> + ) + ); }, [items]); const actions = React.useMemo( @@ -62,7 +77,14 @@ export const RecentAlertsWidget: React.FC = ({ return ( - + ); }; diff --git a/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx b/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx index 2e0bbe1df..aa9d658e5 100644 --- a/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx +++ b/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBasicTableColumn, EuiButton } from '@elastic/eui'; -import { ROUTES } from '../../../../utils/constants'; +import { EuiBasicTableColumn, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { ROUTES, SortDirection } from '../../../../utils/constants'; import React, { useEffect, useState } from 'react'; import { FindingItem } from '../../models/interfaces'; import { TableWidget } from './TableWidget'; @@ -52,12 +52,27 @@ export const RecentFindingsWidget: React.FC = ({ loading = false, }) => { const [findingItems, setFindingItems] = useState([]); + const [widgetEmptyMessage, setWidgetEmptyMessage] = useState( + undefined + ); useEffect(() => { items.sort((a, b) => { return a.time - b.time; }); setFindingItems(items.slice(0, 20)); + setWidgetEmptyMessage( + items.length > 0 ? undefined : ( + + No recent findings.Adjust the time range to + see more results. +

+ } + /> + ) + ); }, [items]); const actions = React.useMemo( @@ -67,7 +82,14 @@ export const RecentFindingsWidget: React.FC = ({ return ( - + ); }; diff --git a/public/pages/Overview/components/Widgets/Summary.tsx b/public/pages/Overview/components/Widgets/Summary.tsx index 91d96d26e..56ae1e54d 100644 --- a/public/pages/Overview/components/Widgets/Summary.tsx +++ b/public/pages/Overview/components/Widgets/Summary.tsx @@ -3,13 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiStat } from '@elastic/eui'; -import { euiPaletteColorBlind } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiLinkColor, + EuiStat, +} from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { WidgetContainer } from './WidgetContainer'; import { summaryGroupByOptions } from '../../utils/constants'; import { - alertsDefaultColor, getChartTimeUnit, getDomainRange, getOverviewVisualizationSpec, @@ -46,8 +51,8 @@ export const Summary: React.FC = ({ }) => { const [groupBy, setGroupBy] = useState(''); const [summaryData, setSummaryData] = useState([]); - const [activeAlerts, setActiveAlerts] = useState(0); - const [totalFindings, setTotalFindings] = useState(0); + const [activeAlerts, setActiveAlerts] = useState(undefined); + const [totalFindings, setTotalFindings] = useState(undefined); const onGroupByChange = useCallback((event) => { setGroupBy(event.target.value); @@ -113,44 +118,62 @@ export const Summary: React.FC = ({ renderVisualization(generateVisualizationSpec(summaryData, groupBy), 'summary-view'); }, [summaryData, groupBy]); + const createStatComponent = useCallback( + (description: string, urlData: { url: string; color: EuiLinkColor }, stat?: number) => ( + + + {stat} + + ) + } + description={description} + textAlign="left" + titleSize="l" + titleColor="subdued" + isLoading={stat === undefined} + /> + + ), + [] + ); + return ( - - - {activeAlerts} - - } - description="Total active alerts" - textAlign="left" - titleColor="primary" - isLoading={!activeAlerts} - /> - - - - {totalFindings} - - } - description="Total findings" - textAlign="left" - titleColor="primary" - isLoading={!totalFindings} - /> - + {createStatComponent( + 'Total active alerts', + { url: ROUTES.ALERTS, color: 'danger' }, + activeAlerts + )} + {createStatComponent( + 'Total findings', + { url: ROUTES.FINDINGS, color: 'primary' }, + totalFindings + )} - + {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.{' '} +

+ } + /> + ) : ( + + )}
diff --git a/public/pages/Overview/components/Widgets/TableWidget.tsx b/public/pages/Overview/components/Widgets/TableWidget.tsx index f59bd4da9..1d9915258 100644 --- a/public/pages/Overview/components/Widgets/TableWidget.tsx +++ b/public/pages/Overview/components/Widgets/TableWidget.tsx @@ -9,16 +9,19 @@ import { TableWidgetItem, TableWidgetProps } from '../../models/types'; export class TableWidget extends React.Component> { render() { - const { columns, items, loading = false } = this.props; + const { columns, items, sorting, message, className, loading = false } = this.props; return ( + className={className} compressed columns={columns} items={items} itemId={(item: T) => `${item.id}`} pagination={{ pageSize: 10, pageSizeOptions: [10] }} + sorting={sorting} loading={loading} + message={message} /> ); } diff --git a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx index 40de1f699..0404f3086 100644 --- a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx +++ b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx @@ -9,6 +9,7 @@ import { FindingItem } from '../../models/interfaces'; import { WidgetContainer } from './WidgetContainer'; import { getTopRulesVisualizationSpec } from '../../utils/helpers'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; +import { EuiEmptyPrompt } from '@elastic/eui'; export interface TopRulesWidgetProps { findings: FindingItem[]; @@ -35,7 +36,21 @@ export const TopRulesWidget: React.FC = ({ findings, loadin return ( - + {findings.length === 0 ? ( + +

+ No findings with detection rules.Adjust + the time range to see more results. +

+ + } + /> + ) : ( + + )}
); }; diff --git a/public/pages/Overview/models/types.ts b/public/pages/Overview/models/types.ts index d5d6b7786..83ed06681 100644 --- a/public/pages/Overview/models/types.ts +++ b/public/pages/Overview/models/types.ts @@ -4,6 +4,7 @@ */ import { EuiBasicTableColumn } from '@elastic/eui'; +import { SortDirection } from '../../../utils/constants'; import { AlertItem, DetectorItem, FindingItem } from './interfaces'; export type TableWidgetItem = FindingItem | AlertItem | DetectorItem; @@ -11,5 +12,13 @@ export type TableWidgetItem = FindingItem | AlertItem | DetectorItem; export type TableWidgetProps = { columns: EuiBasicTableColumn[]; items: T[]; + sorting?: { + sort: { + field: string; + direction: SortDirection; + }; + }; + className?: string; loading?: boolean; + message?: React.ReactNode; };