[Fleet] Replace hash router by router with scoped history (#106267)

This commit is contained in:
Nicolas Chaulet 2021-07-26 07:50:29 -04:00 committed by GitHub
parent 6042e6929c
commit 8924ff3219
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 209 additions and 228 deletions

View file

@ -106,6 +106,12 @@ export default async function ({ readConfigFile }) {
observabilityCases: {
pathname: '/app/observability/cases',
},
fleet: {
pathname: '/app/fleet',
},
integrations: {
pathname: '/app/integrations',
},
},
junit: {
reportName: 'Chrome UI Functional Tests',

View file

@ -10,7 +10,6 @@ import React, { memo, useEffect, useState } from 'react';
import type { AppMountParameters } from 'kibana/public';
import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui';
import type { History } from 'history';
import { createHashHistory } from 'history';
import { Router, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -20,7 +19,10 @@ import useObservable from 'react-use/lib/useObservable';
import type { TopNavMenuData } from 'src/plugins/navigation/public';
import type { FleetConfigType, FleetStartServices } from '../../plugin';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import {
KibanaContextProvider,
RedirectAppLinks,
} from '../../../../../../src/plugins/kibana_react/public';
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
import { PackageInstallProvider, useUrlModal } from '../integrations/hooks';
@ -28,7 +30,6 @@ import { PackageInstallProvider, useUrlModal } from '../integrations/hooks';
import {
ConfigContext,
FleetStatusProvider,
IntraAppStateProvider,
KibanaVersionContext,
sendGetPermissionsCheck,
sendSetup,
@ -215,43 +216,31 @@ export const FleetAppContext: React.FC<{
}> = memo(
({ children, startServices, config, history, kibanaVersion, extensions, routerHistory }) => {
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
const [routerHistoryInstance] = useState(routerHistory || createHashHistory());
// Sync our hash history with Kibana scoped history
useEffect(() => {
const unlistenParentHistory = history.listen(() => {
const newHash = createHashHistory();
if (newHash.location.pathname !== routerHistoryInstance.location.pathname) {
routerHistoryInstance.replace(newHash.location.pathname + newHash.location.search || '');
}
});
return unlistenParentHistory;
}, [history, routerHistoryInstance]);
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={routerHistoryInstance}>
<RedirectAppLinks application={startServices.application}>
<startServices.i18n.Context>
<KibanaContextProvider services={{ ...startServices }}>
<EuiErrorBoundary>
<ConfigContext.Provider value={config}>
<KibanaVersionContext.Provider value={kibanaVersion}>
<EuiThemeProvider darkMode={isDarkMode}>
<UIExtensionsContext.Provider value={extensions}>
<FleetStatusProvider>
<Router history={history}>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
</Router>
</IntraAppStateProvider>
</FleetStatusProvider>
</UIExtensionsContext.Provider>
</EuiThemeProvider>
</KibanaVersionContext.Provider>
</ConfigContext.Provider>
</EuiErrorBoundary>
</KibanaContextProvider>
</startServices.i18n.Context>
</FleetStatusProvider>
</UIExtensionsContext.Provider>
</EuiThemeProvider>
</KibanaVersionContext.Provider>
</ConfigContext.Provider>
</EuiErrorBoundary>
</KibanaContextProvider>
</startServices.i18n.Context>
</RedirectAppLinks>
);
}
);
@ -277,7 +266,7 @@ const FleetTopNav = memo(
defaultMessage: 'Fleet settings',
}),
iconType: 'gear',
run: () => (window.location.href = getModalHref('settings')),
run: () => services.application.navigateToUrl(getModalHref('settings')),
},
];
return (
@ -327,7 +316,26 @@ export const AppRoutes = memo(
<CreatePackagePolicyPage />
</Route>
<Redirect to={FLEET_ROUTING_PATHS.agents} />
<Route
render={({ location }) => {
// BWC < 7.15 Fleet was using a hash router: redirect old routes using hash
const shouldRedirectHash = location.pathname === '' && location.hash.length > 0;
if (!shouldRedirectHash) {
return <Redirect to={FLEET_ROUTING_PATHS.agents} />;
}
const pathname = location.hash.replace(/^#(\/fleet)?/, '');
return (
<Redirect
to={{
...location,
pathname,
hash: undefined,
}}
/>
);
}}
/>
</Switch>
</>
);

View file

@ -150,18 +150,30 @@ const breadcrumbGetters: {
};
export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) {
const { chrome, http } = useStartServices();
const { chrome, http, application } = useStartServices();
const breadcrumbs =
breadcrumbGetters[page]?.(values).map((breadcrumb) => ({
...breadcrumb,
href: breadcrumb.href
breadcrumbGetters[page]?.(values).map((breadcrumb) => {
const href = breadcrumb.href
? http.basePath.prepend(
`${breadcrumb.useIntegrationsBasePath ? INTEGRATIONS_BASE_PATH : FLEET_BASE_PATH}#${
`${breadcrumb.useIntegrationsBasePath ? INTEGRATIONS_BASE_PATH : FLEET_BASE_PATH}${
breadcrumb.href
}`
)
: undefined,
})) || [];
: undefined;
return {
...breadcrumb,
href,
onClick: href
? (ev: React.MouseEvent) => {
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
return;
}
ev.preventDefault();
application.navigateToUrl(href);
}
: undefined,
};
}) || [];
const docTitle: string[] = [...breadcrumbs]
.reverse()
.map((breadcrumb) => breadcrumb.text as string);

View file

@ -31,7 +31,7 @@ describe('when on the package policy create page', () => {
beforeEach(() => {
testRenderer = createFleetTestRendererMock();
mockApiCalls(testRenderer.startServices.http);
testRenderer.history.push(createPageUrlPath);
testRenderer.mountHistory.push(createPageUrlPath);
});
describe('and Route state is provided via Fleet HashRouter', () => {
@ -43,7 +43,7 @@ describe('when on the package policy create page', () => {
onCancelNavigateTo: [PLUGIN_ID, { path: '/cancel/url/here' }],
};
testRenderer.history.replace({
testRenderer.mountHistory.replace({
pathname: createPageUrlPath,
state: expectedRouteState,
});
@ -72,18 +72,18 @@ describe('when on the package policy create page', () => {
expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl);
});
it('should redirect via Fleet HashRouter when cancel link is clicked', () => {
it('should redirect via history when cancel link is clicked', () => {
act(() => {
cancelLink.click();
});
expect(testRenderer.history.location.pathname).toBe('/cancel/url/here');
expect(testRenderer.mountHistory.location.pathname).toBe('/cancel/url/here');
});
it('should redirect via Fleet HashRouter when cancel Button (button bar) is clicked', () => {
it('should redirect via history when cancel Button (button bar) is clicked', () => {
act(() => {
cancelButton.click();
});
expect(testRenderer.history.location.pathname).toBe('/cancel/url/here');
expect(testRenderer.mountHistory.location.pathname).toBe('/cancel/url/here');
});
});
});

View file

@ -39,7 +39,7 @@ export const NoPackagePolicies = memo<{ policyId: string }>(({ policyId }) => {
fill
onClick={() =>
application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
path: `#${pagePathGetters.integrations_all()[1]}`,
path: pagePathGetters.integrations_all()[1],
state: { forAgentPolicyId: policyId },
})
}

View file

@ -199,7 +199,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
iconType="refresh"
onClick={() => {
application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
path: `#${pagePathGetters.integrations_all()[1]}`,
path: pagePathGetters.integrations_all()[1],
state: { forAgentPolicyId: agentPolicy.id },
});
}}

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { Router, Switch, Route, useHistory } from 'react-router-dom';
import { FLEET_ROUTING_PATHS } from '../../constants';
import { useBreadcrumbs } from '../../hooks';
@ -20,9 +20,10 @@ import { EditPackagePolicyPage } from './edit_package_policy_page';
export const AgentPolicyApp: React.FunctionComponent = () => {
useBreadcrumbs('policies');
const history = useHistory();
return (
<Router>
<Router history={history}>
<Switch>
<Route path={FLEET_ROUTING_PATHS.edit_integration}>
<EditPackagePolicyPage />

View file

@ -7,7 +7,7 @@
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
import { Router, Route, Switch, useHistory } from 'react-router-dom';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPortal } from '@elastic/eui';
import { FLEET_ROUTING_PATHS } from '../../constants';
@ -30,7 +30,7 @@ import { FleetServerUpgradeModal } from './components/fleet_server_upgrade_modal
export const AgentsApp: React.FunctionComponent = () => {
useBreadcrumbs('agent_list');
const history = useHistory();
const { agents } = useConfig();
const capabilities = useCapabilities();
@ -118,7 +118,7 @@ export const AgentsApp: React.FunctionComponent = () => {
) : undefined;
return (
<Router>
<Router history={history}>
<Switch>
<Route path={FLEET_ROUTING_PATHS.agent_details}>
<AgentDetailsPage />

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
import { Router, Route, Switch, useHistory } from 'react-router-dom';
import { FLEET_ROUTING_PATHS } from '../../constants';
import { DefaultLayout } from '../../layouts';
@ -14,8 +14,10 @@ import { DefaultLayout } from '../../layouts';
import { DataStreamListPage } from './list_page';
export const DataStreamApp: React.FunctionComponent = () => {
const history = useHistory();
return (
<Router>
<Router history={history}>
<Switch>
<Route path={FLEET_ROUTING_PATHS.data_streams}>
<DefaultLayout section="data_streams">

View file

@ -9,7 +9,6 @@ import React, { memo, useEffect, useState } from 'react';
import type { AppMountParameters } from 'kibana/public';
import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui';
import type { History } from 'history';
import { createHashHistory } from 'history';
import { Router, Redirect, Route, Switch } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -26,7 +25,10 @@ import {
import type { FleetConfigType, FleetStartServices } from '../../plugin';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import {
KibanaContextProvider,
RedirectAppLinks,
} from '../../../../../../src/plugins/kibana_react/public';
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
import { AgentPolicyContextProvider, useUrlModal } from './hooks';
@ -39,7 +41,7 @@ import type { UIExtensionsStorage } from './types';
import { EPMApp } from './sections/epm';
import { DefaultLayout, WithoutHeaderLayout } from './layouts';
import { PackageInstallProvider } from './hooks';
import { useBreadcrumbs, IntraAppStateProvider, UIExtensionsContext } from './hooks';
import { useBreadcrumbs, UIExtensionsContext } from './hooks';
const ErrorLayout = ({ children }: { children: JSX.Element }) => (
<EuiErrorBoundary>
@ -185,25 +187,12 @@ export const IntegrationsAppContext: React.FC<{
kibanaVersion: string;
extensions: UIExtensionsStorage;
/** For testing purposes only */
routerHistory?: History<any>;
}> = memo(
({ children, startServices, config, history, kibanaVersion, extensions, routerHistory }) => {
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
const [routerHistoryInstance] = useState(routerHistory || createHashHistory());
routerHistory?: History<any>; // TODO remove
}> = memo(({ children, startServices, config, history, kibanaVersion, extensions }) => {
const isDarkMode = useObservable<boolean>(startServices.uiSettings.get$('theme:darkMode'));
// Sync our hash history with Kibana scoped history
useEffect(() => {
const unlistenParentHistory = history.listen(() => {
const newHash = createHashHistory();
if (newHash.location.pathname !== routerHistoryInstance.location.pathname) {
routerHistoryInstance.replace(newHash.location.pathname + newHash.location.search || '');
}
});
return unlistenParentHistory;
}, [history, routerHistoryInstance]);
return (
return (
<RedirectAppLinks application={startServices.application}>
<startServices.i18n.Context>
<KibanaContextProvider services={{ ...startServices }}>
<EuiErrorBoundary>
@ -212,15 +201,13 @@ export const IntegrationsAppContext: React.FC<{
<EuiThemeProvider darkMode={isDarkMode}>
<UIExtensionsContext.Provider value={extensions}>
<FleetStatusProvider>
<IntraAppStateProvider kibanaScopedHistory={history}>
<Router history={routerHistoryInstance}>
<AgentPolicyContextProvider>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
</AgentPolicyContextProvider>
</Router>
</IntraAppStateProvider>
<Router history={history}>
<AgentPolicyContextProvider>
<PackageInstallProvider notifications={startServices.notifications}>
{children}
</PackageInstallProvider>
</AgentPolicyContextProvider>
</Router>
</FleetStatusProvider>
</UIExtensionsContext.Provider>
</EuiThemeProvider>
@ -229,9 +216,9 @@ export const IntegrationsAppContext: React.FC<{
</EuiErrorBoundary>
</KibanaContextProvider>
</startServices.i18n.Context>
);
}
);
</RedirectAppLinks>
);
});
export const AppRoutes = memo(() => {
const { modal, setModal } = useUrlModal();
@ -250,7 +237,26 @@ export const AppRoutes = memo(() => {
<Route path={INTEGRATIONS_ROUTING_PATHS.integrations}>
<EPMApp />
</Route>
<Redirect to={INTEGRATIONS_ROUTING_PATHS.integrations_all} />
<Route
render={({ location }) => {
// BWC < 7.15 Fleet was using a hash router: redirect old routes using hash
const shouldRedirectHash = location.pathname === '' && location.hash.length > 0;
if (!shouldRedirectHash) {
return <Redirect to={INTEGRATIONS_ROUTING_PATHS.integrations_all} />;
}
const pathname = location.hash.replace(/^#/, '');
return (
<Redirect
to={{
...location,
pathname,
hash: undefined,
}}
/>
);
}}
/>
</Switch>
</>
);

View file

@ -51,14 +51,23 @@ const breadcrumbGetters: {
};
export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) {
const { chrome, http } = useStartServices();
const { chrome, http, application } = useStartServices();
const breadcrumbs: ChromeBreadcrumb[] =
breadcrumbGetters[page]?.(values).map((breadcrumb) => ({
...breadcrumb,
href: breadcrumb.href
? http.basePath.prepend(`${INTEGRATIONS_BASE_PATH}#${breadcrumb.href}`)
: undefined,
})) || [];
breadcrumbGetters[page]?.(values).map((breadcrumb) => {
const href = breadcrumb.href
? http.basePath.prepend(`${INTEGRATIONS_BASE_PATH}${breadcrumb.href}`)
: undefined;
return {
...breadcrumb,
href,
onClick: href
? (ev: React.MouseEvent) => {
ev.preventDefault();
application.navigateToUrl(href);
}
: undefined,
};
}) || [];
const docTitle: string[] = [...breadcrumbs]
.reverse()
.map((breadcrumb) => breadcrumb.text as string);

View file

@ -45,22 +45,22 @@ describe('when on integration detail', () => {
</Route>
));
beforeEach(() => {
beforeEach(async () => {
testRenderer = createIntegrationsTestRendererMock();
mockedApi = mockApiCalls(testRenderer.startServices.http);
testRenderer.history.push(detailPageUrlPath);
act(() => testRenderer.mountHistory.push(detailPageUrlPath));
});
afterEach(() => {
cleanup();
window.location.hash = '#/';
});
describe('and the package is installed', () => {
beforeEach(() => render());
it('should display agent policy usage count', async () => {
await mockedApi.waitForApi();
await act(() => mockedApi.waitForApi());
expect(renderResult.queryByTestId('agentPolicyCount')).not.toBeNull();
});
@ -105,11 +105,11 @@ describe('when on integration detail', () => {
it('should redirect if custom url is accessed', () => {
act(() => {
testRenderer.history.push(
testRenderer.mountHistory.push(
pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' })[1]
);
});
expect(testRenderer.history.location.pathname).toEqual('/detail/nginx-0.3.7/overview');
expect(testRenderer.mountHistory.location.pathname).toEqual('/detail/nginx-0.3.7/overview');
});
});
@ -153,7 +153,7 @@ describe('when on integration detail', () => {
it('should display custom content when tab is clicked', async () => {
act(() => {
testRenderer.history.push(
testRenderer.mountHistory.push(
pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' })[1]
);
});
@ -200,7 +200,7 @@ describe('when on integration detail', () => {
it('should display custom assets when tab is clicked', async () => {
act(() => {
testRenderer.history.push(
testRenderer.mountHistory.push(
pagePathGetters.integration_details_assets({ pkgkey: 'nginx-0.3.7' })[1]
);
});
@ -215,7 +215,7 @@ describe('when on integration detail', () => {
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'
'http://localhost/mock/app/fleet/integrations/nginx-0.3.7/add-integration'
);
});
});
@ -223,7 +223,7 @@ describe('when on integration detail', () => {
describe('and on the Policies Tab', () => {
const policiesTabURLPath = pagePathGetters.integration_details_policies({ pkgkey })[1];
beforeEach(() => {
testRenderer.history.push(policiesTabURLPath);
testRenderer.mountHistory.push(policiesTabURLPath);
render();
});
@ -238,7 +238,7 @@ describe('when on integration detail', () => {
'integrationNameLink'
)[0] as HTMLAnchorElement;
expect(firstPolicy.href).toEqual(
'http://localhost/mock/app/integrations#/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc'
'http://localhost/mock/app/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc'
);
});

View file

@ -235,22 +235,18 @@ export function Detail() {
redirectToPath = [
PLUGIN_ID,
{
path: `#${
pagePathGetters.policy_details({
policyId: agentPolicyIdFromContext,
})[1]
}`,
path: pagePathGetters.policy_details({
policyId: agentPolicyIdFromContext,
})[1],
},
];
} else {
redirectToPath = [
INTEGRATIONS_PLUGIN_ID,
{
path: `#${
pagePathGetters.integration_details_policies({
pkgkey,
})[1]
}`,
path: pagePathGetters.integration_details_policies({
pkgkey,
})[1],
},
];
}
@ -260,16 +256,16 @@ export function Detail() {
onCancelNavigateTo: [
INTEGRATIONS_PLUGIN_ID,
{
path: currentPath,
path: pagePathGetters.integration_details_overview({
pkgkey,
})[1],
},
],
onCancelUrl: currentPath,
};
services.application.navigateToApp(PLUGIN_ID, {
// Necessary because of Fleet's HashRouter. Can be changed when
// https://github.com/elastic/kibana/issues/96134 is resolved
path: `#${path}`,
path,
state: redirectBackRouteState,
});
},

View file

@ -53,7 +53,7 @@ const LatestVersionLink = ({ name, version }: { name: string; version: string })
pkgkey: `${name}-${version}`,
});
return (
<EuiLink href={`#${settingsPath}`}>
<EuiLink href={settingsPath}>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageLatestVersionLink"
defaultMessage="latest version"

View file

@ -5,72 +5,17 @@
* 2.0.
*/
import React, { memo, useContext, useMemo } from 'react';
import type { AppMountParameters } from 'kibana/public';
import { useLocation } from 'react-router-dom';
import type { AnyIntraAppRouteState } from '../types';
interface IntraAppState<S extends AnyIntraAppRouteState = AnyIntraAppRouteState> {
forRoute: string;
routeState?: S;
}
const IntraAppStateContext = React.createContext<IntraAppState>({ forRoute: '' });
const wasHandled = new WeakSet<IntraAppState>();
/**
* Provides a bridget between Kibana's ScopedHistory instance (normally used with BrowserRouter)
* and the Hash router used within the app in order to enable state to be used between kibana
* apps
*/
export const IntraAppStateProvider = memo<{
kibanaScopedHistory: AppMountParameters['history'];
children: React.ReactNode;
}>(({ kibanaScopedHistory, children }) => {
const internalAppToAppState = useMemo<IntraAppState>(() => {
return {
forRoute: new URL(`${kibanaScopedHistory.location.hash.substr(1)}`, 'http://localhost')
.pathname,
routeState: kibanaScopedHistory.location.state as AnyIntraAppRouteState,
};
}, [kibanaScopedHistory.location.state, kibanaScopedHistory.location.hash]);
return (
<IntraAppStateContext.Provider value={internalAppToAppState}>
{children}
</IntraAppStateContext.Provider>
);
});
/**
* Retrieve UI Route state from the React Router History for the current URL location.
* This state can be used by other Kibana Apps to influence certain behaviours in Ingest, for example,
* redirecting back to an given Application after a craete action.
*/
export function useIntraAppState<S = AnyIntraAppRouteState>():
| IntraAppState<S>['routeState']
| undefined {
export function useIntraAppState<S = AnyIntraAppRouteState>(): S | undefined {
const location = useLocation();
const intraAppState = useContext(IntraAppStateContext);
if (!intraAppState) {
throw new Error('Hook called outside of IntraAppStateContext');
}
return useMemo(() => {
// Due to the use of HashRouter in Ingest, we only want state to be returned
// once so that it does not impact navigation to the page from within the
// ingest app. side affect is that the browser back button would not work
// consistently either.
if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) {
wasHandled.add(intraAppState);
return intraAppState.routeState as S;
}
// 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]);
return location.state as S;
}

View file

@ -27,7 +27,7 @@ export const useLink = () => {
core.http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`),
getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => {
const [basePath, path] = getSeparatePaths(page, values);
return core.http.basePath.prepend(`${basePath}#${path}`);
return core.http.basePath.prepend(`${basePath}${path}`);
},
};
};

View file

@ -6,7 +6,7 @@
*/
import type { History } from 'history';
import { createMemoryHistory, createHashHistory } from 'history';
import { createMemoryHistory } from 'history';
import React, { memo } from 'react';
import type { RenderOptions, RenderResult } from '@testing-library/react';
import { render as reactRender, act } from '@testing-library/react';
@ -47,9 +47,10 @@ export const createFleetTestRendererMock = (): TestRenderer => {
const basePath = '/mock';
const extensions: UIExtensionsStorage = {};
const startServices = createStartServices(basePath);
const history = createMemoryHistory({ initialEntries: [basePath] });
const testRendererMocks: TestRenderer = {
history: createHashHistory(),
mountHistory: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath),
history,
mountHistory: new ScopedHistory(history, basePath),
startServices,
config: createConfigurationMock(),
startInterface: createStartMock(extensions),
@ -89,7 +90,7 @@ export const createIntegrationsTestRendererMock = (): TestRenderer => {
const extensions: UIExtensionsStorage = {};
const startServices = createStartServices(basePath);
const testRendererMocks: TestRenderer = {
history: createHashHistory(),
history: createMemoryHistory(),
mountHistory: new ScopedHistory(createMemoryHistory({ initialEntries: [basePath] }), basePath),
startServices,
config: createConfigurationMock(),

View file

@ -104,6 +104,7 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
core.application.register({
id: INTEGRATIONS_PLUGIN_ID,
category: DEFAULT_APP_CATEGORIES.management,
appRoute: '/app/integrations',
title: i18n.translate('xpack.fleet.integrationsAppTitle', {
defaultMessage: 'Integrations',
}),
@ -137,6 +138,7 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
title: i18n.translate('xpack.fleet.appTitle', { defaultMessage: 'Fleet' }),
order: 9020,
euiIconType: 'logoElastic',
appRoute: '/app/fleet',
mount: async (params: AppMountParameters) => {
const [coreStartServices, startDepsServices] = (await core.getStartServices()) as [
CoreStart,

View file

@ -92,7 +92,7 @@ describe('Package search provider', () => {
title: 'test',
type: 'integration',
url: {
path: 'undefined#/detail/test-test/overview',
path: 'undefined/detail/test-test/overview',
prependBasePath: false,
},
},
@ -102,7 +102,7 @@ describe('Package search provider', () => {
title: 'test1',
type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
path: 'undefined/detail/test1-test1/overview',
prependBasePath: false,
},
},
@ -175,7 +175,7 @@ describe('Package search provider', () => {
title: 'test1',
type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
path: 'undefined/detail/test1-test1/overview',
prependBasePath: false,
},
},
@ -231,7 +231,7 @@ describe('Package search provider', () => {
title: 'test',
type: 'integration',
url: {
path: 'undefined#/detail/test-test/overview',
path: 'undefined/detail/test-test/overview',
prependBasePath: false,
},
},
@ -241,7 +241,7 @@ describe('Package search provider', () => {
title: 'test1',
type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
path: 'undefined/detail/test1-test1/overview',
prependBasePath: false,
},
},
@ -274,7 +274,7 @@ describe('Package search provider', () => {
title: 'test1',
type: 'integration',
url: {
path: 'undefined#/detail/test1-test1/overview',
path: 'undefined/detail/test1-test1/overview',
prependBasePath: false,
},
},

View file

@ -45,10 +45,8 @@ const toSearchResult = (
title: pkg.title,
score: 80,
url: {
// TODO: See https://github.com/elastic/kibana/issues/96134 for details about why we use '#' here. Below should be updated
// as part of migrating to non-hash based router.
// prettier-ignore
path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}#${pagePathGetters.integration_details_overview({ pkgkey })[1]}`,
path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}${pagePathGetters.integration_details_overview({ pkgkey })[1]}`,
prependBasePath: false,
},
};

View file

@ -26,7 +26,7 @@ type EventHandlerCallback = MouseEventHandler<HTMLButtonElement | HTMLAnchorElem
*
* @example
*
* const handleOnClick = useNavigateToAppEventHandler('fleet', {path: '#/policies'})
* const handleOnClick = useNavigateToAppEventHandler('fleet', {path: '/policies'})
* return <EuiLink onClick={handleOnClick}>See policies</EuiLink>
*/
export const useNavigateToAppEventHandler = <S = unknown>(

View file

@ -14,6 +14,7 @@ import {
MANAGEMENT_STORE_GLOBAL_NAMESPACE,
} from '../../../../common/constants';
import { useAppUrl } from '../../../../../common/lib/kibana';
import { pagePathGetters } from '../../../../../../../fleet/public';
export function useEndpointSelector<TSelected>(selector: (state: EndpointState) => TSelected) {
return useSelector(function (state: State) {
@ -47,7 +48,8 @@ export const useAgentDetailsIngestUrl = (
): { url: string; appId: string; appPath: string } => {
const { getAppUrl } = useAppUrl();
return useMemo(() => {
const appPath = `#/fleet/agents/${agentId}/activity`;
const appPath = pagePathGetters.agent_details_logs({ agentId })[1];
return {
url: `${getAppUrl({ appId: 'fleet' })}${appPath}`,
appId: 'fleet',

View file

@ -120,13 +120,13 @@ export const useEndpointActionItems = (
'data-test-subj': 'agentPolicyLink',
navigateAppId: 'fleet',
navigateOptions: {
path: `#${
path: `${
pagePathGetters.policy_details({
policyId: fleetAgentPolicies[endpointPolicyId],
})[1]
}`,
},
href: `${getAppUrl({ appId: 'fleet' })}#${
href: `${getAppUrl({ appId: 'fleet' })}${
pagePathGetters.policy_details({
policyId: fleetAgentPolicies[endpointPolicyId],
})[1]
@ -145,13 +145,13 @@ export const useEndpointActionItems = (
'data-test-subj': 'agentDetailsLink',
navigateAppId: 'fleet',
navigateOptions: {
path: `#${
path: `${
pagePathGetters.agent_details({
agentId: fleetAgentId,
})[1]
}`,
},
href: `${getAppUrl({ appId: 'fleet' })}#${
href: `${getAppUrl({ appId: 'fleet' })}${
pagePathGetters.agent_details({
agentId: fleetAgentId,
})[1]
@ -169,17 +169,17 @@ export const useEndpointActionItems = (
'data-test-subj': 'agentPolicyReassignLink',
navigateAppId: 'fleet',
navigateOptions: {
path: `#${
path: `${
pagePathGetters.agent_details({
agentId: fleetAgentId,
})[1]
}/activity?openReassignFlyout=true`,
}?openReassignFlyout=true`,
},
href: `${getAppUrl({ appId: 'fleet' })}#${
href: `${getAppUrl({ appId: 'fleet' })}${
pagePathGetters.agent_details({
agentId: fleetAgentId,
})[1]
}/activity?openReassignFlyout=true`,
}?openReassignFlyout=true`,
children: (
<FormattedMessage
id="xpack.securitySolution.endpoint.actions.agentPolicyReassign"

View file

@ -1248,17 +1248,17 @@ describe('when on the endpoint list page', () => {
});
it('navigates to the Ingest Agent Policy page', async () => {
const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink');
expect(agentPolicyLink.getAttribute('href')).toEqual(`/app/fleet#/policies/${agentPolicyId}`);
expect(agentPolicyLink.getAttribute('href')).toEqual(`/app/fleet/policies/${agentPolicyId}`);
});
it('navigates to the Ingest Agent Details page', async () => {
const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink');
expect(agentDetailsLink.getAttribute('href')).toEqual(`/app/fleet#/agents/${agentId}`);
expect(agentDetailsLink.getAttribute('href')).toEqual(`/app/fleet/agents/${agentId}`);
});
it('navigates to the Ingest Agent Details page with policy reassign', async () => {
const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink');
expect(agentPolicyReassignLink.getAttribute('href')).toEqual(
`/app/fleet#/agents/${agentId}/activity?openReassignFlyout=true`
`/app/fleet/agents/${agentId}?openReassignFlyout=true`
);
});
});

View file

@ -156,7 +156,7 @@ export const EndpointList = () => {
const handleCreatePolicyClick = useNavigateToAppEventHandler<CreatePackagePolicyRouteState>(
'fleet',
{
path: `#/integrations/${
path: `/integrations/${
endpointPackageVersion ? `/endpoint-${endpointPackageVersion}` : ''
}/add-integration`,
state: {
@ -203,7 +203,7 @@ export const EndpointList = () => {
const handleDeployEndpointsClick = useNavigateToAppEventHandler<AgentPolicyDetailsDeployAgentAction>(
'fleet',
{
path: `#/policies/${selectedPolicyId}?openEnrollmentFlyout=true`,
path: `/policies/${selectedPolicyId}?openEnrollmentFlyout=true`,
state: {
onDoneNavigateTo: [
'securitySolution',

View file

@ -63,7 +63,7 @@ describe('OverviewEmpty', () => {
fill: false,
label: 'Add Endpoint Security',
onClick: undefined,
url: `#/integrations/endpoint-${endpointPackageVersion}/add-integration`,
url: `/integrations/endpoint-${endpointPackageVersion}/add-integration`,
},
});
});

View file

@ -36,7 +36,7 @@ const OverviewEmptyComponent: React.FC = () => {
const endpointIntegrationUrlPath = endpointPackageVersion
? `/endpoint-${endpointPackageVersion}/add-integration`
: '';
const endpointIntegrationUrl = `#/integrations${endpointIntegrationUrlPath}`;
const endpointIntegrationUrl = `/integrations${endpointIntegrationUrlPath}`;
const handleEndpointClick = useNavigateToAppEventHandler('fleet', {
path: endpointIntegrationUrl,
});

View file

@ -24,25 +24,17 @@ export function SyntheticsIntegrationPageProvider({
*
*/
async navigateToPackagePage(packageVersion: string) {
await pageObjects.common.navigateToUrl(
await pageObjects.common.navigateToUrlWithBrowserHistory(
'fleet',
`/integrations/synthetics-${packageVersion}/add-integration`,
{
shouldUseHashForSubUrl: true,
useActualUrl: true,
}
`/integrations/synthetics-${packageVersion}/add-integration`
);
await pageObjects.header.waitUntilLoadingHasFinished();
},
async navigateToPackageEditPage(packageId: string, agentId: string) {
await pageObjects.common.navigateToUrl(
await pageObjects.common.navigateToUrlWithBrowserHistory(
'fleet',
`/policies/${agentId}/edit-integration/${packageId}`,
{
shouldUseHashForSubUrl: true,
useActualUrl: true,
}
`/policies/${agentId}/edit-integration/${packageId}`
);
await pageObjects.header.waitUntilLoadingHasFinished();
},

View file

@ -17,9 +17,10 @@ export function FleetIntegrations({ getService, getPageObjects }: FtrProviderCon
return {
async navigateToIntegrationDetails(pkgkey: string) {
await pageObjects.common.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
hash: pagePathGetters.integration_details_overview({ pkgkey })[1],
});
await pageObjects.common.navigateToUrlWithBrowserHistory(
INTEGRATIONS_PLUGIN_ID,
pagePathGetters.integration_details_overview({ pkgkey })[1]
);
},
async integrationDetailCustomTabExistsOrFail() {