[Fleet] Return to Integration (package) details after create integration policy (#85177)

* useIntraAppState() now also supports state set via Fleet's HashRouter
* Remove use of `<Router>` from inside EPM pages
* Enable round-trip navigation for Integrations add package
This commit is contained in:
Paul Tavares 2020-12-08 15:08:54 -05:00 committed by GitHub
parent 3826283c74
commit e74cb409c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 238 additions and 29 deletions

View file

@ -63,5 +63,12 @@ export function useIntraAppState<S = AnyIntraAppRouteState>():
wasHandled.add(intraAppState);
return intraAppState.routeState as S;
}
}, [intraAppState, location.pathname]);
// Default is to return the state in the Fleet HashRouter, in order to enable use of route state
// that is used via Kibana's ScopedHistory from within the Fleet HashRouter (ex. things like
// `core.application.navigateTo()`
// Once this https://github.com/elastic/kibana/issues/70358 is implemented (move to BrowserHistory
// using kibana's ScopedHistory), then this work-around can be removed.
return location.state as S;
}, [intraAppState, location.pathname, location.state]);
}

View file

@ -0,0 +1,94 @@
/*
* 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 { PAGE_ROUTING_PATHS, pagePathGetters, PLUGIN_ID } from '../../../constants';
import { Route } from 'react-router-dom';
import { CreatePackagePolicyPage } from './index';
import React from 'react';
import { CreatePackagePolicyRouteState } from '../../../types';
import { act } from 'react-test-renderer';
describe('when on the package policy create page', () => {
const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-0.3.7' });
let testRenderer: TestRenderer;
let renderResult: ReturnType<typeof testRenderer.render>;
const render = () =>
(renderResult = testRenderer.render(
<Route path={PAGE_ROUTING_PATHS.add_integration_to_policy}>
<CreatePackagePolicyPage />
</Route>
));
beforeEach(() => {
testRenderer = createTestRendererMock();
mockApiCalls(testRenderer.startServices.http);
testRenderer.history.push(createPageUrlPath);
});
describe('and Route state is provided via Fleet HashRouter', () => {
let expectedRouteState: CreatePackagePolicyRouteState;
beforeEach(() => {
expectedRouteState = {
onCancelUrl: 'http://cancel/url/here',
onCancelNavigateTo: [PLUGIN_ID, { path: '/cancel/url/here' }],
};
testRenderer.history.replace({
pathname: createPageUrlPath,
state: expectedRouteState,
});
});
describe('and the cancel Link or Button is clicked', () => {
let cancelLink: HTMLAnchorElement;
let cancelButton: HTMLAnchorElement;
beforeEach(() => {
render();
act(() => {
cancelLink = renderResult.getByTestId(
'createPackagePolicy_cancelBackLink'
) as HTMLAnchorElement;
cancelButton = renderResult.getByTestId(
'createPackagePolicyCancelButton'
) as HTMLAnchorElement;
});
});
it('should use custom "cancel" URL', () => {
expect(cancelLink.href).toBe(expectedRouteState.onCancelUrl);
expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl);
});
it('should redirect via Fleet HashRouter when cancel link is clicked', () => {
act(() => {
cancelLink.click();
});
expect(testRenderer.history.location.pathname).toBe('/cancel/url/here');
});
it('should redirect via Fleet HashRouter when cancel Button (button bar) is clicked', () => {
act(() => {
cancelButton.click();
});
expect(testRenderer.history.location.pathname).toBe('/cancel/url/here');
});
});
});
});
const mockApiCalls = (http: MockedFleetStartServices['http']) => {
http.get.mockImplementation(async (path) => {
const err = new Error(`API [GET ${path}] is not MOCKED!`);
// eslint-disable-next-line no-console
console.log(err);
throw err;
});
};

View file

@ -18,6 +18,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import { ApplicationStart } from 'kibana/public';
import {
AgentPolicy,
PackageInfo,
@ -49,6 +50,7 @@ import { useIntraAppState } from '../../../hooks/use_intra_app_state';
import { useUIExtension } from '../../../hooks/use_ui_extension';
import { ExtensionWrapper } from '../../../components/extension_wrapper';
import { PackagePolicyEditExtensionComponentProps } from '../../../types';
import { PLUGIN_ID } from '../../../../../../common/constants';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@ -57,10 +59,7 @@ const StepsWithLessPadding = styled(EuiSteps)`
`;
export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const {
notifications,
application: { navigateToApp },
} = useStartServices();
const { notifications } = useStartServices();
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
@ -69,6 +68,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
} = useRouteMatch<{ policyId: string; pkgkey: string }>();
const { getHref, getPath } = useLink();
const history = useHistory();
const handleNavigateTo = useNavigateToCallback();
const routeState = useIntraAppState<CreatePackagePolicyRouteState>();
const from: CreatePackagePolicyFrom = policyId ? 'policy' : 'package';
@ -221,10 +221,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
(ev) => {
if (routeState && routeState.onCancelNavigateTo) {
ev.preventDefault();
navigateToApp(...routeState.onCancelNavigateTo);
handleNavigateTo(routeState.onCancelNavigateTo);
}
},
[routeState, navigateToApp]
[routeState, handleNavigateTo]
);
// Save package policy
@ -247,10 +247,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const { error, data } = await savePackagePolicy();
if (!error) {
if (routeState && routeState.onSaveNavigateTo) {
navigateToApp(
...(typeof routeState.onSaveNavigateTo === 'function'
handleNavigateTo(
typeof routeState.onSaveNavigateTo === 'function'
? routeState.onSaveNavigateTo(data!.item)
: routeState.onSaveNavigateTo)
: routeState.onSaveNavigateTo
);
} else {
history.push(getPath('policy_details', { policyId: agentPolicy?.id || policyId }));
@ -477,3 +477,29 @@ const IntegrationBreadcrumb: React.FunctionComponent<{
useBreadcrumbs('add_integration_to_policy', { pkgTitle, pkgkey });
return null;
};
const useNavigateToCallback = () => {
const history = useHistory();
const {
application: { navigateToApp },
} = useStartServices();
return useCallback(
(navigateToProps: Parameters<ApplicationStart['navigateToApp']>) => {
// If navigateTo appID is `fleet`, then don't use Kibana's navigateTo method, because that
// uses BrowserHistory but within fleet, we are using HashHistory.
// This temporary workaround hook can be removed once this issue is addressed:
// https://github.com/elastic/kibana/issues/70358
if (navigateToProps[0] === PLUGIN_ID) {
const { path = '', state } = navigateToProps[1] || {};
history.push({
pathname: path.charAt(0) === '#' ? path.substr(1) : path,
state,
});
}
return navigateToApp(...navigateToProps);
},
[history, navigateToApp]
);
};

View file

@ -5,7 +5,7 @@
*/
import React from 'react';
import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { Switch, Route } from 'react-router-dom';
import { PAGE_ROUTING_PATHS } from '../../constants';
import { useBreadcrumbs } from '../../hooks';
import { CreatePackagePolicyPage } from '../agent_policy/create_package_policy_page';
@ -16,18 +16,16 @@ export const EPMApp: React.FunctionComponent = () => {
useBreadcrumbs('integrations');
return (
<Router>
<Switch>
<Route path={PAGE_ROUTING_PATHS.add_integration_to_policy}>
<CreatePackagePolicyPage />
</Route>
<Route path={PAGE_ROUTING_PATHS.integration_details}>
<Detail />
</Route>
<Route path={PAGE_ROUTING_PATHS.integrations}>
<EPMHomePage />
</Route>
</Switch>
</Router>
<Switch>
<Route path={PAGE_ROUTING_PATHS.add_integration_to_policy}>
<CreatePackagePolicyPage />
</Route>
<Route path={PAGE_ROUTING_PATHS.integration_details}>
<Detail />
</Route>
<Route path={PAGE_ROUTING_PATHS.integrations}>
<EPMHomePage />
</Route>
</Switch>
);
};

