Merge pull request #17888 from backstage/vinzscam/decouple-entity-lifecycle-picker
EntityLifecyclePicker: loads data using the facets endpoint
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
`EntityAutocompletePicker` add `initialSelectedOptions` prop
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
`EntityLifecycleFilter` loads data using the facets endpoint
|
||||
@@ -195,7 +195,7 @@ export class EntityLifecycleFilter implements EntityFilter {
|
||||
// @public (undocumented)
|
||||
export const EntityLifecyclePicker: (props: {
|
||||
initialFilter?: string[];
|
||||
}) => JSX.Element | null;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const EntityListContext: React_2.Context<
|
||||
|
||||
+12
-2
@@ -54,6 +54,7 @@ export type EntityAutocompletePickerProps<
|
||||
showCounts?: boolean;
|
||||
Filter: ConstructableFilter<NonNullable<T[Name]>>;
|
||||
InputProps?: TextFieldProps;
|
||||
initialSelectedOptions?: string[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
@@ -61,7 +62,15 @@ export function EntityAutocompletePicker<
|
||||
T extends DefaultEntityFilters = DefaultEntityFilters,
|
||||
Name extends AllowedEntityFilters<T> = AllowedEntityFilters<T>,
|
||||
>(props: EntityAutocompletePickerProps<T, Name>) {
|
||||
const { label, name, path, showCounts, Filter, InputProps } = props;
|
||||
const {
|
||||
label,
|
||||
name,
|
||||
path,
|
||||
showCounts,
|
||||
Filter,
|
||||
InputProps,
|
||||
initialSelectedOptions = [],
|
||||
} = props;
|
||||
|
||||
const {
|
||||
updateFilters,
|
||||
@@ -90,7 +99,8 @@ export function EntityAutocompletePicker<
|
||||
const [selectedOptions, setSelectedOptions] = useState(
|
||||
queryParameters.length
|
||||
? queryParameters
|
||||
: (filters[name] as unknown as { values: string[] })?.values ?? [],
|
||||
: (filters[name] as unknown as { values: string[] })?.values ??
|
||||
initialSelectedOptions,
|
||||
);
|
||||
|
||||
// Set selected options on query parameter updates; this happens at initial page load and from
|
||||
|
||||
+127
-139
@@ -14,146 +14,116 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { EntityLifecycleFilter } from '../../filters';
|
||||
import { EntityLifecyclePicker } from './EntityLifecyclePicker';
|
||||
|
||||
const sampleEntities: Entity[] = [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'component-1',
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'production',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'component-2',
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'experimental',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'component-3',
|
||||
},
|
||||
spec: {
|
||||
lifecycle: 'experimental',
|
||||
},
|
||||
},
|
||||
];
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
describe('<EntityLifecyclePicker/>', () => {
|
||||
it('renders all lifecycles', () => {
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
|
||||
const catalogApi = {
|
||||
getEntityFacets: jest.fn(),
|
||||
} as unknown as jest.Mocked<CatalogApi>;
|
||||
|
||||
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
|
||||
sampleEntities
|
||||
.map(e => e.spec?.lifecycle!)
|
||||
.forEach(lifecycle => {
|
||||
expect(screen.getByText(lifecycle as string)).toBeInTheDocument();
|
||||
});
|
||||
beforeEach(() => {
|
||||
catalogApi.getEntityFacets.mockResolvedValue({
|
||||
facets: {
|
||||
'spec.lifecycle': [
|
||||
{ count: 1, value: 'experimental' },
|
||||
{ count: 1, value: 'production' },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unique lifecycles in alphabetical order', () => {
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(screen.getByText('Lifecycle')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
|
||||
|
||||
expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
|
||||
'experimental',
|
||||
'production',
|
||||
]);
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('respects the query parameter filter value', () => {
|
||||
it('renders all lifecycles', async () => {
|
||||
render(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider value={{}}>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(await screen.findByText('Lifecycle')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(await screen.findByTestId('lifecycles-picker-expand'));
|
||||
expect(screen.getByText('experimental')).toBeInTheDocument();
|
||||
expect(screen.getByText('production')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('respects the query parameter filter value', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const queryParameters = { lifecycles: ['experimental'] };
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
queryParameters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['experimental']),
|
||||
});
|
||||
});
|
||||
|
||||
it('adds lifecycles to filters', () => {
|
||||
it('adds lifecycles to filters', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: undefined,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
|
||||
fireEvent.click(await screen.findByTestId('lifecycles-picker-expand'));
|
||||
fireEvent.click(screen.getByText('production'));
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['production']),
|
||||
});
|
||||
});
|
||||
|
||||
it('removes lifecycles from filters', () => {
|
||||
it('removes lifecycles from filters', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
filters: { lifecycles: new EntityLifecycleFilter(['production']) },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
filters: { lifecycles: new EntityLifecycleFilter(['production']) },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['production']),
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
|
||||
fireEvent.click(screen.getByTestId('lifecycles-picker-expand'));
|
||||
expect(screen.getByLabelText('production')).toBeChecked();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('production'));
|
||||
@@ -162,67 +132,85 @@ describe('<EntityLifecyclePicker/>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('responds to external queryParameters changes', () => {
|
||||
it('responds to external queryParameters changes', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['experimental'] },
|
||||
backendEntities: sampleEntities,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['experimental'] },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['experimental']),
|
||||
});
|
||||
|
||||
rendered.rerender(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['production'] },
|
||||
backendEntities: sampleEntities,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['production'] },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['production']),
|
||||
});
|
||||
});
|
||||
it('removes lifecycles from filters if there are no available lifecycles', () => {
|
||||
|
||||
it('removes lifecycles from filters if there are no available lifecycles', async () => {
|
||||
catalogApi.getEntityFacets.mockResolvedValue({
|
||||
facets: {
|
||||
'spec.lifecycle': [],
|
||||
},
|
||||
});
|
||||
|
||||
const updateFilters = jest.fn();
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['experimental'] },
|
||||
backendEntities: [],
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
queryParameters: { lifecycles: ['experimental'] },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: undefined,
|
||||
});
|
||||
});
|
||||
it('responds to initialFilter prop', () => {
|
||||
|
||||
it('responds to initialFilter prop', async () => {
|
||||
const updateFilters = jest.fn();
|
||||
render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker initialFilter={['production']} />
|
||||
</MockEntityListContextProvider>,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
updateFilters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker initialFilter={['production']} />
|
||||
</MockEntityListContextProvider>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['production']),
|
||||
});
|
||||
|
||||
+11
-102
@@ -14,22 +14,10 @@
|
||||
* 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 { makeStyles } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { EntityLifecycleFilter } from '../../filters';
|
||||
import { EntityAutocompletePicker } from '../EntityAutocompletePicker';
|
||||
|
||||
/** @public */
|
||||
export type CatalogReactEntityLifecyclePickerClassKey = 'input';
|
||||
@@ -43,98 +31,19 @@ const useStyles = makeStyles(
|
||||
},
|
||||
);
|
||||
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
/** @public */
|
||||
export const EntityLifecyclePicker = (props: { initialFilter?: string[] }) => {
|
||||
const { initialFilter = [] } = props;
|
||||
const classes = useStyles();
|
||||
const {
|
||||
updateFilters,
|
||||
backendEntities,
|
||||
filters,
|
||||
queryParameters: { lifecycles: lifecyclesParameter },
|
||||
} = useEntityList();
|
||||
|
||||
const queryParamLifecycles = useMemo(
|
||||
() => [lifecyclesParameter].flat().filter(Boolean) as string[],
|
||||
[lifecyclesParameter],
|
||||
);
|
||||
|
||||
const [selectedLifecycles, setSelectedLifecycles] = useState(
|
||||
queryParamLifecycles.length
|
||||
? queryParamLifecycles
|
||||
: filters.lifecycles?.values ?? initialFilter,
|
||||
);
|
||||
|
||||
// Set selected lifecycles on query parameter updates; this happens at initial page load and from
|
||||
// external updates to the page location.
|
||||
useEffect(() => {
|
||||
if (queryParamLifecycles.length) {
|
||||
setSelectedLifecycles(queryParamLifecycles);
|
||||
}
|
||||
}, [queryParamLifecycles]);
|
||||
|
||||
const availableLifecycles = useMemo(
|
||||
() =>
|
||||
[
|
||||
...new Set(
|
||||
backendEntities
|
||||
.map((e: Entity) => e.spec?.lifecycle)
|
||||
.filter(Boolean) as string[],
|
||||
),
|
||||
].sort(),
|
||||
[backendEntities],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilters({
|
||||
lifecycles:
|
||||
selectedLifecycles.length && availableLifecycles.length
|
||||
? new EntityLifecycleFilter(selectedLifecycles)
|
||||
: undefined,
|
||||
});
|
||||
}, [selectedLifecycles, updateFilters, availableLifecycles]);
|
||||
|
||||
if (!availableLifecycles.length) return null;
|
||||
|
||||
return (
|
||||
<Box pb={1} pt={1}>
|
||||
<Typography variant="button" component="label">
|
||||
Lifecycle
|
||||
<Autocomplete
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
options={availableLifecycles}
|
||||
value={selectedLifecycles}
|
||||
onChange={(_: object, value: string[]) =>
|
||||
setSelectedLifecycles(value)
|
||||
}
|
||||
renderOption={(option, { selected }) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
checked={selected}
|
||||
/>
|
||||
}
|
||||
onClick={event => event.preventDefault()}
|
||||
label={option}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
popupIcon={<ExpandMoreIcon data-testid="lifecycle-picker-expand" />}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
className={classes.input}
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<EntityAutocompletePicker
|
||||
label="Lifecycle"
|
||||
name="lifecycles"
|
||||
path="spec.lifecycle"
|
||||
Filter={EntityLifecycleFilter}
|
||||
InputProps={{ className: classes.input }}
|
||||
initialSelectedOptions={initialFilter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user