Skip to content

Commit

Permalink
Empty states for Overview page (#467)
Browse files Browse the repository at this point in the history
* added empty state UX for overview summary vis; added snapshot update command for jest test

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* updated sort direction for overview widgets

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* updated empty states for all widgets

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* refactored component

Signed-off-by: Amardeepsingh Siglani <[email protected]>

---------

Signed-off-by: Amardeepsingh Siglani <[email protected]>
  • Loading branch information
amsiglan authored Mar 8, 2023
1 parent b1af3a3 commit a88c791
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 44 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 4 additions & 0 deletions public/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,7 @@ $euiTextColor: $euiColorDarkestShade !default;
}
}
}

.sa-overview-widget-empty tbody > .euiTableRow > .euiTableRowCell {
border-bottom: none;
}
21 changes: 20 additions & 1 deletion public/pages/Overview/components/Widgets/DetectorsWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,6 +71,23 @@ export const DetectorsWidget: React.FC<DetectorsWidgetProps> = ({
});
}, []);

const widgetEmptyMessage =
detectors.length === 0 ? (
<EuiEmptyPrompt
body={
<p>
<span style={{ display: 'block' }}>No security detectors.</span>Create a detector to
generate findings.
</p>
}
actions={[
<EuiButton fill={false} href={`#${ROUTES.DETECTORS_CREATE}`}>
Create detector
</EuiButton>,
]}
/>
) : undefined;

