[Fleet] Persist category and search in url on package page (#111571)

This commit is contained in:
Thomas Neirynck 2021-09-13 15:12:01 -04:00 committed by GitHub
parent fcd27d17c2
commit d3dd617cd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 114 additions and 43 deletions

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

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

View file

@ -32,7 +32,7 @@ import {
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
import { AgentPolicyContextProvider, useUrlModal } from './hooks';
import { INTEGRATIONS_ROUTING_PATHS } from './constants';
import { INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from './constants';
import { Error, Loading, SettingFlyout } from './components';
@ -242,7 +242,7 @@ export const AppRoutes = memo(() => {
// 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} />;
return <Redirect to={pagePathGetters.integrations_all({})[1]} />;
}
const pathname = location.hash.replace(/^#/, '');

View file

@ -7,7 +7,6 @@
import type { ReactNode } from 'react';
import React, { Fragment, useCallback, useState } from 'react';
import type { Query } from '@elastic/eui';
import {
EuiFlexGrid,
EuiFlexGroup,
@ -34,7 +33,9 @@ interface ListProps {
controls?: ReactNode;
title: string;
list: PackageList;
setSelectedCategory?: (category: string) => void;
initialSearch?: string;
setSelectedCategory: (category: string) => void;
onSearchChange: (search: string) => void;
showMissingIntegrationMessage?: boolean;
}
@ -43,33 +44,28 @@ export function PackageListGrid({
controls,
title,
list,
setSelectedCategory = () => {},
initialSearch,
onSearchChange,
setSelectedCategory,
showMissingIntegrationMessage = false,
}: ListProps) {
const initialQuery = EuiSearchBar.Query.MATCH_ALL;
const [query, setQuery] = useState<Query | null>(initialQuery);
const [searchTerm, setSearchTerm] = useState('');
const [searchTerm, setSearchTerm] = useState(initialSearch || '');
const localSearchRef = useLocalSearch(list);
const onQueryChange = ({
// eslint-disable-next-line @typescript-eslint/no-shadow
query,
queryText: userInput,
error,
}: {
query: Query | null;
queryText: string;
error: { message: string } | null;
}) => {
if (!error) {
setQuery(query);
onSearchChange(userInput);
setSearchTerm(userInput);
}
};
const resetQuery = () => {
setQuery(initialQuery);
setSearchTerm('');
};
@ -99,7 +95,7 @@ export function PackageListGrid({
<EuiFlexItem grow={1}>{controlsContent}</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSearchBar
query={query || undefined}
query={searchTerm || undefined}
box={{
placeholder: i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', {
defaultMessage: 'Search for integrations',

View file

@ -5,13 +5,17 @@
* 2.0.
*/
import React, { memo, useState, useMemo } from 'react';
import { Switch, Route, useLocation, useHistory } from 'react-router-dom';
import React, { memo, useMemo } from 'react';
import { Switch, Route, useLocation, useHistory, useParams } from 'react-router-dom';
import semverLt from 'semver/functions/lt';
import { i18n } from '@kbn/i18n';
import { installationStatuses } from '../../../../../../../common/constants';
import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants';
import {
INTEGRATIONS_ROUTING_PATHS,
INTEGRATIONS_SEARCH_QUERYPARAM,
pagePathGetters,
} from '../../../../constants';
import { useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks';
import { doesPackageHaveIntegrations } from '../../../../services';
import { DefaultLayout } from '../../../../layouts';
@ -20,6 +24,22 @@ import { PackageListGrid } from '../../components/package_list_grid';
import { CategoryFacets } from './category_facets';
export interface CategoryParams {
category?: string;
}
function getParams(params: CategoryParams, search: string) {
const { category } = params;
const selectedCategory = category || '';
const queryParams = new URLSearchParams(search);
const searchParam = queryParams.get(INTEGRATIONS_SEARCH_QUERYPARAM) || '';
return { selectedCategory, searchParam };
}
function categoryExists(category: string, categories: CategorySummaryItem[]) {
return categories.some((c) => c.id === category);
}
export const EPMHomePage: React.FC = memo(() => {
return (
<Switch>
@ -69,7 +89,28 @@ const InstalledPackages: React.FC = memo(() => {
const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({
experimental: true,
});
const [selectedCategory, setSelectedCategory] = useState('');
const { selectedCategory, searchParam } = getParams(
useParams<CategoryParams>(),
useLocation().search
);
const history = useHistory();
function setSelectedCategory(categoryId: string) {
const url = pagePathGetters.integrations_installed({
category: categoryId,
searchTerm: searchParam,
})[1];
history.push(url);
}
function setSearchTerm(search: string) {
// Use .replace so the browser's back button is tied to single keystroke
history.replace(
pagePathGetters.integrations_installed({
category: selectedCategory,
searchTerm: search,
})[1]
);
}
const allInstalledPackages = useMemo(
() =>
@ -114,21 +155,28 @@ const InstalledPackages: React.FC = memo(() => {
[allInstalledPackages.length, updatablePackages.length]
);
const controls = useMemo(
() => (
<CategoryFacets
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)}
/>
),
[categories, selectedCategory]
if (!categoryExists(selectedCategory, categories)) {
history.replace(
pagePathGetters.integrations_installed({ category: '', searchTerm: searchParam })[1]
);
return null;
}
const controls = (
<CategoryFacets
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)}
/>
);
return (
<PackageListGrid
isLoading={isLoadingPackages}
controls={controls}
setSelectedCategory={setSelectedCategory}
onSearchChange={setSearchTerm}
initialSearch={searchParam}
title={title}
list={selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages}
/>
@ -137,10 +185,25 @@ const InstalledPackages: React.FC = memo(() => {
const AvailablePackages: React.FC = memo(() => {
useBreadcrumbs('integrations_all');
const { selectedCategory, searchParam } = getParams(
useParams<CategoryParams>(),
useLocation().search
);
const history = useHistory();
const queryParams = new URLSearchParams(useLocation().search);
const initialCategory = queryParams.get('category') || '';
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
function setSelectedCategory(categoryId: string) {
const url = pagePathGetters.integrations_all({
category: categoryId,
searchTerm: searchParam,
})[1];
history.push(url);
}
function setSearchTerm(search: string) {
// Use .replace so the browser's back button is tied to single keystroke
history.replace(
pagePathGetters.integrations_all({ category: selectedCategory, searchTerm: search })[1]
);
}
const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({
category: '',
});
@ -182,16 +245,17 @@ const AvailablePackages: React.FC = memo(() => {
[allPackages?.length, categoriesRes]
);
if (!categoryExists(selectedCategory, categories)) {
history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]);
return null;
}
const controls = categories ? (
<CategoryFacets
isLoading={isLoadingCategories || isLoadingAllPackages}
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategorySummaryItem) => {
// clear category query param in the url
if (queryParams.get('category')) {
history.push({});
}
setSelectedCategory(id);
}}
/>
@ -202,8 +266,10 @@ const AvailablePackages: React.FC = memo(() => {
isLoading={isLoadingCategoryPackages}
title={title}
controls={controls}
initialSearch={searchParam}
list={packages}
setSelectedCategory={setSelectedCategory}
onSearchChange={setSearchTerm}
showMissingIntegrationMessage
/>
);

View file

@ -11,14 +11,14 @@ export type StaticPage =
| 'base'
| 'overview'
| 'integrations'
| 'integrations_all'
| 'integrations_installed'
| 'policies'
| 'policies_list'
| 'enrollment_tokens'
| 'data_streams';
export type DynamicPage =
| 'integrations_all'
| 'integrations_installed'
| 'integration_details_overview'
| 'integration_details_policies'
| 'integration_details_assets'
@ -65,10 +65,11 @@ export const FLEET_ROUTING_PATHS = {
add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?',
};
export const INTEGRATIONS_SEARCH_QUERYPARAM = 'q';
export const INTEGRATIONS_ROUTING_PATHS = {
integrations: '/:tabId',
integrations_all: '/browse',
integrations_installed: '/installed',
integrations_all: '/browse/:category?',
integrations_installed: '/installed/:category?',
integration_details: '/detail/:pkgkey/:panel?',
integration_details_overview: '/detail/:pkgkey/overview',
integration_details_policies: '/detail/:pkgkey/policies',
@ -87,8 +88,16 @@ export const pagePathGetters: {
base: () => [FLEET_BASE_PATH, '/'],
overview: () => [FLEET_BASE_PATH, '/'],
integrations: () => [INTEGRATIONS_BASE_PATH, '/'],
integrations_all: () => [INTEGRATIONS_BASE_PATH, '/browse'],
integrations_installed: () => [INTEGRATIONS_BASE_PATH, '/installed'],
integrations_all: ({ searchTerm, category }: { searchTerm?: string; category?: string }) => {
const categoryPath = category ? `/${category}` : ``;
const queryParams = searchTerm ? `?${INTEGRATIONS_SEARCH_QUERYPARAM}=${searchTerm}` : ``;
return [INTEGRATIONS_BASE_PATH, `/browse${categoryPath}${queryParams}`];
},
integrations_installed: ({ query, category }: { query?: string; category?: string }) => {
const categoryPath = category ? `/${category}` : ``;
const queryParams = query ? `?${INTEGRATIONS_SEARCH_QUERYPARAM}=${query}` : ``;
return [INTEGRATIONS_BASE_PATH, `/installed${categoryPath}${queryParams}`];
},
integration_details_overview: ({ pkgkey, integration }) => [
INTEGRATIONS_BASE_PATH,
`/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`,