mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SecuritySolutions] Update asset criticality upload page visibility and permissions (#180771)
## Summary Related to https://github.com/elastic/kibana/pull/179891 * Display error message in case user doesn't have write access to asset criticality index  * Display error message if the user navigates to the page when the user advanced (UI) setting is disabled  * Remove links from menus and global search if advanced (UI) setting is disabled  Recoding of what happens when you don't refresh the page:2b67403c
-d58e-4d92-b12d-9f9de4c9a213 ### Expected behaviour: * After the setting is enabled, the user needs to refresh the browser to find the page * If users disable the setting on a different browser/tab and navigate to the page without refreshing, they will see an error message on the page. * If users disable the flag while the page is already rendered and try to upload the file, it will display an error on the last step. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e60d23a2ff
commit
da890be8f2
11 changed files with 209 additions and 11 deletions
|
@ -12,6 +12,7 @@ import { updateAppLinks } from '../../links';
|
|||
import { mockGlobalState } from '../../mock';
|
||||
import type { Capabilities } from '@kbn/core-capabilities-common';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
|
||||
const defaultAppLinks: AppLinkItems = [
|
||||
{
|
||||
|
@ -29,6 +30,7 @@ const defaultAppLinks: AppLinkItems = [
|
|||
];
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
const mockUiSettingsClient = uiSettingsServiceMock.createStartContract();
|
||||
|
||||
describe('helpers', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -36,6 +38,7 @@ describe('helpers', () => {
|
|||
capabilities: {} as unknown as Capabilities,
|
||||
experimentalFeatures: mockGlobalState.app.enableExperimental,
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
});
|
||||
});
|
||||
it('returns the search string', () => {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../comm
|
|||
import type { Capabilities } from '@kbn/core/types';
|
||||
import { mockGlobalState, TestProviders } from '../mock';
|
||||
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import type { AppLinkItems } from './types';
|
||||
import type { AppLinkItems, LinkItem, LinksPermissions } from './types';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
useAppLinks,
|
||||
|
@ -18,11 +18,13 @@ import {
|
|||
needsUrlState,
|
||||
updateAppLinks,
|
||||
useLinkExists,
|
||||
isLinkUiSettingsAllowed,
|
||||
} from './links';
|
||||
import { createCapabilities } from './test_utils';
|
||||
import { hasCapabilities } from '../lib/capabilities';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import React from 'react';
|
||||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
|
||||
const defaultAppLinks: AppLinkItems = [
|
||||
{
|
||||
|
@ -79,6 +81,8 @@ const mockLicense = {
|
|||
hasAtLeast: licensePremiumMock,
|
||||
} as unknown as ILicense;
|
||||
|
||||
const mockUiSettingsClient = uiSettingsServiceMock.createStartContract();
|
||||
|
||||
const renderUseAppLinks = () =>
|
||||
renderHook<{}, AppLinkItems>(() => useAppLinks(), { wrapper: TestProviders });
|
||||
const renderUseLinkExists = (id: SecurityPageName) =>
|
||||
|
@ -95,6 +99,7 @@ describe('Security links', () => {
|
|||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: mockLicense,
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -174,6 +179,7 @@ describe('Security links', () => {
|
|||
} as unknown as typeof mockExperimentalDefaults,
|
||||
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -240,6 +246,7 @@ describe('Security links', () => {
|
|||
} as unknown as typeof mockExperimentalDefaults,
|
||||
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
|
||||
upselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -269,6 +276,7 @@ describe('Security links', () => {
|
|||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
@ -300,6 +308,7 @@ describe('Security links', () => {
|
|||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: { hasAtLeast: licenseBasicMock } as unknown as ILicense,
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
@ -338,6 +347,7 @@ describe('Security links', () => {
|
|||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: mockLicense,
|
||||
upselling: new UpsellingService(),
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -369,6 +379,7 @@ describe('Security links', () => {
|
|||
experimentalFeatures: mockExperimentalDefaults,
|
||||
license: mockLicense,
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
@ -532,4 +543,74 @@ describe('Security links', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLinkUiSettingsAllowed', () => {
|
||||
const SETTING_KEY = 'test setting';
|
||||
const mockedLink: LinkItem = {
|
||||
id: SecurityPageName.entityAnalyticsAssetClassification,
|
||||
title: 'test title',
|
||||
path: '/test_path',
|
||||
};
|
||||
|
||||
const mockedPermissions = {
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
} as unknown as LinksPermissions;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns true when uiSettingRequired is not set', () => {
|
||||
const link: LinkItem = {
|
||||
...mockedLink,
|
||||
uiSettingRequired: undefined,
|
||||
};
|
||||
expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeTruthy();
|
||||
expect(mockUiSettingsClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns true when uiSettingRequired is a string and the corresponding UI setting is true', () => {
|
||||
mockUiSettingsClient.get = jest.fn().mockReturnValue(true);
|
||||
const link: LinkItem = {
|
||||
...mockedLink,
|
||||
uiSettingRequired: SETTING_KEY,
|
||||
};
|
||||
|
||||
expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeTruthy();
|
||||
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
|
||||
});
|
||||
|
||||
it('returns false when uiSettingRequired is a string and the corresponding UI setting is false', () => {
|
||||
mockUiSettingsClient.get = jest.fn().mockReturnValue(false);
|
||||
const link: LinkItem = {
|
||||
...mockedLink,
|
||||
uiSettingRequired: SETTING_KEY,
|
||||
};
|
||||
|
||||
expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeFalsy();
|
||||
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
|
||||
});
|
||||
|
||||
it('returns true when uiSettingRequired is an object and the corresponding UI setting matches the value', () => {
|
||||
const link: LinkItem = {
|
||||
...mockedLink,
|
||||
uiSettingRequired: { key: SETTING_KEY, value: 'any text' },
|
||||
};
|
||||
mockUiSettingsClient.get = jest.fn().mockReturnValue('any text');
|
||||
|
||||
expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeTruthy();
|
||||
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
|
||||
});
|
||||
|
||||
it('returns false when uiSettingRequired is an object and the corresponding UI setting does not match the value', () => {
|
||||
const link: LinkItem = {
|
||||
...mockedLink,
|
||||
uiSettingRequired: { key: SETTING_KEY, value: 'any text' },
|
||||
};
|
||||
mockUiSettingsClient.get = jest.fn().mockReturnValue('different text');
|
||||
|
||||
expect(isLinkUiSettingsAllowed(link, mockedPermissions)).toBeFalsy();
|
||||
expect(mockUiSettingsClient.get).toHaveBeenCalledWith(SETTING_KEY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -153,7 +153,10 @@ const getNormalizedLink = (id: SecurityPageName): Readonly<NormalizedLink> | und
|
|||
|
||||
const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissions): LinkItem[] =>
|
||||
appLinks.reduce<LinkItem[]>((acc, { links, ...appLinkWithoutSublinks }) => {
|
||||
if (!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions)) {
|
||||
if (
|
||||
!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions) ||
|
||||
!isLinkUiSettingsAllowed(appLinkWithoutSublinks, linksPermissions)
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
|
@ -179,6 +182,23 @@ const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissi
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
export const isLinkUiSettingsAllowed = (link: LinkItem, { uiSettingsClient }: LinksPermissions) => {
|
||||
if (!link.uiSettingRequired) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof link.uiSettingRequired === 'string') {
|
||||
return uiSettingsClient.get(link.uiSettingRequired) === true;
|
||||
}
|
||||
|
||||
if (typeof link.uiSettingRequired === 'object') {
|
||||
return uiSettingsClient.get(link.uiSettingRequired.key) === link.uiSettingRequired.value;
|
||||
}
|
||||
|
||||
// unsupported uiSettingRequired type
|
||||
return false;
|
||||
};
|
||||
|
||||
const isLinkExperimentalKeyAllowed = (
|
||||
link: LinkItem,
|
||||
{ experimentalFeatures }: LinksPermissions
|
||||
|
|
|
@ -19,6 +19,7 @@ import type { UpsellingService } from '@kbn/security-solution-upselling/service'
|
|||
import type { AppDeepLinkLocations } from '@kbn/core-application-browser';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { SolutionSideNavItem as ClassicSolutionSideNavItem } from '@kbn/security-solution-side-nav';
|
||||
import type { IUiSettingsClient } from '@kbn/core/public';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import type { RequiredCapabilities } from '../lib/capabilities';
|
||||
|
||||
|
@ -37,6 +38,7 @@ export type SolutionSideNavItem = ClassicSolutionSideNavItem<SolutionPageName>;
|
|||
export interface LinksPermissions {
|
||||
capabilities: Capabilities;
|
||||
experimentalFeatures: Readonly<ExperimentalFeatures>;
|
||||
uiSettingsClient: IUiSettingsClient;
|
||||
upselling: UpsellingService;
|
||||
license?: ILicense;
|
||||
}
|
||||
|
@ -154,6 +156,13 @@ export interface LinkItem {
|
|||
* Locations where the link is visible in the UI
|
||||
*/
|
||||
visibleIn?: AppDeepLinkLocations[];
|
||||
|
||||
/**
|
||||
* Required UI setting to enable a link.
|
||||
* To enable a link when a boolean UiSetting is true, pass the key as a string.
|
||||
* To enable a link when a specific value is set for a UiSetting, pass an object with key and value.
|
||||
*/
|
||||
uiSettingRequired?: string | { key: string; value: unknown };
|
||||
}
|
||||
|
||||
export type AppLinkItems = Readonly<LinkItem[]>;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
|||
import { updateAppLinks } from '../../links';
|
||||
import { appLinks } from '../../../app_links';
|
||||
import { useShowTimeline } from './use_show_timeline';
|
||||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
|
||||
const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' });
|
||||
jest.mock('react-router-dom', () => {
|
||||
|
@ -53,6 +54,7 @@ jest.mock('../../lib/kibana', () => {
|
|||
});
|
||||
|
||||
const mockUpselling = new UpsellingService();
|
||||
const mockUiSettingsClient = uiSettingsServiceMock.createStartContract();
|
||||
|
||||
describe('use show timeline', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -70,6 +72,7 @@ describe('use show timeline', () => {
|
|||
},
|
||||
},
|
||||
upselling: mockUpselling,
|
||||
uiSettingsClient: mockUiSettingsClient,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { UseMutationResult, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
|
||||
import type { SecurityAppError } from '@kbn/securitysolution-t-grid';
|
||||
import type { CriticalityLevelWithUnassigned } from '../../../../common/entity_analytics/asset_criticality/types';
|
||||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../../common/constants';
|
||||
import { useHasSecurityCapability } from '../../../helper_hooks';
|
||||
|
@ -21,21 +22,23 @@ const PRIVILEGES_KEY = 'PRIVILEGES';
|
|||
|
||||
const nonAuthorizedResponse: Promise<EntityAnalyticsPrivileges> = Promise.resolve({
|
||||
has_all_required: false,
|
||||
has_write_permissions: false,
|
||||
has_read_permissions: false,
|
||||
privileges: {
|
||||
elasticsearch: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const useAssetCriticalityPrivileges = (
|
||||
entityName: string
|
||||
): UseQueryResult<EntityAnalyticsPrivileges> => {
|
||||
queryKey: string
|
||||
): UseQueryResult<EntityAnalyticsPrivileges, SecurityAppError> => {
|
||||
const { fetchAssetCriticalityPrivileges } = useEntityAnalyticsRoutes();
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const [isAssetCriticalityEnabled] = useUiSetting$<boolean>(ENABLE_ASSET_CRITICALITY_SETTING);
|
||||
const isEnabled = isAssetCriticalityEnabled && hasEntityAnalyticsCapability;
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ASSET_CRITICALITY_KEY, PRIVILEGES_KEY, entityName, isEnabled],
|
||||
queryKey: [ASSET_CRITICALITY_KEY, PRIVILEGES_KEY, queryKey, isEnabled],
|
||||
queryFn: isEnabled ? fetchAssetCriticalityPrivileges : () => nonAuthorizedResponse,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -16,16 +16,92 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiEmptyPrompt,
|
||||
EuiCallOut,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { ASSET_CRITICALITY_INDEX_PATTERN } from '../../../common/entity_analytics/asset_criticality';
|
||||
import { useUiSetting$, useKibana } from '../../common/lib/kibana';
|
||||
import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../common/constants';
|
||||
import { AssetCriticalityFileUploader } from '../components/asset_criticality_file_uploader/asset_criticality_file_uploader';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useAssetCriticalityPrivileges } from '../components/asset_criticality/use_asset_criticality';
|
||||
import { useHasSecurityCapability } from '../../helper_hooks';
|
||||
|
||||
export const AssetCriticalityUploadPage = () => {
|
||||
const { docLinks } = useKibana().services;
|
||||
const entityAnalyticsLinks = docLinks.links.securitySolution.entityAnalytics;
|
||||
const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics');
|
||||
const [isAssetCriticalityEnabled] = useUiSetting$<boolean>(ENABLE_ASSET_CRITICALITY_SETTING);
|
||||
const {
|
||||
data: privileges,
|
||||
error: privilegesError,
|
||||
isLoading,
|
||||
} = useAssetCriticalityPrivileges('AssetCriticalityUploadPage');
|
||||
const hasWritePermissions = privileges?.has_write_permissions;
|
||||
|
||||
if (isLoading) {
|
||||
// Wait for permission before rendering content to avoid flickering
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!hasEntityAnalyticsCapability ||
|
||||
!isAssetCriticalityEnabled ||
|
||||
privilegesError?.body.status_code === 403
|
||||
) {
|
||||
const errorMessage = privilegesError?.body.message ?? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledMessage"
|
||||
defaultMessage='Please enable "{ENABLE_ASSET_CRITICALITY_SETTING}" on advanced settings to access the page.'
|
||||
values={{
|
||||
ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.advancedSettingDisabledTitle"
|
||||
defaultMessage="This page is disabled"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={<p>{errorMessage}</p>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasWritePermissions) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.noPermissionTitle"
|
||||
defaultMessage="Insufficient index privileges to access this page"
|
||||
/>
|
||||
}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.entityAnalytics.assetCriticalityUploadPage.missingPermissionsCallout.description"
|
||||
defaultMessage="Write permission is required for the {index} index pattern in order to access this page. Contact your administrator for further assistance."
|
||||
values={{
|
||||
index: <EuiCode>{ASSET_CRITICALITY_INDEX_PATTERN}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader
|
||||
|
@ -62,7 +138,7 @@ export const AssetCriticalityUploadPage = () => {
|
|||
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasBorder={true} paddingSize="l" grow={false}>
|
||||
<EuiIcon type={'questionInCircle'} size={'xl'} />
|
||||
<EuiIcon type="questionInCircle" size="xl" />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h3>
|
||||
|
|
|
@ -101,7 +101,7 @@ export const EA_DOCS_ENTITY_RISK_SCORE = i18n.translate(
|
|||
export const PREVIEW_MISSING_PERMISSIONS_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.riskScore.riskScorePreview.missingPermissionsCallout.title',
|
||||
{
|
||||
defaultMessage: 'Insifficient index privileges to preview data',
|
||||
defaultMessage: 'Insufficient index privileges to preview data',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '../../common/endpoint/service/authz';
|
||||
import {
|
||||
BLOCKLIST_PATH,
|
||||
ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
ENDPOINTS_PATH,
|
||||
ENTITY_ANALYTICS_ASSET_CRITICALITY_PATH,
|
||||
ENTITY_ANALYTICS_MANAGEMENT_PATH,
|
||||
|
@ -200,7 +201,7 @@ export const links: LinkItem = {
|
|||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
capabilities: [`${SERVER_APP_ID}.entity-analytics`],
|
||||
licenseType: 'platinum',
|
||||
uiSettingRequired: ENABLE_ASSET_CRITICALITY_SETTING,
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.responseActionsHistory,
|
||||
|
|
|
@ -383,6 +383,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
experimentalFeatures: this.experimentalFeatures,
|
||||
upselling: upsellingService,
|
||||
capabilities: core.application.capabilities,
|
||||
uiSettingsClient: core.uiSettings,
|
||||
...(license.type != null && { license }),
|
||||
};
|
||||
updateAppLinks(links, linksPermissions);
|
||||
|
|
|
@ -196,6 +196,7 @@
|
|||
"@kbn/core-http-server-mocks",
|
||||
"@kbn/data-service",
|
||||
"@kbn/core-chrome-browser",
|
||||
"@kbn/shared-ux-chrome-navigation"
|
||||
"@kbn/shared-ux-chrome-navigation",
|
||||
"@kbn/core-ui-settings-browser-mocks"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue