diff --git a/.changeset/.yellow-parrots-glow.md b/.changeset/.yellow-parrots-glow.md new file mode 100644 index 0000000000..0773a7302c --- /dev/null +++ b/.changeset/.yellow-parrots-glow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +`CatalogIndexPage` now uses `EntitySearchBar` for text-based filtering of entities, saving the search text in the query parameters and debouncing the server requests. diff --git a/.changeset/shiny-flowers-yawn.md b/.changeset/shiny-flowers-yawn.md new file mode 100644 index 0000000000..bb85130f24 --- /dev/null +++ b/.changeset/shiny-flowers-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +`EntitySearchBar` and `EntityTextFilter` have been updated accordingly to persist the status as query params, following the same pattern as the other server side diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index ce2502691f..efc84c7870 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -573,6 +573,8 @@ export class EntityTextFilter implements EntityFilter { fields: string[]; }; // (undocumented) + toQueryValue(): string; + // (undocumented) readonly value: string; } diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx index 94db377e26..7dd8c792d7 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { fireEvent, render, waitFor, screen } from '@testing-library/react'; import { EntitySearchBar } from './EntitySearchBar'; -import { DefaultEntityFilters } from '../../hooks/useEntityListProvider'; import { EntityTextFilter } from '../../filters'; import { MockEntityListContextProvider } from '../../testUtils/providers'; @@ -25,12 +24,15 @@ describe('EntitySearchBar', () => { it('should display search value and execute set callback', async () => { const updateFilters = jest.fn(); - const filters: DefaultEntityFilters = { - text: new EntityTextFilter('hello'), - }; - render( - + , ); diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index 91e411c562..a56119ac70 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -24,8 +24,8 @@ import { } from '@material-ui/core'; import Clear from '@material-ui/icons/Clear'; import Search from '@material-ui/icons/Search'; -import React, { useState } from 'react'; -import useDebounce from 'react-use/esm/useDebounce'; +import React, { useEffect, useMemo, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityTextFilter } from '../../filters'; @@ -52,8 +52,17 @@ const useStyles = makeStyles( export const EntitySearchBar = () => { const classes = useStyles(); - const { filters, updateFilters } = useEntityList(); - const [search, setSearch] = useState(filters.text?.value ?? ''); + const { + updateFilters, + queryParameters: { text: textParameter }, + } = useEntityList(); + + const queryParamTextFilter = useMemo( + () => [textParameter].flat()[0], + [textParameter], + ); + + const [search, setSearch] = useState(queryParamTextFilter ?? ''); useDebounce( () => { @@ -65,6 +74,12 @@ export const EntitySearchBar = () => { [search, updateFilters], ); + useEffect(() => { + if (queryParamTextFilter) { + setSearch(queryParamTextFilter); + } + }, [queryParamTextFilter]); + return ( diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index dda13338a7..08be27a561 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -116,6 +116,10 @@ export class EntityTextFilter implements EntityFilter { }; } + toQueryValue() { + return this.value; + } + private toUpperArray( value: Array, ): Array { diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 8aa5b9e376..674271a044 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -192,7 +192,7 @@ export const CatalogTable = (props: CatalogTableProps) => { columns={tableColumns} emptyContent={emptyContent} isLoading={loading} - title={title} + title={titleDisplay} actions={actions} subtitle={subtitle} options={options} diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTableToolbar.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTableToolbar.tsx new file mode 100644 index 0000000000..16d9923727 --- /dev/null +++ b/plugins/catalog/src/components/CatalogTable/CatalogTableToolbar.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntitySearchBar } from '@backstage/plugin-catalog-react'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@material-ui/core/styles'; + +const useToolbarStyles = makeStyles( + theme => ({ + root: { + paddingTop: theme.spacing(1.25), + paddingLeft: theme.spacing(2.5), + paddingBottom: theme.spacing(0.75), + display: 'flex', + justifyContent: 'space-between', + }, + text: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }), + { name: 'BackstageTableToolbar' }, +); + +export function CatalogTableToolbar(props: { + title?: string | React.ReactElement; +}) { + const styles = useToolbarStyles(); + return ( + + + {props.title} + + + + ); +} diff --git a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx index 34087f1b36..38691b49d2 100644 --- a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx @@ -18,10 +18,7 @@ import React from 'react'; import { Table, TableProps } from '@backstage/core-components'; import { CatalogTableRow } from './types'; -import { - EntityTextFilter, - useEntityList, -} from '@backstage/plugin-catalog-react'; +import { CatalogTableToolbar } from './CatalogTableToolbar'; type PaginatedCatalogTableProps = { prev?(): void; @@ -33,7 +30,6 @@ type PaginatedCatalogTableProps = { */ export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) { const { columns, data, next, prev, title, isLoading, options } = props; - const { updateFilters } = useEntityList(); return ( - updateFilters({ - text: searchText ? new EntityTextFilter(searchText) : undefined, - }) - } onPageChange={page => { if (page > 0) { next?.(); @@ -61,6 +52,9 @@ export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) { prev?.(); } }} + components={{ + Toolbar: CatalogTableToolbar, + }} /* this will enable the prev button accordingly */ page={prev ? 1 : 0} /* this will enable the next button accordingly */