From fee7a6bd49e3a00164e849694790c446a288cc2f Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 15:34:56 -0600 Subject: [PATCH 01/12] Add kind picker to the CatalogPage Signed-off-by: Tim Hansen --- .../software-catalog/catalog-customization.md | 17 ++--- .../ContentHeader/ContentHeader.test.tsx | 2 +- .../layout/ContentHeader/ContentHeader.tsx | 4 +- .../EntityTypePicker.test.tsx | 7 +- .../EntityTypePicker/EntityTypePicker.tsx | 2 +- plugins/catalog-react/src/hooks/index.ts | 1 + .../catalog-react/src/hooks/useEntityKinds.ts | 36 +++++++++ .../CatalogPage/CatalogKindHeader.tsx | 74 +++++++++++++++++++ .../components/CatalogPage/CatalogPage.tsx | 24 +++--- 9 files changed, 139 insertions(+), 28 deletions(-) create mode 100644 plugins/catalog-react/src/hooks/useEntityKinds.ts create mode 100644 plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 07258a55cb..8f855776a0 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -34,15 +34,14 @@ export const CustomCatalogPage = ({ }: CatalogPageProps) => { return ( - - - - All your software catalog entities - - + + + }> + + All your software catalog entities + - - - + + ); }; diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx index 498b859db1..37059766a8 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx @@ -32,7 +32,7 @@ describe('', () => { it('should render with titleComponent', async () => { const title = 'Custom title'; - const titleComponent = () =>

{title}

; + const titleComponent =

{title}

; const rendered = await renderInTestApp( , ); diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx index 4bda2d76af..fa797212e4 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx @@ -77,7 +77,7 @@ const ContentHeaderTitle = ({ type ContentHeaderProps = { title?: ContentHeaderTitleProps['title']; - titleComponent?: ComponentType; + titleComponent?: JSX.Element; description?: string; textAlign?: 'left' | 'right' | 'center'; }; @@ -92,7 +92,7 @@ export const ContentHeader = ({ const classes = useStyles({ textAlign })(); const renderedTitle = TitleComponent ? ( - + TitleComponent ) : ( ); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 903a7ba4eb..a9ecf03e5c 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { capitalize } from 'lodash'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; @@ -26,6 +26,7 @@ import { EntityKindFilter, EntityTypeFilter } from '../../filters'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderWithEffects } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -79,7 +80,7 @@ const apis = ApiRegistry.from([ describe('', () => { it('renders available entity types', async () => { - const rendered = render( + const rendered = await renderWithEffects( ', () => { it('sets the selected type filter', async () => { const updateFilters = jest.fn(); - const rendered = render( + const rendered = await renderWithEffects( { } }, [error, alertApi]); - if (!availableTypes || error) return null; + if (availableTypes.length === 0 || error) return null; const items = [ { value: 'all', label: 'All' }, diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 868e50c852..2f3ce8411d 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -22,6 +22,7 @@ export { } from './useEntityListProvider'; export type { DefaultEntityFilters } from './useEntityListProvider'; export { useEntityTypeFilter } from './useEntityTypeFilter'; +export { useEntityKinds } from './useEntityKinds'; export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts new file mode 100644 index 0000000000..1aaf40981a --- /dev/null +++ b/plugins/catalog-react/src/hooks/useEntityKinds.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2021 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 { useEffect, useState } from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import { catalogApiRef } from '../api'; + +export function useEntityKinds() { + const [kinds, setKinds] = useState(['component']); + const catalogApi = useApi(catalogApiRef); + + useEffect(() => { + async function loadKinds() { + const entities = await catalogApi + .getEntities({ fields: ['kind'] }) + .then(response => response.items); + setKinds([...new Set(entities.map(e => e.kind))].sort()); + } + loadKinds(); + }, [catalogApi]); + + return kinds; +} diff --git a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx new file mode 100644 index 0000000000..985f8fed6a --- /dev/null +++ b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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, { useEffect, useState } from 'react'; +import { + capitalize, + createStyles, + InputBase, + makeStyles, + MenuItem, + Select, + Theme, +} from '@material-ui/core'; +import { + EntityKindFilter, + useEntityKinds, + useEntityListProvider, +} from '@backstage/plugin-catalog-react'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + ...theme.typography.h4, + }, + }), +); + +type CatalogKindHeaderProps = { + initialFilter?: string; +}; + +export const CatalogKindHeader = ({ + initialFilter = 'Component', +}: CatalogKindHeaderProps) => { + const classes = useStyles(); + const allKinds = useEntityKinds(); + const { updateFilters, queryParameters } = useEntityListProvider(); + + const [selectedKind, setSelectedKind] = useState( + [queryParameters.kind].flat()[0] ?? initialFilter, + ); + + useEffect(() => { + updateFilters({ + kind: selectedKind ? new EntityKindFilter(selectedKind) : undefined, + }); + }, [selectedKind, updateFilters]); + + return ( + + ); +}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index a4f6c01bc8..913b3dfeb6 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -43,6 +43,7 @@ import { EntityListContainer, FilterContainer, } from '../FilteredEntityLayout'; +import { CatalogKindHeader } from './CatalogKindHeader'; export type CatalogPageProps = { initiallySelectedFilter?: UserListFilterKind; @@ -61,18 +62,17 @@ export const CatalogPage = ({ return ( - - - - All your software catalog entities - - + + + }> + + All your software catalog entities + - - - + + ); }; From 3ed78fca3b00f44906758149bda69b6b474211b2 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 15:46:03 -0600 Subject: [PATCH 02/12] changesets Signed-off-by: Tim Hansen --- .changeset/cuddly-cooks-fry.md | 6 ++++++ .changeset/nasty-ads-nail.md | 10 ++++++++++ .changeset/short-years-smile.md | 5 +++++ 3 files changed, 21 insertions(+) create mode 100644 .changeset/cuddly-cooks-fry.md create mode 100644 .changeset/nasty-ads-nail.md create mode 100644 .changeset/short-years-smile.md diff --git a/.changeset/cuddly-cooks-fry.md b/.changeset/cuddly-cooks-fry.md new file mode 100644 index 0000000000..028b0291bd --- /dev/null +++ b/.changeset/cuddly-cooks-fry.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added a `useEntityKinds` hook to load a unique list of entity kinds from the catalog. +Fixed a bug in `EntityTypePicker` where the component did not hide when no types were available in returned entities. diff --git a/.changeset/nasty-ads-nail.md b/.changeset/nasty-ads-nail.md new file mode 100644 index 0000000000..e59a54ed84 --- /dev/null +++ b/.changeset/nasty-ads-nail.md @@ -0,0 +1,10 @@ +--- +'@backstage/core-components': minor +--- + +Changed the `titleComponent` prop on `ContentHeader` to accept `JSX.Element` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: + +```diff +- ++}> +``` diff --git a/.changeset/short-years-smile.md b/.changeset/short-years-smile.md new file mode 100644 index 0000000000..6e7b882c1e --- /dev/null +++ b/.changeset/short-years-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Added the ability to switch entity kind on the catalog index page. This is a non-breaking change, but if you created a custom `CatalogPage` and wish to use this feature, make the modifications shown on [#6895](https://github.com/backstage/backstage/pull/6895). From a2f152f6047c1107a3c8fb4d8d1d5e2f660e6318 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 15:50:57 -0600 Subject: [PATCH 03/12] Remove unused imports Signed-off-by: Tim Hansen --- .../core-components/src/layout/ContentHeader/ContentHeader.tsx | 2 +- plugins/catalog/src/components/CatalogPage/CatalogPage.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx index fa797212e4..5891e801e1 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx @@ -18,7 +18,7 @@ * TODO favoriteable capability */ -import React, { ComponentType, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; import { Typography, makeStyles } from '@material-ui/core'; import { Helmet } from 'react-helmet'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 913b3dfeb6..27a1414f27 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -25,7 +25,6 @@ import { } from '@backstage/core-components'; import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { - EntityKindPicker, EntityLifecyclePicker, EntityListProvider, EntityOwnerPicker, From 4b3537ee236b58519b315f6f063db9907d0c0bc0 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 20:36:06 -0600 Subject: [PATCH 04/12] Add test for CatalogKindHeader Signed-off-by: Tim Hansen --- .../catalog-react/src/hooks/useEntityKinds.ts | 3 +- .../catalog-react/src/testUtils/providers.tsx | 6 +- .../CatalogPage/CatalogKindHeader.test.tsx | 112 ++++++++++++++++++ .../CatalogPage/CatalogKindHeader.tsx | 4 +- 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts index 1aaf40981a..21bdad2900 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ b/plugins/catalog-react/src/hooks/useEntityKinds.ts @@ -18,8 +18,9 @@ import { useEffect, useState } from 'react'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '../api'; +// Retrieve a list of unique entity kinds present in the catalog export function useEntityKinds() { - const [kinds, setKinds] = useState(['component']); + const [kinds, setKinds] = useState(['Component']); const catalogApi = useApi(catalogApiRef); useEffect(() => { diff --git a/plugins/catalog-react/src/testUtils/providers.tsx b/plugins/catalog-react/src/testUtils/providers.tsx index 1f9e4f40d6..2172f6c5a9 100644 --- a/plugins/catalog-react/src/testUtils/providers.tsx +++ b/plugins/catalog-react/src/testUtils/providers.tsx @@ -25,12 +25,12 @@ export const MockEntityListContextProvider = ({ children, value, }: PropsWithChildren<{ - value: Partial; + value?: Partial; }>) => { // Provides a default implementation that stores filter state, for testing components that // reflect filter state. const [filters, setFilters] = useState( - value.filters ?? {}, + value?.filters ?? {}, ); const updateFilters = useCallback( ( @@ -60,7 +60,7 @@ export const MockEntityListContextProvider = ({ // Extract value.filters to avoid overwriting it; some tests exercise filter updates. The value // provided is used as the initial seed in useState above. - const { filters: _, ...otherContextFields } = value; + const { filters: _, ...otherContextFields } = value ?? {}; return ( Promise.resolve({ items: entities })), + } as unknown as CatalogApi, + ], +]); + +describe('', () => { + it('renders available kinds', async () => { + const rendered = await renderWithEffects( + + + + + , + ); + + const input = rendered.getByText('Components'); + fireEvent.mouseDown(input); + + entities.map(entity => { + expect( + rendered.getByRole('option', { name: `${entity.kind}s` }), + ).toBeInTheDocument(); + }); + }); + + it('updates the kind filter', async () => { + const updateFilters = jest.fn(); + const rendered = await renderWithEffects( + + + + + , + ); + + const input = rendered.getByText('Components'); + fireEvent.mouseDown(input); + + const option = rendered.getByRole('option', { name: 'Templates' }); + fireEvent.click(option); + + expect(updateFilters).toHaveBeenCalledWith({ + kind: new EntityKindFilter('Template'), + }); + }); +}); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx index 985f8fed6a..16051a0951 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx @@ -67,7 +67,9 @@ export const CatalogKindHeader = ({ classes={classes} > {allKinds.map(kind => ( - {capitalize(kind)}s + + {`${capitalize(kind)}s`} + ))} ); From 286466a90ae4dc42b5e94b91124d2f09a6e464cc Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 21:07:57 -0600 Subject: [PATCH 05/12] useEntityKinds test Signed-off-by: Tim Hansen --- .../src/hooks/useEntityKinds.test.tsx | 90 +++++++++++++++++++ .../CatalogPage/CatalogKindHeader.test.tsx | 2 +- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 plugins/catalog-react/src/hooks/useEntityKinds.test.tsx diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx new file mode 100644 index 0000000000..a1e1463174 --- /dev/null +++ b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright 2021 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, { PropsWithChildren } from 'react'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { catalogApiRef } from '../api'; +import { renderHook } from '@testing-library/react-hooks'; +import { useEntityKinds } from './useEntityKinds'; + +const entities: Entity[] = [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'component-1', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'component-2', + }, + }, + { + apiVersion: '1', + kind: 'Template', + metadata: { + name: 'template', + }, + }, + { + apiVersion: '1', + kind: 'System', + metadata: { + name: 'system', + }, + }, +]; + +const mockCatalogApi: Partial = { + getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), +}; + +const wrapper = ({ children }: PropsWithChildren<{}>) => { + return ( + + {children} + + ); +}; + +describe('useEntityKinds', () => { + it('does not return duplicate kinds', async () => { + const { result, waitForValueToChange } = renderHook( + () => useEntityKinds(), + { + wrapper, + }, + ); + await waitForValueToChange(() => result.current); + expect(result.current.length).toBe(3); + }); + + it('sorts entity kinds', async () => { + const { result, waitForValueToChange } = renderHook( + () => useEntityKinds(), + { + wrapper, + }, + ); + await waitForValueToChange(() => result.current); + expect(result.current).toEqual(['Component', 'System', 'Template']); + }); +}); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx index 95c056cf6b..b39ad08359 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx @@ -65,7 +65,7 @@ const apis = ApiRegistry.from([ getEntities: jest .fn() .mockImplementation(() => Promise.resolve({ items: entities })), - } as unknown as CatalogApi, + } as Partial, ], ]); From 844ab62223c127251b3e0d81e54709de6856c81b Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 22:01:23 -0600 Subject: [PATCH 06/12] api-reports Signed-off-by: Tim Hansen --- packages/core-components/api-report.md | 1 - plugins/catalog-react/api-report.md | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index b4402f67ac..f73eee1f13 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -15,7 +15,6 @@ import { Column } from '@material-table/core'; import { CommonProps } from '@material-ui/core/OverridableComponent'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; -import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index c5f3226874..0eb5a0ba90 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -693,7 +693,7 @@ export const MockEntityListContextProvider: ({ children, value, }: React_2.PropsWithChildren<{ - value: Partial; + value?: Partial> | undefined; }>) => JSX.Element; // Warning: (ae-missing-release-tag) "reduceCatalogFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -750,6 +750,11 @@ export const useEntityCompoundName: () => { // @public (undocumented) export const useEntityFromUrl: () => EntityLoadingStatus; +// Warning: (ae-missing-release-tag) "useEntityKinds" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function useEntityKinds(): string[]; + // Warning: (ae-missing-release-tag) "useEntityListProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From 62987b7d20ed5f19a860dba380195baf65070ef2 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 23:00:47 -0600 Subject: [PATCH 07/12] Switch useEntityKinds to useAsync Signed-off-by: Tim Hansen --- .../src/hooks/useEntityKinds.test.tsx | 5 ++-- .../catalog-react/src/hooks/useEntityKinds.ts | 24 +++++++++---------- .../CatalogPage/CatalogKindHeader.tsx | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx index a1e1463174..96281f4a5a 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx @@ -74,7 +74,8 @@ describe('useEntityKinds', () => { }, ); await waitForValueToChange(() => result.current); - expect(result.current.length).toBe(3); + expect(result.current.kinds).toBeDefined(); + expect(result.current.kinds!.length).toBe(3); }); it('sorts entity kinds', async () => { @@ -85,6 +86,6 @@ describe('useEntityKinds', () => { }, ); await waitForValueToChange(() => result.current); - expect(result.current).toEqual(['Component', 'System', 'Template']); + expect(result.current.kinds).toEqual(['Component', 'System', 'Template']); }); }); diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts index 21bdad2900..082e4a207d 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ b/plugins/catalog-react/src/hooks/useEntityKinds.ts @@ -14,24 +14,24 @@ * limitations under the License. */ -import { useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '../api'; // Retrieve a list of unique entity kinds present in the catalog export function useEntityKinds() { - const [kinds, setKinds] = useState(['Component']); const catalogApi = useApi(catalogApiRef); - useEffect(() => { - async function loadKinds() { - const entities = await catalogApi - .getEntities({ fields: ['kind'] }) - .then(response => response.items); - setKinds([...new Set(entities.map(e => e.kind))].sort()); - } - loadKinds(); - }, [catalogApi]); + const { + error, + loading, + value: kinds, + } = useAsync(async () => { + const entities = await catalogApi + .getEntities({ fields: ['kind'] }) + .then(response => response.items); - return kinds; + return [...new Set(entities.map(e => e.kind))].sort(); + }); + return { error, loading, kinds }; } diff --git a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx index 16051a0951..89b132de53 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx @@ -46,7 +46,7 @@ export const CatalogKindHeader = ({ initialFilter = 'Component', }: CatalogKindHeaderProps) => { const classes = useStyles(); - const allKinds = useEntityKinds(); + const { kinds: allKinds } = useEntityKinds(); const { updateFilters, queryParameters } = useEntityListProvider(); const [selectedKind, setSelectedKind] = useState( @@ -66,7 +66,7 @@ export const CatalogKindHeader = ({ onChange={e => setSelectedKind(e.target.value as string)} classes={classes} > - {allKinds.map(kind => ( + {(allKinds ?? ['Component']).map(kind => ( {`${capitalize(kind)}s`} From f44c4ac388954fa9823411ab5ebff0d1e1d784f1 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 19 Aug 2021 23:17:49 -0600 Subject: [PATCH 08/12] api-reports Signed-off-by: Tim Hansen --- plugins/catalog-react/api-report.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 0eb5a0ba90..5f65426ed3 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -753,7 +753,11 @@ export const useEntityFromUrl: () => EntityLoadingStatus; // Warning: (ae-missing-release-tag) "useEntityKinds" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function useEntityKinds(): string[]; +export function useEntityKinds(): { + error: Error | undefined; + loading: boolean; + kinds: string[] | undefined; +}; // Warning: (ae-missing-release-tag) "useEntityListProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From df51a55426f882c61c3691ac82d36b98b0dc30ea Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 20 Aug 2021 10:55:28 -0600 Subject: [PATCH 09/12] Export CatalogKindHeader Signed-off-by: Tim Hansen --- plugins/catalog/api-report.md | 8 ++++++++ .../CatalogKindHeader.test.tsx | 0 .../CatalogKindHeader.tsx | 0 .../src/components/CatalogKindHeader/index.ts | 16 ++++++++++++++++ .../src/components/CatalogPage/CatalogPage.tsx | 2 +- plugins/catalog/src/index.ts | 1 + 6 files changed, 26 insertions(+), 1 deletion(-) rename plugins/catalog/src/components/{CatalogPage => CatalogKindHeader}/CatalogKindHeader.test.tsx (100%) rename plugins/catalog/src/components/{CatalogPage => CatalogKindHeader}/CatalogKindHeader.tsx (100%) create mode 100644 plugins/catalog/src/components/CatalogKindHeader/index.ts diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index ec2aeafd00..689c617158 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -114,6 +114,14 @@ export const CatalogIndexPage: ({ initiallySelectedFilter, }: CatalogPageProps) => JSX.Element; +// Warning: (ae-forgotten-export) The symbol "CatalogKindHeaderProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "CatalogKindHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const CatalogKindHeader: ({ + initialFilter, +}: CatalogKindHeaderProps) => JSX.Element; + // Warning: (ae-missing-release-tag) "catalogPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx similarity index 100% rename from plugins/catalog/src/components/CatalogPage/CatalogKindHeader.test.tsx rename to plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx diff --git a/plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx similarity index 100% rename from plugins/catalog/src/components/CatalogPage/CatalogKindHeader.tsx rename to plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx diff --git a/plugins/catalog/src/components/CatalogKindHeader/index.ts b/plugins/catalog/src/components/CatalogKindHeader/index.ts new file mode 100644 index 0000000000..0078207d58 --- /dev/null +++ b/plugins/catalog/src/components/CatalogKindHeader/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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. + */ +export { CatalogKindHeader } from './CatalogKindHeader'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 27a1414f27..ec29d3839b 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -42,7 +42,7 @@ import { EntityListContainer, FilterContainer, } from '../FilteredEntityLayout'; -import { CatalogKindHeader } from './CatalogKindHeader'; +import { CatalogKindHeader } from '../CatalogKindHeader'; export type CatalogPageProps = { initiallySelectedFilter?: UserListFilterKind; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index c5cc69f609..31184204a9 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -16,6 +16,7 @@ export { CatalogClientWrapper } from './CatalogClientWrapper'; export * from './components/AboutCard'; +export * from './components/CatalogKindHeader'; export * from './components/CatalogResultListItem'; export { CatalogTable } from './components/CatalogTable'; export type { EntityRow as CatalogTableRow } from './components/CatalogTable'; From 32947cf56fd121388fdb1e88e40fd0fb6770da64 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 26 Aug 2021 10:16:11 -0600 Subject: [PATCH 10/12] Remove kind prefix on name column; case-insensitivity Signed-off-by: Tim Hansen --- .../catalog-react/src/hooks/useEntityKinds.ts | 4 ++- .../CatalogKindHeader.test.tsx | 2 +- .../CatalogKindHeader/CatalogKindHeader.tsx | 14 +++++++---- .../components/CatalogTable/CatalogTable.tsx | 25 +++++++++++-------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts index 082e4a207d..5dae9c4c7f 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ b/plugins/catalog-react/src/hooks/useEntityKinds.ts @@ -31,7 +31,9 @@ export function useEntityKinds() { .getEntities({ fields: ['kind'] }) .then(response => response.items); - return [...new Set(entities.map(e => e.kind))].sort(); + return [ + ...new Set(entities.map(e => e.kind.toLocaleLowerCase('en-US'))), + ].sort(); }); return { error, loading, kinds }; } diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index b39ad08359..bdf5b51b65 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -106,7 +106,7 @@ describe('', () => { fireEvent.click(option); expect(updateFilters).toHaveBeenCalledWith({ - kind: new EntityKindFilter('Template'), + kind: new EntityKindFilter('template'), }); }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index 89b132de53..e4318a6078 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -43,14 +43,16 @@ type CatalogKindHeaderProps = { }; export const CatalogKindHeader = ({ - initialFilter = 'Component', + initialFilter = 'component', }: CatalogKindHeaderProps) => { const classes = useStyles(); - const { kinds: allKinds } = useEntityKinds(); + const { kinds: allKinds = [] } = useEntityKinds(); const { updateFilters, queryParameters } = useEntityListProvider(); const [selectedKind, setSelectedKind] = useState( - [queryParameters.kind].flat()[0] ?? initialFilter, + ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( + 'en-US', + ), ); useEffect(() => { @@ -59,6 +61,8 @@ export const CatalogKindHeader = ({ }); }, [selectedKind, updateFilters]); + const options = [...new Set([selectedKind, ...allKinds])].sort(); + return ( diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 84c2afa0f6..c85a60c7c1 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -27,7 +27,7 @@ import { import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import { capitalize } from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import * as columnFactories from './columns'; import { EntityRow } from './types'; import { @@ -38,16 +38,6 @@ import { WarningPanel, } from '@backstage/core-components'; -const defaultColumns: TableColumn[] = [ - columnFactories.createNameColumn(), - columnFactories.createSystemColumn(), - columnFactories.createOwnerColumn(), - columnFactories.createSpecTypeColumn(), - columnFactories.createSpecLifecycleColumn(), - columnFactories.createMetadataDescriptionColumn(), - columnFactories.createTagsColumn(), -]; - type CatalogTableProps = { columns?: TableColumn[]; actions?: TableProps['actions']; @@ -57,6 +47,19 @@ export const CatalogTable = ({ columns, actions }: CatalogTableProps) => { const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); const { loading, error, entities, filters } = useEntityListProvider(); + const defaultColumns: TableColumn[] = useMemo( + () => [ + columnFactories.createNameColumn({ defaultKind: filters.kind?.value }), + columnFactories.createSystemColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createSpecTypeColumn(), + columnFactories.createSpecLifecycleColumn(), + columnFactories.createMetadataDescriptionColumn(), + columnFactories.createTagsColumn(), + ], + [filters.kind?.value], + ); + const showTypeColumn = filters.type === undefined; // TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar const titlePreamble = capitalize(filters.user?.value ?? 'all'); From e6c88df0161e44a5c53945d4e9921011c9564b84 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 26 Aug 2021 13:20:58 -0600 Subject: [PATCH 11/12] Prefer catalog kind casing Signed-off-by: Tim Hansen --- .../catalog-react/src/hooks/useEntityKinds.ts | 4 +--- .../CatalogKindHeader.test.tsx | 14 ++++++++++++++ .../CatalogKindHeader/CatalogKindHeader.tsx | 17 ++++++++++++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts index 5dae9c4c7f..082e4a207d 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ b/plugins/catalog-react/src/hooks/useEntityKinds.ts @@ -31,9 +31,7 @@ export function useEntityKinds() { .getEntities({ fields: ['kind'] }) .then(response => response.items); - return [ - ...new Set(entities.map(e => e.kind.toLocaleLowerCase('en-US'))), - ].sort(); + return [...new Set(entities.map(e => e.kind))].sort(); }); return { error, loading, kinds }; } diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index bdf5b51b65..c15c50a239 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -89,6 +89,20 @@ describe('', () => { }); }); + it('renders unknown kinds provided in query parameters', async () => { + const rendered = await renderWithEffects( + + + + + , + ); + + expect(rendered.getByText('Frobs')).toBeInTheDocument(); + }); + it('updates the kind filter', async () => { const updateFilters = jest.fn(); const rendered = await renderWithEffects( diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index e4318a6078..41c5888d79 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -61,7 +61,18 @@ export const CatalogKindHeader = ({ }); }, [selectedKind, updateFilters]); - const options = [...new Set([selectedKind, ...allKinds])].sort(); + // Before allKinds is loaded, or when a kind is entered manually in the URL, selectedKind may not + // be present in allKinds. It should still be shown in the dropdown, but may not have the nice + // enforced casing from the catalog-backend. This makes a key/value record for the Select options, + // including selectedKind if it's unknown - but allows the selectedKind to get clobbered by the + // more proper catalog kind if it exists. + const options = [capitalize(selectedKind)] + .concat(allKinds) + .sort() + .reduce((acc, kind) => { + acc[kind.toLocaleLowerCase('en-US')] = kind; + return acc; + }, {} as Record); return ( From 7febe58d6e8fcb12b856a2092802e03947937ee9 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Sat, 28 Aug 2021 22:19:51 -0600 Subject: [PATCH 12/12] Test update, JSX.Element -> ReactNode Signed-off-by: Tim Hansen --- .changeset/nasty-ads-nail.md | 2 +- .../layout/ContentHeader/ContentHeader.tsx | 4 ++-- .../EntityTypePicker.test.tsx | 21 +++++-------------- .../src/hooks/useEntityKinds.test.tsx | 4 ++-- .../CatalogKindHeader.test.tsx | 13 +++--------- 5 files changed, 13 insertions(+), 31 deletions(-) diff --git a/.changeset/nasty-ads-nail.md b/.changeset/nasty-ads-nail.md index e59a54ed84..0518b1e569 100644 --- a/.changeset/nasty-ads-nail.md +++ b/.changeset/nasty-ads-nail.md @@ -2,7 +2,7 @@ '@backstage/core-components': minor --- -Changed the `titleComponent` prop on `ContentHeader` to accept `JSX.Element` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: +Changed the `titleComponent` prop on `ContentHeader` to accept `ReactNode` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: ```diff - diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx index 5891e801e1..2259731148 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx @@ -18,7 +18,7 @@ * TODO favoriteable capability */ -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import { Typography, makeStyles } from '@material-ui/core'; import { Helmet } from 'react-helmet'; @@ -77,7 +77,7 @@ const ContentHeaderTitle = ({ type ContentHeaderProps = { title?: ContentHeaderTitleProps['title']; - titleComponent?: JSX.Element; + titleComponent?: ReactNode; description?: string; textAlign?: 'left' | 'right' | 'center'; }; diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index a9ecf03e5c..9912f3d371 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -61,22 +61,11 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.from([ - [ - catalogApiRef, - { - getEntities: jest - .fn() - .mockImplementation(() => Promise.resolve({ items: entities })), - } as unknown as CatalogApi, - ], - [ - alertApiRef, - { - post: jest.fn(), - } as unknown as AlertApi, - ], -]); +const apis = ApiRegistry.with(catalogApiRef, { + getEntities: jest.fn().mockResolvedValue({ items: entities }), +} as unknown as CatalogApi).with(alertApiRef, { + post: jest.fn(), +} as unknown as AlertApi); describe('', () => { it('renders available entity types', async () => { diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx index 96281f4a5a..d801e508a6 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx @@ -54,12 +54,12 @@ const entities: Entity[] = [ ]; const mockCatalogApi: Partial = { - getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), + getEntities: jest.fn().mockResolvedValue({ items: entities }), }; const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - + {children} ); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index c15c50a239..2730665fb6 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -58,16 +58,9 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.from([ - [ - catalogApiRef, - { - getEntities: jest - .fn() - .mockImplementation(() => Promise.resolve({ items: entities })), - } as Partial, - ], -]); +const apis = ApiRegistry.with(catalogApiRef, { + getEntities: jest.fn().mockResolvedValue({ items: entities }), +} as Partial); describe('', () => { it('renders available kinds', async () => {