Skip to content

Commit

Permalink
[Fleet] Support for showing an Integration Detail Custom (UI Extensio…
Browse files Browse the repository at this point in the history
…n) tab (elastic#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
  • Loading branch information
paul-tavares authored Nov 30, 2020
1 parent eee05ad commit 707dbcd
Show file tree
Hide file tree
Showing 16 changed files with 957 additions and 244 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
261 changes: 261 additions & 0 deletions x-pack/plugins/fleet/public/applications/fleet/app.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 707dbcd

Please sign in to comment.