Skip to content

Commit 707dbcd

Browse files
authored
[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
1 parent eee05ad commit 707dbcd

File tree

16 files changed

+957
-244
lines changed

16 files changed

+957
-244
lines changed

x-pack/plugins/fleet/common/types/models/epm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type InstallSource = 'registry' | 'upload';
3030

3131
export type EpmPackageInstallStatus = 'installed' | 'installing';
3232

33-
export type DetailViewPanelName = 'overview' | 'policies' | 'settings';
33+
export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
3434
export type ServiceName = 'kibana' | 'elasticsearch';
3535
export type AgentAssetType = typeof agentAssetTypes;
3636
export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf<AgentAssetType>;
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { memo, useEffect, useState } from 'react';
8+
import { AppMountParameters } from 'kibana/public';
9+
import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel } from '@elastic/eui';
10+
import { createHashHistory, History } from 'history';
11+
import { Router, Redirect, Route, Switch } from 'react-router-dom';
12+
import { FormattedMessage } from '@kbn/i18n/react';
13+
import { i18n } from '@kbn/i18n';
14+
import styled from 'styled-components';
15+
import useObservable from 'react-use/lib/useObservable';
16+
import {
17+
ConfigContext,
18+
FleetStatusProvider,
19+
KibanaVersionContext,
20+
sendGetPermissionsCheck,
21+
sendSetup,
22+
useBreadcrumbs,
23+
useConfig,
24+
} from './hooks';
25+
import { Error, Loading } from './components';
26+
import { IntraAppStateProvider } from './hooks/use_intra_app_state';
27+
import { PackageInstallProvider } from './sections/epm/hooks';
28+
import { PAGE_ROUTING_PATHS } from './constants';
29+
import { DefaultLayout, WithoutHeaderLayout } from './layouts';
30+
import { EPMApp } from './sections/epm';
31+
import { AgentPolicyApp } from './sections/agent_policy';
32+
import { DataStreamApp } from './sections/data_stream';
33+
import { FleetApp } from './sections/agents';
34+
import { IngestManagerOverview } from './sections/overview';
35+
import { ProtectedRoute } from './index';
36+
import { FleetConfigType, FleetStartServices } from '../../plugin';
37+
import { UIExtensionsStorage } from './types';
38+
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
39+
import { EuiThemeProvider } from '../../../../xpack_legacy/common';
40+
import { UIExtensionsContext } from './hooks/use_ui_extension';
41+
42+
const ErrorLayout = ({ children }: { children: JSX.Element }) => (
43+
<EuiErrorBoundary>
44+
<DefaultLayout showSettings={false}>
45+
<WithoutHeaderLayout>{children}</WithoutHeaderLayout>
46+
</DefaultLayout>
47+
</EuiErrorBoundary>
48+
);
49+
50+
const Panel = styled(EuiPanel)`
51+
max-width: 500px;
52+
margin-right: auto;
53+
margin-left: auto;
54+
`;
55+
56+
export const WithPermissionsAndSetup: React.FC = memo(({ children }) => {
57+
useBreadcrumbs('base');
58+
59+
const [isPermissionsLoading, setIsPermissionsLoading] = useState<boolean>(false);
60+
const [permissionsError, setPermissionsError] = useState<string>();
61+
const [isInitialized, setIsInitialized] = useState(false);
62+
const [initializationError, setInitializationError] = useState<Error | null>(null);
63+
64+
useEffect(() => {
65+
(async () => {
66+
setIsPermissionsLoading(false);
67+
setPermissionsError(undefined);
68+
setIsInitialized(false);
69+
setInitializationError(null);
70+
try {
71+
setIsPermissionsLoading(true);
72+
const permissionsResponse = await sendGetPermissionsCheck();
73+
setIsPermissionsLoading(false);
74+
if (permissionsResponse.data?.success) {
75+
try {
76+
const setupResponse = await sendSetup();
77+
if (setupResponse.error) {
78+
setInitializationError(setupResponse.error);
79+
}
80+
} catch (err) {
81+
setInitializationError(err);
82+
}
83+
setIsInitialized(true);
84+
} else {
85+
setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR');
86+
}
87+
} catch (err) {
88+
setPermissionsError('REQUEST_ERROR');
89+
}
90+
})();
91+
}, []);
92+
93+
if (isPermissionsLoading || permissionsError) {
94+
return (
95+
<ErrorLayout>
96+
{isPermissionsLoading ? (
97+
<Loading />
98+
) : permissionsError === 'REQUEST_ERROR' ? (
99+
<Error
100+
title={
101+
<FormattedMessage
102+
id="xpack.fleet.permissionsRequestErrorMessageTitle"
103+
defaultMessage="Unable to check permissions"
104+
/>
105+
}
106+
error={i18n.translate('xpack.fleet.permissionsRequestErrorMessageDescription', {
107+
defaultMessage: 'There was a problem checking Fleet permissions',
108+
})}
109+
/>
110+
) : (
111+
<Panel>
112+
<EuiEmptyPrompt
113+
iconType="securityApp"
114+
title={
115+
<h2>
116+
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
117+
<FormattedMessage
118+
id="xpack.fleet.permissionDeniedErrorTitle"
119+
defaultMessage="Permission denied"
120+
/>
121+
) : (
122+
<FormattedMessage
123+
id="xpack.fleet.securityRequiredErrorTitle"
124+
defaultMessage="Security is not enabled"
125+
/>
126+
)}
127+
</h2>
128+
}
129+
body={
130+
<p>
131+
{permissionsError === 'MISSING_SUPERUSER_ROLE' ? (
132+
<FormattedMessage
133+
id="xpack.fleet.permissionDeniedErrorMessage"
134+
defaultMessage="You are not authorized to access Fleet. Fleet requires {roleName} privileges."
135+
values={{ roleName: <EuiCode>superuser</EuiCode> }}
136+
/>
137+
) : (
138+
<FormattedMessage
139+
id="xpack.fleet.securityRequiredErrorMessage"
140+
defaultMessage="You must enable security in Kibana and Elasticsearch to use Fleet."
141+
/>
142+
)}
143+
</p>
144+
}
145+
/>
146+
</Panel>
147+
)}
148+
</ErrorLayout>
149+
);
150+
}
151+
152+
if (!isInitialized || initializationError) {
153+
return (
154+
<ErrorLayout>
155+
{initializationError ? (
156+
<Error
157+
title={
158+
<FormattedMessage
159+
id="xpack.fleet.initializationErrorMessageTitle"
160+
defaultMessage="Unable to initialize Fleet"
161+
/>
162+
}
163+
error={initializationError}
164+
/>
165+
) : (
166+
<Loading />
167+
)}
168+
</ErrorLayout>
169+
);
170+
}
171+
172+
return <>{children}</>;
173+
});
174+
175+
/**
176+
* Fleet Application context all the way down to the Router, but with no permissions or setup checks
177+
* and no routes defined
178+
*/
179+
export const FleetAppContext: React.FC<{
180+
basepath: string;
181+
startServices: FleetStartServices;
182+
config: FleetConfigType;
183+
history: AppMountParameters['history'];
184+
kibanaVersion: string;
185+
extensions: UIExtensionsStorage;
186+
/** For testing purposes only */
187+
routerHistory?: History<any>;
188+
}> = memo(
189+
({
190+
children,
191+
startServices,
192+
config,
193+
history,
194+
kibanaVersion,
195+
extensions,
196+
routerHistory = createHashHistory(),
197+
}) => {
198+
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
199+
200+
return (
201+
<startServices.i18n.Context>
202+
<KibanaContextProvider services={{ ...startServices }}>
203+
<EuiErrorBoundary>
204+
<ConfigContext.Provider value={config}>
205+
<KibanaVersionContext.Provider value={kibanaVersion}>
206+
<EuiThemeProvider darkMode={isDarkMode}>
207+
<UIExtensionsContext.Provider value={extensions}>
208+
<FleetStatusProvider>
209+
<IntraAppStateProvider kibanaScopedHistory={history}>
210+
<Router history={routerHistory}>
211+
<PackageInstallProvider notifications={startServices.notifications}>
212+
{children}
213+
</PackageInstallProvider>
214+
</Router>
215+
</IntraAppStateProvider>
216+
</FleetStatusProvider>
217+
</UIExtensionsContext.Provider>
218+
</EuiThemeProvider>
219+
</KibanaVersionContext.Provider>
220+
</ConfigContext.Provider>
221+
</EuiErrorBoundary>
222+
</KibanaContextProvider>
223+
</startServices.i18n.Context>
224+
);
225+
}
226+
);
227+
228+
export const AppRoutes = memo(() => {
229+
const { agents } = useConfig();
230+
231+
return (
232+
<Switch>
233+
<Route path={PAGE_ROUTING_PATHS.integrations}>
234+
<DefaultLayout section="epm">
235+
<EPMApp />
236+
</DefaultLayout>
237+
</Route>
238+
<Route path={PAGE_ROUTING_PATHS.policies}>
239+
<DefaultLayout section="agent_policy">
240+
<AgentPolicyApp />
241+
</DefaultLayout>
242+
</Route>
243+
<Route path={PAGE_ROUTING_PATHS.data_streams}>
244+
<DefaultLayout section="data_stream">
245+
<DataStreamApp />
246+
</DefaultLayout>
247+
</Route>
248+
<ProtectedRoute path={PAGE_ROUTING_PATHS.fleet} isAllowed={agents.enabled}>
249+
<DefaultLayout section="fleet">
250+
<FleetApp />
251+
</DefaultLayout>
252+
</ProtectedRoute>
253+
<Route exact path={PAGE_ROUTING_PATHS.overview}>
254+
<DefaultLayout section="overview">
255+
<IngestManagerOverview />
256+
</DefaultLayout>
257+
</Route>
258+
<Redirect to="/" />
259+
</Switch>
260+
);
261+
});

x-pack/plugins/fleet/public/applications/fleet/hooks/use_url_pagination.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,13 @@ const paginationFromUrlParams = (urlParams: UrlPaginationParams): Pagination =>
6969
// Search params can appear multiple times in the URL, in which case the value for them,
7070
// once parsed, would be an array. In these case, we take the last value defined
7171
pagination.currentPage = Number(
72-
(Array.isArray(urlParams.currentPage)
73-
? urlParams.currentPage[urlParams.currentPage.length - 1]
74-
: urlParams.currentPage) ?? pagination.currentPage
72+
(Array.isArray(urlParams.currentPage) ? urlParams.currentPage.pop() : urlParams.currentPage) ??
73+
pagination.currentPage
7574
);
7675
pagination.pageSize =
7776
Number(
78-
(Array.isArray(urlParams.pageSize)
79-
? urlParams.pageSize[urlParams.pageSize.length - 1]
80-
: urlParams.pageSize) ?? pagination.pageSize
77+
(Array.isArray(urlParams.pageSize) ? urlParams.pageSize.pop() : urlParams.pageSize) ??
78+
pagination.pageSize
8179
) ?? pagination.pageSize;
8280

8381
// If Current Page is not a valid positive integer, set it to 1

0 commit comments

Comments
 (0)