mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] Group contract components into one service (#167517)
## Summary
This PR refactors the public plugin contract of Security Solution,
grouping exposed components into one component service to reduce the
boilerplate code needed when adding new components to the plugin
contract for serverless.
It also refactors the `isILMAvailable` flag that was exposed in the
_start_ contract by the `dataQualityPanelConfig`, which is more
self-explanatory, and it has been exposed in the _setup_ contract
without observable.
### Usage of `ContractComponentsService`
1. Define the component in ess or serverless plugins:
```
securitySolution.setComponents({
getStarted: getSecurityGetStartedComponent(services, productTypes),
dashboardsLandingCallout: getDashboardsLandingCallout(services),
// ... other components
});
```
2. Use the component in the main security plugin
```
const { services: { getComponent$ } } = useKibana();
const GetStartedComponent = useObservable(getComponent$('getStarted'));
return <>{GetStartedComponent}</>;
```
Component names are defined at
84583e4960/x-pack/plugins/security_solution/public/contract_components.ts (L11)
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5364b9f887
commit
fbd820b6c6
13 changed files with 107 additions and 53 deletions
|
@ -16,10 +16,10 @@ jest.mock('../../lib/kibana', () => ({
|
|||
useKibana: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-use/lib/useObservable', () => jest.fn((fn) => fn()));
|
||||
jest.mock('react-use/lib/useObservable', () => jest.fn((component) => component));
|
||||
|
||||
describe('LandingPageComponent', () => {
|
||||
const mockGetStartedComponent = jest.fn();
|
||||
const mockGetComponent = jest.fn();
|
||||
const history = createBrowserHistory();
|
||||
const mockSecuritySolutionTemplateWrapper = jest
|
||||
.fn()
|
||||
|
@ -39,7 +39,7 @@ describe('LandingPageComponent', () => {
|
|||
securityLayout: {
|
||||
getPluginWrapper: jest.fn().mockReturnValue(mockSecuritySolutionTemplateWrapper),
|
||||
},
|
||||
getStartedComponent$: mockGetStartedComponent,
|
||||
getComponent$: mockGetComponent,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ describe('LandingPageComponent', () => {
|
|||
});
|
||||
|
||||
it('renders the get started component', () => {
|
||||
mockGetStartedComponent.mockReturnValue(<div data-test-subj="get-started" />);
|
||||
mockGetComponent.mockReturnValue(<div data-test-subj="get-started" />);
|
||||
const { queryByTestId } = renderPage();
|
||||
|
||||
expect(queryByTestId('get-started')).toBeInTheDocument();
|
||||
|
|
|
@ -10,8 +10,8 @@ import useObservable from 'react-use/lib/useObservable';
|
|||
import { useKibana } from '../../lib/kibana';
|
||||
|
||||
export const LandingPageComponent = memo(() => {
|
||||
const { getStartedComponent$ } = useKibana().services;
|
||||
const GetStartedComponent = useObservable(getStartedComponent$);
|
||||
const { getComponent$ } = useKibana().services;
|
||||
const GetStartedComponent = useObservable(getComponent$('getStarted'));
|
||||
return <>{GetStartedComponent}</>;
|
||||
});
|
||||
|
||||
|
|
|
@ -47,12 +47,11 @@ import { mockApm } from '../apm/service.mock';
|
|||
import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
|
||||
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { NavigationProvider } from '@kbn/security-solution-navigation';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { savedSearchPluginMock } from '@kbn/saved-search-plugin/public/mocks';
|
||||
import { contractStartServicesMock } from '../../../mocks';
|
||||
|
||||
const mockUiSettings: Record<string, unknown> = {
|
||||
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
|
||||
|
@ -125,6 +124,7 @@ export const createStartServicesMock = (
|
|||
|
||||
return {
|
||||
...core,
|
||||
...contractStartServicesMock,
|
||||
apm,
|
||||
cases,
|
||||
unifiedSearch,
|
||||
|
@ -217,8 +217,6 @@ export const createStartServicesMock = (
|
|||
...cloud,
|
||||
isCloudEnabled: false,
|
||||
},
|
||||
isSidebarEnabled$: of(true),
|
||||
upselling: new UpsellingService(),
|
||||
customDataService,
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
savedSearch: savedSearchPluginMock.createStartContract(),
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject, map } from 'rxjs';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
export type ContractComponentName = 'getStarted' | 'dashboardsLandingCallout';
|
||||
|
||||
export type ContractComponents = Partial<Record<ContractComponentName, React.ComponentType>>;
|
||||
|
||||
export type SetComponents = (components: ContractComponents) => void;
|
||||
export type GetComponent$ = (
|
||||
name: ContractComponentName
|
||||
) => Observable<React.ComponentType | undefined>;
|
||||
|
||||
export class ContractComponentsService {
|
||||
private components$: BehaviorSubject<ContractComponents>;
|
||||
|
||||
constructor() {
|
||||
this.components$ = new BehaviorSubject<ContractComponents>({});
|
||||
}
|
||||
|
||||
public setComponents: SetComponents = (components) => {
|
||||
this.components$.next(components);
|
||||
};
|
||||
|
||||
public getComponent$: GetComponent$ = (name) =>
|
||||
this.components$.pipe(map((components) => components[name]));
|
||||
}
|
|
@ -83,8 +83,8 @@ const Header: React.FC<{ canCreateDashboard: boolean }> = ({ canCreateDashboard
|
|||
};
|
||||
|
||||
export const DashboardsLandingPage = () => {
|
||||
const { dashboardsLandingCalloutComponent$ } = useKibana().services;
|
||||
const dashboardLandingCallout = useObservable(dashboardsLandingCalloutComponent$);
|
||||
const { getComponent$ } = useKibana().services;
|
||||
const dashboardLandingCallout = useObservable(getComponent$('dashboardsLandingCallout'));
|
||||
const { links = [] } = useRootNavLink(SecurityPageName.dashboards) ?? {};
|
||||
const urlState = useGlobalQueryString();
|
||||
const { show: canReadDashboard, createNew: canCreateDashboard } =
|
||||
|
|
|
@ -5,30 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import type { BreadcrumbsNav } from './common/breadcrumbs';
|
||||
import type { NavigationLink } from './common/links/types';
|
||||
import type { PluginStart, PluginSetup } from './types';
|
||||
import type { PluginStart, PluginSetup, ContractStartServices } from './types';
|
||||
|
||||
const upselling = new UpsellingService();
|
||||
|
||||
export const contractStartServicesMock: ContractStartServices = {
|
||||
extraRoutes$: of([]),
|
||||
isSidebarEnabled$: of(true),
|
||||
getComponent$: jest.fn(),
|
||||
upselling,
|
||||
dataQualityPanelConfig: undefined,
|
||||
};
|
||||
|
||||
const setupMock = (): PluginSetup => ({
|
||||
resolver: jest.fn(),
|
||||
setAppLinksSwitcher: jest.fn(),
|
||||
setDataQualityPanelConfig: jest.fn(),
|
||||
});
|
||||
|
||||
const upselling = new UpsellingService();
|
||||
|
||||
const startMock = (): PluginStart => ({
|
||||
getNavLinks$: jest.fn(() => new BehaviorSubject<NavigationLink[]>([])),
|
||||
setIsSidebarEnabled: jest.fn(),
|
||||
setGetStartedPage: jest.fn(),
|
||||
setIsILMAvailable: jest.fn(),
|
||||
setComponents: jest.fn(),
|
||||
getBreadcrumbsNav$: jest.fn(
|
||||
() => new BehaviorSubject<BreadcrumbsNav>({ leading: [], trailing: [] })
|
||||
),
|
||||
setExtraRoutes: jest.fn(),
|
||||
getUpselling: () => upselling,
|
||||
setDashboardsLandingCallout: jest.fn(),
|
||||
});
|
||||
|
||||
export const securitySolutionMock = {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import type { SecuritySubPlugin } from '../app/types';
|
||||
import { routes } from './routes';
|
||||
|
||||
export * from './types';
|
||||
|
||||
export class Overview {
|
||||
public setup() {}
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import { useAssistantAvailability } from '../../assistant/use_assistant_availability';
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
|
@ -55,8 +54,10 @@ import type {
|
|||
ReportDataQualityCheckAllCompletedParams,
|
||||
ReportDataQualityIndexCheckedParams,
|
||||
} from '../../common/lib/telemetry';
|
||||
import type { DataQualityPanelConfig } from '../types';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'dataQualityDashboardLastChecked';
|
||||
const defaultDataQualityPanelConfig: DataQualityPanelConfig = { isILMAvailable: true };
|
||||
|
||||
const comboBoxStyle: React.CSSProperties = {
|
||||
width: '322px',
|
||||
|
@ -157,8 +158,8 @@ const DataQualityComponent: React.FC = () => {
|
|||
const [selectedOptions, setSelectedOptions] = useState<EuiComboBoxOptionOption[]>(defaultOptions);
|
||||
const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView();
|
||||
const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex();
|
||||
const { isILMAvailable$, cases } = useKibana().services;
|
||||
const isILMAvailable = useObservable(isILMAvailable$);
|
||||
const { dataQualityPanelConfig = defaultDataQualityPanelConfig, cases } = useKibana().services;
|
||||
const { isILMAvailable } = dataQualityPanelConfig;
|
||||
|
||||
const [startDate, setStartTime] = useState<string>();
|
||||
const [endDate, setEndTime] = useState<string>();
|
||||
|
@ -172,7 +173,7 @@ const DataQualityComponent: React.FC = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isILMAvailable != null && isILMAvailable === false) {
|
||||
if (isILMAvailable === false) {
|
||||
setStartTime(DEFAULT_START_TIME);
|
||||
setEndTime(DEFAULT_END_TIME);
|
||||
}
|
||||
|
@ -255,7 +256,7 @@ const DataQualityComponent: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{indicesExist && isILMAvailable != null ? (
|
||||
{indicesExist ? (
|
||||
<SecuritySolutionPageWrapper data-test-subj="ecsDataQualityDashboardPage">
|
||||
<HeaderPage subtitle={subtitle} title={i18n.DATA_QUALITY_TITLE}>
|
||||
{isILMAvailable && (
|
||||
|
|
10
x-pack/plugins/security_solution/public/overview/types.ts
Normal file
10
x-pack/plugins/security_solution/public/overview/types.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface DataQualityPanelConfig {
|
||||
isILMAvailable: boolean;
|
||||
}
|
|
@ -9,26 +9,24 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import type { RouteProps } from 'react-router-dom';
|
||||
import { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import type { ContractStartServices, PluginSetup, PluginStart } from './types';
|
||||
import type { DataQualityPanelConfig } from './overview/types';
|
||||
import type { AppLinksSwitcher } from './common/links';
|
||||
import { navLinks$ } from './common/links/nav_links';
|
||||
import { breadcrumbsNav$ } from './common/breadcrumbs';
|
||||
import { ContractComponentsService } from './contract_components';
|
||||
|
||||
export class PluginContract {
|
||||
public isILMAvailable$: BehaviorSubject<boolean>;
|
||||
public isSidebarEnabled$: BehaviorSubject<boolean>;
|
||||
public getStartedComponent$: BehaviorSubject<React.ComponentType | null>;
|
||||
public dashboardsLandingCallout$: BehaviorSubject<React.ComponentType | null>;
|
||||
public componentsService: ContractComponentsService;
|
||||
public upsellingService: UpsellingService;
|
||||
public extraRoutes$: BehaviorSubject<RouteProps[]>;
|
||||
public appLinksSwitcher: AppLinksSwitcher;
|
||||
public dataQualityPanelConfig?: DataQualityPanelConfig;
|
||||
|
||||
constructor() {
|
||||
this.extraRoutes$ = new BehaviorSubject<RouteProps[]>([]);
|
||||
this.isILMAvailable$ = new BehaviorSubject<boolean>(true);
|
||||
this.isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
|
||||
this.getStartedComponent$ = new BehaviorSubject<React.ComponentType | null>(null);
|
||||
this.dashboardsLandingCallout$ = new BehaviorSubject<React.ComponentType | null>(null);
|
||||
|
||||
this.componentsService = new ContractComponentsService();
|
||||
this.upsellingService = new UpsellingService();
|
||||
this.appLinksSwitcher = (appLinks) => appLinks;
|
||||
}
|
||||
|
@ -36,11 +34,10 @@ export class PluginContract {
|
|||
public getStartServices(): ContractStartServices {
|
||||
return {
|
||||
extraRoutes$: this.extraRoutes$.asObservable(),
|
||||
isILMAvailable$: this.isILMAvailable$.asObservable(),
|
||||
isSidebarEnabled$: this.isSidebarEnabled$.asObservable(),
|
||||
getStartedComponent$: this.getStartedComponent$.asObservable(),
|
||||
dashboardsLandingCalloutComponent$: this.dashboardsLandingCallout$.asObservable(),
|
||||
getComponent$: this.componentsService.getComponent$.bind(this.componentsService),
|
||||
upselling: this.upsellingService,
|
||||
dataQualityPanelConfig: this.dataQualityPanelConfig,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -50,6 +47,9 @@ export class PluginContract {
|
|||
setAppLinksSwitcher: (appLinksSwitcher) => {
|
||||
this.appLinksSwitcher = appLinksSwitcher;
|
||||
},
|
||||
setDataQualityPanelConfig: (dataQualityPanelConfig) => {
|
||||
this.dataQualityPanelConfig = dataQualityPanelConfig;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -59,12 +59,8 @@ export class PluginContract {
|
|||
setExtraRoutes: (extraRoutes) => this.extraRoutes$.next(extraRoutes),
|
||||
setIsSidebarEnabled: (isSidebarEnabled: boolean) =>
|
||||
this.isSidebarEnabled$.next(isSidebarEnabled),
|
||||
setIsILMAvailable: (isILMAvailable: boolean) => this.isILMAvailable$.next(isILMAvailable),
|
||||
setGetStartedPage: (getStartedComponent) => {
|
||||
this.getStartedComponent$.next(getStartedComponent);
|
||||
},
|
||||
setDashboardsLandingCallout: (dashboardsLandingCallout) => {
|
||||
this.dashboardsLandingCallout$.next(dashboardsLandingCallout);
|
||||
setComponents: (components) => {
|
||||
this.componentsService.setComponents(components);
|
||||
},
|
||||
getBreadcrumbsNav$: () => breadcrumbsNav$,
|
||||
getUpselling: () => this.upsellingService,
|
||||
|
|
|
@ -78,6 +78,8 @@ import type { TelemetryClientStart } from './common/lib/telemetry';
|
|||
import type { Dashboards } from './dashboards';
|
||||
import type { BreadcrumbsNav } from './common/breadcrumbs/types';
|
||||
import type { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';
|
||||
import type { DataQualityPanelConfig } from './overview/types';
|
||||
import type { SetComponents, GetComponent$ } from './contract_components';
|
||||
|
||||
export interface SetupPlugins {
|
||||
cloud?: CloudSetup;
|
||||
|
@ -141,11 +143,10 @@ export interface StartPluginsDependencies extends StartPlugins {
|
|||
|
||||
export interface ContractStartServices {
|
||||
extraRoutes$: Observable<RouteProps[]>;
|
||||
isILMAvailable$: Observable<boolean>;
|
||||
isSidebarEnabled$: Observable<boolean>;
|
||||
getStartedComponent$: Observable<React.ComponentType | null>;
|
||||
dashboardsLandingCalloutComponent$: Observable<React.ComponentType | null>;
|
||||
getComponent$: GetComponent$;
|
||||
upselling: UpsellingService;
|
||||
dataQualityPanelConfig: DataQualityPanelConfig | undefined;
|
||||
}
|
||||
|
||||
export type StartServices = CoreStart &
|
||||
|
@ -173,15 +174,14 @@ export type StartServices = CoreStart &
|
|||
export interface PluginSetup {
|
||||
resolver: () => Promise<ResolverPluginSetup>;
|
||||
setAppLinksSwitcher: (appLinksSwitcher: AppLinksSwitcher) => void;
|
||||
setDataQualityPanelConfig: (dataQualityPanelConfig: DataQualityPanelConfig) => void;
|
||||
}
|
||||
|
||||
export interface PluginStart {
|
||||
getNavLinks$: () => Observable<NavigationLink[]>;
|
||||
setExtraRoutes: (extraRoutes: RouteProps[]) => void;
|
||||
setIsILMAvailable: (isILMAvailable: boolean) => void;
|
||||
setIsSidebarEnabled: (isSidebarEnabled: boolean) => void;
|
||||
setGetStartedPage: (getStartedComponent: React.ComponentType) => void;
|
||||
setDashboardsLandingCallout: (dashboardsLandingCallout: React.ComponentType) => void;
|
||||
setComponents: SetComponents;
|
||||
getBreadcrumbsNav$: () => Observable<BreadcrumbsNav>;
|
||||
getUpselling: () => UpsellingService;
|
||||
}
|
||||
|
|
|
@ -28,8 +28,11 @@ export class SecuritySolutionEssPlugin
|
|||
{
|
||||
public setup(
|
||||
_core: CoreSetup,
|
||||
_setupDeps: SecuritySolutionEssPluginSetupDeps
|
||||
setupDeps: SecuritySolutionEssPluginSetupDeps
|
||||
): SecuritySolutionEssPluginSetup {
|
||||
const { securitySolution } = setupDeps;
|
||||
securitySolution.setDataQualityPanelConfig({ isILMAvailable: true });
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -44,8 +47,9 @@ export class SecuritySolutionEssPlugin
|
|||
registerUpsellings(securitySolution.getUpselling(), license, services);
|
||||
});
|
||||
|
||||
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services));
|
||||
securitySolution.setIsILMAvailable(true);
|
||||
securitySolution.setComponents({
|
||||
getStarted: getSecurityGetStartedComponent(services),
|
||||
});
|
||||
|
||||
subscribeBreadcrumbs(services);
|
||||
|
||||
|
|
|
@ -41,7 +41,9 @@ export class SecuritySolutionServerlessPlugin
|
|||
_core: CoreSetup,
|
||||
setupDeps: SecuritySolutionServerlessPluginSetupDeps
|
||||
): SecuritySolutionServerlessPluginSetup {
|
||||
setupDeps.securitySolution.setAppLinksSwitcher(projectAppLinksSwitcher);
|
||||
const { securitySolution } = setupDeps;
|
||||
securitySolution.setAppLinksSwitcher(projectAppLinksSwitcher);
|
||||
securitySolution.setDataQualityPanelConfig({ isILMAvailable: false });
|
||||
|
||||
return {};
|
||||
}
|
||||
|
@ -57,9 +59,10 @@ export class SecuritySolutionServerlessPlugin
|
|||
|
||||
registerUpsellings(securitySolution.getUpselling(), this.config.productTypes, services);
|
||||
|
||||
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes));
|
||||
securitySolution.setDashboardsLandingCallout(getDashboardsLandingCallout(services));
|
||||
securitySolution.setIsILMAvailable(false);
|
||||
securitySolution.setComponents({
|
||||
getStarted: getSecurityGetStartedComponent(services, productTypes),
|
||||
dashboardsLandingCallout: getDashboardsLandingCallout(services),
|
||||
});
|
||||
|
||||
configureNavigation(services, this.config);
|
||||
setRoutes(services);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue