feat(pickers): implement EntityLifecyclePicker and EntityOwnerPicker
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
@@ -22,7 +22,9 @@ import {
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
EntityKindPicker,
|
||||
EntityLifecyclePicker,
|
||||
EntityListProvider,
|
||||
EntityOwnerPicker,
|
||||
EntityTagPicker,
|
||||
EntityTypePicker,
|
||||
UserListFilterKind,
|
||||
@@ -71,6 +73,8 @@ export const ApiExplorerPage = ({
|
||||
<EntityKindPicker initialFilter="api" hidden />
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
<EntityOwnerPicker />
|
||||
<EntityLifecyclePicker />
|
||||
<EntityTagPicker />
|
||||
</div>
|
||||
<CatalogTable columns={columns} />
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { EntityLifecycleFilter } from '../../types';
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<EntityLifecyclePicker/>', () => {
|
||||
it('renders all lifecycles', () => {
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Lifecycle')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
|
||||
sampleEntities
|
||||
.map(e => e.spec?.lifecycle!)
|
||||
.forEach(lifecycle => {
|
||||
expect(rendered.getByText(lifecycle as string)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unique lifecycles in alphabetical order', () => {
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Lifecycle')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
|
||||
|
||||
expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([
|
||||
'experimental',
|
||||
'production',
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds lifecycles to filters', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
|
||||
fireEvent.click(rendered.getByText('production'));
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: new EntityLifecycleFilter(['production']),
|
||||
});
|
||||
});
|
||||
|
||||
it('removes lifecycles from filters', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
filters: { lifecycles: new EntityLifecycleFilter(['production']) },
|
||||
}}
|
||||
>
|
||||
<EntityLifecyclePicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.getByTestId('lifecycle-picker-expand'));
|
||||
expect(rendered.getByLabelText('production')).toBeChecked();
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('production'));
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
lifecycles: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
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, { useMemo } from 'react';
|
||||
import { useEntityListProvider } from '../../hooks/useEntityListProvider';
|
||||
import { EntityLifecycleFilter } from '../../types';
|
||||
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
export const EntityLifecyclePicker = () => {
|
||||
const { updateFilters, backendEntities, filters } = useEntityListProvider();
|
||||
const availableLifecycles = useMemo(
|
||||
() =>
|
||||
[
|
||||
...new Set(
|
||||
backendEntities
|
||||
.map((e: Entity) => e.spec?.lifecycle)
|
||||
.filter(Boolean) as string[],
|
||||
),
|
||||
].sort(),
|
||||
[backendEntities],
|
||||
);
|
||||
|
||||
if (!availableLifecycles.length) return null;
|
||||
|
||||
const onChange = (lifecycles: string[]) => {
|
||||
updateFilters({
|
||||
lifecycles: lifecycles.length
|
||||
? new EntityLifecycleFilter(lifecycles)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="button">Lifecycle</Typography>
|
||||
<Autocomplete<string>
|
||||
multiple
|
||||
options={availableLifecycles}
|
||||
value={filters.lifecycles?.values ?? []}
|
||||
onChange={(_: object, value: string[]) => onChange(value)}
|
||||
renderOption={(option, { selected }) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
checked={selected}
|
||||
/>
|
||||
}
|
||||
label={option}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
popupIcon={<ExpandMoreIcon data-testid="lifecycle-picker-expand" />}
|
||||
renderInput={params => <TextField {...params} variant="outlined" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { EntityLifecyclePicker } from './EntityLifecyclePicker';
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
import { EntityOwnerFilter } from '../../types';
|
||||
import { EntityOwnerPicker } from './EntityOwnerPicker';
|
||||
|
||||
const sampleEntities: Entity[] = [
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'component-1',
|
||||
},
|
||||
spec: {
|
||||
owner: 'some-owner',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'component-2',
|
||||
},
|
||||
spec: {
|
||||
owner: 'another-owner',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'component-3',
|
||||
},
|
||||
spec: {
|
||||
owner: 'some-owner',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<EntityOwnerPicker/>', () => {
|
||||
it('renders all owners', () => {
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
|
||||
>
|
||||
<EntityOwnerPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Owner')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
|
||||
sampleEntities
|
||||
.map(e => e.spec?.owner!)
|
||||
.forEach(owner => {
|
||||
expect(rendered.getByText(owner as string)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unique owners in alphabetical order', () => {
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
|
||||
>
|
||||
<EntityOwnerPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Owner')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
|
||||
|
||||
expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([
|
||||
'another-owner',
|
||||
'some-owner',
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds owners to filters', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
}}
|
||||
>
|
||||
<EntityOwnerPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
|
||||
fireEvent.click(rendered.getByText('some-owner'));
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
owners: new EntityOwnerFilter(['some-owner']),
|
||||
});
|
||||
});
|
||||
|
||||
it('removes owners from filters', () => {
|
||||
const updateFilters = jest.fn();
|
||||
const rendered = render(
|
||||
<MockEntityListContextProvider
|
||||
value={{
|
||||
entities: sampleEntities,
|
||||
backendEntities: sampleEntities,
|
||||
updateFilters,
|
||||
filters: { owners: new EntityOwnerFilter(['some-owner']) },
|
||||
}}
|
||||
>
|
||||
<EntityOwnerPicker />
|
||||
</MockEntityListContextProvider>,
|
||||
);
|
||||
expect(updateFilters).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
|
||||
expect(rendered.getByLabelText('some-owner')).toBeChecked();
|
||||
|
||||
fireEvent.click(rendered.getByLabelText('some-owner'));
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
owner: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
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, { useMemo } from 'react';
|
||||
import { useEntityListProvider } from '../../hooks/useEntityListProvider';
|
||||
import { EntityOwnerFilter } from '../../types';
|
||||
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
export const EntityOwnerPicker = () => {
|
||||
const { updateFilters, backendEntities, filters } = useEntityListProvider();
|
||||
const availableOwners = useMemo(
|
||||
() =>
|
||||
[
|
||||
...new Set(
|
||||
backendEntities
|
||||
.map((e: Entity) => e.spec?.owner)
|
||||
.filter(Boolean) as string[],
|
||||
),
|
||||
].sort(),
|
||||
[backendEntities],
|
||||
);
|
||||
|
||||
if (!availableOwners.length) return null;
|
||||
|
||||
const onChange = (owners: string[]) => {
|
||||
updateFilters({
|
||||
owners: owners.length ? new EntityOwnerFilter(owners) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="button">Owner</Typography>
|
||||
<Autocomplete<string>
|
||||
multiple
|
||||
options={availableOwners}
|
||||
value={filters.owners?.values ?? []}
|
||||
onChange={(_: object, value: string[]) => onChange(value)}
|
||||
renderOption={(option, { selected }) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
checked={selected}
|
||||
/>
|
||||
}
|
||||
label={option}
|
||||
/>
|
||||
)}
|
||||
size="small"
|
||||
popupIcon={<ExpandMoreIcon data-testid="owner-picker-expand" />}
|
||||
renderInput={params => <TextField {...params} variant="outlined" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { EntityOwnerPicker } from './EntityOwnerPicker';
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './EntityKindPicker';
|
||||
export * from './EntityLifecyclePicker';
|
||||
export * from './EntityOwnerPicker';
|
||||
export * from './EntityProvider';
|
||||
export * from './EntityRefLink';
|
||||
export * from './EntityTable';
|
||||
|
||||
@@ -29,6 +29,8 @@ import { catalogApiRef } from '../api';
|
||||
import {
|
||||
EntityFilter,
|
||||
EntityKindFilter,
|
||||
EntityLifecycleFilter,
|
||||
EntityOwnerFilter,
|
||||
EntityTagFilter,
|
||||
EntityTypeFilter,
|
||||
UserListFilter,
|
||||
@@ -39,6 +41,8 @@ export type DefaultEntityFilters = {
|
||||
kind?: EntityKindFilter;
|
||||
type?: EntityTypeFilter;
|
||||
user?: UserListFilter;
|
||||
owners?: EntityOwnerFilter;
|
||||
lifecycles?: EntityLifecycleFilter;
|
||||
tags?: EntityTagFilter;
|
||||
};
|
||||
|
||||
|
||||
@@ -61,6 +61,22 @@ export class EntityTagFilter implements EntityFilter {
|
||||
}
|
||||
}
|
||||
|
||||
export class EntityOwnerFilter implements EntityFilter {
|
||||
constructor(readonly values: string[]) {}
|
||||
|
||||
filterEntity(entity: Entity): boolean {
|
||||
return this.values.some(v => entity.spec?.owner === v);
|
||||
}
|
||||
}
|
||||
|
||||
export class EntityLifecycleFilter implements EntityFilter {
|
||||
constructor(readonly values: string[]) {}
|
||||
|
||||
filterEntity(entity: Entity): boolean {
|
||||
return this.values.some(v => entity.spec?.lifecycle === v);
|
||||
}
|
||||
}
|
||||
|
||||
export type UserListFilterKind = 'owned' | 'starred' | 'all';
|
||||
export class UserListFilter implements EntityFilter {
|
||||
constructor(
|
||||
|
||||
Reference in New Issue
Block a user