Persistent list filters (#12229)

* add PersistentFilters component

* add PersistentFilters test

* add persistent filters to all list pages

* update tests

* clear sessionStorage on logout

* fix persistent filter on wfjt detail; cleanup
This commit is contained in:
Keith Grant 2022-06-06 13:56:45 -07:00 committed by GitHub
parent faa5df19ca
commit fdd560747d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 277 additions and 51 deletions

View File

@ -11,7 +11,7 @@
},
"babelOptions": {
"presets": ["@babel/preset-react"]
}
}
},
"plugins": ["react-hooks", "jsx-a11y", "i18next", "@babel"],
"extends": [
@ -96,9 +96,18 @@
"modifier",
"data-cy",
"fieldName",
"splitButtonVariant"
"splitButtonVariant",
"pageKey"
],
"ignore": [
"Ansible",
"Tower",
"JSON",
"YAML",
"lg",
"hh:mm AM/PM",
"Twilio"
],
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg", "hh:mm AM/PM", "Twilio"],
"ignoreComponent": [
"AboutModal",
"code",
@ -139,7 +148,7 @@
"object-curly-newline": "off",
"no-trailing-spaces": ["error"],
"no-unused-expressions": ["error", { "allowShortCircuit": true }],
"react/jsx-props-no-spreading":["off"],
"react/jsx-props-no-spreading": ["off"],
"react/prefer-stateless-function": "off",
"react/prop-types": "off",
"react/sort-comp": ["error", {}],

View File

@ -0,0 +1,33 @@
import { useEffect } from 'react';
import { useLocation, useHistory } from 'react-router';
import { PERSISTENT_FILTER_KEY } from '../../constants';
export default function PersistentFilters({ pageKey, children }) {
const location = useLocation();
const history = useHistory();
useEffect(() => {
if (!location.search.includes('restoreFilters=true')) {
return;
}
const filterString = sessionStorage.getItem(PERSISTENT_FILTER_KEY);
const filter = filterString ? JSON.parse(filterString) : { qs: '' };
if (filter.pageKey === pageKey) {
history.replace(`${location.pathname}${filter.qs}`);
} else {
history.replace(location.pathname);
}
}, [history, location, pageKey]);
useEffect(() => {
const filter = {
pageKey,
qs: location.search,
};
sessionStorage.setItem(PERSISTENT_FILTER_KEY, JSON.stringify(filter));
}, [location.search, pageKey]);
return children;
}

View File

@ -0,0 +1,111 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { Router, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import PersistentFilters from './PersistentFilters';
const KEY = 'awx-persistent-filter';
describe('PersistentFilters', () => {
test('should initialize filter in sessionStorage', () => {
expect(sessionStorage.getItem(KEY)).toEqual(null);
const history = createMemoryHistory({
initialEntries: ['/templates'],
});
render(
<Router history={history}>
<PersistentFilters pageKey="templates">test</PersistentFilters>
</Router>
);
expect(JSON.parse(sessionStorage.getItem(KEY))).toEqual({
pageKey: 'templates',
qs: '',
});
});
test('should restore filters from sessionStorage', () => {
expect(
sessionStorage.setItem(
KEY,
JSON.stringify({
pageKey: 'templates',
qs: '?page=2&name=foo',
})
)
);
const history = createMemoryHistory({
initialEntries: ['/templates?restoreFilters=true'],
});
render(
<Router history={history}>
<PersistentFilters pageKey="templates">test</PersistentFilters>
</Router>
);
expect(history.location.search).toEqual('?page=2&name=foo');
});
test('should not restore filters without restoreFilters query param', () => {
expect(
sessionStorage.setItem(
KEY,
JSON.stringify({
pageKey: 'templates',
qs: '?page=2&name=foo',
})
)
);
const history = createMemoryHistory({
initialEntries: ['/templates'],
});
render(
<Router history={history}>
<PersistentFilters pageKey="templates">test</PersistentFilters>
</Router>
);
expect(history.location.search).toEqual('');
});
test("should not restore filters if page key doesn't match", () => {
expect(
sessionStorage.setItem(
KEY,
JSON.stringify({
pageKey: 'projects',
qs: '?page=2&name=foo',
})
)
);
const history = createMemoryHistory({
initialEntries: ['/templates?restoreFilters=true'],
});
render(
<Router history={history}>
<PersistentFilters pageKey="templates">test</PersistentFilters>
</Router>
);
expect(history.location.search).toEqual('');
});
test('should update stored filters when qs changes', async () => {
const history = createMemoryHistory({
initialEntries: ['/templates'],
});
render(
<Router history={history}>
<PersistentFilters pageKey="templates">test</PersistentFilters>
</Router>
);
history.push('/templates?page=3');
await waitFor(() => true);
expect(JSON.parse(sessionStorage.getItem(KEY))).toEqual({
pageKey: 'templates',
qs: '?page=3',
});
});
});

View File

@ -0,0 +1 @@
export { default } from './PersistentFilters';

View File

@ -24,7 +24,11 @@ function RoutedTabs({ tabsArray }) {
const handleTabSelect = (event, eventKey) => {
const match = tabsArray.find((tab) => tab.id === eventKey);
if (match) {
history.push(match.link);
event.preventDefault();
const link = match.isBackButton
? `${match.link}?restoreFilters=true`
: match.link;
history.push(link);
}
};
@ -39,7 +43,7 @@ function RoutedTabs({ tabsArray }) {
aria-label={typeof tab.name === 'string' ? tab.name : null}
eventKey={tab.id}
key={tab.id}
link={tab.link}
href={`#${tab.link}`}
title={<TabTitleText>{tab.name}</TabTitleText>}
aria-controls=""
ouiaId={`${tab.name}-tab`}

View File

@ -37,7 +37,12 @@ describe('<RoutedTabs />', () => {
});
test('should update history when new tab selected', async () => {
wrapper.find('Tabs').invoke('onSelect')({}, 2);
wrapper.find('Tabs').invoke('onSelect')(
{
preventDefault: () => {},
},
2
);
wrapper.update();
expect(history.location.pathname).toEqual('/organizations/19/access');

View File

@ -119,9 +119,10 @@ describe('<Schedule />', () => {
});
test('expect all tabs to exist, including Back to Schedules', async () => {
expect(
wrapper.find('button[link="/templates/job_template/1/schedules"]').length
).toBe(1);
expect(wrapper.find('button[aria-label="Details"]').length).toBe(1);
const routedTabs = wrapper.find('RoutedTabs');
const tabs = routedTabs.prop('tabsArray');
expect(tabs[0].link).toEqual('/templates/job_template/1/schedules');
expect(tabs[1].name).toEqual('Details');
});
});

View File

@ -10,3 +10,4 @@ export const JOB_TYPE_URL_SEGMENTS = {
export const SESSION_TIMEOUT_KEY = 'awx-session-timeout';
export const SESSION_REDIRECT_URL = 'awx-redirect-url';
export const PERSISTENT_FILTER_KEY = 'awx-persistent-filter';

View File

@ -102,6 +102,7 @@ function SessionProvider({ children }) {
if (!isSessionExpired.current) {
setAuthRedirectTo('/logout');
}
sessionStorage.clear();
await RootAPI.logout();
setSessionTimeout(0);
setSessionCountdown(0);

View File

@ -74,6 +74,7 @@ function Application({ setBreadcrumb }) {
),
link: '/applications',
id: 0,
isBackButton: true,
},
{ name: t`Details`, link: `/applications/${id}/details`, id: 1 },
{ name: t`Tokens`, link: `/applications/${id}/tokens`, id: 2 },

View File

@ -11,6 +11,7 @@ import {
} from '@patternfly/react-core';
import ScreenHeader from 'components/ScreenHeader';
import { Detail, DetailList } from 'components/DetailList';
import PersistentFilters from 'components/PersistentFilters';
import ApplicationsList from './ApplicationsList';
import ApplicationAdd from './ApplicationAdd';
import Application from './Application';
@ -56,7 +57,9 @@ function Applications() {
<Application setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/applications">
<ApplicationsList />
<PersistentFilters pageKey="applications">
<ApplicationsList />
</PersistentFilters>
</Route>
</Switch>
{applicationModalSource && (

View File

@ -67,6 +67,7 @@ function Credential({ setBreadcrumb }) {
),
link: `/credentials`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `/credentials/${id}/details`, id: 0 },
{

View File

@ -4,6 +4,7 @@ import { Route, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import { Config } from 'contexts/Config';
import ScreenHeader from 'components/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import Credential from './Credential';
import CredentialAdd from './CredentialAdd';
import { CredentialList } from './CredentialList';
@ -44,7 +45,9 @@ function Credentials() {
<Credential setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/credentials">
<CredentialList />
<PersistentFilters pageKey="credentials">
<CredentialList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -57,6 +57,7 @@ function CredentialType({ setBreadcrumb }) {
),
link: '/credential_types',
id: 99,
isBackButton: true,
},
{
name: t`Details`,

View File

@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
import { t } from '@lingui/macro';
import { Route, Switch } from 'react-router-dom';
import PersistentFilters from 'components/PersistentFilters';
import ScreenHeader from 'components/ScreenHeader';
import CredentialTypeAdd from './CredentialTypeAdd';
import CredentialTypeList from './CredentialTypeList';
@ -40,7 +40,9 @@ function CredentialTypes() {
<CredentialType setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/credential_types">
<CredentialTypeList />
<PersistentFilters pageKey="credentialTypes">
<CredentialTypeList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -59,6 +59,7 @@ function ExecutionEnvironment({ setBreadcrumb }) {
),
link: '/execution_environments',
id: 99,
isBackButton: true,
},
{
name: t`Details`,

View File

@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
import { t } from '@lingui/macro';
import { Route, Switch } from 'react-router-dom';
import PersistentFilters from 'components/PersistentFilters';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import ExecutionEnvironment from './ExecutionEnvironment';
import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd';
@ -40,7 +40,9 @@ function ExecutionEnvironments() {
<ExecutionEnvironment setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/execution_environments">
<ExecutionEnvironmentList />
<PersistentFilters pageKey="executionEnvironments">
<ExecutionEnvironmentList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -52,6 +52,7 @@ function Host({ setBreadcrumb }) {
),
link: `/hosts`,
id: 99,
isBackButton: true,
},
{
name: t`Details`,

View File

@ -1,11 +1,10 @@
import React, { useState, useCallback } from 'react';
import { Route, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import { Config } from 'contexts/Config';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import HostList from './HostList';
import HostAdd from './HostAdd';
import Host from './Host';
@ -47,7 +46,9 @@ function Hosts() {
</Config>
</Route>
<Route path="/hosts">
<HostList />
<PersistentFilters pageKey="hosts">
<HostList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -63,6 +63,7 @@ function InstanceGroup({ setBreadcrumb }) {
),
link: '/instance_groups',
id: 99,
isBackButton: true,
},
{
name: t`Details`,

View File

@ -9,6 +9,7 @@ import useRequest from 'hooks/useRequest';
import { SettingsAPI } from 'api';
import ScreenHeader from 'components/ScreenHeader';
import ContentLoading from 'components/ContentLoading';
import PersistentFilters from 'components/PersistentFilters';
import InstanceGroupAdd from './InstanceGroupAdd';
import InstanceGroupList from './InstanceGroupList';
import InstanceGroup from './InstanceGroup';
@ -103,11 +104,13 @@ function InstanceGroups() {
<InstanceGroup setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/instance_groups">
<InstanceGroupList
isKubernetes={isKubernetes}
isSettingsRequestLoading={isSettingsRequestLoading}
settingsRequestError={settingsRequestError}
/>
<PersistentFilters pageKey="instanceGroups">
<InstanceGroupList
isKubernetes={isKubernetes}
isSettingsRequestLoading={isSettingsRequestLoading}
settingsRequestError={settingsRequestError}
/>
</PersistentFilters>
</Route>
</Switch>
)}

View File

@ -20,6 +20,7 @@ function Instance({ setBreadcrumb }) {
),
link: `/instances`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `${match.url}/details`, id: 0 },
];

View File

@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
import { t } from '@lingui/macro';
import { Route, Switch } from 'react-router-dom';
import ScreenHeader from 'components/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import { InstanceList } from './InstanceList';
import Instance from './Instance';
@ -30,7 +31,9 @@ function Instances() {
<Instance setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/instances">
<InstanceList />
<PersistentFilters pageKey="instances">
<InstanceList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -5,6 +5,7 @@ import { Route, Switch } from 'react-router-dom';
import { Config } from 'contexts/Config';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import { InventoryList } from './InventoryList';
import Inventory from './Inventory';
import SmartInventory from './SmartInventory';
@ -119,7 +120,9 @@ function Inventories() {
<SmartInventory setBreadcrumb={setBreadcrumbConfig} />
</Route>
<Route path="/inventories">
<InventoryList />
<PersistentFilters pageKey="inventories">
<InventoryList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -59,6 +59,7 @@ function Inventory({ setBreadcrumb }) {
),
link: `/inventories`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `${match.url}/details`, id: 0 },
{ name: t`Access`, link: `${match.url}/access`, id: 1 },

View File

@ -59,12 +59,14 @@ describe('<InventoryGroup />', () => {
});
test('expect all tabs to exist, including Back to Groups', async () => {
expect(
wrapper.find('button[link="/inventories/inventory/1/groups"]').length
).toBe(1);
expect(wrapper.find('button[aria-label="Details"]').length).toBe(1);
expect(wrapper.find('button[aria-label="Related Groups"]').length).toBe(1);
expect(wrapper.find('button[aria-label="Hosts"]').length).toBe(1);
const routedTabs = wrapper.find('RoutedTabs');
expect(routedTabs).toHaveLength(1);
const tabs = routedTabs.prop('tabsArray');
expect(tabs[0].link).toEqual('/inventories/inventory/1/groups');
expect(tabs[1].name).toEqual('Details');
expect(tabs[2].name).toEqual('Related Groups');
expect(tabs[3].name).toEqual('Hosts');
});
test('should show content error when user attempts to navigate to erroneous route', async () => {

View File

@ -111,6 +111,7 @@ function Job({ setBreadcrumb }) {
</>
),
link: `/jobs`,
isBackButton: true,
id: 99,
},
{ name: t`Details`, link: `${match.url}/details`, id: 0 },

View File

@ -5,6 +5,7 @@ import { t } from '@lingui/macro';
import { PageSection } from '@patternfly/react-core';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import JobList from 'components/JobList';
import PersistentFilters from 'components/PersistentFilters';
import Job from './Job';
import JobTypeRedirect from './JobTypeRedirect';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
@ -41,7 +42,9 @@ function Jobs() {
<Switch>
<Route exact path={match.path}>
<PageSection>
<JobList showTypeColumn />
<PersistentFilters pageKey="jobs">
<JobList showTypeColumn />
</PersistentFilters>
</PageSection>
</Route>
<Route path={`${match.path}/:id/details`}>

View File

@ -98,6 +98,7 @@ function ManagementJob({ setBreadcrumb }) {
{t`Back to management jobs`}
</>
),
isBackButton: true,
},
];

View File

@ -1,9 +1,8 @@
import React, { useState, useCallback } from 'react';
import { t } from '@lingui/macro';
import { Route, Switch } from 'react-router-dom';
import ScreenHeader from 'components/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import ManagementJob from './ManagementJob';
import ManagementJobList from './ManagementJobList';
@ -37,7 +36,9 @@ function ManagementJobs() {
<ManagementJob setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path={basePath}>
<ManagementJobList setBreadcrumb={buildBreadcrumbConfig} />
<PersistentFilters pageKey="managementJobs">
<ManagementJobList setBreadcrumb={buildBreadcrumbConfig} />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -78,6 +78,7 @@ function NotificationTemplate({ setBreadcrumb }) {
),
link: `/notification_templates`,
id: 99,
isBackButton: true,
},
{
name: t`Details`,

View File

@ -3,6 +3,7 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import NotificationTemplateList from './NotificationTemplateList';
import NotificationTemplateAdd from './NotificationTemplateAdd';
import NotificationTemplate from './NotificationTemplate';
@ -39,7 +40,9 @@ function NotificationTemplates() {
<NotificationTemplate setBreadcrumb={updateBreadcrumbConfig} />
</Route>
<Route path={`${match.url}`}>
<NotificationTemplateList />
<PersistentFilters pageKey="notificationTemplates">
<NotificationTemplateList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -118,6 +118,7 @@ function Organization({ setBreadcrumb, me }) {
),
link: `/organizations`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `${match.url}/details`, id: 0 },
{ name: t`Access`, link: `${match.url}/access`, id: 1 },

View File

@ -5,7 +5,7 @@ import { t } from '@lingui/macro';
import { Config } from 'contexts/Config';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import OrganizationsList from './OrganizationList/OrganizationList';
import OrganizationAdd from './OrganizationAdd/OrganizationAdd';
import Organization from './Organization';
@ -54,7 +54,9 @@ function Organizations() {
</Config>
</Route>
<Route path={`${match.path}`}>
<OrganizationsList />
<PersistentFilters pageKey="organizations">
<OrganizationsList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -99,6 +99,7 @@ function Project({ setBreadcrumb }) {
),
link: `/projects`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `/projects/${id}/details` },
{ name: t`Access`, link: `/projects/${id}/access` },

View File

@ -1,10 +1,8 @@
import React, { useState, useCallback } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import ProjectsList from './ProjectList/ProjectList';
import ProjectAdd from './ProjectAdd/ProjectAdd';
import Project from './Project';
@ -49,7 +47,9 @@ function Projects() {
<Project setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/projects">
<ProjectsList />
<PersistentFilters pageKey="projects">
<ProjectsList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -52,6 +52,7 @@ function Team({ setBreadcrumb }) {
),
link: `/teams`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `/teams/${id}/details`, id: 0 },
{ name: t`Access`, link: `/teams/${id}/access`, id: 1 },

View File

@ -5,6 +5,7 @@ import { t } from '@lingui/macro';
import { Config } from 'contexts/Config';
import ScreenHeader from 'components/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import TeamList from './TeamList';
import TeamAdd from './TeamAdd';
import Team from './Team';
@ -43,9 +44,11 @@ function Teams() {
<Team setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/teams">
<Config>
{({ me }) => <TeamList path="/teams" me={me || {}} />}
</Config>
<PersistentFilters pageKey="teams">
<Config>
{({ me }) => <TeamList path="/teams" me={me || {}} />}
</Config>
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -129,6 +129,7 @@ function Template({ setBreadcrumb }) {
</>
),
link: `/templates`,
isBackButton: true,
id: 99,
},
{ name: t`Details`, link: `${match.url}/details` },

View File

@ -6,6 +6,7 @@ import { PageSection } from '@patternfly/react-core';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import TemplateList from 'components/TemplateList';
import PersistentFilters from 'components/PersistentFilters';
import Template from './Template';
import WorkflowJobTemplate from './WorkflowJobTemplate';
import JobTemplateAdd from './JobTemplateAdd';
@ -78,7 +79,9 @@ function Templates() {
</Route>
<Route path="/templates">
<PageSection>
<TemplateList />
<PersistentFilters pageKey="templates">
<TemplateList />
</PersistentFilters>
</PageSection>
</Route>
</Switch>

View File

@ -111,6 +111,7 @@ function WorkflowJobTemplate({ setBreadcrumb }) {
</>
),
link: `/templates`,
isBackButton: true,
id: 99,
},
{ name: t`Details`, link: `${match.url}/details` },

View File

@ -59,6 +59,7 @@ function User({ setBreadcrumb, me }) {
),
link: `/users`,
id: 99,
isBackButton: true,
},
{ name: t`Details`, link: `${match.url}/details`, id: 0 },
{

View File

@ -4,8 +4,8 @@ import { Route, useRouteMatch, Switch } from 'react-router-dom';
import { t } from '@lingui/macro';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import { Config } from 'contexts/Config';
import UsersList from './UserList/UserList';
import UserAdd from './UserAdd/UserAdd';
import User from './User';
@ -51,7 +51,9 @@ function Users() {
</Config>
</Route>
<Route path={`${match.path}`}>
<UsersList />
<PersistentFilters pageKey="users">
<UsersList />
</PersistentFilters>
</Route>
</Switch>
</>

View File

@ -70,6 +70,7 @@ function WorkflowApproval({ setBreadcrumb }) {
</>
),
link: `/workflow_approvals`,
isBackButton: true,
id: 99,
},
{

View File

@ -3,6 +3,7 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { t } from '@lingui/macro';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import PersistentFilters from 'components/PersistentFilters';
import WorkflowApprovalList from './WorkflowApprovalList';
import WorkflowApproval from './WorkflowApproval';
@ -35,7 +36,9 @@ function WorkflowApprovals() {
<WorkflowApproval setBreadcrumb={updateBreadcrumbConfig} />
</Route>
<Route path={`${match.url}`}>
<WorkflowApprovalList />
<PersistentFilters pageKey="workflowApprovals">
<WorkflowApprovalList />
</PersistentFilters>
</Route>
</Switch>
</>