From c8c3c5d68f855e83cbeb9ba3a47425d984f33bb1 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 18 Nov 2020 15:04:06 -0500 Subject: [PATCH 01/20] Support for rendering a custom component in Integration Details --- .../plugins/fleet/common/types/models/epm.ts | 2 +- .../sections/epm/screens/detail/content.tsx | 16 +++++++++++ .../sections/epm/screens/detail/index.tsx | 27 +++++++++++++++---- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5d79d41b7a631..68f7d5270a505 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -30,7 +30,7 @@ export type InstallSource = 'registry' | 'upload'; export type EpmPackageInstallStatus = 'installed' | 'installing'; -export type DetailViewPanelName = 'overview' | 'policies' | 'settings'; +export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AgentAssetType = typeof agentAssetTypes; export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx index 40346cde7f50f..6732c4ce7d849 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; +import { Redirect, useParams } from 'react-router-dom'; import { DetailParams } from '.'; import { PackageInfo } from '../../../../types'; import { AssetsFacetGroup } from '../../components/assets_facet_group'; @@ -14,6 +15,9 @@ import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { OverviewPanel } from './overview_panel'; import { PackagePoliciesPanel } from './package_policies_panel'; import { SettingsPanel } from './settings_panel'; +import { useUIExtension } from '../../../../hooks/use_ui_extension'; +import { ExtensionWrapper } from '../../../../components/extension_wrapper'; +import { useLink } from '../../../../hooks'; type ContentProps = PackageInfo & Pick; @@ -46,6 +50,10 @@ export function Content(props: ContentProps) { type ContentPanelProps = PackageInfo & Pick; export function ContentPanel(props: ContentPanelProps) { const { panel, name, version, assets, title, removable, latestVersion } = props; + const CustomView = useUIExtension(name, 'package-detail-custom'); + const { getPath } = useLink(); + const { pkgkey } = useParams<{ pkgkey?: string }>(); + switch (panel) { case 'settings': return ( @@ -60,6 +68,14 @@ export function ContentPanel(props: ContentPanelProps) { ); case 'policies': return ; + case 'custom': + return ( + (CustomView && ( + + + + )) || + ); case 'overview': default: return ; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index 2535a53589bd9..9abb63fdc7d31 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -35,6 +35,7 @@ import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components import { UpdateIcon } from '../../components/icons'; import { Content } from './content'; import { WithHeaderLayoutProps } from '../../../../layouts/with_header'; +import { useUIExtension } from '../../../../hooks/use_ui_extension'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -53,6 +54,9 @@ const PanelDisplayNames: Record = { settings: i18n.translate('xpack.fleet.epm.packageDetailsNav.settingsLinkText', { defaultMessage: 'Settings', }), + custom: i18n.translate('xpack.fleet.epm.packageDetailsNav.packageCustomLinkText', { + defaultMessage: 'Custom', + }), }; const DetailWrapper = styled.div` @@ -101,6 +105,9 @@ export function Detail() { pkgkey ); + const showCustomTab = + useUIExtension(packageInfoData?.response.name ?? '', 'package-detail-custom') !== undefined; + // Track install status state useEffect(() => { if (packageInfoData?.response) { @@ -246,10 +253,20 @@ export function Detail() { return (entries(PanelDisplayNames) .filter(([panelId]) => { - return ( - panelId !== 'policies' || - (packageInfoData?.response.status === InstallStatus.installed && false) // Remove `false` when ready to implement policies tab - ); + // Don't show `Policies` tab if package is not installed + if ( + panelId === 'policies' && + packageInfoData?.response.status !== InstallStatus.installed + ) { + return false; + } + + // Don't show `custom` tab if a custom component is not registered + if (panelId === 'custom' && !showCustomTab) { + return false; + } + + return true; }) .map(([panelId, display]) => { return { @@ -262,7 +279,7 @@ export function Detail() { }), }; }) as unknown) as WithHeaderLayoutProps['tabs']; - }, [getHref, packageInfo, packageInfoData?.response?.status, panel]); + }, [getHref, packageInfo, panel, showCustomTab, packageInfoData]); return ( From e2e51c9763e74bc434261d649a1b3811055008de Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 18 Nov 2020 16:20:56 -0500 Subject: [PATCH 02/20] Refactor Fleet app initialization contexts in order to support testing setup --- .../fleet/public/applications/fleet/app.tsx | 257 ++++++++++++++++++ .../fleet/public/applications/fleet/index.tsx | 242 ++--------------- 2 files changed, 277 insertions(+), 222 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/app.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx new file mode 100644 index 0000000000000..595e9d7d26ce3 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useEffect, useState } from 'react'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui'; +import { HashRouter as Router, Redirect, Route, Switch } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import useObservable from 'react-use/lib/useObservable'; +import { + ConfigContext, + DepsContext, + FleetStatusProvider, + KibanaVersionContext, + sendGetPermissionsCheck, + sendSetup, + useBreadcrumbs, + useConfig, +} from './hooks'; +import { Error, Loading } from './components'; +import { IntraAppStateProvider } from './hooks/use_intra_app_state'; +import { PackageInstallProvider } from './sections/epm/hooks'; +import { PAGE_ROUTING_PATHS } from './constants'; +import { DefaultLayout, WithoutHeaderLayout } from './layouts'; +import { EPMApp } from './sections/epm'; +import { AgentPolicyApp } from './sections/agent_policy'; +import { DataStreamApp } from './sections/data_stream'; +import { FleetApp } from './sections/agents'; +import { IngestManagerOverview } from './sections/overview'; +import { ProtectedRoute } from './index'; +import { + IngestManagerConfigType, + IngestManagerSetupDeps, + IngestManagerStartDeps, +} from '../../plugin'; +import { UIExtensionsStorage } from './types'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { EuiThemeProvider } from '../../../../xpack_legacy/common'; +import { UIExtensionsContext } from './hooks/use_ui_extension'; + +const ErrorLayout = ({ children }: { children: JSX.Element }) => ( + + + {children} + + +); + +const Panel = styled(EuiPanel)` + max-width: 500px; + margin-right: auto; + margin-left: auto; +`; + +export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { + useBreadcrumbs('base'); + + const [isPermissionsLoading, setIsPermissionsLoading] = useState(false); + const [permissionsError, setPermissionsError] = useState(); + const [isInitialized, setIsInitialized] = useState(false); + const [initializationError, setInitializationError] = useState(null); + + useEffect(() => { + (async () => { + setIsPermissionsLoading(false); + setPermissionsError(undefined); + setIsInitialized(false); + setInitializationError(null); + try { + setIsPermissionsLoading(true); + const permissionsResponse = await sendGetPermissionsCheck(); + setIsPermissionsLoading(false); + if (permissionsResponse.data?.success) { + try { + const setupResponse = await sendSetup(); + if (setupResponse.error) { + setInitializationError(setupResponse.error); + } + } catch (err) { + setInitializationError(err); + } + setIsInitialized(true); + } else { + setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR'); + } + } catch (err) { + setPermissionsError('REQUEST_ERROR'); + } + })(); + }, []); + + if (isPermissionsLoading || permissionsError) { + return ( + + {isPermissionsLoading ? ( + + ) : permissionsError === 'REQUEST_ERROR' ? ( + + } + error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', { + defaultMessage: 'There was a problem checking Fleet permissions', + })} + /> + ) : ( + + + {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( + + ) : ( + + )} + + } + body={ +

+ {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( + superuser }} + /> + ) : ( + + )} +

+ } + /> +
+ )} +
+ ); + } + + if (!isInitialized || initializationError) { + return ( + + {initializationError ? ( + + } + error={initializationError} + /> + ) : ( + + )} + + ); + } + + return <>{children}; +}); + +/** + * Fleet Application context all the way down to the Router, but with no permissions or setup checks + * and no routes defined + */ +export const FleetAppContext: React.FC<{ + basepath: string; + coreStart: CoreStart; + setupDeps: IngestManagerSetupDeps; + startDeps: IngestManagerStartDeps; + config: IngestManagerConfigType; + history: AppMountParameters['history']; + kibanaVersion: string; + extensions: UIExtensionsStorage; +}> = memo( + ({ children, coreStart, setupDeps, startDeps, config, history, kibanaVersion, extensions }) => { + const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); + + return ( + + + + + + + + + + + + {children} + + + + + + + + + + + + ); + } +); + +export const AppRoutes = memo(() => { + const { agents } = useConfig(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index d4e652ad95831..5eb1da134f5f0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -3,41 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useEffect, useState } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; -import useObservable from 'react-use/lib/useObservable'; -import { HashRouter as Router, Redirect, Switch, Route, RouteProps } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; -import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { Redirect, Route, RouteProps } from 'react-router-dom'; import { CoreStart, AppMountParameters } from 'src/core/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../xpack_legacy/common'; import { IngestManagerSetupDeps, IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { PAGE_ROUTING_PATHS } from './constants'; -import { DefaultLayout, WithoutHeaderLayout } from './layouts'; -import { Loading, Error } from './components'; -import { IngestManagerOverview, EPMApp, AgentPolicyApp, FleetApp, DataStreamApp } from './sections'; -import { - DepsContext, - ConfigContext, - useConfig, - useCore, - sendSetup, - sendGetPermissionsCheck, - licenseService, - KibanaVersionContext, -} from './hooks'; -import { PackageInstallProvider } from './sections/epm/hooks'; -import { FleetStatusProvider, useBreadcrumbs } from './hooks'; -import { IntraAppStateProvider } from './hooks/use_intra_app_state'; +import { licenseService } from './hooks'; import { UIExtensionsStorage } from './types'; -import { UIExtensionsContext } from './hooks/use_ui_extension'; +import { AppRoutes, FleetAppContext, WithPermissionsAndSetup } from './app'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; @@ -52,184 +29,7 @@ export const ProtectedRoute: React.FunctionComponent = ({ return isAllowed ? : ; }; -const Panel = styled(EuiPanel)` - max-width: 500px; - margin-right: auto; - margin-left: auto; -`; - -const ErrorLayout = ({ children }: { children: JSX.Element }) => ( - - - {children} - - -); - -const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basepath: string }>( - ({ history, ...rest }) => { - useBreadcrumbs('base'); - const { agents } = useConfig(); - - const { notifications } = useCore(); - - const [isPermissionsLoading, setIsPermissionsLoading] = useState(false); - const [permissionsError, setPermissionsError] = useState(); - const [isInitialized, setIsInitialized] = useState(false); - const [initializationError, setInitializationError] = useState(null); - - useEffect(() => { - (async () => { - setIsPermissionsLoading(false); - setPermissionsError(undefined); - setIsInitialized(false); - setInitializationError(null); - try { - setIsPermissionsLoading(true); - const permissionsResponse = await sendGetPermissionsCheck(); - setIsPermissionsLoading(false); - if (permissionsResponse.data?.success) { - try { - const setupResponse = await sendSetup(); - if (setupResponse.error) { - setInitializationError(setupResponse.error); - } - } catch (err) { - setInitializationError(err); - } - setIsInitialized(true); - } else { - setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR'); - } - } catch (err) { - setPermissionsError('REQUEST_ERROR'); - } - })(); - }, []); - - if (isPermissionsLoading || permissionsError) { - return ( - - {isPermissionsLoading ? ( - - ) : permissionsError === 'REQUEST_ERROR' ? ( - - } - error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', { - defaultMessage: 'There was a problem checking Fleet permissions', - })} - /> - ) : ( - - - {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( - - ) : ( - - )} - - } - body={ -

- {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( - superuser }} - /> - ) : ( - - )} -

- } - /> -
- )} -
- ); - } - - if (!isInitialized || initializationError) { - return ( - - {initializationError ? ( - - } - error={initializationError} - /> - ) : ( - - )} - - ); - } - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } -); - -const IngestManagerApp = ({ +const FleetApp = ({ basepath, coreStart, setupDeps, @@ -248,23 +48,21 @@ const IngestManagerApp = ({ kibanaVersion: string; extensions: UIExtensionsStorage; }) => { - const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); return ( - - - - - - - - - - - - - - - + + + + + ); }; @@ -278,7 +76,7 @@ export function renderApp( extensions: UIExtensionsStorage ) { ReactDOM.render( - Date: Thu, 19 Nov 2020 13:11:08 -0500 Subject: [PATCH 03/20] Initial implementation of test rendering helper tool --- .../fleet/mock/create_test_renderer.tsx | 81 +++++++++++++++++++ .../public/applications/fleet/mock/index.ts | 10 +++ .../fleet/mock/plugin_configuration.ts | 31 +++++++ .../fleet/mock/plugin_dependencies.ts | 11 +++ .../fleet/mock/plugin_interfaces.ts | 18 +++++ 5 files changed, 151 insertions(+) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/index.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx new file mode 100644 index 0000000000000..761fcadf9e7a0 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMemoryHistory } from 'history'; +import React, { memo } from 'react'; +import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react'; +import { ScopedHistory } from 'kibana/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { FleetAppContext } from '../app'; +import { + IngestManagerConfigType, + IngestManagerSetupDeps, + IngestManagerStart, + IngestManagerStartDeps, +} from '../../../plugin'; +import { createSetupDepsMock, createStartDepsMock } from './plugin_dependencies'; +import { createPluginConfigurationMock } from './plugin_configuration'; +import { UIExtensionsStorage } from '../types'; +import { createIngestManagerStartMock } from './plugin_interfaces'; + +type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; + +export interface TestRenderer { + history: ScopedHistory; + coreStart: ReturnType; + setupDeps: IngestManagerSetupDeps; + startDeps: IngestManagerStartDeps; + config: IngestManagerConfigType; + startInterface: IngestManagerStart; + AppWrapper: React.FC; + render: UiRender; +} + +export const createTestRendererMock = (): TestRenderer => { + const basePath = '/mock'; + const history = new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath); + const coreStart = coreMock.createStart({ basePath: '/mock' }); + const setupDeps = createSetupDepsMock(); + const startDeps = createStartDepsMock(); + const config = createPluginConfigurationMock(); + const extensions: UIExtensionsStorage = {}; + const startInterface: IngestManagerStart = createIngestManagerStartMock(extensions); + + const AppWrapper: React.FC = memo(({ children }) => { + return ( + + {children} + + ); + }); + + const render: UiRender = (ui, options) => { + return reactRender(ui, { + wrapper: AppWrapper, + ...options, + }); + }; + + return { + history, + coreStart, + setupDeps, + startDeps, + config, + startInterface, + AppWrapper, + render, + }; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts new file mode 100644 index 0000000000000..68f157322d288 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_test_renderer'; +export * from './plugin_configuration'; +export * from './plugin_dependencies'; +export * from './plugin_interfaces'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts new file mode 100644 index 0000000000000..f59512733dbb8 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestManagerConfigType } from '../../../plugin'; + +export const createPluginConfigurationMock = (): IngestManagerConfigType => { + return { + enabled: true, + registryUrl: '', + registryProxyUrl: '', + agents: { + enabled: true, + tlsCheckDisabled: true, + pollingRequestTimeout: 1000, + maxConcurrentConnections: 100, + kibana: { + host: '', + ca_sha256: '', + }, + elasticsearch: { + host: '', + ca_sha256: '', + }, + agentPolicyRolloutRateLimitIntervalMs: 100, + agentPolicyRolloutRateLimitRequestPerInterval: 1000, + }, + }; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts new file mode 100644 index 0000000000000..1cbe487000a9a --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestManagerSetupDeps, IngestManagerStartDeps } from '../../../plugin'; + +export const createSetupDepsMock = (): IngestManagerSetupDeps => {}; + +export const createStartDepsMock = (): IngestManagerStartDeps => {}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts new file mode 100644 index 0000000000000..5960003d40cc6 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestManagerStart } from '../../../plugin'; +import { UIExtensionsStorage } from '../types'; +import { createExtensionRegistrationCallback } from '../services/ui_extensions'; + +export const createIngestManagerStartMock = ( + extensionsStorage: UIExtensionsStorage +): IngestManagerStart => { + return { + isInitialized: jest.fn().mockResolvedValue(true), + registerExtension: createExtensionRegistrationCallback(extensionsStorage), + }; +}; From 82a475ffbe7298e92f260c9bdd7f24f29c2ca70e Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 19 Nov 2020 13:15:24 -0500 Subject: [PATCH 04/20] adjust order of contexts to match prior implementation --- x-pack/plugins/fleet/public/applications/fleet/app.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 595e9d7d26ce3..5eef9c3490bea 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -204,9 +204,11 @@ export const FleetAppContext: React.FC<{ - - {children} - + + + {children} + + From df94c92dc018b97e77d4d4cf68957d0913130e31 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 19 Nov 2020 13:25:59 -0500 Subject: [PATCH 05/20] more specific implementation around custom tab component --- .../fleet/sections/epm/screens/detail/content.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx index 6732c4ce7d849..8782d7947a23e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx @@ -52,7 +52,6 @@ export function ContentPanel(props: ContentPanelProps) { const { panel, name, version, assets, title, removable, latestVersion } = props; const CustomView = useUIExtension(name, 'package-detail-custom'); const { getPath } = useLink(); - const { pkgkey } = useParams<{ pkgkey?: string }>(); switch (panel) { case 'settings': @@ -69,12 +68,12 @@ export function ContentPanel(props: ContentPanelProps) { case 'policies': return ; case 'custom': - return ( - (CustomView && ( - - - - )) || + return CustomView ? ( + + + + ) : ( + ); case 'overview': default: From 028e0e1938744166360ae541654a7754047d2cc8 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 19 Nov 2020 13:54:16 -0500 Subject: [PATCH 06/20] Plugin dependencies mocks + renaming + expose mocks for other plugins --- .../fleet/mock/create_test_renderer.tsx | 8 ++++---- .../fleet/mock/plugin_configuration.ts | 2 +- .../fleet/mock/plugin_dependencies.ts | 17 +++++++++++++++-- .../fleet/mock/plugin_interfaces.ts | 4 ++-- .../sections/epm/screens/detail/content.tsx | 2 +- x-pack/plugins/fleet/public/mocks.ts | 11 +++++++++++ 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/fleet/public/mocks.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx index 761fcadf9e7a0..34466f6c76a30 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -17,9 +17,9 @@ import { IngestManagerStartDeps, } from '../../../plugin'; import { createSetupDepsMock, createStartDepsMock } from './plugin_dependencies'; -import { createPluginConfigurationMock } from './plugin_configuration'; +import { createConfigurationMock } from './plugin_configuration'; import { UIExtensionsStorage } from '../types'; -import { createIngestManagerStartMock } from './plugin_interfaces'; +import { createStartMock } from './plugin_interfaces'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -40,9 +40,9 @@ export const createTestRendererMock = (): TestRenderer => { const coreStart = coreMock.createStart({ basePath: '/mock' }); const setupDeps = createSetupDepsMock(); const startDeps = createStartDepsMock(); - const config = createPluginConfigurationMock(); + const config = createConfigurationMock(); const extensions: UIExtensionsStorage = {}; - const startInterface: IngestManagerStart = createIngestManagerStartMock(extensions); + const startInterface = createStartMock(extensions); const AppWrapper: React.FC = memo(({ children }) => { return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index f59512733dbb8..7eb5b818eadb8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -6,7 +6,7 @@ import { IngestManagerConfigType } from '../../../plugin'; -export const createPluginConfigurationMock = (): IngestManagerConfigType => { +export const createConfigurationMock = (): IngestManagerConfigType => { return { enabled: true, registryUrl: '', diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts index 1cbe487000a9a..9302b5bba0049 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts @@ -5,7 +5,20 @@ */ import { IngestManagerSetupDeps, IngestManagerStartDeps } from '../../../plugin'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { licensingMock } from '../../../../../licensing/public/mocks'; +import { homePluginMock } from '../../../../../../../src/plugins/home/public/mocks'; -export const createSetupDepsMock = (): IngestManagerSetupDeps => {}; +export const createSetupDepsMock = (): IngestManagerSetupDeps => { + return { + licensing: licensingMock.createSetup(), + data: dataPluginMock.createSetupContract(), + home: homePluginMock.createSetupContract(), + }; +}; -export const createStartDepsMock = (): IngestManagerStartDeps => {}; +export const createStartDepsMock = (): IngestManagerStartDeps => { + return { + data: dataPluginMock.createStartContract(), + }; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts index 5960003d40cc6..3bc3472e6de11 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts @@ -8,8 +8,8 @@ import { IngestManagerStart } from '../../../plugin'; import { UIExtensionsStorage } from '../types'; import { createExtensionRegistrationCallback } from '../services/ui_extensions'; -export const createIngestManagerStartMock = ( - extensionsStorage: UIExtensionsStorage +export const createStartMock = ( + extensionsStorage: UIExtensionsStorage = {} ): IngestManagerStart => { return { isInitialized: jest.fn().mockResolvedValue(true), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx index 8782d7947a23e..4b7d5f672f585 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { Redirect, useParams } from 'react-router-dom'; +import { Redirect } from 'react-router-dom'; import { DetailParams } from '.'; import { PackageInfo } from '../../../../types'; import { AssetsFacetGroup } from '../../components/assets_facet_group'; diff --git a/x-pack/plugins/fleet/public/mocks.ts b/x-pack/plugins/fleet/public/mocks.ts new file mode 100644 index 0000000000000..63aac3332aa03 --- /dev/null +++ b/x-pack/plugins/fleet/public/mocks.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createStartMock } from './applications/fleet/mock'; + +export const fleetMock = { + createStartMock, +}; From 8bba5d12c149349b0040f25aefc0cf9b59b878cf Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 19 Nov 2020 13:54:36 -0500 Subject: [PATCH 07/20] refactor Endpoint to use mock builder from ingest --- .../public/common/mock/endpoint/dependencies_start_mock.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts index 3388fb5355845..3362fa91c6c11 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts @@ -9,6 +9,7 @@ import { dataPluginMock, Start as DataPublicStartMock, } from '../../../../../../../src/plugins/data/public/mocks'; +import { fleetMock } from '../../../../../fleet/public/mocks'; type DataMock = Omit & { indexPatterns: Omit & { @@ -56,9 +57,6 @@ export const depsStartMock: () => DepsStartMock = () => { return { data: dataMock, - ingestManager: { - isInitialized: () => Promise.resolve(true), - registerExtension: jest.fn(), - }, + ingestManager: fleetMock.createStartMock(), }; }; From 88784fbdd0518ec7d6f6dc627d320f1cacd657f0 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 23 Nov 2020 10:16:25 -0500 Subject: [PATCH 08/20] Fix plugin.ts after merge from master --- x-pack/plugins/fleet/public/plugin.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 31b53f41b3a91..ac50e061dc57e 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -104,7 +104,15 @@ export class FleetPlugin implements Plugin { unmount(); From 4bd5c757da4ded35c803f57502c94c9600a8537e Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 23 Nov 2020 10:55:40 -0500 Subject: [PATCH 09/20] Adjustment to test renderer to use values from returned object during render --- .../fleet/mock/create_test_renderer.tsx | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx index 34466f6c76a30..e54cb851aa7d0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -10,12 +10,7 @@ import { render as reactRender, RenderOptions, RenderResult } from '@testing-lib import { ScopedHistory } from 'kibana/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { FleetAppContext } from '../app'; -import { - IngestManagerConfigType, - IngestManagerSetupDeps, - IngestManagerStart, - IngestManagerStartDeps, -} from '../../../plugin'; +import { FleetConfigType, FleetSetupDeps, FleetStartDeps, FleetStart } from '../../../plugin'; import { createSetupDepsMock, createStartDepsMock } from './plugin_dependencies'; import { createConfigurationMock } from './plugin_configuration'; import { UIExtensionsStorage } from '../types'; @@ -26,10 +21,11 @@ type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResul export interface TestRenderer { history: ScopedHistory; coreStart: ReturnType; - setupDeps: IngestManagerSetupDeps; - startDeps: IngestManagerStartDeps; - config: IngestManagerConfigType; - startInterface: IngestManagerStart; + setupDeps: FleetSetupDeps; + startDeps: FleetStartDeps; + config: FleetConfigType; + startInterface: FleetStart; + kibanaVersion: string; AppWrapper: React.FC; render: UiRender; } @@ -44,38 +40,37 @@ export const createTestRendererMock = (): TestRenderer => { const extensions: UIExtensionsStorage = {}; const startInterface = createStartMock(extensions); - const AppWrapper: React.FC = memo(({ children }) => { - return ( - - {children} - - ); - }); - - const render: UiRender = (ui, options) => { - return reactRender(ui, { - wrapper: AppWrapper, - ...options, - }); - }; - - return { + const testRendererMocks: TestRenderer = { history, coreStart, setupDeps, startDeps, config, startInterface, - AppWrapper, - render, + kibanaVersion: '8.0.0', + AppWrapper: memo(({ children }) => { + return ( + + {children} + + ); + }), + render: (ui, options) => { + return reactRender(ui, { + wrapper: testRendererMocks.AppWrapper, + ...options, + }); + }, }; + + return testRendererMocks; }; From 60a7ff3a0229c1bc415f33bf5b99ff0b330032d1 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 23 Nov 2020 12:46:20 -0500 Subject: [PATCH 10/20] More fixes after merge from master --- .../fleet/public/applications/fleet/app.tsx | 71 ++++++++----------- .../fleet/public/applications/fleet/index.tsx | 29 +++----- .../fleet/mock/create_test_renderer.tsx | 4 +- x-pack/plugins/fleet/public/plugin.ts | 10 +-- 4 files changed, 42 insertions(+), 72 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 5eef9c3490bea..c30feaa9d8945 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -14,7 +14,6 @@ import styled from 'styled-components'; import useObservable from 'react-use/lib/useObservable'; import { ConfigContext, - DepsContext, FleetStatusProvider, KibanaVersionContext, sendGetPermissionsCheck, @@ -33,11 +32,7 @@ import { DataStreamApp } from './sections/data_stream'; import { FleetApp } from './sections/agents'; import { IngestManagerOverview } from './sections/overview'; import { ProtectedRoute } from './index'; -import { - IngestManagerConfigType, - IngestManagerSetupDeps, - IngestManagerStartDeps, -} from '../../plugin'; +import { FleetConfigType } from '../../plugin'; import { UIExtensionsStorage } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../xpack_legacy/common'; @@ -183,45 +178,39 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { export const FleetAppContext: React.FC<{ basepath: string; coreStart: CoreStart; - setupDeps: IngestManagerSetupDeps; - startDeps: IngestManagerStartDeps; - config: IngestManagerConfigType; + config: FleetConfigType; history: AppMountParameters['history']; kibanaVersion: string; extensions: UIExtensionsStorage; -}> = memo( - ({ children, coreStart, setupDeps, startDeps, config, history, kibanaVersion, extensions }) => { - const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); +}> = memo(({ children, coreStart, config, history, kibanaVersion, extensions }) => { + const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); - return ( - - - - - - - - - - - - - {children} - - - - - - - - - - - - - ); - } -); + return ( + + + + + + + + + + + + {children} + + + + + + + + + + + + ); +}); export const AppRoutes = memo(() => { const { agents } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index 84a98c295da66..172d257a20268 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Redirect, Route, RouteProps } from 'react-router-dom'; import { CoreStart, AppMountParameters } from 'src/core/public'; -import { FleetSetupDeps, FleetConfigType, FleetStartDeps } from '../../plugin'; +import { FleetConfigType } from '../../plugin'; import { licenseService } from './hooks'; import { UIExtensionsStorage } from './types'; import { AppRoutes, FleetAppContext, WithPermissionsAndSetup } from './app'; @@ -25,31 +25,26 @@ export const ProtectedRoute: React.FunctionComponent = ({ return isAllowed ? : ; }; +interface FleetAppProps { + basepath: string; + coreStart: CoreStart; + config: FleetConfigType; + history: AppMountParameters['history']; + kibanaVersion: string; + extensions: UIExtensionsStorage; +} const FleetApp = ({ basepath, coreStart, - setupDeps, - startDeps, config, history, kibanaVersion, extensions, -}: { - basepath: string; - coreStart: CoreStart; - setupDeps: FleetSetupDeps; - startDeps: FleetStartDeps; - config: FleetConfigType; - history: AppMountParameters['history']; - kibanaVersion: string; - extensions: UIExtensionsStorage; -}) => { +}: FleetAppProps) => { return ( { { unmount(); From 333c10e80b294cbae92d680873aec76c15c0a7ca Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 23 Nov 2020 14:44:05 -0500 Subject: [PATCH 11/20] Additional fixes after master merge (jen's changes) --- .../fleet/public/applications/fleet/app.tsx | 18 ++--- .../fleet/public/applications/fleet/index.tsx | 12 ++-- .../fleet/mock/create_test_renderer.tsx | 69 +++++++++++++------ x-pack/plugins/fleet/public/plugin.ts | 2 +- 4 files changed, 63 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index c30feaa9d8945..50b4a1c1fa65a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -5,7 +5,7 @@ */ import React, { memo, useEffect, useState } from 'react'; -import { AppMountParameters, CoreStart } from 'kibana/public'; +import { AppMountParameters } from 'kibana/public'; import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui'; import { HashRouter as Router, Redirect, Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -32,7 +32,7 @@ import { DataStreamApp } from './sections/data_stream'; import { FleetApp } from './sections/agents'; import { IngestManagerOverview } from './sections/overview'; import { ProtectedRoute } from './index'; -import { FleetConfigType } from '../../plugin'; +import { FleetConfigType, FleetStartServices } from '../../plugin'; import { UIExtensionsStorage } from './types'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../../xpack_legacy/common'; @@ -177,17 +177,17 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { */ export const FleetAppContext: React.FC<{ basepath: string; - coreStart: CoreStart; + startServices: FleetStartServices; config: FleetConfigType; history: AppMountParameters['history']; kibanaVersion: string; extensions: UIExtensionsStorage; -}> = memo(({ children, coreStart, config, history, kibanaVersion, extensions }) => { - const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); +}> = memo(({ children, startServices, config, history, kibanaVersion, extensions }) => { + const isDarkMode = useObservable(startServices.uiSettings.get$('theme:darkMode')); return ( - - + + @@ -196,7 +196,7 @@ export const FleetAppContext: React.FC<{ - + {children} @@ -208,7 +208,7 @@ export const FleetAppContext: React.FC<{ - + ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index 172d257a20268..35abda1490dce 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Redirect, Route, RouteProps } from 'react-router-dom'; import { CoreStart, AppMountParameters } from 'src/core/public'; -import { FleetConfigType } from '../../plugin'; +import { FleetConfigType, FleetStartServices } from '../../plugin'; import { licenseService } from './hooks'; import { UIExtensionsStorage } from './types'; import { AppRoutes, FleetAppContext, WithPermissionsAndSetup } from './app'; @@ -27,7 +27,7 @@ export const ProtectedRoute: React.FunctionComponent = ({ interface FleetAppProps { basepath: string; - coreStart: CoreStart; + startServices: FleetStartServices; config: FleetConfigType; history: AppMountParameters['history']; kibanaVersion: string; @@ -35,7 +35,7 @@ interface FleetAppProps { } const FleetApp = ({ basepath, - coreStart, + startServices, config, history, kibanaVersion, @@ -44,7 +44,7 @@ const FleetApp = ({ return ( RenderResult; export interface TestRenderer { history: ScopedHistory; - coreStart: ReturnType; - setupDeps: FleetSetupDeps; - startDeps: FleetStartDeps; + startServices: FleetStartServices; config: FleetConfigType; + /** The Interface returned by the Fleet plugin `start()` phase */ startInterface: FleetStart; kibanaVersion: string; AppWrapper: React.FC; render: UiRender; } +const createMockStore = (): MockedKeys => { + let store: Record = {}; + return { + getItem: jest.fn().mockImplementation((key) => store[key]), + setItem: jest.fn().mockImplementation((key, value) => (store[key] = value)), + removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), + clear: jest.fn().mockImplementation(() => (store = {})), + }; +}; + +const configureStartServices = (services: FleetStartServices): void => { + // Store the http for use by useRequest + setHttpClient(services.http); + + // Set Fleet available capabilities + services.application.capabilities = { + ...services.application.capabilities, + fleet: { + read: true, + write: true, + }, + }; +}; + export const createTestRendererMock = (): TestRenderer => { const basePath = '/mock'; - const history = new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath); - const coreStart = coreMock.createStart({ basePath: '/mock' }); - const setupDeps = createSetupDepsMock(); - const startDeps = createStartDepsMock(); - const config = createConfigurationMock(); const extensions: UIExtensionsStorage = {}; - const startInterface = createStartMock(extensions); + const startServices: FleetStartServices = { + ...coreMock.createStart({ basePath }), + ...createStartDepsMock(), + storage: new Storage(createMockStore()), + }; + + configureStartServices(startServices); const testRendererMocks: TestRenderer = { - history, - coreStart, - setupDeps, - startDeps, - config, - startInterface, + history: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath), + startServices, + config: createConfigurationMock(), + startInterface: createStartMock(extensions), kibanaVersion: '8.0.0', AppWrapper: memo(({ children }) => { return ( diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 7c6cdeb64884a..31b53f41b3a91 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -104,7 +104,7 @@ export class FleetPlugin implements Plugin { unmount(); From 80a5e96fb9e49c8aeee30f0a2816fe41ad3cc7b1 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 23 Nov 2020 14:51:04 -0500 Subject: [PATCH 12/20] Initial test file for Integration details --- .../epm/screens/detail/index.test.tsx | 37 +++++++++++++++++++ .../sections/epm/screens/detail/index.tsx | 6 +-- 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx new file mode 100644 index 0000000000000..20e663abf073d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTestRendererMock, TestRenderer } from '../../../../mock'; +import { Detail } from './index'; +import React from 'react'; +import { pagePathGetters } from '../../../../constants'; + +describe('when on integration detail', () => { + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => (renderResult = testRenderer.render()); + + beforeEach(() => { + testRenderer = createTestRendererMock(); + testRenderer.history.push(pagePathGetters.integration_details({ pkgkey: 'ngnix' })); + }); + + describe('and a custom UI extension is NOT registered', () => { + beforeEach(() => render()); + + it('should not show a custom tab', () => { + expect(renderResult.queryByTestId('tab-custom')).toBeNull(); + }); + + it.todo('should redirect if custom url is accessed'); + }); + + describe('and a custom UI extension is registered', () => { + it.todo('should display "custom" tab in navigation'); + + it.todo('should display custom content when tab is clicked'); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index 3d63b2ea05433..46042285d1793 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -76,8 +76,7 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) { } export function Detail() { - // TODO: fix forced cast if possible - const { pkgkey, panel = DEFAULT_PANEL } = useParams() as DetailParams; + const { pkgkey, panel = DEFAULT_PANEL } = useParams(); const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; @@ -263,13 +262,14 @@ export function Detail() { id: panelId, name: display, isSelected: panelId === panel, + 'data-test-subj': `tab-${panelId}`, href: getHref('integration_details', { pkgkey: `${packageInfo?.name}-${packageInfo?.version}`, panel: panelId, }), }; }) as unknown) as WithHeaderLayoutProps['tabs']; - }, [getHref, packageInfo, panel, showCustomTab, packageInfoData]); + }, [getHref, packageInfo, panel, showCustomTab]); return ( Date: Tue, 24 Nov 2020 10:23:55 -0500 Subject: [PATCH 13/20] apply feedback from prior code review --- .../applications/fleet/hooks/use_url_pagination.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_url_pagination.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_url_pagination.ts index f9c351899fe0a..40539ed749285 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_url_pagination.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_url_pagination.ts @@ -69,15 +69,13 @@ const paginationFromUrlParams = (urlParams: UrlPaginationParams): Pagination => // Search params can appear multiple times in the URL, in which case the value for them, // once parsed, would be an array. In these case, we take the last value defined pagination.currentPage = Number( - (Array.isArray(urlParams.currentPage) - ? urlParams.currentPage[urlParams.currentPage.length - 1] - : urlParams.currentPage) ?? pagination.currentPage + (Array.isArray(urlParams.currentPage) ? urlParams.currentPage.pop() : urlParams.currentPage) ?? + pagination.currentPage ); pagination.pageSize = Number( - (Array.isArray(urlParams.pageSize) - ? urlParams.pageSize[urlParams.pageSize.length - 1] - : urlParams.pageSize) ?? pagination.pageSize + (Array.isArray(urlParams.pageSize) ? urlParams.pageSize.pop() : urlParams.pageSize) ?? + pagination.pageSize ) ?? pagination.pageSize; // If Current Page is not a valid positive integer, set it to 1 From d013587ec7e96bb988d126adff33b28abaec57ae Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 24 Nov 2020 13:15:36 -0500 Subject: [PATCH 14/20] Refactor FleetStartServices mock --- .../fleet/mock/create_test_renderer.tsx | 52 ++++------------- .../fleet/mock/fleet_start_services.tsx | 56 +++++++++++++++++++ .../public/applications/fleet/mock/index.ts | 2 + .../fleet/mock/plugin_dependencies.ts | 6 +- .../public/applications/fleet/mock/types.ts | 14 +++++ .../epm/screens/detail/index.test.tsx | 2 +- 6 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/fleet_start_services.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/mock/types.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx index 68ed6401dccc9..46c651202079c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -6,18 +6,14 @@ import { createMemoryHistory } from 'history'; import React, { memo } from 'react'; -import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react'; +import { render as reactRender, RenderOptions, RenderResult, act } from '@testing-library/react'; import { ScopedHistory } from '../../../../../../../src/core/public'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { IStorage, Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { FleetAppContext } from '../app'; import { FleetConfigType, FleetStart, FleetStartServices } from '../../../plugin'; -import { createStartDepsMock } from './plugin_dependencies'; import { createConfigurationMock } from './plugin_configuration'; import { UIExtensionsStorage } from '../types'; import { createStartMock } from './plugin_interfaces'; -import { MockedKeys } from '../../../../../../../packages/kbn-utility-types/jest/index'; -import { setHttpClient } from '../hooks/use_request'; +import { createStartServices } from './fleet_start_services'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -32,41 +28,10 @@ export interface TestRenderer { render: UiRender; } -const createMockStore = (): MockedKeys => { - let store: Record = {}; - return { - getItem: jest.fn().mockImplementation((key) => store[key]), - setItem: jest.fn().mockImplementation((key, value) => (store[key] = value)), - removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), - clear: jest.fn().mockImplementation(() => (store = {})), - }; -}; - -const configureStartServices = (services: FleetStartServices): void => { - // Store the http for use by useRequest - setHttpClient(services.http); - - // Set Fleet available capabilities - services.application.capabilities = { - ...services.application.capabilities, - fleet: { - read: true, - write: true, - }, - }; -}; - export const createTestRendererMock = (): TestRenderer => { const basePath = '/mock'; const extensions: UIExtensionsStorage = {}; - const startServices: FleetStartServices = { - ...coreMock.createStart({ basePath }), - ...createStartDepsMock(), - storage: new Storage(createMockStore()), - }; - - configureStartServices(startServices); - + const startServices = createStartServices(basePath); const testRendererMocks: TestRenderer = { history: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath), startServices, @@ -88,10 +53,15 @@ export const createTestRendererMock = (): TestRenderer => { ); }), render: (ui, options) => { - return reactRender(ui, { - wrapper: testRendererMocks.AppWrapper, - ...options, + let renderResponse: RenderResult; + act(() => { + renderResponse = reactRender(ui, { + wrapper: testRendererMocks.AppWrapper, + ...options, + }); }); + // @ts-ignore + return renderResponse; }, }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/fleet_start_services.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/fleet_start_services.tsx new file mode 100644 index 0000000000000..cc24d946e46af --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/fleet_start_services.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { createStartDepsMock } from './plugin_dependencies'; +import { IStorage, Storage } from '../../../../../../../src/plugins/kibana_utils/public'; +import { MockedKeys } from '../../../../../../../packages/kbn-utility-types/jest/index'; +import { setHttpClient } from '../hooks/use_request'; +import { MockedFleetStartServices } from './types'; + +// Taken from core. See: src/plugins/kibana_utils/public/storage/storage.test.ts +const createMockStore = (): MockedKeys => { + let store: Record = {}; + return { + getItem: jest.fn().mockImplementation((key) => store[key]), + setItem: jest.fn().mockImplementation((key, value) => (store[key] = value)), + removeItem: jest.fn().mockImplementation((key: string) => delete store[key]), + clear: jest.fn().mockImplementation(() => (store = {})), + }; +}; + +const configureStartServices = (services: MockedFleetStartServices): void => { + // Store the http for use by useRequest + setHttpClient(services.http); + + // Set Fleet available capabilities + services.application.capabilities = { + ...services.application.capabilities, + fleet: { + read: true, + write: true, + }, + }; + + // Setup the `i18n.Context` component + services.i18n.Context.mockImplementation(({ children }: { children: React.ReactNode }) => ( + {children} + )); +}; + +export const createStartServices = (basePath: string = '/mock'): MockedFleetStartServices => { + const startServices: MockedFleetStartServices = { + ...coreMock.createStart({ basePath }), + ...createStartDepsMock(), + storage: new Storage(createMockStore()) as jest.Mocked, + }; + + configureStartServices(startServices); + + return startServices; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts index 68f157322d288..6202f2cfee1fb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/index.ts @@ -8,3 +8,5 @@ export * from './create_test_renderer'; export * from './plugin_configuration'; export * from './plugin_dependencies'; export * from './plugin_interfaces'; +export * from './fleet_start_services'; +export * from './types'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts index 9302b5bba0049..51de2970f9ba6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_dependencies.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerSetupDeps, IngestManagerStartDeps } from '../../../plugin'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { licensingMock } from '../../../../../licensing/public/mocks'; import { homePluginMock } from '../../../../../../../src/plugins/home/public/mocks'; +import { MockedFleetSetupDeps, MockedFleetStartDeps } from './types'; -export const createSetupDepsMock = (): IngestManagerSetupDeps => { +export const createSetupDepsMock = (): MockedFleetSetupDeps => { return { licensing: licensingMock.createSetup(), data: dataPluginMock.createSetupContract(), @@ -17,7 +17,7 @@ export const createSetupDepsMock = (): IngestManagerSetupDeps => { }; }; -export const createStartDepsMock = (): IngestManagerStartDeps => { +export const createStartDepsMock = (): MockedFleetStartDeps => { return { data: dataPluginMock.createStartContract(), }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts new file mode 100644 index 0000000000000..bfe8f58dcedde --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockedKeys } from '../../../../../../../packages/kbn-utility-types/jest/index'; +import { FleetSetupDeps, FleetStartDeps, FleetStartServices } from '../../../plugin'; + +export type MockedFleetStartServices = MockedKeys; + +export type MockedFleetSetupDeps = MockedKeys; + +export type MockedFleetStartDeps = MockedKeys; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 20e663abf073d..e961e8217bb97 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -16,7 +16,7 @@ describe('when on integration detail', () => { beforeEach(() => { testRenderer = createTestRendererMock(); - testRenderer.history.push(pagePathGetters.integration_details({ pkgkey: 'ngnix' })); + testRenderer.history.push(pagePathGetters.integration_details({ pkgkey: 'ngnix-99.0.0' })); }); describe('and a custom UI extension is NOT registered', () => { From b29029ac3cf570cb0a4333a6c1e4284cc9b42813 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 24 Nov 2020 16:29:41 -0500 Subject: [PATCH 15/20] Fix TestRender's history instance due to use of Hash Router --- .../fleet/public/applications/fleet/app.tsx | 71 +++-- .../fleet/mock/create_test_renderer.tsx | 23 +- .../epm/screens/detail/index.test.tsx | 284 +++++++++++++++++- 3 files changed, 339 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 50b4a1c1fa65a..766ad961674af 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -7,7 +7,8 @@ import React, { memo, useEffect, useState } from 'react'; import { AppMountParameters } from 'kibana/public'; import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui'; -import { HashRouter as Router, Redirect, Route, Switch } from 'react-router-dom'; +import { createHashHistory, History } from 'history'; +import { Router, Redirect, Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; @@ -182,35 +183,47 @@ export const FleetAppContext: React.FC<{ history: AppMountParameters['history']; kibanaVersion: string; extensions: UIExtensionsStorage; -}> = memo(({ children, startServices, config, history, kibanaVersion, extensions }) => { - const isDarkMode = useObservable(startServices.uiSettings.get$('theme:darkMode')); + /** For testing purposes only */ + routerHistory?: History; +}> = memo( + ({ + children, + startServices, + config, + history, + kibanaVersion, + extensions, + routerHistory = createHashHistory(), + }) => { + const isDarkMode = useObservable(startServices.uiSettings.get$('theme:darkMode')); - return ( - - - - - - - - - - - - {children} - - - - - - - - - - - - ); -}); + return ( + + + + + + + + + + + + {children} + + + + + + + + + + + + ); + } +); export const AppRoutes = memo(() => { const { agents } = useConfig(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx index 46c651202079c..9280385d0468d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -4,22 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History, createHashHistory } from 'history'; import React, { memo } from 'react'; import { render as reactRender, RenderOptions, RenderResult, act } from '@testing-library/react'; import { ScopedHistory } from '../../../../../../../src/core/public'; import { FleetAppContext } from '../app'; -import { FleetConfigType, FleetStart, FleetStartServices } from '../../../plugin'; +import { FleetConfigType, FleetStart } from '../../../plugin'; import { createConfigurationMock } from './plugin_configuration'; import { UIExtensionsStorage } from '../types'; import { createStartMock } from './plugin_interfaces'; import { createStartServices } from './fleet_start_services'; +import { MockedFleetStartServices } from './types'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; +/** + * Test Renderer that includes mocked services and interfaces used during Fleet applicaiton rendering. + * Any of the properties in this interface can be manipulated prior to `render()` if wanting to customize + * the rendering context. + */ export interface TestRenderer { - history: ScopedHistory; - startServices: FleetStartServices; + /** History instance currently used by the Fleet UI Hash Router */ + history: History; + /** history instance provided to the Fleet plugin during application `mount()` */ + mountHistory: ScopedHistory; + startServices: MockedFleetStartServices; config: FleetConfigType; /** The Interface returned by the Fleet plugin `start()` phase */ startInterface: FleetStart; @@ -33,7 +42,8 @@ export const createTestRendererMock = (): TestRenderer => { const extensions: UIExtensionsStorage = {}; const startServices = createStartServices(basePath); const testRendererMocks: TestRenderer = { - history: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath), + history: createHashHistory(), + mountHistory: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath), startServices, config: createConfigurationMock(), startInterface: createStartMock(extensions), @@ -44,9 +54,10 @@ export const createTestRendererMock = (): TestRenderer => { basepath={basePath} startServices={testRendererMocks.startServices} config={testRendererMocks.config} - history={testRendererMocks.history} + history={testRendererMocks.mountHistory} kibanaVersion={testRendererMocks.kibanaVersion} extensions={extensions} + routerHistory={testRendererMocks.history} > {children} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index e961e8217bb97..5cf40c23025fd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -4,24 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createTestRendererMock, TestRenderer } from '../../../../mock'; +import { createTestRendererMock, MockedFleetStartServices, TestRenderer } from '../../../../mock'; import { Detail } from './index'; import React from 'react'; -import { pagePathGetters } from '../../../../constants'; +import { PAGE_ROUTING_PATHS, pagePathGetters } from '../../../../constants'; +import { Route } from 'react-router-dom'; +import { GetInfoResponse } from '../../../../../../../common/types/rest_spec'; +import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models'; describe('when on integration detail', () => { let testRenderer: TestRenderer; let renderResult: ReturnType; - const render = () => (renderResult = testRenderer.render()); + const render = () => + (renderResult = testRenderer.render( + + + + )); beforeEach(() => { testRenderer = createTestRendererMock(); - testRenderer.history.push(pagePathGetters.integration_details({ pkgkey: 'ngnix-99.0.0' })); + mockApiCalls(testRenderer.startServices.http); + testRenderer.history.push(`${pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7' })}`); }); describe('and a custom UI extension is NOT registered', () => { beforeEach(() => render()); + it('should show overview and settings tabs', () => { + const tabs: DetailViewPanelName[] = ['overview', 'settings']; + for (const tab of tabs) { + expect(renderResult.getByTestId(`tab-${tab}`)); + } + }); + it('should not show a custom tab', () => { expect(renderResult.queryByTestId('tab-custom')).toBeNull(); }); @@ -35,3 +51,263 @@ describe('when on integration detail', () => { it.todo('should display custom content when tab is clicked'); }); }); + +const mockApiCalls = (http: MockedFleetStartServices['http']) => { + // @ts-ignore + const epmPackageResponse: GetInfoResponse = { + response: { + name: 'nginx', + title: 'Nginx', + version: '0.3.7', + release: 'experimental', + description: 'Nginx Integration', + type: 'integration', + download: '/epr/nginx/nginx-0.3.7.zip', + path: '/package/nginx/0.3.7', + icons: [ + { + src: '/img/logo_nginx.svg', + path: '/package/nginx/0.3.7/img/logo_nginx.svg', + title: 'logo nginx', + size: '32x32', + type: 'image/svg+xml', + }, + ], + format_version: '1.0.0', + readme: '/package/nginx/0.3.7/docs/README.md', + license: 'basic', + categories: ['web', 'security'], + conditions: { 'kibana.version': '^7.9.0' }, + screenshots: [ + { + src: '/img/kibana-nginx.png', + path: '/package/nginx/0.3.7/img/kibana-nginx.png', + title: 'kibana nginx', + size: '1218x1266', + type: 'image/png', + }, + { + src: '/img/metricbeat-nginx.png', + path: '/package/nginx/0.3.7/img/metricbeat-nginx.png', + title: 'metricbeat nginx', + size: '2560x2100', + type: 'image/png', + }, + ], + assets: { + kibana: { + dashboard: [ + { + pkgkey: 'nginx-0.3.7', + service: 'kibana', + type: 'dashboard' as KibanaAssetType, + file: 'nginx-023d2930-f1a5-11e7-a9ef-93c69af7b129.json', + }, + ], + search: [ + { + pkgkey: 'nginx-0.3.7', + service: 'kibana', + type: 'search' as KibanaAssetType, + file: 'nginx-6d9e66d0-a1f0-11e7-928f-5dbe6f6f5519.json', + }, + ], + visualization: [ + { + pkgkey: 'nginx-0.3.7', + service: 'kibana', + type: 'visualization' as KibanaAssetType, + file: 'nginx-0dd6f320-a29f-11e7-928f-5dbe6f6f5519.json', + }, + ], + }, + }, + policy_templates: [ + { + name: 'nginx', + title: 'Nginx logs and metrics', + description: 'Collect logs and metrics from Nginx instances', + inputs: [ + { + type: 'logfile', + title: 'Collect logs from Nginx instances', + description: 'Collecting Nginx access, error and ingress controller logs', + }, + { + type: 'nginx/metrics', + vars: [ + { + name: 'hosts', + type: 'text', + title: 'Hosts', + multi: true, + required: true, + show_user: true, + default: ['http://127.0.0.1:80'], + }, + ], + title: 'Collect metrics from Nginx instances', + description: 'Collecting Nginx stub status metrics', + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'nginx.access', + title: 'Nginx access logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/access.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx access logs', + description: 'Collect Nginx access logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'access', + }, + { + type: 'logs', + dataset: 'nginx.error', + title: 'Nginx error logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/error.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx error logs', + description: 'Collect Nginx error logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'error', + }, + { + type: 'logs', + dataset: 'nginx.ingress_controller', + title: 'Nginx ingress_controller logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/ingress.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx ingress controller logs', + description: 'Collect Nginx ingress controller logs', + enabled: false, + }, + ], + package: 'nginx', + path: 'ingress_controller', + }, + { + type: 'metrics', + dataset: 'nginx.stubstatus', + title: 'Nginx stubstatus metrics', + release: 'experimental', + streams: [ + { + input: 'nginx/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '10s', + }, + { + name: 'server_status_path', + type: 'text', + title: 'Server Status Path', + multi: false, + required: true, + show_user: false, + default: '/nginx_status', + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx stub status metrics', + description: 'Collect Nginx stub status metrics', + enabled: true, + }, + ], + package: 'nginx', + path: 'stubstatus', + }, + ], + owner: { github: 'elastic/integrations-services' }, + latestVersion: '0.3.7', + removable: true, + status: 'not_installed', + }, + } as GetInfoResponse; + + const packageReadMe = ` +# Nginx Integration + +This integration periodically fetches metrics from [Nginx](https://nginx.org/) servers. It can parse access and error +logs created by the HTTP server. + +## Compatibility + +The Nginx \`stubstatus\` metrics was tested with Nginx 1.9 and are expected to work with all version >= 1.9. +The logs were tested with version 1.10. +On Windows, the module was tested with Nginx installed from the Chocolatey repository. +`; + + http.get.mockImplementation(async (path) => { + if (typeof path === 'string') { + if (path === '/api/fleet/epm/packages/nginx-0.3.7') { + return epmPackageResponse; + } + + if (path === '/api/fleet/epm/packages/nginx/0.3.7/docs/README.md') { + return packageReadMe; + } + } + }); +}; From 789ad259a4f0cd3cf96825521a2293692b3608b5 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Tue, 24 Nov 2020 16:42:53 -0500 Subject: [PATCH 16/20] Fixed some left over types --- .../public/applications/fleet/mock/create_test_renderer.tsx | 6 +++--- .../public/applications/fleet/mock/plugin_configuration.ts | 4 ++-- .../public/applications/fleet/mock/plugin_interfaces.ts | 6 ++---- .../plugins/fleet/public/applications/fleet/mock/types.ts | 4 +++- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx index 9280385d0468d..b4ee7184b8d53 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -9,12 +9,12 @@ import React, { memo } from 'react'; import { render as reactRender, RenderOptions, RenderResult, act } from '@testing-library/react'; import { ScopedHistory } from '../../../../../../../src/core/public'; import { FleetAppContext } from '../app'; -import { FleetConfigType, FleetStart } from '../../../plugin'; +import { FleetConfigType } from '../../../plugin'; import { createConfigurationMock } from './plugin_configuration'; import { UIExtensionsStorage } from '../types'; import { createStartMock } from './plugin_interfaces'; import { createStartServices } from './fleet_start_services'; -import { MockedFleetStartServices } from './types'; +import { MockedFleetStart, MockedFleetStartServices } from './types'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -31,7 +31,7 @@ export interface TestRenderer { startServices: MockedFleetStartServices; config: FleetConfigType; /** The Interface returned by the Fleet plugin `start()` phase */ - startInterface: FleetStart; + startInterface: MockedFleetStart; kibanaVersion: string; AppWrapper: React.FC; render: UiRender; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 7eb5b818eadb8..735c1d11a9837 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerConfigType } from '../../../plugin'; +import { FleetConfigType } from '../../../plugin'; -export const createConfigurationMock = (): IngestManagerConfigType => { +export const createConfigurationMock = (): FleetConfigType => { return { enabled: true, registryUrl: '', diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts index 3bc3472e6de11..786702863b0d6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_interfaces.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerStart } from '../../../plugin'; import { UIExtensionsStorage } from '../types'; import { createExtensionRegistrationCallback } from '../services/ui_extensions'; +import { MockedFleetStart } from './types'; -export const createStartMock = ( - extensionsStorage: UIExtensionsStorage = {} -): IngestManagerStart => { +export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): MockedFleetStart => { return { isInitialized: jest.fn().mockResolvedValue(true), registerExtension: createExtensionRegistrationCallback(extensionsStorage), diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts index bfe8f58dcedde..c5830b0a2a560 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/types.ts @@ -5,10 +5,12 @@ */ import { MockedKeys } from '../../../../../../../packages/kbn-utility-types/jest/index'; -import { FleetSetupDeps, FleetStartDeps, FleetStartServices } from '../../../plugin'; +import { FleetSetupDeps, FleetStart, FleetStartDeps, FleetStartServices } from '../../../plugin'; export type MockedFleetStartServices = MockedKeys; export type MockedFleetSetupDeps = MockedKeys; export type MockedFleetStartDeps = MockedKeys; + +export type MockedFleetStart = MockedKeys; From ae81b29b67ae2f2e06da95e01cdfca9f89b843df Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 25 Nov 2020 10:11:30 -0500 Subject: [PATCH 17/20] Fix eslint issue --- .../fleet/sections/epm/screens/detail/index.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index 46042285d1793..ba667200571ba 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -94,6 +94,7 @@ export function Detail() { pkgkey ); + const packageInstallStatus = packageInfoData?.response.status; const showCustomTab = useUIExtension(packageInfoData?.response.name ?? '', 'package-detail-custom') !== undefined; @@ -243,10 +244,7 @@ export function Detail() { return (entries(PanelDisplayNames) .filter(([panelId]) => { // Don't show `Policies` tab if package is not installed - if ( - panelId === 'policies' && - packageInfoData?.response.status !== InstallStatus.installed - ) { + if (panelId === 'policies' && packageInstallStatus !== InstallStatus.installed) { return false; } @@ -269,7 +267,7 @@ export function Detail() { }), }; }) as unknown) as WithHeaderLayoutProps['tabs']; - }, [getHref, packageInfo, panel, showCustomTab]); + }, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]); return ( Date: Wed, 25 Nov 2020 10:39:37 -0500 Subject: [PATCH 18/20] remove ts-ignore --- .../public/applications/fleet/mock/create_test_renderer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx index b4ee7184b8d53..2a96ba63ac18a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/create_test_renderer.tsx @@ -71,8 +71,7 @@ export const createTestRendererMock = (): TestRenderer => { ...options, }); }); - // @ts-ignore - return renderResponse; + return renderResponse!; }, }; From b3ddc2f31c83b48a7f7e2145fc0fe203e19b34bb Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 25 Nov 2020 13:06:10 -0500 Subject: [PATCH 19/20] some more tests --- .../epm/screens/detail/index.test.tsx | 81 +++++++++++++++++-- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 5cf40c23025fd..23f682a4042cb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -6,13 +6,19 @@ import { createTestRendererMock, MockedFleetStartServices, TestRenderer } from '../../../../mock'; import { Detail } from './index'; -import React from 'react'; +import React, { lazy, memo } from 'react'; import { PAGE_ROUTING_PATHS, pagePathGetters } from '../../../../constants'; import { Route } from 'react-router-dom'; -import { GetInfoResponse } from '../../../../../../../common/types/rest_spec'; +import { + GetFleetStatusResponse, + GetInfoResponse, +} from '../../../../../../../common/types/rest_spec'; import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models'; +import { epmRouteService, fleetSetupRouteService } from '../../../../../../../common/services'; +import { act, fireEvent } from '@testing-library/react'; describe('when on integration detail', () => { + const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7' }); let testRenderer: TestRenderer; let renderResult: ReturnType; const render = () => @@ -25,7 +31,7 @@ describe('when on integration detail', () => { beforeEach(() => { testRenderer = createTestRendererMock(); mockApiCalls(testRenderer.startServices.http); - testRenderer.history.push(`${pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7' })}`); + testRenderer.history.push(detailPageUrlPath); }); describe('and a custom UI extension is NOT registered', () => { @@ -42,13 +48,61 @@ describe('when on integration detail', () => { expect(renderResult.queryByTestId('tab-custom')).toBeNull(); }); - it.todo('should redirect if custom url is accessed'); + it('should redirect if custom url is accessed', () => { + act(() => { + testRenderer.history.push( + pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' }) + ); + }); + expect(testRenderer.history.location.pathname).toEqual(detailPageUrlPath); + }); }); describe('and a custom UI extension is registered', () => { - it.todo('should display "custom" tab in navigation'); + let lazyComponentWasRendered: Promise; + + beforeEach(() => { + let setWasRendered: () => void; + lazyComponentWasRendered = new Promise((resolve) => { + setWasRendered = resolve; + }); + + const CustomComponent = lazy(async () => { + return { + default: memo(() => { + setWasRendered(); + return
hello
; + }), + }; + }); + + testRenderer.startInterface.registerExtension({ + package: 'nginx', + view: 'package-detail-custom', + component: CustomComponent, + }); + + render(); + }); + + afterEach(() => { + // @ts-ignore + lazyComponentWasRendered = undefined; + }); - it.todo('should display custom content when tab is clicked'); + it('should display "custom" tab in navigation', () => { + expect(renderResult.getByTestId('tab-custom')); + }); + + it.skip('should display custom content when tab is clicked', async () => { + const customTab = renderResult.getByTestId('tab-custom'); + act(() => { + fireEvent.click(customTab, { button: 1 }); + }); + // testRenderer.history; + await lazyComponentWasRendered; + expect(renderResult.getByTestId('custom-hello')); + }); }); }); @@ -299,15 +353,26 @@ The logs were tested with version 1.10. On Windows, the module was tested with Nginx installed from the Chocolatey repository. `; + const agentsSetupResponse: GetFleetStatusResponse = { isReady: true, missing_requirements: [] }; + http.get.mockImplementation(async (path) => { if (typeof path === 'string') { - if (path === '/api/fleet/epm/packages/nginx-0.3.7') { + if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) { return epmPackageResponse; } - if (path === '/api/fleet/epm/packages/nginx/0.3.7/docs/README.md') { + if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) { return packageReadMe; } + + if (path === fleetSetupRouteService.getFleetSetupPath()) { + return agentsSetupResponse; + } + + const err = new Error(`API [GET ${path}] is not MOCKED!`); + // eslint-disable-next-line no-console + console.log(err); + throw err; } }); }; From 093fecbe65993c6a66b6628fd7df4bdcb16556c7 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Wed, 25 Nov 2020 14:54:55 -0500 Subject: [PATCH 20/20] Complete test cases for custom tab in integrations --- .../fleet/sections/epm/screens/detail/index.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index 23f682a4042cb..9dfc1b5581533 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -15,7 +15,7 @@ import { } from '../../../../../../../common/types/rest_spec'; import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models'; import { epmRouteService, fleetSetupRouteService } from '../../../../../../../common/services'; -import { act, fireEvent } from '@testing-library/react'; +import { act } from '@testing-library/react'; describe('when on integration detail', () => { const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7' }); @@ -59,6 +59,8 @@ describe('when on integration detail', () => { }); describe('and a custom UI extension is registered', () => { + // Because React Lazy components are loaded async (Promise), we setup this "watcher" Promise + // that is `resolved` once the lazy components actually renders. let lazyComponentWasRendered: Promise; beforeEach(() => { @@ -94,12 +96,12 @@ describe('when on integration detail', () => { expect(renderResult.getByTestId('tab-custom')); }); - it.skip('should display custom content when tab is clicked', async () => { - const customTab = renderResult.getByTestId('tab-custom'); + it('should display custom content when tab is clicked', async () => { act(() => { - fireEvent.click(customTab, { button: 1 }); + testRenderer.history.push( + pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' }) + ); }); - // testRenderer.history; await lazyComponentWasRendered; expect(renderResult.getByTestId('custom-hello')); });