[Fleet] Support for showing an Integration Detail Custom (UI Extension) tab (#83805)
* Support for rendering a custom component in Integration Details * Refactor Fleet app initialization contexts in order to support testing setup * New test rendering helper tool * refactor Endpoint to use mock builder from Fleet
This commit is contained in:
parent
eee05ad82a
commit
707dbcd2b1
|
@ -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<AgentAssetType>;
|
||||
|
|
261
x-pack/plugins/fleet/public/applications/fleet/app.tsx
Normal file
261
x-pack/plugins/fleet/public/applications/fleet/app.tsx
Normal file
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* 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 } from 'kibana/public';
|
||||
import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui';
|
||||
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';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import {
|
||||
ConfigContext,
|
||||
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 { FleetConfigType, FleetStartServices } 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 }) => (
|
||||
<EuiErrorBoundary>
|
||||
<DefaultLayout showSettings={false}>
|
||||
<WithoutHeaderLayout>{children}</WithoutHeaderLayout>
|
||||
</DefaultLayout>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
|
||||
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<boolean>(false);
|
||||
const [permissionsError, setPermissionsError] = useState<string>();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [initializationError, setInitializationError] = useState<Error | null>(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 (
|
||||
<ErrorLayout>
|
||||
{isPermissionsLoading ? (
|
||||
<Loading />
|
||||
) : permissionsError === 'REQUEST_ERROR' ? (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionsRequestErrorMessageTitle"
|
||||
defaultMessage="Unable to check permissions"
|
||||
/>
|
||||
}
|
||||
error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', {
|
||||
defaultMessage: 'There was a problem checking Fleet permissions',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h2>
|
||||
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorTitle"
|
||||
defaultMessage="Permission denied"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.securityRequiredErrorTitle"
|
||||
defaultMessage="Security is not enabled"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorMessage"
|
||||
defaultMessage="You are not authorized to access Fleet. Fleet requires {roleName} privileges."
|
||||
values={{ roleName: <EuiCode>superuser</EuiCode> }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.securityRequiredErrorMessage"
|
||||
defaultMessage="You must enable security in Kibana and Elasticsearch to use Fleet."
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isInitialized || initializationError) {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
{initializationError ? (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.initializationErrorMessageTitle"
|
||||
defaultMessage="Unable to initialize Fleet"
|
||||
/>
|
||||
}
|
||||
error={initializationError}
|
||||
/>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
startServices: FleetStartServices;
|
||||
config: FleetConfigType;
|
||||
history: AppMountParameters['history'];
|
||||
kibanaVersion: string;
|
||||
extensions: UIExtensionsStorage;
|
||||
/** For testing purposes only */
|
||||
routerHistory?: History<any>;
|
||||
}> = memo(
|
||||
({
|
||||
children,
|
||||
startServices,
|
||||
config,
|
||||
history,
|
||||
kibanaVersion,
|
||||
extensions,
|
||||
routerHistory = createHashHistory(),
|
||||
}) => {
|
||||
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
|
||||
|
||||
return (
|
||||
<startServices.i18n.Context>
|
||||
<KibanaContextProvider services={{ ...startServices }}>
|
||||
<EuiErrorBoundary>
|
||||
<ConfigContext.Provider value={config}>
|
||||
<KibanaVersionContext.Provider value={kibanaVersion}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<FleetStatusProvider>
|
||||
<IntraAppStateProvider kibanaScopedHistory={history}>
|
||||
<Router history={routerHistory}>
|
||||
<PackageInstallProvider notifications={startServices.notifications}>
|
||||
{children}
|
||||
</PackageInstallProvider>
|
||||
</Router>
|
||||
</IntraAppStateProvider>
|
||||
</FleetStatusProvider>
|
||||
</UIExtensionsContext.Provider>
|
||||
</EuiThemeProvider>
|
||||
</KibanaVersionContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</EuiErrorBoundary>
|
||||
</KibanaContextProvider>
|
||||
</startServices.i18n.Context>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const AppRoutes = memo(() => {
|
||||
const { agents } = useConfig();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={PAGE_ROUTING_PATHS.integrations}>
|
||||
<DefaultLayout section="epm">
|
||||
<EPMApp />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<Route path={PAGE_ROUTING_PATHS.policies}>
|
||||
<DefaultLayout section="agent_policy">
|
||||
<AgentPolicyApp />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<Route path={PAGE_ROUTING_PATHS.data_streams}>
|
||||
<DefaultLayout section="data_stream">
|
||||
<DataStreamApp />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<ProtectedRoute path={PAGE_ROUTING_PATHS.fleet} isAllowed={agents.enabled}>
|
||||
<DefaultLayout section="fleet">
|
||||
<FleetApp />
|
||||
</DefaultLayout>
|
||||
</ProtectedRoute>
|
||||
<Route exact path={PAGE_ROUTING_PATHS.overview}>
|
||||
<DefaultLayout section="overview">
|
||||
<IngestManagerOverview />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
);
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -3,36 +3,14 @@
|
|||
* 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 { FleetConfigType, FleetStartServices } 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 {
|
||||
ConfigContext,
|
||||
useConfig,
|
||||
useStartServices,
|
||||
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;
|
||||
|
@ -47,213 +25,35 @@ export const ProtectedRoute: React.FunctionComponent<ProtectedRouteProps> = ({
|
|||
return isAllowed ? <Route {...routeProps} /> : <Redirect to={{ pathname: restrictedPath }} />;
|
||||
};
|
||||
|
||||
const Panel = styled(EuiPanel)`
|
||||
max-width: 500px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const ErrorLayout = ({ children }: { children: JSX.Element }) => (
|
||||
<EuiErrorBoundary>
|
||||
<DefaultLayout showSettings={false}>
|
||||
<WithoutHeaderLayout>{children}</WithoutHeaderLayout>
|
||||
</DefaultLayout>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
|
||||
const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basepath: string }>(
|
||||
({ history, ...rest }) => {
|
||||
useBreadcrumbs('base');
|
||||
const { agents } = useConfig();
|
||||
|
||||
const { notifications } = useStartServices();
|
||||
|
||||
const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false);
|
||||
const [permissionsError, setPermissionsError] = useState<string>();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [initializationError, setInitializationError] = useState<Error | null>(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 (
|
||||
<ErrorLayout>
|
||||
{isPermissionsLoading ? (
|
||||
<Loading />
|
||||
) : permissionsError === 'REQUEST_ERROR' ? (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionsRequestErrorMessageTitle"
|
||||
defaultMessage="Unable to check permissions"
|
||||
/>
|
||||
}
|
||||
error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', {
|
||||
defaultMessage: 'There was a problem checking Fleet permissions',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h2>
|
||||
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorTitle"
|
||||
defaultMessage="Permission denied"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.securityRequiredErrorTitle"
|
||||
defaultMessage="Security is not enabled"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorMessage"
|
||||
defaultMessage="You are not authorized to access Fleet. Fleet requires {roleName} privileges."
|
||||
values={{ roleName: <EuiCode>superuser</EuiCode> }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.securityRequiredErrorMessage"
|
||||
defaultMessage="You must enable security in Kibana and Elasticsearch to use Fleet."
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isInitialized || initializationError) {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
{initializationError ? (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.initializationErrorMessageTitle"
|
||||
defaultMessage="Unable to initialize Fleet"
|
||||
/>
|
||||
}
|
||||
error={initializationError}
|
||||
/>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<FleetStatusProvider>
|
||||
<IntraAppStateProvider kibanaScopedHistory={history}>
|
||||
<Router {...rest}>
|
||||
<PackageInstallProvider notifications={notifications}>
|
||||
<Switch>
|
||||
<Route path={PAGE_ROUTING_PATHS.integrations}>
|
||||
<DefaultLayout section="epm">
|
||||
<EPMApp />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<Route path={PAGE_ROUTING_PATHS.policies}>
|
||||
<DefaultLayout section="agent_policy">
|
||||
<AgentPolicyApp />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<Route path={PAGE_ROUTING_PATHS.data_streams}>
|
||||
<DefaultLayout section="data_stream">
|
||||
<DataStreamApp />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<ProtectedRoute path={PAGE_ROUTING_PATHS.fleet} isAllowed={agents.enabled}>
|
||||
<DefaultLayout section="fleet">
|
||||
<FleetApp />
|
||||
</DefaultLayout>
|
||||
</ProtectedRoute>
|
||||
<Route exact path={PAGE_ROUTING_PATHS.overview}>
|
||||
<DefaultLayout section="overview">
|
||||
<IngestManagerOverview />
|
||||
</DefaultLayout>
|
||||
</Route>
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
</PackageInstallProvider>
|
||||
</Router>
|
||||
</IntraAppStateProvider>
|
||||
</FleetStatusProvider>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const IngestManagerApp = ({
|
||||
basepath,
|
||||
startServices,
|
||||
config,
|
||||
history,
|
||||
kibanaVersion,
|
||||
extensions,
|
||||
}: {
|
||||
interface FleetAppProps {
|
||||
basepath: string;
|
||||
startServices: FleetStartServices;
|
||||
config: FleetConfigType;
|
||||
history: AppMountParameters['history'];
|
||||
kibanaVersion: string;
|
||||
extensions: UIExtensionsStorage;
|
||||
}) => {
|
||||
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
|
||||
}
|
||||
const FleetApp = ({
|
||||
basepath,
|
||||
startServices,
|
||||
config,
|
||||
history,
|
||||
kibanaVersion,
|
||||
extensions,
|
||||
}: FleetAppProps) => {
|
||||
return (
|
||||
<startServices.i18n.Context>
|
||||
<KibanaContextProvider services={{ ...startServices }}>
|
||||
<ConfigContext.Provider value={config}>
|
||||
<KibanaVersionContext.Provider value={kibanaVersion}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<UIExtensionsContext.Provider value={extensions}>
|
||||
<IngestManagerRoutes history={history} basepath={basepath} />
|
||||
</UIExtensionsContext.Provider>
|
||||
</EuiThemeProvider>
|
||||
</KibanaVersionContext.Provider>
|
||||
</ConfigContext.Provider>
|
||||
</KibanaContextProvider>
|
||||
</startServices.i18n.Context>
|
||||
<FleetAppContext
|
||||
basepath={basepath}
|
||||
startServices={startServices}
|
||||
config={config}
|
||||
history={history}
|
||||
kibanaVersion={kibanaVersion}
|
||||
extensions={extensions}
|
||||
>
|
||||
<WithPermissionsAndSetup>
|
||||
<AppRoutes />
|
||||
</WithPermissionsAndSetup>
|
||||
</FleetAppContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -265,7 +65,7 @@ export function renderApp(
|
|||
extensions: UIExtensionsStorage
|
||||
) {
|
||||
ReactDOM.render(
|
||||
<IngestManagerApp
|
||||
<FleetApp
|
||||
basepath={appBasePath}
|
||||
startServices={startServices}
|
||||
config={config}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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, 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 } from '../../../plugin';
|
||||
import { createConfigurationMock } from './plugin_configuration';
|
||||
import { UIExtensionsStorage } from '../types';
|
||||
import { createStartMock } from './plugin_interfaces';
|
||||
import { createStartServices } from './fleet_start_services';
|
||||
import { MockedFleetStart, 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 instance currently used by the Fleet UI Hash Router */
|
||||
history: History<any>;
|
||||
/** 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: MockedFleetStart;
|
||||
kibanaVersion: string;
|
||||
AppWrapper: React.FC<any>;
|
||||
render: UiRender;
|
||||
}
|
||||
|
||||
export const createTestRendererMock = (): TestRenderer => {
|
||||
const basePath = '/mock';
|
||||
const extensions: UIExtensionsStorage = {};
|
||||
const startServices = createStartServices(basePath);
|
||||
const testRendererMocks: TestRenderer = {
|
||||
history: createHashHistory(),
|
||||
mountHistory: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath),
|
||||
startServices,
|
||||
config: createConfigurationMock(),
|
||||
startInterface: createStartMock(extensions),
|
||||
kibanaVersion: '8.0.0',
|
||||
AppWrapper: memo(({ children }) => {
|
||||
return (
|
||||
<FleetAppContext
|
||||
basepath={basePath}
|
||||
startServices={testRendererMocks.startServices}
|
||||
config={testRendererMocks.config}
|
||||
history={testRendererMocks.mountHistory}
|
||||
kibanaVersion={testRendererMocks.kibanaVersion}
|
||||
extensions={extensions}
|
||||
routerHistory={testRendererMocks.history}
|
||||
>
|
||||
{children}
|
||||
</FleetAppContext>
|
||||
);
|
||||
}),
|
||||
render: (ui, options) => {
|
||||
let renderResponse: RenderResult;
|
||||
act(() => {
|
||||
renderResponse = reactRender(ui, {
|
||||
wrapper: testRendererMocks.AppWrapper,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
return renderResponse!;
|
||||
},
|
||||
};
|
||||
|
||||
return testRendererMocks;
|
||||
};
|
|
@ -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<IStorage> => {
|
||||
let store: Record<string, any> = {};
|
||||
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 }) => (
|
||||
<I18nProvider>{children}</I18nProvider>
|
||||
));
|
||||
};
|
||||
|
||||
export const createStartServices = (basePath: string = '/mock'): MockedFleetStartServices => {
|
||||
const startServices: MockedFleetStartServices = {
|
||||
...coreMock.createStart({ basePath }),
|
||||
...createStartDepsMock(),
|
||||
storage: new Storage(createMockStore()) as jest.Mocked<Storage>,
|
||||
};
|
||||
|
||||
configureStartServices(startServices);
|
||||
|
||||
return startServices;
|
||||
};
|
12
x-pack/plugins/fleet/public/applications/fleet/mock/index.ts
Normal file
12
x-pack/plugins/fleet/public/applications/fleet/mock/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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';
|
||||
export * from './fleet_start_services';
|
||||
export * from './types';
|
|
@ -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 { FleetConfigType } from '../../../plugin';
|
||||
|
||||
export const createConfigurationMock = (): FleetConfigType => {
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { 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 = (): MockedFleetSetupDeps => {
|
||||
return {
|
||||
licensing: licensingMock.createSetup(),
|
||||
data: dataPluginMock.createSetupContract(),
|
||||
home: homePluginMock.createSetupContract(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createStartDepsMock = (): MockedFleetStartDeps => {
|
||||
return {
|
||||
data: dataPluginMock.createStartContract(),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { UIExtensionsStorage } from '../types';
|
||||
import { createExtensionRegistrationCallback } from '../services/ui_extensions';
|
||||
import { MockedFleetStart } from './types';
|
||||
|
||||
export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): MockedFleetStart => {
|
||||
return {
|
||||
isInitialized: jest.fn().mockResolvedValue(true),
|
||||
registerExtension: createExtensionRegistrationCallback(extensionsStorage),
|
||||
};
|
||||
};
|
16
x-pack/plugins/fleet/public/applications/fleet/mock/types.ts
Normal file
16
x-pack/plugins/fleet/public/applications/fleet/mock/types.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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, FleetStart, FleetStartDeps, FleetStartServices } from '../../../plugin';
|
||||
|
||||
export type MockedFleetStartServices = MockedKeys<FleetStartServices>;
|
||||
|
||||
export type MockedFleetSetupDeps = MockedKeys<FleetSetupDeps>;
|
||||
|
||||
export type MockedFleetStartDeps = MockedKeys<FleetStartDeps>;
|
||||
|
||||
export type MockedFleetStart = MockedKeys<FleetStart>;
|
|
@ -7,6 +7,7 @@
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Redirect } 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<DetailParams, 'panel'>;
|
||||
|
||||
|
@ -49,6 +53,9 @@ export function Content(props: ContentProps) {
|
|||
type ContentPanelProps = PackageInfo & Pick<DetailParams, 'panel'>;
|
||||
export function ContentPanel(props: ContentPanelProps) {
|
||||
const { panel, name, version, assets, title, removable, latestVersion } = props;
|
||||
const CustomView = useUIExtension(name, 'package-detail-custom');
|
||||
const { getPath } = useLink();
|
||||
|
||||
switch (panel) {
|
||||
case 'settings':
|
||||
return (
|
||||
|
@ -63,6 +70,14 @@ export function ContentPanel(props: ContentPanelProps) {
|
|||
);
|
||||
case 'policies':
|
||||
return <PackagePoliciesPanel name={name} version={version} />;
|
||||
case 'custom':
|
||||
return CustomView ? (
|
||||
<ExtensionWrapper>
|
||||
<CustomView />
|
||||
</ExtensionWrapper>
|
||||
) : (
|
||||
<Redirect to={getPath('integration_details', { pkgkey: `${name}-${version}` })} />
|
||||
);
|
||||
case 'overview':
|
||||
default:
|
||||
return <OverviewPanel {...props} />;
|
||||
|
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* 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, MockedFleetStartServices, TestRenderer } from '../../../../mock';
|
||||
import { Detail } from './index';
|
||||
import React, { lazy, memo } from 'react';
|
||||
import { PAGE_ROUTING_PATHS, pagePathGetters } from '../../../../constants';
|
||||
import { Route } from 'react-router-dom';
|
||||
import {
|
||||
GetFleetStatusResponse,
|
||||
GetInfoResponse,
|
||||
} from '../../../../../../../common/types/rest_spec';
|
||||
import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models';
|
||||
import { epmRouteService, fleetSetupRouteService } from '../../../../../../../common/services';
|
||||
import { act } 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<typeof testRenderer.render>;
|
||||
const render = () =>
|
||||
(renderResult = testRenderer.render(
|
||||
<Route path={PAGE_ROUTING_PATHS.integration_details}>
|
||||
<Detail />
|
||||
</Route>
|
||||
));
|
||||
|
||||
beforeEach(() => {
|
||||
testRenderer = createTestRendererMock();
|
||||
mockApiCalls(testRenderer.startServices.http);
|
||||
testRenderer.history.push(detailPageUrlPath);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
// 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<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
let setWasRendered: () => void;
|
||||
lazyComponentWasRendered = new Promise((resolve) => {
|
||||
setWasRendered = resolve;
|
||||
});
|
||||
|
||||
const CustomComponent = lazy(async () => {
|
||||
return {
|
||||
default: memo(() => {
|
||||
setWasRendered();
|
||||
return <div data-test-subj="custom-hello">hello</div>;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
testRenderer.startInterface.registerExtension({
|
||||
package: 'nginx',
|
||||
view: 'package-detail-custom',
|
||||
component: CustomComponent,
|
||||
});
|
||||
|
||||
render();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-ignore
|
||||
lazyComponentWasRendered = undefined;
|
||||
});
|
||||
|
||||
it('should display "custom" tab in navigation', () => {
|
||||
expect(renderResult.getByTestId('tab-custom'));
|
||||
});
|
||||
|
||||
it('should display custom content when tab is clicked', async () => {
|
||||
act(() => {
|
||||
testRenderer.history.push(
|
||||
pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' })
|
||||
);
|
||||
});
|
||||
await lazyComponentWasRendered;
|
||||
expect(renderResult.getByTestId('custom-hello'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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.
|
||||
`;
|
||||
|
||||
const agentsSetupResponse: GetFleetStatusResponse = { isReady: true, missing_requirements: [] };
|
||||
|
||||
http.get.mockImplementation(async (path) => {
|
||||
if (typeof path === 'string') {
|
||||
if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) {
|
||||
return epmPackageResponse;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -35,6 +35,7 @@ import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components
|
|||
import { UpdateIcon } from '../../components/icons';
|
||||
import { Content } from './content';
|
||||
import './index.scss';
|
||||
import { useUIExtension } from '../../../../hooks/use_ui_extension';
|
||||
|
||||
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
|
||||
|
||||
|
@ -53,6 +54,9 @@ const PanelDisplayNames: Record<DetailViewPanelName, string> = {
|
|||
settings: i18n.translate('xpack.fleet.epm.packageDetailsNav.settingsLinkText', {
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
custom: i18n.translate('xpack.fleet.epm.packageDetailsNav.packageCustomLinkText', {
|
||||
defaultMessage: 'Custom',
|
||||
}),
|
||||
};
|
||||
|
||||
const Divider = styled.div`
|
||||
|
@ -72,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<DetailParams>();
|
||||
const { getHref } = useLink();
|
||||
const hasWriteCapabilites = useCapabilities().write;
|
||||
|
||||
|
@ -91,6 +94,10 @@ export function Detail() {
|
|||
pkgkey
|
||||
);
|
||||
|
||||
const packageInstallStatus = packageInfoData?.response.status;
|
||||
const showCustomTab =
|
||||
useUIExtension(packageInfoData?.response.name ?? '', 'package-detail-custom') !== undefined;
|
||||
|
||||
// Track install status state
|
||||
useEffect(() => {
|
||||
if (packageInfoData?.response) {
|
||||
|
@ -236,22 +243,31 @@ export function Detail() {
|
|||
|
||||
return (entries(PanelDisplayNames)
|
||||
.filter(([panelId]) => {
|
||||
return (
|
||||
panelId !== 'policies' || packageInfoData?.response.status === InstallStatus.installed
|
||||
);
|
||||
// Don't show `Policies` tab if package is not installed
|
||||
if (panelId === 'policies' && packageInstallStatus !== 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 {
|
||||
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, packageInfoData?.response?.status, panel]);
|
||||
}, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
|
|
11
x-pack/plugins/fleet/public/mocks.ts
Normal file
11
x-pack/plugins/fleet/public/mocks.ts
Normal file
|
@ -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,
|
||||
};
|
|
@ -9,6 +9,7 @@ import {
|
|||
dataPluginMock,
|
||||
Start as DataPublicStartMock,
|
||||
} from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { fleetMock } from '../../../../../fleet/public/mocks';
|
||||
|
||||
type DataMock = Omit<DataPublicStartMock, 'indexPatterns' | 'query'> & {
|
||||
indexPatterns: Omit<DataPublicStartMock['indexPatterns'], 'getFieldsForWildcard'> & {
|
||||
|
@ -56,9 +57,6 @@ export const depsStartMock: () => DepsStartMock = () => {
|
|||
|
||||
return {
|
||||
data: dataMock,
|
||||
fleet: {
|
||||
isInitialized: () => Promise.resolve(true),
|
||||
registerExtension: jest.fn(),
|
||||
},
|
||||
fleet: fleetMock.createStartMock(),
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue