([]);
- const context = useContext(filterGroupsContext);
- if (!context) {
- throw new Error(`Must be used inside an EntityFilterGroupsProvider`);
- }
- const setSelectedTagsFilter = context?.setSelectedTags;
-
- const updateSelectedTags = useCallback(
- (tags: string[]) => {
- setSelectedTags(tags);
- setSelectedTagsFilter(tags);
- },
- [setSelectedTags, setSelectedTagsFilter],
- );
-
- return (
- <>
-
-
- Refine Results
- {' '}
-
-
-
-
- Tags
-
-
- {availableTags.map(t => {
- const labelId = `checkbox-list-label-${t}`;
- return (
-
- updateSelectedTags(
- selectedTags.includes(t)
- ? selectedTags.filter(s => s !== t)
- : [...selectedTags, t],
- )
- }
- >
-
-
-
- );
- })}
-
- >
- );
-};
diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx
deleted file mode 100644
index 69ae9b83d6..0000000000
--- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright 2020 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 { useApi } from '@backstage/core';
-import { catalogApiRef } from '@backstage/plugin-catalog-react';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { useAsyncFn } from 'react-use';
-import { filterGroupsContext, FilterGroupsContext } from './context';
-import {
- EntityFilterFn,
- FilterGroup,
- FilterGroupState,
- FilterGroupStates,
-} from './types';
-
-/**
- * Implementation of the shared filter groups state.
- */
-export const EntityFilterGroupsProvider = ({
- children,
-}: {
- children?: React.ReactNode;
-}) => {
- const state = useProvideEntityFilters();
- return (
-
- {children}
-
- );
-};
-
-// The hook that implements the actual context building
-function useProvideEntityFilters(): FilterGroupsContext {
- const catalogApi = useApi(catalogApiRef);
- const [{ value: entities, error }, doReload] = useAsyncFn(async () => {
- const response = await catalogApi.getEntities({
- filter: { kind: 'Component' },
- });
- return response.items;
- });
-
- const filterGroups = useRef<{
- [filterGroupId: string]: FilterGroup;
- }>({});
- const selectedFilterKeys = useRef<{
- [filterGroupId: string]: Set;
- }>({});
- const selectedTags = useRef([]);
- const [filterGroupStates, setFilterGroupStates] = useState<{
- [filterGroupId: string]: FilterGroupStates;
- }>({});
- const [matchingEntities, setMatchingEntities] = useState([]);
- const [availableTags, setAvailableTags] = useState([]);
- const [isCatalogEmpty, setCatalogEmpty] = useState(false);
-
- useEffect(() => {
- doReload();
- }, [doReload]);
-
- const rebuild = useCallback(() => {
- setFilterGroupStates(
- buildStates(
- filterGroups.current,
- selectedFilterKeys.current,
- selectedTags.current,
- entities,
- error,
- ),
- );
- setMatchingEntities(
- buildMatchingEntities(
- filterGroups.current,
- selectedFilterKeys.current,
- selectedTags.current,
- entities,
- ),
- );
- setAvailableTags(collectTags(entities));
- setCatalogEmpty(entities !== undefined && entities.length === 0);
- }, [entities, error]);
-
- const register = useCallback(
- (
- filterGroupId: string,
- filterGroup: FilterGroup,
- initialSelectedFilterIds?: string[],
- ) => {
- filterGroups.current[filterGroupId] = filterGroup;
- selectedFilterKeys.current[filterGroupId] = new Set(
- initialSelectedFilterIds ?? [],
- );
- rebuild();
- },
- [rebuild],
- );
-
- const unregister = useCallback(
- (filterGroupId: string) => {
- delete filterGroups.current[filterGroupId];
- delete selectedFilterKeys.current[filterGroupId];
- rebuild();
- },
- [rebuild],
- );
-
- const setGroupSelectedFilters = useCallback(
- (filterGroupId: string, filters: string[]) => {
- selectedFilterKeys.current[filterGroupId] = new Set(filters);
- rebuild();
- },
- [rebuild],
- );
-
- const setSelectedTags = useCallback(
- (tags: string[]) => {
- selectedTags.current = tags;
- rebuild();
- },
- [rebuild],
- );
-
- const reload = useCallback(async () => {
- await doReload();
- }, [doReload]);
-
- return {
- register,
- unregister,
- setGroupSelectedFilters,
- setSelectedTags,
- reload,
- loading: !error && !entities,
- error,
- filterGroupStates,
- matchingEntities,
- availableTags,
- isCatalogEmpty,
- };
-}
-
-// Given all filter groups and what filters are actually selected, along with
-// the loading state for entities, generate the state of each individual filter
-function buildStates(
- filterGroups: { [filterGroupId: string]: FilterGroup },
- selectedFilterKeys: { [filterGroupId: string]: Set },
- selectedTags: string[],
- entities?: Entity[],
- error?: Error,
-): { [filterGroupId: string]: FilterGroupStates } {
- // On error - all entries are an error state
- if (error) {
- return Object.fromEntries(
- Object.keys(filterGroups).map(filterGroupId => [
- filterGroupId,
- { type: 'error', error },
- ]),
- );
- }
-
- // On startup - all entries are a loading state
- if (!entities) {
- return Object.fromEntries(
- Object.keys(filterGroups).map(filterGroupId => [
- filterGroupId,
- { type: 'loading' },
- ]),
- );
- }
-
- const result: { [filterGroupId: string]: FilterGroupStates } = {};
- for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) {
- const otherMatchingEntities = buildMatchingEntities(
- filterGroups,
- selectedFilterKeys,
- selectedTags,
- entities,
- filterGroupId,
- );
- const groupState: FilterGroupState = { filters: {} };
- for (const [filterId, filterFn] of Object.entries(filterGroup.filters)) {
- const isSelected = !!selectedFilterKeys[filterGroupId]?.has(filterId);
- const matchCount = otherMatchingEntities.filter(entity =>
- filterFn(entity),
- ).length;
- groupState.filters[filterId] = { isSelected, matchCount };
- }
- result[filterGroupId] = { type: 'ready', state: groupState };
- }
-
- return result;
-}
-
-// Given all entites, find all possible tags and provide them in a sorted list.
-function collectTags(entities?: Entity[]): string[] {
- const tags = new Set();
- (entities || []).forEach(e => {
- if (e.metadata.tags) {
- e.metadata.tags.forEach(t => tags.add(t));
- }
- });
- return Array.from(tags).sort();
-}
-
-// Given all filter groups and what filters are actually selected, extract all
-// entities that match all those filter groups.
-function buildMatchingEntities(
- filterGroups: { [filterGroupId: string]: FilterGroup },
- selectedFilterKeys: { [filterGroupId: string]: Set },
- selectedTags: string[],
- entities?: Entity[],
- excludeFilterGroupId?: string,
-): Entity[] {
- // Build one filter fn per filter group
- const allFilters: EntityFilterFn[] = [];
- for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) {
- if (excludeFilterGroupId === filterGroupId) {
- continue;
- }
-
- // Pick out all of the filter functions in the group that are actually
- // selected
- const groupFilters: EntityFilterFn[] = [];
- for (const [filterId, filterFn] of Object.entries(filterGroup.filters)) {
- if (!!selectedFilterKeys[filterGroupId]?.has(filterId)) {
- groupFilters.push(filterFn);
- }
- }
-
- // Need to match any of the selected filters in the group - if there is
- // any at all
- if (groupFilters.length) {
- allFilters.push(entity => groupFilters.some(fn => fn(entity)));
- }
- }
-
- // Filter by tags, if at least one tag is selected. Include all entities
- // that have at least one of the selected tags
- if (selectedTags.length > 0) {
- allFilters.push(
- entity =>
- !!entity.metadata.tags &&
- entity.metadata.tags.some(t => selectedTags.includes(t)),
- );
- }
-
- // All filter groups that had any checked filters need to match. Note that
- // every() always returns true for an empty array.
- return entities?.filter(entity => allFilters.every(fn => fn(entity))) ?? [];
-}
diff --git a/plugins/catalog/src/filter/context.ts b/plugins/catalog/src/filter/context.ts
deleted file mode 100644
index c025480fa6..0000000000
--- a/plugins/catalog/src/filter/context.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2020 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 { createContext } from 'react';
-import { FilterGroup, FilterGroupStates } from './types';
-
-export type FilterGroupsContext = {
- register: (
- filterGroupId: string,
- filterGroup: FilterGroup,
- initialSelectedFilterIds?: string[],
- ) => void;
- unregister: (filterGroupId: string) => void;
- setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void;
- setSelectedTags: (tags: string[]) => void;
- reload: () => Promise;
- loading: boolean;
- error?: Error;
- filterGroupStates: { [filterGroupId: string]: FilterGroupStates };
- matchingEntities: Entity[];
- availableTags: string[];
- isCatalogEmpty: boolean;
-};
-
-/**
- * The context that maintains shared state for all visible filter groups.
- */
-export const filterGroupsContext = createContext<
- FilterGroupsContext | undefined
->(undefined);
diff --git a/plugins/catalog/src/filter/index.ts b/plugins/catalog/src/filter/index.ts
deleted file mode 100644
index da73147ef9..0000000000
--- a/plugins/catalog/src/filter/index.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2020 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 { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider';
-export type {
- EntityFilterFn,
- FilterGroup,
- FilterGroupState,
- FilterGroupStates,
- FilterGroupStatesError,
- FilterGroupStatesLoading,
- FilterGroupStatesReady,
-} from './types';
-export { useEntityFilterGroup } from './useEntityFilterGroup';
-export { useFilteredEntities } from './useFilteredEntities';
diff --git a/plugins/catalog/src/filter/types.ts b/plugins/catalog/src/filter/types.ts
deleted file mode 100644
index ed08b131bf..0000000000
--- a/plugins/catalog/src/filter/types.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2020 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';
-
-export type EntityFilterFn = (entity: Entity) => boolean;
-
-export type FilterGroup = {
- filters: {
- [filterId: string]: EntityFilterFn;
- };
-};
-
-export type FilterGroupState = {
- filters: {
- [filterId: string]: {
- isSelected: boolean;
- matchCount: number;
- };
- };
-};
-
-export type FilterGroupStatesReady = {
- type: 'ready';
- state: FilterGroupState;
-};
-
-export type FilterGroupStatesError = {
- type: 'error';
- error: Error;
-};
-
-export type FilterGroupStatesLoading = {
- type: 'loading';
-};
-
-export type FilterGroupStates =
- | FilterGroupStatesReady
- | FilterGroupStatesError
- | FilterGroupStatesLoading;
diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx
deleted file mode 100644
index 8a5bf60b06..0000000000
--- a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2020 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 { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core';
-import { catalogApiRef } from '@backstage/plugin-catalog-react';
-import { MockStorageApi } from '@backstage/test-utils';
-import { act, renderHook } from '@testing-library/react-hooks';
-import React from 'react';
-import { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider';
-import { FilterGroup, FilterGroupStatesReady } from './types';
-import { useEntityFilterGroup } from './useEntityFilterGroup';
-
-describe('useEntityFilterGroup', () => {
- let catalogApi: jest.Mocked;
- let wrapper: ({ children }: { children?: React.ReactNode }) => JSX.Element;
-
- beforeEach(() => {
- catalogApi = {
- /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
- addLocation: jest.fn(_a => new Promise(() => {})),
- getEntities: jest.fn(),
- getOriginLocationByEntity: jest.fn(),
- getLocationByEntity: jest.fn(),
- getLocationById: jest.fn(),
- removeLocationById: jest.fn(),
- removeEntityByUid: jest.fn(),
- getEntityByName: jest.fn(),
- };
- const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
- storageApiRef,
- MockStorageApi.create(),
- );
- wrapper = ({ children }: { children?: React.ReactNode }) => (
-
- {children}
-
- );
- });
-
- it('works for an empty set of filters', async () => {
- catalogApi.getEntities.mockResolvedValue({ items: [] });
- const group: FilterGroup = { filters: {} };
- const { result, waitFor } = renderHook(
- () => useEntityFilterGroup('g1', group),
- { wrapper },
- );
-
- await waitFor(() => expect(result.current.state.type).toBe('ready'));
- });
-
- it('works for a single group', async () => {
- catalogApi.getEntities.mockResolvedValue({
- items: [
- {
- apiVersion: 'backstage.io/v1alpha1',
- kind: 'Component',
- metadata: { name: 'n' },
- },
- ],
- });
- const group: FilterGroup = {
- filters: {
- f1: e => e.metadata.name === 'n',
- f2: e => e.metadata.name !== 'n',
- },
- };
- const { result, waitFor } = renderHook(
- () => useEntityFilterGroup('g1', group),
- { wrapper },
- );
-
- await waitFor(() => expect(result.current.state.type).toEqual('ready'));
- let state = result.current.state as FilterGroupStatesReady;
- expect(state.state.filters.f1).toEqual({
- isSelected: false,
- matchCount: 1,
- });
- expect(state.state.filters.f2).toEqual({
- isSelected: false,
- matchCount: 0,
- });
-
- act(() => result.current.setSelectedFilters(['f1']));
-
- await waitFor(() => expect(result.current.state.type).toEqual('ready'));
- state = result.current.state as FilterGroupStatesReady;
- expect(state.state.filters.f1).toEqual({
- isSelected: true,
- matchCount: 1,
- });
- expect(state.state.filters.f2).toEqual({
- isSelected: false,
- matchCount: 0,
- });
-
- act(() => result.current.setSelectedFilters(['f2']));
-
- await waitFor(() => expect(result.current.state.type).toEqual('ready'));
- state = result.current.state as FilterGroupStatesReady;
- expect(state.state.filters.f1).toEqual({
- isSelected: false,
- matchCount: 1,
- });
- expect(state.state.filters.f2).toEqual({
- isSelected: true,
- matchCount: 0,
- });
- });
-});
diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.ts b/plugins/catalog/src/filter/useEntityFilterGroup.ts
deleted file mode 100644
index 30214fad78..0000000000
--- a/plugins/catalog/src/filter/useEntityFilterGroup.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2020 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 { useCallback, useContext, useEffect, useMemo } from 'react';
-import { filterGroupsContext } from './context';
-import { FilterGroup, FilterGroupStates } from './types';
-
-export type EntityFilterGroupOutput = {
- state: FilterGroupStates;
- setSelectedFilters: (filterIds: string[]) => void;
-};
-
-/**
- * Hook that exposes the relevant data and operations for a single filter
- * group.
- */
-export const useEntityFilterGroup = (
- filterGroupId: string,
- filterGroup: FilterGroup,
- initialSelectedFilters?: string[],
-): EntityFilterGroupOutput => {
- const context = useContext(filterGroupsContext);
- if (!context) {
- throw new Error(`Must be used inside an EntityFilterGroupsProvider`);
- }
- const {
- register,
- unregister,
- setGroupSelectedFilters,
- filterGroupStates,
- } = context;
-
- // on state changes unregisters and registers the filtergroup
- // ensure that it re-registers with the correct filter as the prop changes and not the default
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const initialMemo = useMemo(() => {
- return initialSelectedFilters?.slice();
- }, [initialSelectedFilters]);
-
- // Register the group on mount, and unregister on unmount
- useEffect(() => {
- register(filterGroupId, filterGroup, initialMemo);
- return () => unregister(filterGroupId);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [register, unregister, filterGroupId, filterGroup]);
-
- const setSelectedFilters = useCallback(
- (filters: string[]) => {
- setGroupSelectedFilters(filterGroupId, filters);
- },
- [setGroupSelectedFilters, filterGroupId],
- );
-
- let state = filterGroupStates[filterGroupId];
- if (!state) {
- state = { type: 'loading' };
- }
-
- return { state, setSelectedFilters };
-};
diff --git a/plugins/catalog/src/filter/useFilteredEntities.ts b/plugins/catalog/src/filter/useFilteredEntities.ts
deleted file mode 100644
index 2d7dcfd89d..0000000000
--- a/plugins/catalog/src/filter/useFilteredEntities.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2020 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 { useContext } from 'react';
-import { filterGroupsContext } from './context';
-
-/**
- * Hook that exposes the result of applying a set of filter groups.
- */
-export function useFilteredEntities() {
- const context = useContext(filterGroupsContext);
- if (!context) {
- throw new Error(`Must be used inside an EntityFilterGroupsProvider`);
- }
-
- return {
- loading: context.loading,
- error: context.error,
- matchingEntities: context.matchingEntities,
- availableTags: context.availableTags,
- isCatalogEmpty: context.isCatalogEmpty,
- reload: context.reload,
- };
-}