diff --git a/plugins/catalog-react/src/components/EntityNamespacePicker/EntityNamespacePicker.tsx b/plugins/catalog-react/src/components/EntityNamespacePicker/EntityNamespacePicker.tsx new file mode 100644 index 0000000000..30df7fc175 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityNamespacePicker/EntityNamespacePicker.tsx @@ -0,0 +1,139 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { + Box, + Checkbox, + FormControlLabel, + makeStyles, + TextField, + Typography, +} from '@material-ui/core'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { Autocomplete } from '@material-ui/lab'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useEntityList } from '../../hooks/useEntityListProvider'; +import { EntityNamespaceFilter } from '../../filters'; + +/** @public */ +export type CatalogReactEntityNamespacePickerClassKey = 'input'; + +const useStyles = makeStyles( + { + input: {}, + }, + { + name: 'CatalogReactEntityNamespacePicker', + }, +); + +const icon = ; +const checkedIcon = ; + +/** @public */ +export const EntityNamespacePicker = (props: { initialFilter?: string[] }) => { + const { initialFilter = [] } = props; + const classes = useStyles(); + const { + updateFilters, + backendEntities, + filters, + queryParameters: { namespace: namespaceParameter }, + } = useEntityList(); + + const queryParamNamespace = useMemo( + () => [namespaceParameter].flat().filter(Boolean) as string[], + [namespaceParameter], + ); + + const [selectedNamespace, setSelectedNamespace] = useState( + queryParamNamespace.length + ? queryParamNamespace + : filters.namespace?.values ?? initialFilter, + ); + + // Set selected namespace on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamNamespace.length) { + setSelectedNamespace(queryParamNamespace); + } + }, [queryParamNamespace]); + + const availableNamespace = useMemo( + () => + [ + ...new Set( + backendEntities + .map((e: Entity) => e.metadata.namespace) + .filter(Boolean) as string[], + ), + ].sort(), + [backendEntities], + ); + + useEffect(() => { + updateFilters({ + namespace: + selectedNamespace.length && availableNamespace.length + ? new EntityNamespaceFilter(selectedNamespace) + : undefined, + }); + }, [selectedNamespace, updateFilters, availableNamespace]); + + if (!availableNamespace.length) return null; + + if (availableNamespace.every(namespace => namespace === 'default')) + return null; + + return ( + + + Namespace + setSelectedNamespace(value)} + renderOption={(option, { selected }) => ( + + } + label={option} + /> + )} + size="small" + popupIcon={} + renderInput={params => ( + + )} + /> + + + ); +}; diff --git a/plugins/catalog-react/src/components/EntityNamespacePicker/index.ts b/plugins/catalog-react/src/components/EntityNamespacePicker/index.ts new file mode 100644 index 0000000000..d08536ea2c --- /dev/null +++ b/plugins/catalog-react/src/components/EntityNamespacePicker/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { EntityNamespacePicker } from './EntityNamespacePicker'; +export type { CatalogReactEntityNamespacePickerClassKey } from './EntityNamespacePicker'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index a4380ddef0..6d8aa393ed 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -29,3 +29,4 @@ export * from './InspectEntityDialog'; export * from './UnregisterEntityDialog'; export * from './UserListPicker'; export * from './EntityProcessingStatusPicker'; +export * from './EntityNamespacePicker'; diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 056b3b92c1..2fc2b3b382 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -149,6 +149,22 @@ export class EntityLifecycleFilter implements EntityFilter { } } +/** + * Filters entities on namespace. + * @public + */ +export class EntityNamespaceFilter implements EntityFilter { + constructor(readonly values: string[]) {} + + filterEntity(entity: Entity): boolean { + return this.values.some(v => entity.metadata.namespace === v); + } + + toQueryValue(): string[] { + return this.values; + } +} + /** * Filters entities based on whatever the user has starred or owns them. * @public diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index b5dfb4d8f7..3a20c814c0 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -40,6 +40,7 @@ import { EntityTextFilter, EntityTypeFilter, UserListFilter, + EntityNamespaceFilter, } from '../filters'; import { EntityFilter } from '../types'; import { reduceCatalogFilters, reduceEntityFilters } from '../utils'; @@ -56,6 +57,7 @@ export type DefaultEntityFilters = { text?: EntityTextFilter; orphan?: EntityOrphanFilter; error?: EntityErrorFilter; + namespace?: EntityNamespaceFilter; }; /** @public */ diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index 644453f5fb..b0fb52b4f0 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -35,6 +35,7 @@ import { UserListFilterKind, UserListPicker, EntityKindPicker, + EntityNamespacePicker, } from '@backstage/plugin-catalog-react'; import React, { ReactNode } from 'react'; import { createComponentRouteRef } from '../../routes'; @@ -90,6 +91,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) { + { ]; function createEntitySpecificColumns(): TableColumn[] { + const baseColumns = [ + columnFactories.createSystemColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createSpecTypeColumn(), + columnFactories.createSpecLifecycleColumn(), + ]; switch (filters.kind?.value) { case 'user': return []; @@ -104,15 +110,14 @@ export const CatalogTable = (props: CatalogTableProps) => { columnFactories.createSpecTargetsColumn(), ]; default: - return [ - columnFactories.createSystemColumn(), - columnFactories.createOwnerColumn(), - columnFactories.createSpecTypeColumn(), - columnFactories.createSpecLifecycleColumn(), - ]; + return entities.every( + entity => entity.metadata.namespace === 'default', + ) + ? baseColumns + : [...baseColumns, columnFactories.createNamespaceColumn()]; } } - }, [filters.kind?.value]); + }, [filters.kind?.value, entities]); const showTypeColumn = filters.type === undefined; // TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 7f3cd5ef82..fc6e1aaf6b 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -193,4 +193,11 @@ export const columnFactories = Object.freeze({ width: 'auto', }; }, + createNamespaceColumn(): TableColumn { + return { + title: 'Namespace', + field: 'entity.metadata.namespace', + width: 'auto', + }; + }, });