[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:
Sergi Massaneda 2022-07-14 12:07:50 +02:00 committed by GitHub
parent ccaa38e7f1
commit d35a687c29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 663 additions and 13 deletions

View file

@ -42,7 +42,8 @@
"home",
"telemetry",
"dataViewFieldEditor",
"osquery"
"osquery",
"savedObjectsTaggingOss"
],
"server": true,
"ui": true,

View file

@ -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}
/>
);
};

View file

@ -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',
}
);

View file

@ -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);
});
});

View file

@ -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;
};

View file

@ -53,6 +53,11 @@ export const useKibana = jest.fn().mockReturnValue({
},
},
timelines: createTGridMocks(),
savedObjectsTagging: {
ui: {
getTableColumnDefinition: jest.fn(),
},
},
},
});
export const useUiSetting = jest.fn(createUseUiSettingMock());

View file

@ -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);
});
});

View file

@ -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';

View file

@ -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();
});
});

View file

@ -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>
);

View file

@ -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',
});

View file

@ -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,
};

View file

@ -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 {