diff --git a/.changeset/curly-badgers-sit.md b/.changeset/curly-badgers-sit.md
new file mode 100644
index 0000000000..ca740e907a
--- /dev/null
+++ b/.changeset/curly-badgers-sit.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Store filter values set in `EntityListProvider` in query parameters. This allows selected filters to be restored when returning to pages that list catalog entities.
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index 99552cdb59..c65e06df29 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -39,6 +39,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
"lodash": "^4.17.15",
+ "qs": "^6.9.4",
"react": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
index 417b085cce..d6404fcf07 100644
--- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
@@ -37,4 +37,23 @@ describe('', () => {
kind: new EntityKindFilter('component'),
});
});
+
+ it('respects the query parameter filter value', () => {
+ const updateFilters = jest.fn();
+ const queryParameters = { kind: 'API' };
+ render(
+
+
+ ,
+ );
+
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ kind: new EntityKindFilter('API'),
+ });
+ });
});
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx
index 5b3f95b7ba..6f9119c404 100644
--- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx
+++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx
@@ -30,8 +30,7 @@ export const EntityKindPicker = ({
}: EntityKindFilterProps) => {
const { updateFilters, queryParameters } = useEntityListProvider();
const [selectedKind] = useState(
- // TODO Cast here is not great 🤔
- (queryParameters.kind as string) ?? initialFilter,
+ [queryParameters.kind].flat()[0] ?? initialFilter,
);
useEffect(() => {
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
index 8fa0789c10..f7d915ef5b 100644
--- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
@@ -91,6 +91,27 @@ describe('', () => {
]);
});
+ it('respects the query parameter filter value', () => {
+ const updateFilters = jest.fn();
+ const queryParameters = { lifecycles: ['experimental'] };
+ render(
+
+
+ ,
+ );
+
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ lifecycles: new EntityLifecycleFilter(['experimental']),
+ });
+ });
+
it('adds lifecycles to filters', () => {
const updateFilters = jest.fn();
const rendered = render(
@@ -104,7 +125,9 @@ describe('', () => {
,
);
- expect(updateFilters).not.toHaveBeenCalled();
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ lifecycles: undefined,
+ });
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
fireEvent.click(rendered.getByText('production'));
@@ -127,7 +150,9 @@ describe('', () => {
,
);
- expect(updateFilters).not.toHaveBeenCalled();
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ lifecycles: new EntityLifecycleFilter(['production']),
+ });
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
expect(rendered.getByLabelText('production')).toBeChecked();
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
index 31ab2911f7..9a899e161d 100644
--- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
@@ -26,7 +26,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { Autocomplete } from '@material-ui/lab';
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { useEntityListProvider } from '../../hooks/useEntityListProvider';
import { EntityLifecycleFilter } from '../../filters';
@@ -34,7 +34,30 @@ const icon = ;
const checkedIcon = ;
export const EntityLifecyclePicker = () => {
- const { updateFilters, backendEntities, filters } = useEntityListProvider();
+ const {
+ updateFilters,
+ backendEntities,
+ filters,
+ queryParameters,
+ } = useEntityListProvider();
+
+ const queryParamLifecycles = [queryParameters.lifecycles]
+ .flat()
+ .filter(Boolean) as string[];
+ const [selectedLifecycles, setSelectedLifecycles] = useState(
+ queryParamLifecycles.length
+ ? queryParamLifecycles
+ : filters.lifecycles?.values ?? [],
+ );
+
+ useEffect(() => {
+ updateFilters({
+ lifecycles: selectedLifecycles.length
+ ? new EntityLifecycleFilter(selectedLifecycles)
+ : undefined,
+ });
+ }, [selectedLifecycles, updateFilters]);
+
const availableLifecycles = useMemo(
() =>
[
@@ -49,22 +72,14 @@ export const EntityLifecyclePicker = () => {
if (!availableLifecycles.length) return null;
- const onChange = (lifecycles: string[]) => {
- updateFilters({
- lifecycles: lifecycles.length
- ? new EntityLifecycleFilter(lifecycles)
- : undefined,
- });
- };
-
return (
Lifecycle
multiple
options={availableLifecycles}
- value={filters.lifecycles?.values ?? []}
- onChange={(_: object, value: string[]) => onChange(value)}
+ value={selectedLifecycles}
+ onChange={(_: object, value: string[]) => setSelectedLifecycles(value)}
renderOption={(option, { selected }) => (
', () => {
]);
});
+ it('respects the query parameter filter value', () => {
+ const updateFilters = jest.fn();
+ const queryParameters = { owners: ['another-owner'] };
+ render(
+
+
+ ,
+ );
+
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ owners: new EntityOwnerFilter(['another-owner']),
+ });
+ });
+
it('adds owners to filters', () => {
const updateFilters = jest.fn();
const rendered = render(
@@ -134,7 +155,9 @@ describe('', () => {
,
);
- expect(updateFilters).not.toHaveBeenCalled();
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ owners: undefined,
+ });
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
fireEvent.click(rendered.getByText('some-owner'));
@@ -157,7 +180,9 @@ describe('', () => {
,
);
- expect(updateFilters).not.toHaveBeenCalled();
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ owners: new EntityOwnerFilter(['some-owner']),
+ });
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
expect(rendered.getByLabelText('some-owner')).toBeChecked();
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
index 955f917c8c..90d94e32b5 100644
--- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
@@ -26,7 +26,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { Autocomplete } from '@material-ui/lab';
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { useEntityListProvider } from '../../hooks/useEntityListProvider';
import { EntityOwnerFilter } from '../../filters';
import { getEntityRelations } from '../../utils';
@@ -36,7 +36,28 @@ const icon = ;
const checkedIcon = ;
export const EntityOwnerPicker = () => {
- const { updateFilters, backendEntities, filters } = useEntityListProvider();
+ const {
+ updateFilters,
+ backendEntities,
+ filters,
+ queryParameters,
+ } = useEntityListProvider();
+
+ const queryParamOwners = [queryParameters.owners]
+ .flat()
+ .filter(Boolean) as string[];
+ const [selectedOwners, setSelectedOwners] = useState(
+ queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [],
+ );
+
+ useEffect(() => {
+ updateFilters({
+ owners: selectedOwners.length
+ ? new EntityOwnerFilter(selectedOwners)
+ : undefined,
+ });
+ }, [selectedOwners, updateFilters]);
+
const availableOwners = useMemo(
() =>
[
@@ -55,20 +76,14 @@ export const EntityOwnerPicker = () => {
if (!availableOwners.length) return null;
- const onChange = (owners: string[]) => {
- updateFilters({
- owners: owners.length ? new EntityOwnerFilter(owners) : undefined,
- });
- };
-
return (
Owner
multiple
options={availableOwners}
- value={filters.owners?.values ?? []}
- onChange={(_: object, value: string[]) => onChange(value)}
+ value={selectedOwners}
+ onChange={(_: object, value: string[]) => setSelectedOwners(value)}
renderOption={(option, { selected }) => (
', () => {
]);
});
+ it('respects the query parameter filter value', () => {
+ const updateFilters = jest.fn();
+ const queryParameters = { tags: ['tag3'] };
+ render(
+
+
+ ,
+ );
+
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ tags: new EntityTagFilter(['tag3']),
+ });
+ });
+
it('adds tags to filters', () => {
const updateFilters = jest.fn();
const rendered = render(
@@ -92,7 +113,9 @@ describe('', () => {
,
);
- expect(updateFilters).not.toHaveBeenCalled();
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ tags: undefined,
+ });
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
fireEvent.click(rendered.getByText('tag1'));
@@ -115,7 +138,9 @@ describe('', () => {
,
);
- expect(updateFilters).not.toHaveBeenCalled();
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ tags: new EntityTagFilter(['tag1']),
+ });
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
expect(rendered.getByLabelText('tag1')).toBeChecked();
diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx
index 744ad447d8..b7fa8f1186 100644
--- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx
+++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx
@@ -26,7 +26,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { Autocomplete } from '@material-ui/lab';
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { useEntityListProvider } from '../../hooks/useEntityListProvider';
import { EntityTagFilter } from '../../filters';
@@ -34,7 +34,26 @@ const icon = ;
const checkedIcon = ;
export const EntityTagPicker = () => {
- const { updateFilters, backendEntities, filters } = useEntityListProvider();
+ const {
+ updateFilters,
+ backendEntities,
+ filters,
+ queryParameters,
+ } = useEntityListProvider();
+
+ const queryParamTags = [queryParameters.tags]
+ .flat()
+ .filter(Boolean) as string[];
+ const [selectedTags, setSelectedTags] = useState(
+ queryParamTags.length ? queryParamTags : filters.tags?.values ?? [],
+ );
+
+ useEffect(() => {
+ updateFilters({
+ tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined,
+ });
+ }, [selectedTags, updateFilters]);
+
const availableTags = useMemo(
() =>
[
@@ -49,20 +68,14 @@ export const EntityTagPicker = () => {
if (!availableTags.length) return null;
- const onChange = (tags: string[]) => {
- updateFilters({
- tags: tags.length ? new EntityTagFilter(tags) : undefined,
- });
- };
-
return (
Tags
multiple
options={availableTags}
- value={filters.tags?.values ?? []}
- onChange={(_: object, value: string[]) => onChange(value)}
+ value={selectedTags}
+ onChange={(_: object, value: string[]) => setSelectedTags(value)}
renderOption={(option, { selected }) => (
{