mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security] Dashboards table in landing page (#136221)
* dashboard landing cards * useSecurityDashboards hook implementation * useSecurityDashboards hook implementation * add savedObjectsTagging to security * tests implemented * rename section titles * remove test code * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * PR suggestions Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ccaa38e7f1
commit
d35a687c29
13 changed files with 663 additions and 13 deletions
|
@ -42,7 +42,8 @@
|
|||
"home",
|
||||
"telemetry",
|
||||
"dataViewFieldEditor",
|
||||
"osquery"
|
||||
"osquery",
|
||||
"savedObjectsTaggingOss"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Search } from '@elastic/eui';
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import {
|
||||
useSecurityDashboardsTableItems,
|
||||
useDashboardsTableColumns,
|
||||
} from '../../containers/dashboards/use_security_dashboards';
|
||||
|
||||
const DASHBOARDS_TABLE_SEARCH: Search = {
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DashboardsTable: React.FC = () => {
|
||||
const items = useSecurityDashboardsTableItems();
|
||||
const columns = useDashboardsTableColumns();
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="dashboards-table"
|
||||
items={items}
|
||||
columns={columns}
|
||||
search={DASHBOARDS_TABLE_SEARCH}
|
||||
pagination={true}
|
||||
sorting={true}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const DASHBOARD_TITLE = i18n.translate('xpack.securitySolution.dashboards.title', {
|
||||
defaultMessage: 'Title',
|
||||
});
|
||||
|
||||
export const DASHBOARDS_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.dashboards.description',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { TestProviders } from '../../mock/test_providers';
|
||||
import type { DashboardTableItem } from './use_security_dashboards';
|
||||
import {
|
||||
useDashboardsTableColumns,
|
||||
useSecurityDashboardsTableItems,
|
||||
} from './use_security_dashboards';
|
||||
|
||||
jest.mock('../../lib/kibana');
|
||||
|
||||
const TAG_ID = 'securityTagId';
|
||||
const basicResponse: DashboardTableItem[] = [
|
||||
{
|
||||
id: 'dashboardId1',
|
||||
type: 'dashboard',
|
||||
attributes: {
|
||||
title: 'title1',
|
||||
description: 'description1',
|
||||
},
|
||||
references: [{ type: 'tag', id: TAG_ID, name: 'tagName' }],
|
||||
},
|
||||
{
|
||||
id: 'dashboardId2',
|
||||
type: 'dashboard',
|
||||
attributes: {
|
||||
title: 'title2',
|
||||
description: 'description2',
|
||||
},
|
||||
references: [{ type: 'tag', id: TAG_ID, name: 'tagName' }],
|
||||
},
|
||||
];
|
||||
|
||||
const renderUseSecurityDashboardsTableItems = async () => {
|
||||
const renderedHook = renderHook(() => useSecurityDashboardsTableItems(), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
await act(async () => {
|
||||
// needed to let dashboard items to be updated from saved objects response
|
||||
await renderedHook.waitForNextUpdate();
|
||||
});
|
||||
return renderedHook;
|
||||
};
|
||||
|
||||
const renderUseDashboardsTableColumns = () =>
|
||||
renderHook(() => useDashboardsTableColumns(), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
describe('Security Dashboards hooks', () => {
|
||||
const mockSavedObjectsFind = useKibana().services.savedObjects.client.find as jest.Mock;
|
||||
mockSavedObjectsFind.mockImplementation(async (req) => {
|
||||
if (req.type === 'tag') {
|
||||
return { savedObjects: [{ id: TAG_ID }] };
|
||||
} else if (req.type === 'dashboard') {
|
||||
return { savedObjects: basicResponse };
|
||||
}
|
||||
return { savedObjects: [] };
|
||||
});
|
||||
|
||||
const mockGetRedirectUrl = jest.fn(() => '/path');
|
||||
useKibana().services.dashboard = {
|
||||
locator: { getRedirectUrl: mockGetRedirectUrl },
|
||||
} as unknown as DashboardStart;
|
||||
|
||||
const mockTaggingGetTableColumnDefinition = useKibana().services.savedObjectsTagging?.ui
|
||||
.getTableColumnDefinition as jest.Mock;
|
||||
const tagsColumn = {
|
||||
field: 'id', // set existing field to prevent test error
|
||||
name: 'Tags',
|
||||
'data-test-subj': 'dashboard-tags-field',
|
||||
};
|
||||
mockTaggingGetTableColumnDefinition.mockReturnValue(tagsColumn);
|
||||
|
||||
afterEach(() => {
|
||||
mockTaggingGetTableColumnDefinition.mockClear();
|
||||
mockGetRedirectUrl.mockClear();
|
||||
mockSavedObjectsFind.mockClear();
|
||||
});
|
||||
|
||||
describe('useSecurityDashboardsTableItems', () => {
|
||||
afterEach(() => {
|
||||
mockSavedObjectsFind.mockClear();
|
||||
});
|
||||
|
||||
it('should request when renders', async () => {
|
||||
await renderUseSecurityDashboardsTableItems();
|
||||
|
||||
expect(mockSavedObjectsFind).toHaveBeenCalledTimes(2);
|
||||
expect(mockSavedObjectsFind).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'tag', search: 'security' })
|
||||
);
|
||||
expect(mockSavedObjectsFind).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'dashboard', hasReference: { id: TAG_ID, type: 'tag' } })
|
||||
);
|
||||
});
|
||||
|
||||
it('should not re-request when re-rendered', async () => {
|
||||
const { rerender } = await renderUseSecurityDashboardsTableItems();
|
||||
|
||||
expect(mockSavedObjectsFind).toHaveBeenCalledTimes(2);
|
||||
act(() => rerender());
|
||||
expect(mockSavedObjectsFind).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('returns a memoized value', async () => {
|
||||
const { result, rerender } = await renderUseSecurityDashboardsTableItems();
|
||||
|
||||
const result1 = result.current;
|
||||
act(() => rerender());
|
||||
const result2 = result.current;
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
|
||||
it('should return dashboard items', async () => {
|
||||
const { result } = await renderUseSecurityDashboardsTableItems();
|
||||
|
||||
const [dashboard1, dashboard2] = basicResponse;
|
||||
expect(result.current).toStrictEqual([
|
||||
{
|
||||
...dashboard1,
|
||||
title: dashboard1.attributes.title,
|
||||
description: dashboard1.attributes.description,
|
||||
},
|
||||
{
|
||||
...dashboard2,
|
||||
title: dashboard2.attributes.title,
|
||||
description: dashboard2.attributes.description,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDashboardsTableColumns', () => {
|
||||
it('should call getTableColumnDefinition to get tags column', () => {
|
||||
renderUseDashboardsTableColumns();
|
||||
expect(mockTaggingGetTableColumnDefinition).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return dashboard columns', () => {
|
||||
const { result } = renderUseDashboardsTableColumns();
|
||||
|
||||
expect(result.current).toEqual([
|
||||
expect.objectContaining({
|
||||
field: 'title',
|
||||
name: 'Title',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
}),
|
||||
expect.objectContaining(tagsColumn),
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns a memoized value', async () => {
|
||||
const { result, rerender } = await renderUseSecurityDashboardsTableItems();
|
||||
|
||||
const result1 = result.current;
|
||||
act(() => rerender());
|
||||
const result2 = result.current;
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a table with consistent items and columns', async () => {
|
||||
const { result: itemsResult } = await renderUseSecurityDashboardsTableItems();
|
||||
const { result: columnsResult } = renderUseDashboardsTableColumns();
|
||||
|
||||
const result = render(
|
||||
<EuiBasicTable items={itemsResult.current} columns={columnsResult.current} />,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.getAllByText('Title').length).toBeGreaterThan(0);
|
||||
expect(result.getAllByText('Description').length).toBeGreaterThan(0);
|
||||
expect(result.getAllByText('Tags').length).toBeGreaterThan(0);
|
||||
|
||||
expect(result.getByText('title1')).toBeInTheDocument();
|
||||
expect(result.getByText('description1')).toBeInTheDocument();
|
||||
expect(result.getByText('title2')).toBeInTheDocument();
|
||||
expect(result.getByText('description2')).toBeInTheDocument();
|
||||
|
||||
expect(result.queryAllByTestId('dashboard-title-field')).toHaveLength(2);
|
||||
expect(result.queryAllByTestId('dashboard-description-field')).toHaveLength(2);
|
||||
expect(result.queryAllByTestId('dashboard-tags-field')).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import type { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { SavedObjectsClientContract, SavedObject } from '@kbn/core/public';
|
||||
import { LinkAnchor } from '../../components/links';
|
||||
import { useKibana, useNavigateTo } from '../../lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface DashboardTableItem extends SavedObject<SavedObjectAttributes> {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const SECURITY_TAG_NAME = 'security' as const;
|
||||
const EMPTY_DESCRIPTION = '-' as const;
|
||||
|
||||
const getSecurityDashboardItems = async (
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): Promise<DashboardTableItem[]> => {
|
||||
if (savedObjectsClient) {
|
||||
const tagResponse = await savedObjectsClient.find<SavedObjectAttributes>({
|
||||
type: 'tag',
|
||||
searchFields: ['name'],
|
||||
search: SECURITY_TAG_NAME,
|
||||
});
|
||||
|
||||
const tagId = tagResponse.savedObjects[0]?.id;
|
||||
|
||||
if (tagId) {
|
||||
const dashboardsResponse = await savedObjectsClient.find<SavedObjectAttributes>({
|
||||
type: 'dashboard',
|
||||
hasReference: { id: tagId, type: 'tag' },
|
||||
});
|
||||
|
||||
return dashboardsResponse.savedObjects.map((item) => ({
|
||||
...item,
|
||||
title: item.attributes.title?.toString() ?? undefined,
|
||||
description: item.attributes.description?.toString() ?? undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const useSecurityDashboardsTableItems = () => {
|
||||
const [dashboardItems, setDashboardItems] = useState<DashboardTableItem[]>([]);
|
||||
|
||||
const {
|
||||
savedObjects: { client: savedObjectsClient },
|
||||
} = useKibana().services;
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
const fetchDashboards = async () => {
|
||||
const items = await getSecurityDashboardItems(savedObjectsClient);
|
||||
if (!ignore) {
|
||||
setDashboardItems(items);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDashboards();
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [savedObjectsClient]);
|
||||
|
||||
return dashboardItems;
|
||||
};
|
||||
|
||||
export const useDashboardsTableColumns = (): Array<EuiBasicTableColumn<DashboardTableItem>> => {
|
||||
const { savedObjectsTagging, dashboard: { locator } = {} } = useKibana().services;
|
||||
const { navigateTo } = useNavigateTo();
|
||||
|
||||
const getNavigationHandler = useCallback(
|
||||
(href: string): MouseEventHandler =>
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
navigateTo({ url: href });
|
||||
},
|
||||
[navigateTo]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
(): Array<EuiBasicTableColumn<DashboardTableItem>> => [
|
||||
{
|
||||
field: 'title',
|
||||
name: i18n.DASHBOARD_TITLE,
|
||||
'data-test-subj': 'dashboard-title-field',
|
||||
render: (title: string, { id }) => {
|
||||
const href = locator?.getRedirectUrl({ dashboardId: id });
|
||||
return href ? (
|
||||
<LinkAnchor href={href} onClick={getNavigationHandler(href)}>
|
||||
{title}
|
||||
</LinkAnchor>
|
||||
) : (
|
||||
title
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: i18n.DASHBOARDS_DESCRIPTION,
|
||||
'data-test-subj': 'dashboard-description-field',
|
||||
render: (description: string) => description || EMPTY_DESCRIPTION,
|
||||
},
|
||||
// adds the tags table column based on the saved object items
|
||||
...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []),
|
||||
],
|
||||
[getNavigationHandler, locator, savedObjectsTagging]
|
||||
);
|
||||
|
||||
return columns;
|
||||
};
|
|
@ -53,6 +53,11 @@ export const useKibana = jest.fn().mockReturnValue({
|
|||
},
|
||||
},
|
||||
timelines: createTGridMocks(),
|
||||
savedObjectsTagging: {
|
||||
ui: {
|
||||
getTableColumnDefinition: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export const useUiSetting = jest.fn(createUseUiSettingMock());
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { SecurityPageName } from '../../app/types';
|
||||
import type { NavLinkItem } from '../../common/components/navigation/types';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { LandingLinksImages } from './landing_links_images';
|
||||
import { LandingLinksImages, LandingImageCards } from './landing_links_images';
|
||||
|
||||
const DEFAULT_NAV_ITEM: NavLinkItem = {
|
||||
id: SecurityPageName.overview,
|
||||
|
@ -57,3 +57,30 @@ describe('LandingLinksImages', () => {
|
|||
expect(getByTestId('LandingLinksImage')).toHaveAttribute('src', image);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LandingImageCards', () => {
|
||||
it('renders', () => {
|
||||
const title = 'test label';
|
||||
|
||||
const { queryByText } = render(
|
||||
<TestProviders>
|
||||
<LandingImageCards items={[{ ...DEFAULT_NAV_ITEM, title }]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByText(title)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders image', () => {
|
||||
const image = 'test_image.jpeg';
|
||||
const title = 'TEST_LABEL';
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<LandingImageCards items={[{ ...DEFAULT_NAV_ITEM, image, title }]} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId('LandingImageCard-image')).toHaveAttribute('src', image);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,13 +4,21 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { withSecuritySolutionLink } from '../../common/components/links';
|
||||
import type { NavLinkItem } from '../../common/components/navigation/types';
|
||||
|
||||
interface LandingLinksImagesProps {
|
||||
interface LandingImagesProps {
|
||||
items: NavLinkItem[];
|
||||
}
|
||||
|
||||
|
@ -31,13 +39,13 @@ const StyledFlexItem = styled(EuiFlexItem)`
|
|||
align-items: center;
|
||||
`;
|
||||
|
||||
const SecuritySolutionLink = withSecuritySolutionLink(Link);
|
||||
|
||||
const Content = styled(EuiFlexItem)`
|
||||
padding-left: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
export const LandingLinksImages: React.FC<LandingLinksImagesProps> = ({ items }) => (
|
||||
const SecuritySolutionLink = withSecuritySolutionLink(Link);
|
||||
|
||||
export const LandingLinksImages: React.FC<LandingImagesProps> = ({ items }) => (
|
||||
<EuiFlexGroup direction="column">
|
||||
{items.map(({ title, description, image, id }) => (
|
||||
<EuiFlexItem key={id} data-test-subj="LandingItem">
|
||||
|
@ -71,3 +79,58 @@ export const LandingLinksImages: React.FC<LandingLinksImagesProps> = ({ items })
|
|||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const LandingImageCardItem = styled(EuiFlexItem)`
|
||||
max-width: 364px;
|
||||
`;
|
||||
|
||||
const LandingCardDescripton = styled(EuiText)`
|
||||
padding-top: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
// Needed to use the primary color in the title underlining on hover
|
||||
const PrimaryTitleCard = styled(EuiCard)`
|
||||
.euiCard__title {
|
||||
color: ${(props) => props.theme.eui.euiColorPrimary};
|
||||
}
|
||||
`;
|
||||
|
||||
const SecuritySolutionCard = withSecuritySolutionLink(PrimaryTitleCard);
|
||||
|
||||
export const LandingImageCards: React.FC<LandingImagesProps> = React.memo(({ items }) => (
|
||||
<EuiFlexGroup direction="row" wrap>
|
||||
{items.map(({ id, image, title, description }) => (
|
||||
<LandingImageCardItem key={id} data-test-subj="LandingImageCard-item" grow={false}>
|
||||
<SecuritySolutionCard
|
||||
deepLinkId={id}
|
||||
hasBorder
|
||||
textAlign="left"
|
||||
paddingSize="m"
|
||||
image={
|
||||
image && (
|
||||
<EuiImage
|
||||
data-test-subj="LandingImageCard-image"
|
||||
role="presentation"
|
||||
size={364}
|
||||
alt={title}
|
||||
src={image}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={
|
||||
<PrimaryEuiTitle size="xs">
|
||||
<h2>{title}</h2>
|
||||
</PrimaryEuiTitle>
|
||||
}
|
||||
description={
|
||||
<LandingCardDescripton size="xs" color="text">
|
||||
{description}
|
||||
</LandingCardDescripton>
|
||||
}
|
||||
/>
|
||||
</LandingImageCardItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
));
|
||||
|
||||
LandingImageCards.displayName = 'LandingImageCards';
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { DashboardsLandingPage } from './dashboards';
|
||||
import type { NavLinkItem } from '../../common/components/navigation/types';
|
||||
|
||||
jest.mock('../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null }));
|
||||
|
||||
const OVERVIEW_ITEM_LABEL = 'Overview';
|
||||
const DETECTION_RESPONSE_ITEM_LABEL = 'Detection & Response';
|
||||
|
||||
const defaultAppDashboardsLink: NavLinkItem = {
|
||||
id: SecurityPageName.dashboardsLanding,
|
||||
title: 'Dashboards',
|
||||
links: [
|
||||
{
|
||||
id: SecurityPageName.overview,
|
||||
title: OVERVIEW_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon1',
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.detectionAndResponse,
|
||||
title: DETECTION_RESPONSE_ITEM_LABEL,
|
||||
description: '',
|
||||
icon: 'testIcon2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockAppManageLink = jest.fn(() => defaultAppDashboardsLink);
|
||||
jest.mock('../../common/components/navigation/nav_links', () => ({
|
||||
useAppRootNavLink: () => mockAppManageLink(),
|
||||
}));
|
||||
|
||||
const dashboardTableItems = [
|
||||
{
|
||||
id: 'id 1',
|
||||
title: 'dashboard title 1',
|
||||
description: 'dashboard desc 1',
|
||||
},
|
||||
{
|
||||
id: 'id 2',
|
||||
title: 'dashboard title 2',
|
||||
description: 'dashboard desc 2',
|
||||
},
|
||||
];
|
||||
const mockUseSecurityDashboardsTableItems = jest.fn(() => dashboardTableItems);
|
||||
jest.mock('../../common/containers/dashboards/use_security_dashboards', () => {
|
||||
const actual = jest.requireActual('../../common/containers/dashboards/use_security_dashboards');
|
||||
return {
|
||||
...actual,
|
||||
useSecurityDashboardsTableItems: () => mockUseSecurityDashboardsTableItems(),
|
||||
};
|
||||
});
|
||||
|
||||
const renderDashboardLanding = () => render(<DashboardsLandingPage />, { wrapper: TestProviders });
|
||||
|
||||
describe('Dashboards landing', () => {
|
||||
it('should render items', () => {
|
||||
const { queryByText } = renderDashboardLanding();
|
||||
|
||||
expect(queryByText(OVERVIEW_ITEM_LABEL)).toBeInTheDocument();
|
||||
expect(queryByText(DETECTION_RESPONSE_ITEM_LABEL)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render items in the same order as defined', () => {
|
||||
mockAppManageLink.mockReturnValueOnce({
|
||||
...defaultAppDashboardsLink,
|
||||
});
|
||||
const { queryAllByTestId } = renderDashboardLanding();
|
||||
|
||||
const renderedItems = queryAllByTestId('LandingImageCard-item');
|
||||
|
||||
expect(renderedItems[0]).toHaveTextContent(OVERVIEW_ITEM_LABEL);
|
||||
expect(renderedItems[1]).toHaveTextContent(DETECTION_RESPONSE_ITEM_LABEL);
|
||||
});
|
||||
|
||||
it('should not render items if all items filtered', () => {
|
||||
mockAppManageLink.mockReturnValueOnce({
|
||||
...defaultAppDashboardsLink,
|
||||
links: [],
|
||||
});
|
||||
const { queryByText } = renderDashboardLanding();
|
||||
|
||||
expect(queryByText(OVERVIEW_ITEM_LABEL)).not.toBeInTheDocument();
|
||||
expect(queryByText(DETECTION_RESPONSE_ITEM_LABEL)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render dashboards table', () => {
|
||||
const result = renderDashboardLanding();
|
||||
|
||||
expect(result.getByTestId('dashboards-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render dashboards table rows', () => {
|
||||
const result = renderDashboardLanding();
|
||||
|
||||
expect(mockUseSecurityDashboardsTableItems).toHaveBeenCalled();
|
||||
|
||||
expect(result.queryAllByText(dashboardTableItems[0].title).length).toBeGreaterThan(0);
|
||||
expect(result.queryAllByText(dashboardTableItems[0].description).length).toBeGreaterThan(0);
|
||||
|
||||
expect(result.queryAllByText(dashboardTableItems[1].title).length).toBeGreaterThan(0);
|
||||
expect(result.queryAllByText(dashboardTableItems[1].description).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render dashboards table rows filtered by search term', () => {
|
||||
const result = renderDashboardLanding();
|
||||
|
||||
const input = result.getByRole('searchbox');
|
||||
fireEvent.change(input, { target: { value: dashboardTableItems[0].title } });
|
||||
|
||||
expect(result.queryAllByText(dashboardTableItems[0].title).length).toBeGreaterThan(0);
|
||||
expect(result.queryAllByText(dashboardTableItems[0].description).length).toBeGreaterThan(0);
|
||||
|
||||
expect(result.queryByText(dashboardTableItems[1].title)).not.toBeInTheDocument();
|
||||
expect(result.queryByText(dashboardTableItems[1].description)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -4,22 +4,39 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
import { DashboardsTable } from '../../common/components/dashboards/dashboards_table';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { useAppRootNavLink } from '../../common/components/navigation/nav_links';
|
||||
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
import { LandingLinksImages } from '../components/landing_links_images';
|
||||
import { DASHBOARDS_PAGE_TITLE } from './translations';
|
||||
import { LandingImageCards } from '../components/landing_links_images';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const DashboardsLandingPage = () => {
|
||||
const dashboardLinks = useAppRootNavLink(SecurityPageName.dashboardsLanding)?.links ?? [];
|
||||
|
||||
return (
|
||||
<SecuritySolutionPageWrapper>
|
||||
<HeaderPage title={DASHBOARDS_PAGE_TITLE} />
|
||||
<LandingLinksImages items={dashboardLinks} />
|
||||
<HeaderPage title={i18n.DASHBOARDS_PAGE_TITLE} />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiTitle size="xxxs">
|
||||
<h2>{i18n.DASHBOARDS_PAGE_SECTION_DEFAULT}</h2>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<LandingImageCards items={dashboardLinks} />
|
||||
<EuiSpacer size="xxl" />
|
||||
|
||||
<EuiTitle size="xxxs">
|
||||
<h2>{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}</h2>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiSpacer size="m" />
|
||||
<DashboardsTable />
|
||||
|
||||
<SpyRoute pageName={SecurityPageName.dashboardsLanding} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,20 @@ export const DASHBOARDS_PAGE_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const DASHBOARDS_PAGE_SECTION_DEFAULT = i18n.translate(
|
||||
'xpack.securitySolution.landing.dashboards.section.default',
|
||||
{
|
||||
defaultMessage: 'DEFAULT',
|
||||
}
|
||||
);
|
||||
|
||||
export const DASHBOARDS_PAGE_SECTION_CUSTOM = i18n.translate(
|
||||
'xpack.securitySolution.landing.dashboards.section.custom',
|
||||
{
|
||||
defaultMessage: 'CUSTOM',
|
||||
}
|
||||
);
|
||||
|
||||
export const MANAGE_PAGE_TITLE = i18n.translate('xpack.securitySolution.landing.manage.pageTitle', {
|
||||
defaultMessage: 'Manage',
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ import type {
|
|||
AppObservableLibs,
|
||||
SubPlugins,
|
||||
StartedSubPlugins,
|
||||
StartPluginsDependencies,
|
||||
} from './types';
|
||||
import { initTelemetry } from './common/lib/telemetry';
|
||||
import { KibanaServices } from './common/lib/kibana/services';
|
||||
|
@ -94,7 +95,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
*/
|
||||
private _store?: SecurityAppStore;
|
||||
|
||||
public setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins): PluginSetup {
|
||||
public setup(
|
||||
core: CoreSetup<StartPluginsDependencies, PluginStart>,
|
||||
plugins: SetupPlugins
|
||||
): PluginSetup {
|
||||
initTelemetry(
|
||||
{
|
||||
usageCollection: plugins.usageCollection,
|
||||
|
@ -122,13 +126,16 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
* in the `setup` lifecycle phase.
|
||||
*/
|
||||
const startServices: Promise<StartServices> = (async () => {
|
||||
const [coreStart, startPlugins] = await core.getStartServices();
|
||||
const [coreStart, startPluginsDeps] = await core.getStartServices();
|
||||
const { apm } = await import('@elastic/apm-rum');
|
||||
|
||||
const { savedObjectsTaggingOss, ...startPlugins } = startPluginsDeps;
|
||||
|
||||
const services: StartServices = {
|
||||
...coreStart,
|
||||
...startPlugins,
|
||||
apm,
|
||||
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
|
||||
storage: this.storage,
|
||||
security: plugins.security,
|
||||
};
|
||||
|
|
|
@ -34,6 +34,10 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public';
|
|||
import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { ApmBase } from '@elastic/apm-rum';
|
||||
import type {
|
||||
SavedObjectsTaggingApi,
|
||||
SavedObjectTaggingOssPluginStart,
|
||||
} from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import type { ResolverPluginSetup } from './resolver/types';
|
||||
import type { Inspect } from '../common/search_strategy';
|
||||
import type { Detections } from './detections';
|
||||
|
@ -83,10 +87,15 @@ export interface StartPlugins {
|
|||
security: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
export interface StartPluginsDependencies extends StartPlugins {
|
||||
savedObjectsTaggingOss: SavedObjectTaggingOssPluginStart;
|
||||
}
|
||||
|
||||
export type StartServices = CoreStart &
|
||||
StartPlugins & {
|
||||
storage: Storage;
|
||||
apm: ApmBase;
|
||||
savedObjectsTagging?: SavedObjectsTaggingApi;
|
||||
};
|
||||
|
||||
export interface PluginSetup {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue