From 86eac5b4583c3e2e614865c8a2ead7568fe513c1 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 18 Aug 2020 18:35:45 +0200 Subject: [PATCH 1/4] feat(catalog): add tags to entity metadata This add a field tags to the entity metadata to specifiy a user defined classifications. Closes #1997 --- .../software-catalog/descriptor-format.md | 17 +++++++++++++++++ packages/catalog-model/src/entity/Entity.ts | 6 ++++++ .../policies/FieldFormatEntityPolicy.test.ts | 8 ++++++++ .../entity/policies/FieldFormatEntityPolicy.ts | 6 ++++++ .../policies/ReservedFieldsEntityPolicy.test.ts | 3 +++ .../policies/ReservedFieldsEntityPolicy.ts | 1 + .../policies/SchemaValidEntityPolicy.test.ts | 8 ++++++++ .../entity/policies/SchemaValidEntityPolicy.ts | 1 + .../src/validation/makeValidator.ts | 1 + packages/catalog-model/src/validation/types.ts | 1 + 10 files changed, 52 insertions(+) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 4751bd88f7..a7d57890dc 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -42,6 +42,7 @@ software catalog API. "labels": { "system": "public-websites" }, + "tags": ["java"], "name": "artist-web", "uid": "2152f463-549d-4d8d-a94d-ce2b7676c6e2" }, @@ -66,6 +67,8 @@ metadata: annotations: example.com/service-discovery: artistweb circleci.com/project-slug: gh/example-org/artist-website + tags: + - java spec: type: website lifecycle: production @@ -232,6 +235,20 @@ The `backstage.io/` prefix is reserved for use by Backstage core components. Values can be of any length, but are limited to being strings. +### `tags` [optional] + +A list of single-valued strings, for example to classify catalog entities in +various ways. This is different to the labels in metadata, as labels are +key-value pairs. + +The values are user defined, for example the programming language used for the +component, like `java` or `go`. + +This field is optional, and currently has no special semantics. + +Each tag must be sequences of `[a-zA-Z0-9]` separated by `-`, at most 63 +characters in total. + ## Kind: Component Describes the following entity kind: diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 4f245da8b7..7f323043b1 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -113,6 +113,12 @@ export type EntityMeta = JsonObject & { * entity. */ annotations?: Record; + + /** + * A list of single-valued strings, to for example classify catalog entities in + * various ways. + */ + tags?: string[]; }; /** diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts index 80cf70fbab..edf04a64cc 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.test.ts @@ -35,6 +35,9 @@ describe('FieldFormatEntityPolicy', () => { backstage.io/custom: ValueStuff annotations: example.com/bindings: are-secret + tags: + - java + - data-service spec: custom: stuff `); @@ -102,4 +105,9 @@ describe('FieldFormatEntityPolicy', () => { data.metadata.annotations.a = 7; await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i); }); + + it('rejects bad tag value', async () => { + data.metadata.tags.push('Hello World'); + await expect(policy.enforce(data)).rejects.toThrow(/tags.*"Hello World"/i); + }); }); diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 4ccd8ec711..20a3724079 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -83,6 +83,12 @@ export class FieldFormatEntityPolicy implements EntityPolicy { require(`annotations.${k}`, v, this.validators.isValidAnnotationValue); } + const tags = entity.metadata.tags ?? []; + + for (let i = 0; i < tags.length; ++i) { + require(`tags.${i}`, tags[i], this.validators.isValidTag); + } + return entity; } } diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts index 04acd58686..5d4e60a1a3 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.test.ts @@ -35,6 +35,9 @@ describe('ReservedFieldsEntityPolicy', () => { backstage.io/custom: ValueStuff annotations: example.com/bindings: are-secret + tags: + - java + - data spec: custom: stuff `); diff --git a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts index bbe374e811..edb63cf05c 100644 --- a/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/ReservedFieldsEntityPolicy.ts @@ -29,6 +29,7 @@ const DEFAULT_RESERVED_ENTITY_FIELDS: string[] = [ 'metadata.description', 'metadata.labels', 'metadata.annotations', + 'metadata.tags', // The below items are known to appear in core kinds, and therefore should // not be appearing in metadata (which would indicate that the user made a // mistake in where to place them). diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts index 2113772af4..d63ba49dbe 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.test.ts @@ -36,6 +36,9 @@ describe('SchemaValidEntityPolicy', () => { backstage.io/custom: ValueStuff annotations: example.com/bindings: are-secret + tags: + - java + - data spec: custom: stuff `); @@ -190,6 +193,11 @@ describe('SchemaValidEntityPolicy', () => { await expect(policy.enforce(data)).rejects.toThrow(/annotations/); }); + it('rejects bad tags type', async () => { + data.metadata.tags = 7; + await expect(policy.enforce(data)).rejects.toThrow(/tags/); + }); + // // spec // diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index e901e82d8b..a6d376b4ac 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -31,6 +31,7 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({ description: yup.string().notRequired(), labels: yup.object>().notRequired(), annotations: yup.object>().notRequired(), + tags: yup.array().notRequired(), }) .required(), spec: yup.object({}).notRequired(), diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 7ca01365e0..3602c01a63 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -28,6 +28,7 @@ const defaultValidators: Validators = { isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, + isValidTag: CommonValidatorFunctions.isValidDnsLabel, }; export function makeValidator(overrides: Partial = {}): Validators { diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index 81209bfb75..ff00036991 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -24,4 +24,5 @@ export type Validators = { isValidLabelValue(value: any): boolean; isValidAnnotationKey(value: any): boolean; isValidAnnotationValue(value: any): boolean; + isValidTag(value: any): boolean; }; From bd6195d79f43d0d299463bf3b8cb54f55108c449 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 18 Aug 2020 18:36:05 +0200 Subject: [PATCH 2/4] test(catalog): add test-data for tags --- packages/catalog-model/examples/artist-lookup-component.yaml | 3 +++ packages/catalog-model/examples/podcast-api-component.yaml | 2 ++ packages/catalog-model/examples/queue-proxy-component.yaml | 3 +++ packages/catalog-model/examples/searcher-component.yaml | 2 ++ packages/catalog-model/examples/shuffle-api-component.yaml | 2 ++ packages/catalog-model/examples/streetlights-api.yaml | 2 ++ 6 files changed, 14 insertions(+) diff --git a/packages/catalog-model/examples/artist-lookup-component.yaml b/packages/catalog-model/examples/artist-lookup-component.yaml index 448dd13fa3..79dd3bccf6 100644 --- a/packages/catalog-model/examples/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/artist-lookup-component.yaml @@ -3,6 +3,9 @@ kind: Component metadata: name: artist-lookup description: Artist Lookup + tags: + - java + - data spec: type: service lifecycle: experimental diff --git a/packages/catalog-model/examples/podcast-api-component.yaml b/packages/catalog-model/examples/podcast-api-component.yaml index 19f9a3d4de..c1b1c9281c 100644 --- a/packages/catalog-model/examples/podcast-api-component.yaml +++ b/packages/catalog-model/examples/podcast-api-component.yaml @@ -3,6 +3,8 @@ kind: Component metadata: name: podcast-api description: Podcast API + tags: + - java spec: type: service lifecycle: experimental diff --git a/packages/catalog-model/examples/queue-proxy-component.yaml b/packages/catalog-model/examples/queue-proxy-component.yaml index a9f41b8776..c9b130db52 100644 --- a/packages/catalog-model/examples/queue-proxy-component.yaml +++ b/packages/catalog-model/examples/queue-proxy-component.yaml @@ -3,6 +3,9 @@ kind: Component metadata: name: queue-proxy description: Queue Proxy + tags: + - go + - website spec: type: website lifecycle: production diff --git a/packages/catalog-model/examples/searcher-component.yaml b/packages/catalog-model/examples/searcher-component.yaml index 33d765117c..042fcb24a9 100644 --- a/packages/catalog-model/examples/searcher-component.yaml +++ b/packages/catalog-model/examples/searcher-component.yaml @@ -3,6 +3,8 @@ kind: Component metadata: name: searcher description: Searcher + tags: + - go spec: type: service lifecycle: production diff --git a/packages/catalog-model/examples/shuffle-api-component.yaml b/packages/catalog-model/examples/shuffle-api-component.yaml index 275c48d6ba..1f9de46d4e 100644 --- a/packages/catalog-model/examples/shuffle-api-component.yaml +++ b/packages/catalog-model/examples/shuffle-api-component.yaml @@ -3,6 +3,8 @@ kind: Component metadata: name: shuffle-api description: Shuffle API + tags: + - go spec: type: service lifecycle: production diff --git a/packages/catalog-model/examples/streetlights-api.yaml b/packages/catalog-model/examples/streetlights-api.yaml index 69a6d101cb..4aeef993bb 100644 --- a/packages/catalog-model/examples/streetlights-api.yaml +++ b/packages/catalog-model/examples/streetlights-api.yaml @@ -3,6 +3,8 @@ kind: API metadata: name: streetlights description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + tags: + - unstable spec: type: asyncapi definition: | From f06588def71fd6abd5e1ad3a766460b501e5179c Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 18 Aug 2020 19:52:49 +0200 Subject: [PATCH 3/4] feat(catalog): show entity tags in the catalog table --- packages/core/src/components/Table/Table.tsx | 3 ++- .../components/CatalogTable/CatalogTable.tsx | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index d200b07bc6..882272cb43 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -127,7 +127,8 @@ function convertColumns( ): TableColumn[] { return columns.map(column => { const headerStyle: React.CSSProperties = {}; - const cellStyle: React.CSSProperties = {}; + const cellStyle: React.CSSProperties = + typeof column.cellStyle === 'object' ? column.cellStyle : {}; if (column.highlight) { headerStyle.color = theme.palette.textContrast; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index b5afbcaa0b..00dacdc0fe 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -15,7 +15,7 @@ */ import { Entity, LocationSpec } from '@backstage/catalog-model'; import { Table, TableColumn, TableProps } from '@backstage/core'; -import { Link } from '@material-ui/core'; +import { Link, Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; import { Alert } from '@material-ui/lab'; @@ -63,6 +63,21 @@ const columns: TableColumn[] = [ title: 'Description', field: 'metadata.description', }, + { + title: 'Tags', + field: 'metadata.tags', + cellStyle: { + padding: '0px 16px 0px 20px', + }, + render: (entity: Entity) => ( + <> + {entity.metadata.tags && + entity.metadata.tags.map(t => ( + + ))} + + ), + }, ]; type CatalogTableProps = { From 5e6f1c424839aee432b5cede42313619a0eecd33 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 18 Aug 2020 21:57:37 +0200 Subject: [PATCH 4/4] feat(catalog): add filter by tags in the catalog --- .../components/CatalogPage/CatalogPage.tsx | 9 +- .../ResultsFilter/ResultsFilter.test.tsx | 102 +++++++++++++++ .../ResultsFilter/ResultsFilter.tsx | 120 ++++++++++++++++++ .../src/filter/EntityFilterGroupsProvider.tsx | 39 ++++++ plugins/catalog/src/filter/context.ts | 2 + .../catalog/src/filter/useFilteredEntities.ts | 1 + 6 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 plugins/catalog/src/components/ResultsFilter/ResultsFilter.test.tsx create mode 100644 plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index e58b2d407b..6a3d7362aa 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -35,6 +35,7 @@ import { CatalogTable } from '../CatalogTable/CatalogTable'; import CatalogLayout from './CatalogLayout'; import { CatalogTabs, LabeledComponentType } from './CatalogTabs'; import { WelcomeBanner } from './WelcomeBanner'; +import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -47,7 +48,12 @@ const useStyles = makeStyles(theme => ({ const CatalogPageContents = () => { const styles = useStyles(); - const { loading, error, matchingEntities } = useFilteredEntities(); + const { + loading, + error, + matchingEntities, + availableTags, + } = useFilteredEntities(); const { isStarredEntity } = useStarredEntities(); const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); @@ -140,6 +146,7 @@ const CatalogPageContents = () => { onChange={({ label }) => setSelectedSidebarItem(label)} initiallySelected="owned" /> + { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity1', + tags: ['java'], + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity2', + }, + spec: { + owner: 'not-tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity3', + tags: ['java', 'test'], + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + ] as Entity[]), + }; + + const indentityApi: Partial = { + getUserId: () => 'tools@example.com', + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children}, + , + ), + ); + + it('should render all available tags', async () => { + const tags = ['test', 'java']; + const { findByText } = renderWrapped( + , + ); + for (const tag of tags) { + expect(await findByText(tag)).toBeInTheDocument(); + } + }); +}); diff --git a/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx new file mode 100644 index 0000000000..eb500da651 --- /dev/null +++ b/plugins/catalog/src/components/ResultsFilter/ResultsFilter.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + Button, + Checkbox, + Divider, + List, + ListItem, + ListItemText, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; +import React, { useCallback, useContext, useState } from 'react'; +import { filterGroupsContext } from '../../filter/context'; + +const useStyles = makeStyles(theme => ({ + filterBox: { + display: 'flex', + margin: theme.spacing(2, 0, 0, 0), + }, + filterBoxTitle: { + margin: theme.spacing(1, 0, 0, 1), + fontWeight: 'bold', + flex: 1, + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + checkbox: { + padding: theme.spacing(0, 1, 0, 1), + }, +})); + +type Props = { + availableTags: string[]; +}; + +/** + * The additional results filter in the sidebar. + */ +export const ResultsFilter = ({ availableTags }: Props) => { + const classes = useStyles(); + + const [selectedTags, setSelectedTags] = useState([]); + const context = useContext(filterGroupsContext); + if (!context) { + throw new Error(`Must be used inside an EntityFilterGroupsProvider`); + } + const setSelectedTagsFilter = context?.setSelectedTags; + + const updateSelectedTags = useCallback( + (tags: string[]) => { + setSelectedTags(tags); + setSelectedTagsFilter(tags); + }, + [setSelectedTags, setSelectedTagsFilter], + ); + + return ( + <> +
+ + Refine Results + {' '} + +
+ + + Tags + + + {availableTags.map(t => { + const labelId = `checkbox-list-label-${t}`; + return ( + + updateSelectedTags( + selectedTags.includes(t) + ? selectedTags.filter(s => s !== t) + : [...selectedTags, t], + ) + } + > + + + + ); + })} + + + ); +}; diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx index dd557a4322..db8e21e071 100644 --- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx +++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx @@ -54,16 +54,19 @@ function useProvideEntityFilters(): FilterGroupsContext { const selectedFilterKeys = useRef<{ [filterGroupId: string]: Set; }>({}); + const selectedTags = useRef([]); const [filterGroupStates, setFilterGroupStates] = useState<{ [filterGroupId: string]: FilterGroupStates; }>({}); const [matchingEntities, setMatchingEntities] = useState([]); + const [availableTags, setAvailableTags] = useState([]); const rebuild = useCallback(() => { setFilterGroupStates( buildStates( filterGroups.current, selectedFilterKeys.current, + selectedTags.current, entities, error, ), @@ -72,9 +75,11 @@ function useProvideEntityFilters(): FilterGroupsContext { buildMatchingEntities( filterGroups.current, selectedFilterKeys.current, + selectedTags.current, entities, ), ); + setAvailableTags(collectTags(entities)); }, [entities, error]); const register = useCallback( @@ -109,14 +114,24 @@ function useProvideEntityFilters(): FilterGroupsContext { [rebuild], ); + const setSelectedTags = useCallback( + (tags: string[]) => { + selectedTags.current = tags; + rebuild(); + }, + [rebuild], + ); + return { register, unregister, setGroupSelectedFilters, + setSelectedTags, loading: !error && !entities, error, filterGroupStates, matchingEntities, + availableTags, }; } @@ -125,6 +140,7 @@ function useProvideEntityFilters(): FilterGroupsContext { function buildStates( filterGroups: { [filterGroupId: string]: FilterGroup }, selectedFilterKeys: { [filterGroupId: string]: Set }, + selectedTags: string[], entities?: Entity[], error?: Error, ): { [filterGroupId: string]: FilterGroupStates } { @@ -153,6 +169,7 @@ function buildStates( const otherMatchingEntities = buildMatchingEntities( filterGroups, selectedFilterKeys, + selectedTags, entities, filterGroupId, ); @@ -170,11 +187,23 @@ function buildStates( return result; } +// Given all entites, find all possible tags and provide them in a sorted list. +function collectTags(entities?: Entity[]): string[] { + const tags = new Set(); + (entities || []).forEach(e => { + if (e.metadata.tags) { + e.metadata.tags.forEach(t => tags.add(t)); + } + }); + return Array.from(tags).sort(); +} + // Given all filter groups and what filters are actually selected, extract all // entities that match all those filter groups. function buildMatchingEntities( filterGroups: { [filterGroupId: string]: FilterGroup }, selectedFilterKeys: { [filterGroupId: string]: Set }, + selectedTags: string[], entities?: Entity[], excludeFilterGroupId?: string, ): Entity[] { @@ -201,6 +230,16 @@ function buildMatchingEntities( } } + // Filter by tags, if at least one tag is selected. Include all entities + // that have at least one of the selected tags + if (selectedTags.length > 0) { + allFilters.push( + entity => + !!entity.metadata.tags && + entity.metadata.tags.some(t => selectedTags.includes(t)), + ); + } + // All filter groups that had any checked filters need to match. Note that // every() always returns true for an empty array. return entities?.filter(entity => allFilters.every(fn => fn(entity))) ?? []; diff --git a/plugins/catalog/src/filter/context.ts b/plugins/catalog/src/filter/context.ts index 31e9c91b97..7f16107f09 100644 --- a/plugins/catalog/src/filter/context.ts +++ b/plugins/catalog/src/filter/context.ts @@ -26,10 +26,12 @@ export type FilterGroupsContext = { ) => void; unregister: (filterGroupId: string) => void; setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; + setSelectedTags: (tags: string[]) => void; loading: boolean; error?: Error; filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; matchingEntities: Entity[]; + availableTags: string[]; }; /** diff --git a/plugins/catalog/src/filter/useFilteredEntities.ts b/plugins/catalog/src/filter/useFilteredEntities.ts index 256f5247c7..3f70c0bd85 100644 --- a/plugins/catalog/src/filter/useFilteredEntities.ts +++ b/plugins/catalog/src/filter/useFilteredEntities.ts @@ -30,5 +30,6 @@ export function useFilteredEntities() { loading: context.loading, error: context.error, matchingEntities: context.matchingEntities, + availableTags: context.availableTags, }; }