diff --git a/.changeset/chilly-ants-taste.md b/.changeset/chilly-ants-taste.md
new file mode 100644
index 0000000000..2c68e374f8
--- /dev/null
+++ b/.changeset/chilly-ants-taste.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Implement a `EntityLifecyclePicker` and `EntityOwnerPicker`
diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx
index 36cd988c8c..e5dffa7e73 100644
--- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx
+++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx
@@ -22,7 +22,9 @@ import {
} from '@backstage/core';
import {
EntityKindPicker,
+ EntityLifecyclePicker,
EntityListProvider,
+ EntityOwnerPicker,
EntityTagPicker,
EntityTypePicker,
UserListFilterKind,
@@ -71,6 +73,8 @@ export const ApiExplorerPage = ({
+
+
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
new file mode 100644
index 0000000000..b8fa9ec1c8
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
@@ -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('', () => {
+ it('renders all lifecycles', () => {
+ const rendered = render(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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,
+ });
+ });
+});
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
new file mode 100644
index 0000000000..eed0218784
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
@@ -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 = ;
+const checkedIcon = ;
+
+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 (
+ <>
+ Lifecycle
+
+ multiple
+ options={availableLifecycles}
+ value={filters.lifecycles?.values ?? []}
+ onChange={(_: object, value: string[]) => onChange(value)}
+ renderOption={(option, { selected }) => (
+
+ }
+ label={option}
+ />
+ )}
+ size="small"
+ popupIcon={}
+ renderInput={params => }
+ />
+ >
+ );
+};
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/index.ts b/plugins/catalog-react/src/components/EntityLifecyclePicker/index.ts
new file mode 100644
index 0000000000..b947ad520c
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/index.ts
@@ -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';
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx
new file mode 100644
index 0000000000..5473f3a5de
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx
@@ -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('', () => {
+ it('renders all owners', () => {
+ const rendered = render(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ 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,
+ });
+ });
+});
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
new file mode 100644
index 0000000000..96e955969f
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
@@ -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 = ;
+const checkedIcon = ;
+
+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 (
+ <>
+ Owner
+
+ multiple
+ options={availableOwners}
+ value={filters.owners?.values ?? []}
+ onChange={(_: object, value: string[]) => onChange(value)}
+ renderOption={(option, { selected }) => (
+
+ }
+ label={option}
+ />
+ )}
+ size="small"
+ popupIcon={}
+ renderInput={params => }
+ />
+ >
+ );
+};
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/index.ts b/plugins/catalog-react/src/components/EntityOwnerPicker/index.ts
new file mode 100644
index 0000000000..d46565b7c4
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/index.ts
@@ -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';
diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts
index 4aad517b32..06e155aa43 100644
--- a/plugins/catalog-react/src/components/index.ts
+++ b/plugins/catalog-react/src/components/index.ts
@@ -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';
diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx
index a410c243a1..6168a3ac3c 100644
--- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx
+++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx
@@ -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;
};
diff --git a/plugins/catalog-react/src/types.ts b/plugins/catalog-react/src/types.ts
index 98ea2fb9b0..07abc9d332 100644
--- a/plugins/catalog-react/src/types.ts
+++ b/plugins/catalog-react/src/types.ts
@@ -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(