From fbe180aa874f36b4f8c24d47c06c2b5aba435879 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 4 Jun 2021 18:16:05 +0100 Subject: [PATCH 01/18] Add support for only filtering available options in the UserListPicker Signed-off-by: Mike Lewis Co-authored-by: Tim Hansen Co-authored-by: Himanshu Mishra --- .../UserListPicker/UserListPicker.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 52127a63bc..65ccb423da 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -106,13 +106,27 @@ function getFilterGroups(orgName: string | undefined): ButtonGroup[] { type UserListPickerProps = { initialFilter?: UserListFilterKind; + availableFilters?: UserListFilterKind[]; }; -export const UserListPicker = ({ initialFilter }: UserListPickerProps) => { +export const UserListPicker = ({ + initialFilter, + availableFilters, +}: UserListPickerProps) => { const classes = useStyles(); const configApi = useApi(configApiRef); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; - const filterGroups = getFilterGroups(orgName); + + // Remove group items that aren't in availableFilters and exclude + // any now-empty groups. + const filterGroups = getFilterGroups(orgName) + .map(filterGroup => ({ + ...filterGroup, + items: filterGroup.items.filter( + ({ id }) => !availableFilters || availableFilters.includes(id), + ), + })) + .filter(({ items }) => !!items.length); const { value: user } = useOwnUser(); const { isStarredEntity } = useStarredEntities(); From f3a53bf04631c654028daaf4afa712d70a7d8fbb Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 4 Jun 2021 18:18:21 +0100 Subject: [PATCH 02/18] Start working on refactoring the ScaffolderPage to use the useEntityListProvider hook Signed-off-by: Mike Lewis Co-authored-by: Tim Hansen Co-authored-by: Himanshu Mishra --- .../src/hooks/useEntityListProvider.tsx | 3 + .../ScaffolderPage/ScaffolderPage.tsx | 103 +++++++----------- 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index a6802a6d87..13948b4331 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -113,6 +113,9 @@ export const EntityListProvider = ({ compact(Object.values(outputState.appliedFilters)), ); + // TODO(mtlewis): currently entities will never be requested unless + // there's at least one filter, we should allow an initial request + // to happen with no filters. if (!isEqual(previousBackendFilter, backendFilter)) { // TODO(timbonicus): should limit fields here, but would need filter // fields + table columns diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 1d16e50568..f1e47a1879 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,22 +14,11 @@ * limitations under the License. */ -import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { useStarredEntities } from '@backstage/plugin-catalog-react'; -import { Button, Link, makeStyles, Typography } from '@material-ui/core'; -import StarIcon from '@material-ui/icons/Star'; -import React, { useEffect, useMemo, useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; -import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; -import { registerComponentRouteRef } from '../../routes'; -import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; -import { ScaffolderFilter } from '../ScaffolderFilter'; -import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter'; -import SearchToolbar from '../SearchToolbar/SearchToolbar'; -import { TemplateCard } from '../TemplateCard'; - -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; - +import { + Entity, + EntityMeta, + TemplateEntityV1alpha1, +} from '@backstage/catalog-model'; import { Content, ContentHeader, @@ -41,6 +30,20 @@ import { SupportButton, WarningPanel, } from '@backstage/core-components'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { + EntityKindPicker, + EntityListProvider, + EntityTypePicker, + useEntityListProvider, + UserListPicker, +} from '@backstage/plugin-catalog-react'; +import { Button, Link, makeStyles, Typography } from '@material-ui/core'; +import React, { useEffect, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { registerComponentRouteRef } from '../../routes'; +import SearchToolbar from '../SearchToolbar/SearchToolbar'; +import { TemplateCard } from '../TemplateCard'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -53,45 +56,10 @@ const useStyles = makeStyles(theme => ({ export const ScaffolderPageContents = () => { const styles = useStyles(); - const { - loading, - error, - filteredEntities, - availableCategories, - } = useFilteredEntities(); - const configApi = useApi(configApiRef); - const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; - const { isStarredEntity } = useStarredEntities(); - const filterGroups = useMemo( - () => [ - { - name: orgName, - items: [ - { - id: 'all', - label: 'All', - filterFn: () => true, - }, - ], - }, - { - name: 'Personal', - items: [ - { - id: 'starred', - label: 'Starred', - icon: StarIcon, - filterFn: isStarredEntity, - }, - ], - }, - ], - [isStarredEntity, orgName], - ); + const { loading, error, entities } = useEntityListProvider(); + const [search, setSearch] = useState(''); - const [matchingEntities, setMatchingEntities] = useState( - [] as TemplateEntityV1alpha1[], - ); + const [matchingEntities, setMatchingEntities] = useState([] as Entity[]); const matchesQuery = (metadata: EntityMeta, query: string) => `${metadata.title}`.toLocaleUpperCase('en-US').includes(query) || @@ -101,14 +69,14 @@ export const ScaffolderPageContents = () => { useEffect(() => { if (search.length === 0) { - return setMatchingEntities(filteredEntities); + return setMatchingEntities(entities); } return setMatchingEntities( - filteredEntities.filter(template => + entities.filter(template => matchesQuery(template.metadata, search.toLocaleUpperCase('en-US')), ), ); - }, [search, filteredEntities]); + }, [search, entities]); return ( @@ -142,14 +110,21 @@ export const ScaffolderPageContents = () => {
+ {/* TODO(mtlewis) extract SearchToolbar as a frontend filter */} -
+ {/* TODO(mtlewis) figure out flash of error state when entities are loading */} + {/* TODO(mtlewis) move loading, error handling etc. inside card list */} {loading && } {error && ( @@ -177,7 +152,7 @@ export const ScaffolderPageContents = () => { matchingEntities.map((template, i) => ( ))} @@ -190,7 +165,7 @@ export const ScaffolderPageContents = () => { }; export const ScaffolderPage = () => ( - + - + ); From bc2c35b2e13223e4799e3ef745f50838a5500980 Mon Sep 17 00:00:00 2001 From: Chase Rutherford-Jenkins Date: Fri, 11 Jun 2021 14:39:14 -0700 Subject: [PATCH 03/18] move search logic into SearchToolbar Co-authored-by: Tim Signed-off-by: Chase Rutherford-Jenkins --- .../src/hooks/useEntityListProvider.tsx | 2 + plugins/catalog-react/src/types.ts | 19 +++++++ .../ScaffolderPage/ScaffolderPage.tsx | 56 +++++-------------- .../SearchToolbar/SearchToolbar.test.tsx | 48 +++++++++++++++- .../SearchToolbar/SearchToolbar.tsx | 30 ++++++---- 5 files changed, 101 insertions(+), 54 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 13948b4331..525ce0a146 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -31,6 +31,7 @@ import { EntityLifecycleFilter, EntityOwnerFilter, EntityTagFilter, + EntityTextFilter, EntityTypeFilter, UserListFilter, } from '../types'; @@ -44,6 +45,7 @@ export type DefaultEntityFilters = { owners?: EntityOwnerFilter; lifecycles?: EntityLifecycleFilter; tags?: EntityTagFilter; + text?: EntityTextFilter; }; export type EntityListContextProps< diff --git a/plugins/catalog-react/src/types.ts b/plugins/catalog-react/src/types.ts index 7932e4b735..a41e08024a 100644 --- a/plugins/catalog-react/src/types.ts +++ b/plugins/catalog-react/src/types.ts @@ -66,6 +66,25 @@ export class EntityTagFilter implements EntityFilter { } } +// TODO(chaseajen): add unit test for logic +export class EntityTextFilter implements EntityFilter { + constructor(readonly value: string) {} + + filterEntity(entity: Entity): boolean { + const upperCaseValue = this.value.toLocaleUpperCase('en-US'); + + return ( + `${entity.metadata.title}` + .toLocaleUpperCase('en-US') + .includes(upperCaseValue) || + entity.metadata.tags + ?.join('') + .toLocaleUpperCase('en-US') + .indexOf(upperCaseValue) !== -1 + ); + } +} + export class EntityOwnerFilter implements EntityFilter { constructor(readonly values: string[]) {} diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index f1e47a1879..f8043791ee 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - Entity, - EntityMeta, - TemplateEntityV1alpha1, -} from '@backstage/catalog-model'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, ContentHeader, @@ -39,7 +35,7 @@ import { UserListPicker, } from '@backstage/plugin-catalog-react'; import { Button, Link, makeStyles, Typography } from '@material-ui/core'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { registerComponentRouteRef } from '../../routes'; import SearchToolbar from '../SearchToolbar/SearchToolbar'; @@ -58,26 +54,8 @@ export const ScaffolderPageContents = () => { const styles = useStyles(); const { loading, error, entities } = useEntityListProvider(); - const [search, setSearch] = useState(''); - const [matchingEntities, setMatchingEntities] = useState([] as Entity[]); - - const matchesQuery = (metadata: EntityMeta, query: string) => - `${metadata.title}`.toLocaleUpperCase('en-US').includes(query) || - metadata.tags?.join('').toLocaleUpperCase('en-US').indexOf(query) !== -1; - const registerComponentLink = useRouteRef(registerComponentRouteRef); - useEffect(() => { - if (search.length === 0) { - return setMatchingEntities(entities); - } - return setMatchingEntities( - entities.filter(template => - matchesQuery(template.metadata, search.toLocaleUpperCase('en-US')), - ), - ); - }, [search, entities]); - return (
{
- {/* TODO(mtlewis) extract SearchToolbar as a frontend filter */} - +
diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx new file mode 100644 index 0000000000..4b9a7030d9 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -0,0 +1,64 @@ +/* + * 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 from 'react'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { + ItemCardGrid, + Progress, + WarningPanel, +} from '@backstage/core-components'; +import { useEntityListProvider } from '@backstage/plugin-catalog-react'; +import { Link, Typography } from '@material-ui/core'; +import { TemplateCard } from '../TemplateCard'; + +export const TemplateList = () => { + const { loading, error, entities } = useEntityListProvider(); + return ( + <> + {/* TODO(mtlewis) figure out flash of error state when entities are loading */} + {loading && } + + {error && ( + + {error.message} + + )} + + {!error && !loading && entities && !entities.length && ( + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + )} + + + {entities && + entities?.length > 0 && + entities.map((template, i) => ( + + ))} + + + ); +}; diff --git a/plugins/scaffolder/src/components/TemplateList/index.ts b/plugins/scaffolder/src/components/TemplateList/index.ts new file mode 100644 index 0000000000..b9ec700d74 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateList/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 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 { TemplateList } from './TemplateList'; From d5b3c9c7ecbb63d59b0bb14a3ebdd5e2c02659d9 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 24 Jun 2021 20:43:10 -0600 Subject: [PATCH 08/18] Set useEntityListProvider initial loading state Signed-off-by: Tim Hansen --- .../src/hooks/useEntityListProvider.tsx | 60 ++++++++++--------- .../components/TemplateList/TemplateList.tsx | 3 +- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 1b96b1f759..0aa52d2227 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -107,34 +107,40 @@ export const EntityListProvider = ({ // The main async filter worker. Note that while it has a lot of dependencies // in terms of its implementation, the triggering only happens (debounced) // based on the requested filters changing. - const [{ loading, error }, refresh] = useAsyncFn(async () => { - const compacted = compact(Object.values(requestedFilters)); - const entityFilter = reduceEntityFilters(compacted); - const backendFilter = reduceCatalogFilters(compacted); - const previousBackendFilter = reduceCatalogFilters( - compact(Object.values(outputState.appliedFilters)), - ); + const [{ loading, error }, refresh] = useAsyncFn( + async () => { + const compacted = compact(Object.values(requestedFilters)); + const entityFilter = reduceEntityFilters(compacted); + const backendFilter = reduceCatalogFilters(compacted); + const previousBackendFilter = reduceCatalogFilters( + compact(Object.values(outputState.appliedFilters)), + ); - // TODO(mtlewis): currently entities will never be requested unless - // there's at least one filter, we should allow an initial request - // to happen with no filters. - if (!isEqual(previousBackendFilter, backendFilter)) { - // TODO(timbonicus): should limit fields here, but would need filter - // fields + table columns - const response = await catalogApi.getEntities({ filter: backendFilter }); - setOutputState({ - appliedFilters: requestedFilters, - backendEntities: response.items, - entities: response.items.filter(entityFilter), - }); - } else { - setOutputState({ - appliedFilters: requestedFilters, - backendEntities: outputState.backendEntities, - entities: outputState.backendEntities.filter(entityFilter), - }); - } - }, [catalogApi, requestedFilters, outputState]); + // TODO(mtlewis): currently entities will never be requested unless + // there's at least one filter, we should allow an initial request + // to happen with no filters. + if (!isEqual(previousBackendFilter, backendFilter)) { + // TODO(timbonicus): should limit fields here, but would need filter + // fields + table columns + const response = await catalogApi.getEntities({ + filter: backendFilter, + }); + setOutputState({ + appliedFilters: requestedFilters, + backendEntities: response.items, + entities: response.items.filter(entityFilter), + }); + } else { + setOutputState({ + appliedFilters: requestedFilters, + backendEntities: outputState.backendEntities, + entities: outputState.backendEntities.filter(entityFilter), + }); + } + }, + [catalogApi, requestedFilters, outputState], + { loading: true }, + ); // Slight debounce on the refresh, since (especially on page load) several // filters will be calling this in rapid succession. diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index 4b9a7030d9..f012ab68d0 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -29,7 +29,6 @@ export const TemplateList = () => { const { loading, error, entities } = useEntityListProvider(); return ( <> - {/* TODO(mtlewis) figure out flash of error state when entities are loading */} {loading && } {error && ( @@ -38,7 +37,7 @@ export const TemplateList = () => { )} - {!error && !loading && entities && !entities.length && ( + {!error && !loading && !entities.length && ( No templates found that match your filter. Learn more about{' '} From 8631f422d4470b24ba315959afd9522c1b64adbc Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 24 Jun 2021 20:52:14 -0600 Subject: [PATCH 09/18] Add tag filter to ScaffolderPage Signed-off-by: Tim Hansen --- .../src/components/ScaffolderPage/ScaffolderPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 56c371e825..89710a6e34 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -27,6 +27,7 @@ import { EntityKindPicker, EntityListProvider, EntitySearchBar, + EntityTagPicker, EntityTypePicker, UserListPicker, } from '@backstage/plugin-catalog-react'; @@ -90,7 +91,7 @@ export const ScaffolderPageContents = () => { /> {/* TODO(mtlewis) replace with custom checkbox list? maybe multiselect */} - {/* TODO(mtlewis) consider adding tag picker? */} +
From 7f3f5c9b91ae88b32b2f8339f6d916c630b01ce8 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Mon, 28 Jun 2021 18:24:24 +0100 Subject: [PATCH 10/18] Add accessor method to retrieve array filter value from EntityTypeFilter Co-authored-by: Tim Hansen Co-authored-by: Chase Rutherford-Jenkins Co-authored-by: Himanshu Mishra Co-authored-by: Joe Porpeglia Signed-off-by: Mike Lewis --- plugins/catalog-react/src/filters.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 7a80eb6d31..418afeab92 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -32,10 +32,14 @@ export class EntityKindFilter implements EntityFilter { } export class EntityTypeFilter implements EntityFilter { - constructor(readonly value: string) {} + constructor(readonly value: string | string[]) {} + + getTypes() { + return Array.isArray(this.value) ? this.value : [this.value]; + } getCatalogFilters(): Record { - return { 'spec.type': this.value }; + return { 'spec.type': this.getTypes() }; } } From 77186cf6376379fd0a12c8ee500baf39125bd590 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Mon, 28 Jun 2021 18:28:16 +0100 Subject: [PATCH 11/18] Add ability to include multiple types in useEntityTypeFilter Co-authored-by: Tim Hansen Co-authored-by: Chase Rutherford-Jenkins Co-authored-by: Himanshu Mishra Co-authored-by: Joe Porpeglia Signed-off-by: Mike Lewis --- .../src/hooks/useEntityTypeFilter.tsx | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index aeda12c6f8..e925103ebd 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -30,6 +30,7 @@ type EntityTypeReturn = { types: string[]; selectedType: string | undefined; setType: (type: string | undefined) => void; + setTypes: (types: string[]) => void; }; /** @@ -43,7 +44,7 @@ export function useEntityTypeFilter(): EntityTypeReturn { updateFilters, } = useEntityListProvider(); - const [types, setTypes] = useState([]); + const [allTypes, setAllTypes] = useState([]); const kind = useMemo(() => kindFilter?.value, [kindFilter]); // Load all valid spec.type values straight from the catalogApi, paying attention to only the @@ -69,29 +70,44 @@ export function useEntityTypeFilter(): EntityTypeReturn { (entities ?? []).map(e => e.spec?.type).filter(Boolean) as string[], ), ].sort(); - setTypes(newTypes); + setAllTypes(newTypes); - // Reset type filter if no longer applicable - updateFilters((oldFilters: DefaultEntityFilters) => - oldFilters.type && !newTypes.includes(oldFilters.type.value) - ? { type: undefined } - : {}, - ); + // Update type filter to only valid values when the list of available types has changed + updateFilters((oldFilters: DefaultEntityFilters) => { + // No filter previously set; no-op + if (!oldFilters.type) { + return {}; + } + const stillValidTypes = oldFilters.type + .getTypes() + .filter(value => newTypes.includes(value)); + if (!stillValidTypes.length) { + // None of the previously selected types are present any more; clear the filter + return { type: undefined }; + } + return { type: new EntityTypeFilter(stillValidTypes) }; + }); }, [updateFilters, entities]); - const setType = useCallback( - (type: string | undefined) => + const setTypes = useCallback( + (types: string[]) => updateFilters({ - type: type === undefined ? undefined : new EntityTypeFilter(type), + type: types.length ? undefined : new EntityTypeFilter(types), }), [updateFilters], ); + const setType = (type: string | undefined) => + setTypes(type === undefined ? [] : [type]); + + // TODO(timbonicus): selectedType should be selectedTypes + // TODO(timbonicus): remove setType, make this only array-based return { loading, error, - types, + types: allTypes, selectedType: typeFilter?.value, setType, + setTypes, }; } From 2669b41f2fb6c2a24aab2cb4a3490c26cb2c350c Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Mon, 28 Jun 2021 18:29:24 +0100 Subject: [PATCH 12/18] Introduce initial TemplateTypePicker component in scaffolder Co-authored-by: Tim Hansen Co-authored-by: Chase Rutherford-Jenkins Co-authored-by: Himanshu Mishra Co-authored-by: Joe Porpeglia Signed-off-by: Mike Lewis --- plugins/scaffolder/package.json | 1 + .../TemplateTypePicker.test.tsx | 134 ++++++++++++++++++ .../TemplateTypePicker/TemplateTypePicker.tsx | 97 +++++++++++++ .../components/TemplateTypePicker/index.ts | 17 +++ 4 files changed, 249 insertions(+) create mode 100644 plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx create mode 100644 plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx create mode 100644 plugins/scaffolder/src/components/TemplateTypePicker/index.ts diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 88845b402b..c02b63f761 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -51,6 +51,7 @@ "humanize-duration": "^3.25.1", "immer": "^9.0.1", "json-schema": "^0.3.0", + "lodash": "^4.17.21", "luxon": "^1.25.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx new file mode 100644 index 0000000000..fa74b34717 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { capitalize } from 'lodash'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { EntityTypePicker } from './EntityTypePicker'; +import { MockEntityListContextProvider } from '../../testUtils/providers'; +import { catalogApiRef } from '../../api'; +import { EntityKindFilter, EntityTypeFilter } from '../../filters'; + +import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; + +const entities: Entity[] = [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'component-1', + }, + spec: { + type: 'service', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'component-2', + }, + spec: { + type: 'website', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'component-3', + }, + spec: { + type: 'library', + }, + }, +]; + +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, + ], +]); + +describe('', () => { + it('renders available entity types', async () => { + const rendered = render( + + + + + , + ); + expect(rendered.getByText('Type')).toBeInTheDocument(); + + const input = rendered.getByTestId('select'); + fireEvent.click(input); + + await waitFor(() => rendered.getByText('Service')); + + entities.forEach(entity => { + expect( + rendered.getByText(capitalize(entity.spec!.type as string)), + ).toBeInTheDocument(); + }); + }); + + it('sets the selected type filter', async () => { + const updateFilters = jest.fn(); + const rendered = render( + + + + + , + ); + const input = rendered.getByTestId('select'); + fireEvent.click(input); + + await waitFor(() => rendered.getByText('Service')); + fireEvent.click(rendered.getByText('Service')); + + expect(updateFilters).toHaveBeenLastCalledWith({ + type: new EntityTypeFilter('service'), + }); + + fireEvent.click(input); + fireEvent.click(rendered.getByText('All')); + + expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined }); + }); +}); diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx new file mode 100644 index 0000000000..780f9d2d1e --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.tsx @@ -0,0 +1,97 @@ +/* + * 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 from 'react'; +import { + Typography, + List, + ListItem, + makeStyles, + Theme, + Checkbox, + ListItemText, +} from '@material-ui/core'; +import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; +import { alertApiRef, useApi } from '@backstage/core-plugin-api'; + +const useStyles = makeStyles(theme => ({ + checkbox: { + padding: theme.spacing(0, 1, 0, 1), + }, +})); + +export const TemplateTypePicker = () => { + const classes = useStyles(); + const alertApi = useApi(alertApiRef); + // TODO(timbonicus): Use new setTypes returned from the hook + const { error, types, selectedType } = useEntityTypeFilter(); + + if (!types) return null; + + if (error) { + alertApi.post({ + message: `Failed to load entity types`, + severity: 'error', + }); + return null; + } + + return ( + <> + Categories + + {types.map(type => { + const labelId = `checkbox-list-label-${type}`; + return ( + {}} + // TODO(timbonicus): Update to use setTypes + // setSelectedCategories( + // selectedCategories.includes(type) + // ? selectedCategories.filter( + // selectedCategory => selectedCategory !== type, + // ) + // : [...selectedCategories, type], + // ) + // } + > + + + + ); + })} + + + ); +}; diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/index.ts b/plugins/scaffolder/src/components/TemplateTypePicker/index.ts new file mode 100644 index 0000000000..2dcd091311 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateTypePicker/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { TemplateTypePicker } from './TemplateTypePicker'; From e5abca34f601ab11076c6aaf1b61b281799a5cc0 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Mon, 28 Jun 2021 18:29:57 +0100 Subject: [PATCH 13/18] Use new TemplateTypePicker component in ScaffolderPage Co-authored-by: Tim Hansen Co-authored-by: Chase Rutherford-Jenkins Co-authored-by: Himanshu Mishra Co-authored-by: Joe Porpeglia Signed-off-by: Mike Lewis --- .../src/components/ScaffolderPage/ScaffolderPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 89710a6e34..3fed099500 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -28,7 +28,6 @@ import { EntityListProvider, EntitySearchBar, EntityTagPicker, - EntityTypePicker, UserListPicker, } from '@backstage/plugin-catalog-react'; import { Button, makeStyles } from '@material-ui/core'; @@ -36,6 +35,7 @@ import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { registerComponentRouteRef } from '../../routes'; import { TemplateList } from '../TemplateList'; +import { TemplateTypePicker } from '../TemplateTypePicker'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -89,8 +89,7 @@ export const ScaffolderPageContents = () => { initialFilter="all" availableFilters={['all', 'starred']} /> - {/* TODO(mtlewis) replace with custom checkbox list? maybe multiselect */} - +
From 315deb854401339111c01c4494254d5a05691531 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Mon, 28 Jun 2021 16:04:43 -0600 Subject: [PATCH 14/18] entityTypeFilter improvements - Supports selecting multiple types - Switch TemplateTypePicker to FormGroup instead of List Co-authored-by: Joe Porpeglia Signed-off-by: Tim Hansen --- .../EntityTypePicker/EntityTypePicker.tsx | 19 ++-- plugins/catalog-react/src/filters.ts | 3 +- .../src/hooks/useEntityListProvider.tsx | 2 +- .../src/hooks/useEntityTypeFilter.tsx | 48 +++++----- .../components/CatalogTable/CatalogTable.tsx | 2 +- .../TemplateTypePicker/TemplateTypePicker.tsx | 89 +++++++++---------- 6 files changed, 84 insertions(+), 79 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index 52b8ec8ff1..0a7e94d142 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { capitalize } from 'lodash'; +import capitalize from 'lodash/capitalize'; import { Box } from '@material-ui/core'; import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; @@ -24,9 +24,14 @@ import { Select } from '@backstage/core-components'; export const EntityTypePicker = () => { const alertApi = useApi(alertApiRef); - const { error, types, selectedType, setType } = useEntityTypeFilter(); + const { + error, + availableTypes, + selectedTypes, + setSelectedTypes, + } = useEntityTypeFilter(); - if (!types) return null; + if (!availableTypes) return null; if (error) { alertApi.post({ @@ -38,7 +43,7 @@ export const EntityTypePicker = () => { const items = [ { value: 'all', label: 'All' }, - ...types.map((type: string) => ({ + ...availableTypes.map((type: string) => ({ value: type, label: capitalize(type), })), @@ -49,8 +54,10 @@ export const EntityTypePicker = () => {