View file

@ -106,6 +106,37 @@ describe('when on integration detail', () => {
expect(renderResult.getByTestId('custom-hello'));
});
});
describe('and the Add integration button is clicked', () => {
beforeEach(() => render());
it('should link to the create page', () => {
const addButton = renderResult.getByTestId('addIntegrationPolicyButton') as HTMLAnchorElement;
expect(addButton.href).toEqual(
'http://localhost/mock/app/fleet#/integrations/nginx-0.3.7/add-integration'
);
});
it('should link to create page with route state for return trip', () => {
const addButton = renderResult.getByTestId('addIntegrationPolicyButton') as HTMLAnchorElement;
act(() => addButton.click());
expect(testRenderer.history.location.state).toEqual({
onCancelNavigateTo: [
'fleet',
{
path: '#/integrations/detail/nginx-0.3.7',
},
],
onCancelUrl: '#/integrations/detail/nginx-0.3.7',
onSaveNavigateTo: [
'fleet',
{
path: '#/integrations/detail/nginx-0.3.7',
},
],
});
});
});
});
const mockApiCalls = (http: MockedFleetStartServices['http']) => {

View file

@ -3,8 +3,8 @@
* 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, { useEffect, useState, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import React, { useEffect, useState, useMemo, useCallback, ReactEventHandler } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -20,7 +20,13 @@ import {
EuiDescriptionListTitle,
EuiDescriptionListDescription,
} from '@elastic/eui';
import { DetailViewPanelName, entries, InstallStatus, PackageInfo } from '../../../../types';
import {
CreatePackagePolicyRouteState,
DetailViewPanelName,
entries,
InstallStatus,
PackageInfo,
} from '../../../../types';
import { Loading, Error } from '../../../../components';
import {
useGetPackageInfoByKey,
@ -36,6 +42,7 @@ import { UpdateIcon } from '../../components/icons';
import { Content } from './content';
import './index.scss';
import { useUIExtension } from '../../../../hooks/use_ui_extension';
import { PLUGIN_ID } from '../../../../../../../common/constants';
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
@ -77,8 +84,10 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
export function Detail() {
const { pkgkey, panel = DEFAULT_PANEL } = useParams<DetailParams>();
const { getHref } = useLink();
const { getHref, getPath } = useLink();
const hasWriteCapabilites = useCapabilities().write;
const history = useHistory();
const location = useLocation();
// Package info state
const [packageInfo, setPackageInfo] = useState<PackageInfo | null>(null);
@ -173,6 +182,40 @@ export function Detail() {
[getHref, isLoading, packageInfo]
);
const handleAddIntegrationPolicyClick = useCallback<ReactEventHandler>(
(ev) => {
ev.preventDefault();
// The object below, given to `createHref` is explicitly accessing keys of `location` in order
// to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable)
const currentPath = history.createHref({
pathname: location.pathname,
search: location.search,
hash: location.hash,
});
const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
CreatePackagePolicyRouteState['onCancelNavigateTo'] = [
PLUGIN_ID,
{
path: currentPath,
},
];
const redirectBackRouteState: CreatePackagePolicyRouteState = {
onSaveNavigateTo: redirectToPath,
onCancelNavigateTo: redirectToPath,
onCancelUrl: currentPath,
};
history.push({
pathname: getPath('add_integration_to_policy', {
pkgkey,
}),
state: redirectBackRouteState,
});
},
[getPath, history, location.hash, location.pathname, location.search, pkgkey]
);
const headerRightContent = useMemo(
() =>
packageInfo ? (
@ -198,6 +241,7 @@ export function Detail() {
{ isDivider: true },
{
content: (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiButton
fill
isDisabled={!hasWriteCapabilites}
@ -205,6 +249,8 @@ export function Detail() {
href={getHref('add_integration_to_policy', {
pkgkey,
})}
onClick={handleAddIntegrationPolicyClick}
data-test-subj="addIntegrationPolicyButton"
>
<FormattedMessage
id="xpack.fleet.epm.addPackagePolicyButtonText"
@ -233,7 +279,14 @@ export function Detail() {
</EuiFlexGroup>
</>
) : undefined,
[getHref, hasWriteCapabilites, packageInfo, pkgkey, updateAvailable]
[
getHref,
handleAddIntegrationPolicyClick,
hasWriteCapabilites,
packageInfo,
pkgkey,
updateAvailable,
]
);
const tabs = useMemo<WithHeaderLayoutProps['tabs']>(() => {