add namespace filter and column

Signed-off-by: David An <a-daan@expediagroup.com>
This commit is contained in:
David An
2023-02-13 14:08:41 -08:00
parent 92444b3521
commit d19d87dc13
8 changed files with 197 additions and 7 deletions
@@ -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 = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
/** @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 (
<Box pb={1} pt={1}>
<Typography variant="button" component="label">
Namespace
<Autocomplete
multiple
options={availableNamespace}
value={selectedNamespace}
onChange={(_: object, value: string[]) => setSelectedNamespace(value)}
renderOption={(option, { selected }) => (
<FormControlLabel
control={
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
}
label={option}
/>
)}
size="small"
popupIcon={<ExpandMoreIcon data-testid="namespace-picker-expand" />}
renderInput={params => (
<TextField
{...params}
className={classes.input}
variant="outlined"
/>
)}
/>
</Typography>
</Box>
);
};
@@ -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';
@@ -29,3 +29,4 @@ export * from './InspectEntityDialog';
export * from './UnregisterEntityDialog';
export * from './UserListPicker';
export * from './EntityProcessingStatusPicker';
export * from './EntityNamespacePicker';
+16
View File
@@ -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
@@ -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 */
@@ -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) {
<EntityLifecyclePicker />
<EntityTagPicker />
<EntityProcessingStatusPicker />
<EntityNamespacePicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable
@@ -89,6 +89,12 @@ export const CatalogTable = (props: CatalogTableProps) => {
];
function createEntitySpecificColumns(): TableColumn<CatalogTableRow>[] {
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
@@ -193,4 +193,11 @@ export const columnFactories = Object.freeze({
width: 'auto',
};
},
createNamespaceColumn(): TableColumn<CatalogTableRow> {
return {
title: 'Namespace',
field: 'entity.metadata.namespace',
width: 'auto',
};
},
});