const actions = React.useMemo(
() => [
<EuiButton href={`#${ROUTES.DETECTORS}`}>View all detectors</EuiButton>,
Expand All @@ -85,6 +102,8 @@ export const DetectorsWidget: React.FC<DetectorsWidgetProps> = ({
columns={getColumns(detectorIdToHit, showDetectorDetails)}
items={detectors}
loading={loading}
message={widgetEmptyMessage}
className={widgetEmptyMessage ? 'sa-overview-widget-empty' : undefined}
/>
</WidgetContainer>
);
Expand Down
28 changes: 25 additions & 3 deletions public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,6 +45,9 @@ export const RecentAlertsWidget: React.FC<RecentAlertsWidgetProps> = ({
loading = false,
}) => {
const [alertItems, setAlertItems] = useState<AlertItem[]>([]);
const [widgetEmptyMessage, setwidgetEmptyMessage] = useState<React.ReactNode | undefined>(
undefined
);

useEffect(() => {
items.sort((a, b) => {
Expand All @@ -53,6 +56,18 @@ export const RecentAlertsWidget: React.FC<RecentAlertsWidgetProps> = ({
return timeA - timeB;
});
setAlertItems(items.slice(0, 20));
setwidgetEmptyMessage(
items.length > 0 ? undefined : (
<EuiEmptyPrompt
body={
<p>
<span style={{ display: 'block' }}>No recent alerts.</span>Adjust the time range to
see more results.
</p>
}
/>
)
);
}, [items]);

const actions = React.useMemo(
Expand All @@ -62,7 +77,14 @@ export const RecentAlertsWidget: React.FC<RecentAlertsWidgetProps> = ({

return (
<WidgetContainer title={'Recent alerts'} actions={actions}>
<TableWidget columns={columns} items={alertItems} loading={loading} />
<TableWidget
columns={columns}
items={alertItems}
sorting={{ sort: { field: 'time', direction: SortDirection.DESC } }}
loading={loading}
message={widgetEmptyMessage}
className={widgetEmptyMessage ? 'sa-overview-widget-empty' : undefined}
/>
</WidgetContainer>
);
};
28 changes: 25 additions & 3 deletions public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,12 +52,27 @@ export const RecentFindingsWidget: React.FC<RecentFindingsWidgetProps> = ({
loading = false,
}) => {
const [findingItems, setFindingItems] = useState<FindingItem[]>([]);
const [widgetEmptyMessage, setWidgetEmptyMessage] = useState<React.ReactNode | undefined>(
undefined
);

useEffect(() => {
items.sort((a, b) => {
return a.time - b.time;
});
setFindingItems(items.slice(0, 20));
setWidgetEmptyMessage(
items.length > 0 ? undefined : (
<EuiEmptyPrompt
body={
<p>
<span style={{ display: 'block' }}>No recent findings.</span>Adjust the time range to
see more results.
</p>
}
/>
)
);
}, [items]);

const actions = React.useMemo(
Expand All @@ -67,7 +82,14 @@ export const RecentFindingsWidget: React.FC<RecentFindingsWidgetProps> = ({

return (
<WidgetContainer title={'Recent findings'} actions={actions}>
<TableWidget columns={columns} items={findingItems} loading={loading} />
<TableWidget
columns={columns}
items={findingItems}
sorting={{ sort: { field: 'time', direction: SortDirection.DESC } }}
loading={loading}
message={widgetEmptyMessage}
className={widgetEmptyMessage ? 'sa-overview-widget-empty' : undefined}
/>
</WidgetContainer>
);
};
93 changes: 58 additions & 35 deletions public/pages/Overview/components/Widgets/Summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -46,8 +51,8 @@ export const Summary: React.FC<SummaryProps> = ({
}) => {
const [groupBy, setGroupBy] = useState('');
const [summaryData, setSummaryData] = useState<SummaryData[]>([]);
const [activeAlerts, setActiveAlerts] = useState(0);
const [totalFindings, setTotalFindings] = useState(0);
const [activeAlerts, setActiveAlerts] = useState<undefined | number>(undefined);
const [totalFindings, setTotalFindings] = useState<undefined | number>(undefined);

const onGroupByChange = useCallback((event) => {
setGroupBy(event.target.value);
Expand Down Expand Up @@ -113,44 +118,62 @@ export const Summary: React.FC<SummaryProps> = ({
renderVisualization(generateVisualizationSpec(summaryData, groupBy), 'summary-view');
}, [summaryData, groupBy]);

const createStatComponent = useCallback(
(description: string, urlData: { url: string; color: EuiLinkColor }, stat?: number) => (
<EuiFlexItem grow={false}>
<EuiStat
title={
stat === 0 ? (
0
) : (
<EuiLink href={`#${urlData.url}`} color={urlData.color}>
{stat}
</EuiLink>
)
}
description={description}
textAlign="left"
titleSize="l"
titleColor="subdued"
isLoading={stat === undefined}
/>
</EuiFlexItem>
),
[]
);

return (
<WidgetContainer title="Findings and alert count" actions={createVisualizationActions(groupBy)}>
<EuiFlexGroup gutterSize="s" direction="column">
<EuiFlexItem>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
<EuiStat
title={
<EuiLink href={`#${ROUTES.ALERTS}`} style={{ color: alertsDefaultColor }}>
{activeAlerts}
</EuiLink>
}
description="Total active alerts"
textAlign="left"
titleColor="primary"
isLoading={!activeAlerts}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
title={
<EuiLink
href={`#${ROUTES.FINDINGS}`}
style={{ color: euiPaletteColorBlind()[1] }}
>
{totalFindings}
</EuiLink>
}
description="Total findings"
textAlign="left"
titleColor="primary"
isLoading={!totalFindings}
/>
</EuiFlexItem>
{createStatComponent(
'Total active alerts',
{ url: ROUTES.ALERTS, color: 'danger' },
activeAlerts
)}
{createStatComponent(
'Total findings',
{ url: ROUTES.FINDINGS, color: 'primary' },
totalFindings
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<ChartContainer chartViewId={'summary-view'} loading={loading} />
{activeAlerts === 0 && totalFindings === 0 ? (
<EuiEmptyPrompt
title={<h2>No alerts and findings found</h2>}
body={
<p>
Adjust the time range to see more results or{' '}
<EuiLink href={`#${ROUTES.DETECTORS_CREATE}`}>create a detector</EuiLink> to
generate findings.{' '}
</p>
}
/>
) : (
<ChartContainer chartViewId={'summary-view'} loading={loading} />
)}
</EuiFlexItem>
</EuiFlexGroup>
</WidgetContainer>
Expand Down
5 changes: 4 additions & 1 deletion public/pages/Overview/components/Widgets/TableWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import { TableWidgetItem, TableWidgetProps } from '../../models/types';

export class TableWidget<T extends TableWidgetItem> extends React.Component<TableWidgetProps<T>> {
render() {
const { columns, items, loading = false } = this.props;
const { columns, items, sorting, message, className, loading = false } = this.props;

return (
<EuiInMemoryTable<T>
className={className}
compressed
columns={columns}
items={items}
itemId={(item: T) => `${item.id}`}
pagination={{ pageSize: 10, pageSizeOptions: [10] }}
sorting={sorting}
loading={loading}
message={message}
/>
);
}
Expand Down
17 changes: 16 additions & 1 deletion public/pages/Overview/components/Widgets/TopRulesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -35,7 +36,21 @@ export const TopRulesWidget: React.FC<TopRulesWidgetProps> = ({ findings, loadin

return (
<WidgetContainer title="Most frequent detection rules">
<ChartContainer chartViewId={'top-rules-view'} loading={loading} />
{findings.length === 0 ? (
<EuiEmptyPrompt
style={{ position: 'relative' }}
body={
<div style={{ display: 'flex', justifyContent: 'center' }}>
<p style={{ position: 'absolute', top: 'calc(50% - 20px)' }}>
<span style={{ display: 'block' }}>No findings with detection rules.</span>Adjust
the time range to see more results.
</p>
</div>
}
/>
) : (
<ChartContainer chartViewId={'top-rules-view'} loading={loading} />
)}
</WidgetContainer>
);
};
9 changes: 9 additions & 0 deletions public/pages/Overview/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@
*/

import { EuiBasicTableColumn } from '@elastic/eui';
import { SortDirection } from '../../../utils/constants';
import { AlertItem, DetectorItem, FindingItem } from './interfaces';

export type TableWidgetItem = FindingItem | AlertItem | DetectorItem;

export type TableWidgetProps<T extends TableWidgetItem> = {
columns: EuiBasicTableColumn<T>[];
items: T[];
sorting?: {
sort: {
field: string;
direction: SortDirection;
};
};
className?: string;
loading?: boolean;
message?: React.ReactNode;
};

0 comments on commit a88c791

Please sign in to comment.