Implement EntityKindPicker
This change implements the EntityKindPicker allowing for selecting multiple kinds. Signed-off-by: Crevil <bjoern.soerensen@gmail.com>
This commit is contained in:
@@ -14,41 +14,115 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useEntityList } from '../../hooks';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
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 useAsync from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { EntityKindFilter } from '../../filters';
|
||||
import { useEntityList } from '../../hooks';
|
||||
|
||||
/**
|
||||
* Props for {@link EntityKindPicker}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface EntityKindPickerProps {
|
||||
initialFilter?: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
input: {},
|
||||
},
|
||||
{
|
||||
name: 'CatalogReactEntityKindPicker',
|
||||
},
|
||||
);
|
||||
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
/** @public */
|
||||
export const EntityKindPicker = (props: EntityKindPickerProps) => {
|
||||
const { initialFilter, hidden } = props;
|
||||
|
||||
export const EntityKindPicker = () => {
|
||||
const classes = useStyles();
|
||||
const {
|
||||
updateFilters,
|
||||
queryParameters: { kind: kindParameter },
|
||||
filters,
|
||||
queryParameters: { kind: kindsParameter },
|
||||
} = useEntityList();
|
||||
const [selectedKind] = useState([kindParameter].flat()[0] ?? initialFilter);
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value: availableKinds } = useAsync(async () => {
|
||||
const facet = 'kind';
|
||||
const { facets } = await catalogApi.getEntityFacets({
|
||||
facets: [facet],
|
||||
});
|
||||
|
||||
return facets[facet].map(({ value }) => value);
|
||||
}, [filters.kind]);
|
||||
|
||||
const queryParamKinds = useMemo(
|
||||
() => [kindsParameter].flat().filter(Boolean) as string[],
|
||||
[kindsParameter],
|
||||
);
|
||||
|
||||
const [selectedKinds, setSelectedKinds] = useState(
|
||||
queryParamKinds.length ? queryParamKinds : filters.kind?.getKinds() ?? [],
|
||||
);
|
||||
|
||||
// Set selected kinds on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamKinds.length) {
|
||||
setSelectedKinds(queryParamKinds);
|
||||
}
|
||||
}, [queryParamKinds]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilters({
|
||||
kind: selectedKind ? new EntityKindFilter(selectedKind) : undefined,
|
||||
kind: selectedKinds.length
|
||||
? new EntityKindFilter(selectedKinds)
|
||||
: undefined,
|
||||
});
|
||||
}, [selectedKind, updateFilters]);
|
||||
}, [selectedKinds, updateFilters]);
|
||||
|
||||
if (hidden) return null;
|
||||
if (!availableKinds?.length) return null;
|
||||
|
||||
// TODO(timbonicus): This should load available kinds from the catalog-backend, similar to
|
||||
// EntityTypePicker.
|
||||
|
||||
return <Alert severity="warning">Kind filter not yet available</Alert>;
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<Typography variant="button" component="label">
|
||||
Kinds
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={availableKinds}
|
||||
value={selectedKinds}
|
||||
onChange={(_: object, value: string[]) => setSelectedKinds(value)}
|
||||
renderOption={(option, { selected }) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
checked={selected}
|
||||
/>
|
||||
}
|
||||
label={option}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
popupIcon={<ExpandMoreIcon data-testid="kind-picker-expand" />}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
className={classes.input}
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,14 +28,19 @@ import { getEntityRelations } from './utils';
|
||||
* @public
|
||||
*/
|
||||
export class EntityKindFilter implements EntityFilter {
|
||||
constructor(readonly value: string) {}
|
||||
constructor(readonly value: string | string[]) {}
|
||||
|
||||
getCatalogFilters(): Record<string, string | string[]> {
|
||||
return { kind: this.value };
|
||||
// Simplify `string | string[]` for consumers, always returns an array
|
||||
getKinds(): string[] {
|
||||
return Array.isArray(this.value) ? this.value : [this.value];
|
||||
}
|
||||
|
||||
toQueryValue(): string {
|
||||
return this.value;
|
||||
getCatalogFilters(): Record<string, string | string[]> {
|
||||
return { kind: this.getKinds() };
|
||||
}
|
||||
|
||||
toQueryValue(): string[] {
|
||||
return this.getKinds();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
EntityTypePicker,
|
||||
UserListFilterKind,
|
||||
UserListPicker,
|
||||
EntityKindPicker,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { createComponentRouteRef } from '../../routes';
|
||||
@@ -83,6 +84,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
|
||||
</ContentHeader>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntityKindPicker />
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
<EntityOwnerPicker />
|
||||
|
||||
@@ -70,7 +70,12 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
const defaultColumns: TableColumn<CatalogTableRow>[] = useMemo(() => {
|
||||
return [
|
||||
columnFactories.createTitleColumn({ hidden: true }),
|
||||
columnFactories.createNameColumn({ defaultKind: filters.kind?.value }),
|
||||
columnFactories.createNameColumn({
|
||||
defaultKind:
|
||||
filters.kind?.getKinds()?.length === 1
|
||||
? filters.kind?.getKinds()[0]
|
||||
: undefined,
|
||||
}),
|
||||
...createEntitySpecificColumns(),
|
||||
columnFactories.createMetadataDescriptionColumn(),
|
||||
columnFactories.createTagsColumn(),
|
||||
@@ -100,7 +105,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
|
||||
];
|
||||
}
|
||||
}
|
||||
}, [filters.kind?.value]);
|
||||
}, [filters.kind]);
|
||||
|
||||
const showTypeColumn = filters.type === undefined;
|
||||
// TODO(timbonicus): remove the title from the CatalogTable once using EntitySearchBar
|
||||
|
||||
Reference in New Issue
Block a user