[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:
parent
3826283c74
commit
e74cb409c8
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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']) => {
|
||||
|
|
|
@ -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']>(() => {
|
||||
|
|
Loading…
Reference in a new issue