diff --git a/.changeset/brave-bags-sniff.md b/.changeset/brave-bags-sniff.md
new file mode 100644
index 0000000000..3fa542a4e3
--- /dev/null
+++ b/.changeset/brave-bags-sniff.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-catalog': minor
+---
+
+Fixes in kind selectors (now OwnershipCards work again)
+EntityKindPicker now accepts an optional allowedKinds prop, just like CatalogKindHeader.
diff --git a/.changeset/neat-lies-know.md b/.changeset/neat-lies-know.md
new file mode 100644
index 0000000000..01fc002668
--- /dev/null
+++ b/.changeset/neat-lies-know.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Cleanup and small fixes for the kind selector
diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt
index bba59094e3..e05f1acd91 100644
--- a/.github/vale/Vocab/Backstage/accept.txt
+++ b/.github/vale/Vocab/Backstage/accept.txt
@@ -9,6 +9,7 @@ Airbrake
Airbrakes
Alaria
Alef
+allowedKinds
Anddddd
Apdex
api
diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md
index e8f0b0e6ee..92bfc4de96 100644
--- a/plugins/catalog-react/api-report.md
+++ b/plugins/catalog-react/api-report.md
@@ -175,6 +175,7 @@ export const EntityKindPicker: (
// @public
export interface EntityKindPickerProps {
+ allowedKinds?: string[];
// (undocumented)
hidden?: boolean;
// (undocumented)
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
index 8a6b7206c6..04d8df3d16 100644
--- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
@@ -147,37 +147,56 @@ describe('', () => {
});
});
- it('responds to external queryParameters changes', async () => {
- const updateFilters = jest.fn();
- const rendered = await renderWithEffects(
+ it('renders unknown kinds provided in query parameters', async () => {
+ await renderWithEffects(
,
);
- expect(updateFilters).toHaveBeenLastCalledWith({
- kind: new EntityKindFilter('component'),
- });
- rendered.rerender(
+
+ expect(screen.getByText('FROb')).toBeInTheDocument();
+ });
+
+ it('limits kinds when allowedKinds is set', async () => {
+ await renderWithEffects(
-
-
+
+
,
);
- expect(updateFilters).toHaveBeenLastCalledWith({
- kind: new EntityKindFilter('domain'),
- });
+
+ const input = screen.getByTestId('select');
+ fireEvent.click(input);
+
+ expect(
+ screen.getByRole('option', { name: 'Component' }),
+ ).toBeInTheDocument();
+ expect(screen.getByRole('option', { name: 'Domain' })).toBeInTheDocument();
+ expect(
+ screen.queryByRole('option', { name: 'Template' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders kind from the query parameter even when not in allowedKinds', async () => {
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText('Frob')).toBeInTheDocument();
+
+ const input = screen.getByTestId('select');
+ fireEvent.click(input);
+ expect(screen.getByRole('option', { name: 'Domain' })).toBeInTheDocument();
});
});
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx
index c626a68d10..65ec949b4a 100644
--- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx
+++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx
@@ -17,64 +17,15 @@
import { Select } from '@backstage/core-components';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { Box } from '@material-ui/core';
-import capitalize from 'lodash/capitalize';
-import sortBy from 'lodash/sortBy';
-import React, { useEffect, useMemo, useRef, useState } from 'react';
-import useAsync from 'react-use/lib/useAsync';
-import { catalogApiRef } from '../../api';
+import React, { useEffect, useMemo, useState } from 'react';
import { EntityKindFilter } from '../../filters';
import { useEntityList } from '../../hooks';
-
-function useAvailableKinds() {
- const catalogApi = useApi(catalogApiRef);
-
- const [availableKinds, setAvailableKinds] = useState([]);
-
- const {
- error,
- loading,
- value: facets,
- } = useAsync(async () => {
- const facet = 'kind';
- const items = await catalogApi
- .getEntityFacets({
- facets: [facet],
- })
- .then(response => response.facets[facet] || []);
-
- return items;
- }, [catalogApi]);
-
- const facetsRef = useRef(facets);
- useEffect(() => {
- const oldFacets = facetsRef.current;
- facetsRef.current = facets;
- // Delay processing hook until facets load updates have settled to generate list of kinds;
- // This prevents resetting the kind filter due to saved kind value from query params not matching the
- // empty set of kind values while values are still being loaded; also only run this hook on changes
- // to facets
- if (loading || oldFacets === facets || !facets) {
- return;
- }
-
- const newKinds = [
- ...new Set(
- sortBy(facets, f => f.value).map(f =>
- f.value.toLocaleLowerCase('en-US'),
- ),
- ),
- ];
-
- setAvailableKinds(newKinds);
- }, [loading, facets, setAvailableKinds]);
-
- return { loading, error, availableKinds };
-}
+import { filterKinds, useAllKinds } from './kindFilterUtils';
function useEntityKindFilter(opts: { initialFilter: string }): {
loading: boolean;
error?: Error;
- availableKinds: string[];
+ allKinds: string[];
selectedKind: string;
setSelectedKind: (kind: string) => void;
} {
@@ -84,22 +35,22 @@ function useEntityKindFilter(opts: { initialFilter: string }): {
updateFilters,
} = useEntityList();
- const flattenedQueryKind = useMemo(
+ const queryParamKind = useMemo(
() => [kindParameter].flat()[0],
[kindParameter],
);
const [selectedKind, setSelectedKind] = useState(
- flattenedQueryKind ?? filters.kind?.value ?? opts.initialFilter,
+ queryParamKind ?? filters.kind?.value ?? opts.initialFilter,
);
// Set selected kinds on query parameter updates; this happens at initial page load and from
// external updates to the page location.
useEffect(() => {
- if (flattenedQueryKind) {
- setSelectedKind(flattenedQueryKind);
+ if (queryParamKind) {
+ setSelectedKind(queryParamKind);
}
- }, [flattenedQueryKind]);
+ }, [queryParamKind]);
// Set selected kind from filters; this happens when the kind filter is
// updated from another component
@@ -109,18 +60,18 @@ function useEntityKindFilter(opts: { initialFilter: string }): {
}
}, [filters.kind]);
- const { availableKinds, loading, error } = useAvailableKinds();
-
useEffect(() => {
updateFilters({
kind: selectedKind ? new EntityKindFilter(selectedKind) : undefined,
});
}, [selectedKind, updateFilters]);
+ const { allKinds, loading, error } = useAllKinds();
+
return {
loading,
error,
- availableKinds,
+ allKinds: allKinds ?? [],
selectedKind,
setSelectedKind,
};
@@ -132,17 +83,22 @@ function useEntityKindFilter(opts: { initialFilter: string }): {
* @public
*/
export interface EntityKindPickerProps {
+ /**
+ * Entity kinds to show in the dropdown; by default all kinds are fetched from the catalog and
+ * displayed.
+ */
+ allowedKinds?: string[];
initialFilter?: string;
hidden?: boolean;
}
/** @public */
export const EntityKindPicker = (props: EntityKindPickerProps) => {
- const { hidden, initialFilter = 'component' } = props;
+ const { allowedKinds, hidden, initialFilter = 'component' } = props;
const alertApi = useApi(alertApiRef);
- const { error, availableKinds, selectedKind, setSelectedKind } =
+ const { error, allKinds, selectedKind, setSelectedKind } =
useEntityKindFilter({
initialFilter: initialFilter,
});
@@ -156,21 +112,21 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => {
}
}, [error, alertApi]);
- if (availableKinds?.length === 0 || error) return null;
+ if (error) return null;
- const items = [
- ...availableKinds.map((kind: string) => ({
- value: kind,
- label: capitalize(kind),
- })),
- ];
+ const options = filterKinds(allKinds, allowedKinds, selectedKind);
+
+ const items = Object.keys(options).map(key => ({
+ value: key,
+ label: options[key],
+ }));
return hidden ? null : (
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/kindFilterUtils.ts b/plugins/catalog-react/src/components/EntityKindPicker/kindFilterUtils.ts
new file mode 100644
index 0000000000..aed4c41295
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityKindPicker/kindFilterUtils.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 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 { useApi } from '@backstage/core-plugin-api';
+import useAsync from 'react-use/lib/useAsync';
+import { catalogApiRef } from '../../api';
+
+/**
+ * Fetch and return all availible kinds.
+ */
+export function useAllKinds(): {
+ loading: boolean;
+ error?: Error;
+ allKinds: string[];
+} {
+ const catalogApi = useApi(catalogApiRef);
+
+ const {
+ error,
+ loading,
+ value: allKinds,
+ } = useAsync(async () => {
+ const items = await catalogApi
+ .getEntityFacets({ facets: ['kind'] })
+ .then(response => response.facets.kind?.map(f => f.value).sort() || []);
+ return items;
+ }, [catalogApi]);
+
+ return { loading, error, allKinds: allKinds ?? [] };
+}
+
+/**
+ * Filter and capitalize accessible kinds.
+ */
+export function filterKinds(
+ allKinds: string[],
+ allowedKinds?: string[],
+ forcedKinds?: string,
+): Record {
+ // 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.
+ let availableKinds = allKinds;
+ if (allowedKinds) {
+ availableKinds = availableKinds.filter(k =>
+ allowedKinds.some(
+ a => a.toLocaleLowerCase('en-US') === k.toLocaleLowerCase('en-US'),
+ ),
+ );
+ }
+ if (
+ forcedKinds &&
+ !allKinds.some(
+ a =>
+ a.toLocaleLowerCase('en-US') === forcedKinds.toLocaleLowerCase('en-US'),
+ )
+ ) {
+ availableKinds = availableKinds.concat([forcedKinds]);
+ }
+
+ const kindsMap = availableKinds.sort().reduce((acc, kind) => {
+ acc[kind.toLocaleLowerCase('en-US')] = kind;
+ return acc;
+ }, {} as Record);
+
+ return kindsMap;
+}
diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx
index d8c89e0858..b77c7f5619 100644
--- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx
+++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx
@@ -166,7 +166,7 @@ describe('', () => {
location: `/catalog?${query}`,
},
});
- await waitFor(() => !!result.current.queryParameters);
+ await act(() => waitFor(() => !!result.current.queryParameters));
expect(result.current.queryParameters).toEqual({
kind: 'component',
type: 'service',
diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx
index f511f58c58..d6a1fb433a 100644
--- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx
+++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx
@@ -15,7 +15,7 @@
*/
import React from 'react';
-import { fireEvent, screen } from '@testing-library/react';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
@@ -106,14 +106,14 @@ describe('', () => {
await renderWithEffects(
,
);
- expect(screen.getByText('Frobs')).toBeInTheDocument();
+ expect(screen.getByText('FRObs')).toBeInTheDocument();
});
it('updates the kind filter', async () => {
@@ -166,9 +166,11 @@ describe('', () => {
,
);
- expect(updateFilters).toHaveBeenLastCalledWith({
- kind: new EntityKindFilter('template'),
- });
+ await waitFor(() =>
+ expect(updateFilters).toHaveBeenLastCalledWith({
+ kind: new EntityKindFilter('template'),
+ }),
+ );
});
it('limits kinds when allowedKinds is set', async () => {
diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx
index 67631a3b98..1455c2a182 100644
--- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx
+++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx
@@ -16,7 +16,6 @@
import React, { useEffect, useState, useMemo } from 'react';
import {
- capitalize,
createStyles,
InputBase,
makeStyles,
@@ -25,13 +24,11 @@ import {
Theme,
} from '@material-ui/core';
import {
- catalogApiRef,
EntityKindFilter,
useEntityList,
} from '@backstage/plugin-catalog-react';
-import useAsync from 'react-use/lib/useAsync';
-import { useApi } from '@backstage/core-plugin-api';
import pluralize from 'pluralize';
+import { filterKinds, useAllKinds } from './kindFilterUtils';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -66,12 +63,7 @@ export interface CatalogKindHeaderProps {
export function CatalogKindHeader(props: CatalogKindHeaderProps) {
const { initialFilter = 'component', allowedKinds } = props;
const classes = useStyles();
- const catalogApi = useApi(catalogApiRef);
- const { value: allKinds } = useAsync(async () => {
- return await catalogApi
- .getEntityFacets({ facets: ['kind'] })
- .then(response => response.facets.kind?.map(f => f.value).sort() || []);
- });
+ const { allKinds } = useAllKinds();
const {
filters,
updateFilters,
@@ -79,13 +71,22 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) {
} = useEntityList();
const queryParamKind = useMemo(
- () => [kindParameter].flat()[0]?.toLocaleLowerCase('en-US'),
+ () => [kindParameter].flat()[0],
[kindParameter],
);
+
const [selectedKind, setSelectedKind] = useState(
- queryParamKind ?? initialFilter,
+ queryParamKind ?? filters.kind?.value ?? initialFilter,
);
+ // Set selected kinds on query parameter updates; this happens at initial page load and from
+ // external updates to the page location.
+ useEffect(() => {
+ if (queryParamKind) {
+ setSelectedKind(queryParamKind);
+ }
+ }, [queryParamKind]);
+
// Set selected kind from filters; this happens when the kind filter is
// updated from another component
useEffect(() => {
@@ -100,37 +101,12 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) {
});
}, [selectedKind, updateFilters]);
- // Set selected Kind on query parameter updates; this happens at initial page load and from
- // external updates to the page location.
- useEffect(() => {
- if (queryParamKind) {
- setSelectedKind(queryParamKind);
- }
- }, [queryParamKind]);
-
- // 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 availableKinds = [capitalize(selectedKind)].concat(
- allKinds?.filter(k =>
- allowedKinds
- ? allowedKinds.some(
- a => a.toLocaleLowerCase('en-US') === k.toLocaleLowerCase('en-US'),
- )
- : true,
- ) ?? [],
- );
- const options = availableKinds.sort().reduce((acc, kind) => {
- acc[kind.toLocaleLowerCase('en-US')] = kind;
- return acc;
- }, {} as Record);
+ const options = filterKinds(allKinds, allowedKinds, selectedKind);
return (
}
- value={selectedKind}
+ input={}
+ value={selectedKind.toLocaleLowerCase('en-US')}
onChange={e => setSelectedKind(e.target.value as string)}
classes={classes}
>
diff --git a/plugins/catalog/src/components/CatalogKindHeader/kindFilterUtils.ts b/plugins/catalog/src/components/CatalogKindHeader/kindFilterUtils.ts
new file mode 100644
index 0000000000..4f74d10a6c
--- /dev/null
+++ b/plugins/catalog/src/components/CatalogKindHeader/kindFilterUtils.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 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 { useApi } from '@backstage/core-plugin-api';
+import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import useAsync from 'react-use/lib/useAsync';
+
+/**
+ * Fetch and return all availible kinds.
+ */
+export function useAllKinds(): {
+ loading: boolean;
+ error?: Error;
+ allKinds: string[];
+} {
+ const catalogApi = useApi(catalogApiRef);
+
+ const {
+ error,
+ loading,
+ value: allKinds,
+ } = useAsync(async () => {
+ const items = await catalogApi
+ .getEntityFacets({ facets: ['kind'] })
+ .then(response => response.facets.kind?.map(f => f.value).sort() || []);
+ return items;
+ }, [catalogApi]);
+
+ return { loading, error, allKinds: allKinds ?? [] };
+}
+
+/**
+ * Filter and capitalize accessible kinds.
+ */
+export function filterKinds(
+ allKinds: string[],
+ allowedKinds?: string[],
+ forcedKinds?: string,
+): Record {
+ // 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.
+ let availableKinds = allKinds;
+ if (allowedKinds) {
+ availableKinds = availableKinds.filter(k =>
+ allowedKinds.some(
+ a => a.toLocaleLowerCase('en-US') === k.toLocaleLowerCase('en-US'),
+ ),
+ );
+ }
+ if (
+ forcedKinds &&
+ !allKinds.some(
+ a =>
+ a.toLocaleLowerCase('en-US') === forcedKinds.toLocaleLowerCase('en-US'),
+ )
+ ) {
+ availableKinds = availableKinds.concat([forcedKinds]);
+ }
+
+ const kindsMap = availableKinds.sort().reduce((acc, kind) => {
+ acc[kind.toLocaleLowerCase('en-US')] = kind;
+ return acc;
+ }, {} as Record);
+
+ return kindsMap;
+}