diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx index 8faa86c3ca..417b085cce 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -17,7 +17,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityKindFilter } from '../../types'; +import { EntityKindFilter } from '../../filters'; import { EntityKindPicker } from './EntityKindPicker'; describe('', () => { diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx index db695d58cc..92831ea252 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx @@ -17,7 +17,7 @@ import React, { useEffect, useState } from 'react'; import { Alert } from '@material-ui/lab'; import { useEntityListProvider } from '../../hooks'; -import { EntityKindFilter } from '../../types'; +import { EntityKindFilter } from '../../filters'; type EntityKindFilterProps = { initialFilter?: string; diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index 8e8bb5aeba..8fa0789c10 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -18,7 +18,7 @@ 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 { EntityLifecycleFilter } from '../../filters'; import { EntityLifecyclePicker } from './EntityLifecyclePicker'; const sampleEntities: Entity[] = [ diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index a5fe16ae40..31ab2911f7 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -28,7 +28,7 @@ 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'; +import { EntityLifecycleFilter } from '../../filters'; const icon = ; const checkedIcon = ; diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index c39fcb887a..c46f6ed8f8 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -18,7 +18,7 @@ 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 { EntityOwnerFilter } from '../../filters'; import { EntityOwnerPicker } from './EntityOwnerPicker'; const sampleEntities: Entity[] = [ diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 665529fddb..955f917c8c 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -28,7 +28,7 @@ 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'; +import { EntityOwnerFilter } from '../../filters'; import { getEntityRelations } from '../../utils'; import { formatEntityRefTitle } from '../EntityRefLink'; diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx index 8b4ceb4821..0b9ea9cb98 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx @@ -17,30 +17,10 @@ import React from 'react'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { EntitySearchBar } from './EntitySearchBar'; -import { Entity } from '@backstage/catalog-model'; import { DefaultEntityFilters } from '../../hooks/useEntityListProvider'; -import { EntityTextFilter } from '../../types'; +import { EntityTextFilter } from '../../filters'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -const entities: Entity[] = [ - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'react-app', - tags: ['react', 'experimental'], - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'gRPC service', - tags: ['gRPC', 'java'], - }, - }, -]; - describe('EntitySearchBar', () => { it('should display search value and execute set callback', async () => { const updateFilters = jest.fn(); @@ -50,9 +30,7 @@ describe('EntitySearchBar', () => { }; const { getByDisplayValue } = render( - + , ); diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index dbee44a464..9fd30b5182 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -27,7 +27,7 @@ import Search from '@material-ui/icons/Search'; import React, { useState } from 'react'; import { useDebounce } from 'react-use'; import { useEntityListProvider } from '../../hooks/useEntityListProvider'; -import { EntityTextFilter } from '../../types'; +import { EntityTextFilter } from '../../filters'; const useStyles = makeStyles(_theme => ({ searchToolbar: { diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 9d65221b76..8b1b97a261 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityTagFilter } from '../../types'; +import { EntityTagFilter } from '../../filters'; import { EntityTagPicker } from './EntityTagPicker'; const taggedEntities: Entity[] = [ diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index bd92b68478..744ad447d8 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -28,7 +28,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useMemo } from 'react'; import { useEntityListProvider } from '../../hooks/useEntityListProvider'; -import { EntityTagFilter } from '../../types'; +import { EntityTagFilter } from '../../filters'; const icon = ; const checkedIcon = ; diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 29ec36d625..fa74b34717 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -22,7 +22,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityTypePicker } from './EntityTypePicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { catalogApiRef } from '../../api'; -import { EntityKindFilter, EntityTypeFilter } from '../../types'; +import { EntityKindFilter, EntityTypeFilter } from '../../filters'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index fb70c72d5d..da7c26228d 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -23,7 +23,7 @@ import { } from '@backstage/catalog-model'; import { UserListPicker } from './UserListPicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityTagFilter, UserListFilter } from '../../types'; +import { EntityTagFilter, UserListFilter } from '../../filters'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { MockStorageApi } from '@backstage/test-utils'; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 65ccb423da..e03ef79bad 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -16,7 +16,8 @@ import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { compact } from 'lodash'; -import { UserListFilter, UserListFilterKind } from '../../types'; +import { UserListFilterKind } from '../../types'; +import { UserListFilter } from '../../filters'; import { useEntityListProvider, useOwnUser, diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts new file mode 100644 index 0000000000..fa787cbf53 --- /dev/null +++ b/plugins/catalog-react/src/filters.test.ts @@ -0,0 +1,92 @@ +/* + * 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, TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { EntityTextFilter } from './filters'; + +const entities: Entity[] = [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'react-app', + tags: ['react', 'experimental'], + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'gRPC service', + tags: ['gRPC', 'java'], + }, + }, +]; + +const templates: TemplateEntityV1beta2[] = [ + { + apiVersion: 'backstage.io/v1beta2', + kind: 'Template', + metadata: { + name: 'react-app', + title: 'Create React App Template', + tags: ['react', 'experimental'], + }, + spec: { + type: '', + steps: [], + }, + }, + { + apiVersion: 'backstage.io/v1beta2', + kind: 'Template', + metadata: { + name: 'gRPC service', + title: 'Spring Boot gRPC Service', + tags: ['gRPC', 'java'], + }, + spec: { + type: '', + steps: [], + }, + }, +]; + +describe('EntityTextFilter', () => { + it('should search name', () => { + const filter = new EntityTextFilter('app'); + expect(filter.filterEntity(entities[0])).toBeTruthy(); + expect(filter.filterEntity(entities[1])).toBeFalsy(); + }); + + it('should search template title', () => { + const filter = new EntityTextFilter('spring'); + expect(filter.filterEntity(templates[0])).toBeFalsy(); + expect(filter.filterEntity(templates[1])).toBeTruthy(); + }); + + it('should search tags', () => { + const filter = new EntityTextFilter('java'); + expect(filter.filterEntity(entities[0])).toBeFalsy(); + expect(filter.filterEntity(entities[1])).toBeTruthy(); + }); + + it('should be case insensitive', () => { + const filter = new EntityTextFilter('JaVa'); + expect(filter.filterEntity(entities[0])).toBeFalsy(); + expect(filter.filterEntity(entities[1])).toBeTruthy(); + }); +}); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts new file mode 100644 index 0000000000..7a80eb6d31 --- /dev/null +++ b/plugins/catalog-react/src/filters.ts @@ -0,0 +1,108 @@ +/* + * 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, + UserEntity, + RELATION_OWNED_BY, +} from '@backstage/catalog-model'; +import { EntityFilter, UserListFilterKind } from './types'; +import { getEntityRelations, isOwnerOf } from './utils'; +import { formatEntityRefTitle } from './components/EntityRefLink'; + +export class EntityKindFilter implements EntityFilter { + constructor(readonly value: string) {} + + getCatalogFilters(): Record { + return { kind: this.value }; + } +} + +export class EntityTypeFilter implements EntityFilter { + constructor(readonly value: string) {} + + getCatalogFilters(): Record { + return { 'spec.type': this.value }; + } +} + +export class EntityTagFilter implements EntityFilter { + constructor(readonly values: string[]) {} + + filterEntity(entity: Entity): boolean { + return this.values.every(v => (entity.metadata.tags ?? []).includes(v)); + } +} + +export class EntityTextFilter implements EntityFilter { + constructor(readonly value: string) {} + + filterEntity(entity: Entity): boolean { + const upperCaseValue = this.value.toLocaleUpperCase('en-US'); + + return ( + entity.metadata.name + .toLocaleUpperCase('en-US') + .includes(upperCaseValue) || + `${entity.metadata.title}` + .toLocaleUpperCase('en-US') + .includes(upperCaseValue) || + entity.metadata.tags + ?.join('') + .toLocaleUpperCase('en-US') + .indexOf(upperCaseValue) !== -1 + ); + } +} + +export class EntityOwnerFilter implements EntityFilter { + constructor(readonly values: string[]) {} + + filterEntity(entity: Entity): boolean { + return this.values.some(v => + getEntityRelations(entity, RELATION_OWNED_BY).some( + o => formatEntityRefTitle(o, { defaultKind: 'group' }) === v, + ), + ); + } +} + +export class EntityLifecycleFilter implements EntityFilter { + constructor(readonly values: string[]) {} + + filterEntity(entity: Entity): boolean { + return this.values.some(v => entity.spec?.lifecycle === v); + } +} + +export class UserListFilter implements EntityFilter { + constructor( + readonly value: UserListFilterKind, + readonly user: UserEntity | undefined, + readonly isStarredEntity: (entity: Entity) => boolean, + ) {} + + filterEntity(entity: Entity): boolean { + switch (this.value) { + case 'owned': + return this.user !== undefined && isOwnerOf(this.user, entity); + case 'starred': + return this.isStarredEntity(entity); + default: + return true; + } + } +} diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 7ad5b1c228..ee5da0bb12 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -24,12 +24,8 @@ import { useEntityListProvider, } from './useEntityListProvider'; import { catalogApiRef } from '../api'; -import { - EntityKindFilter, - EntityTypeFilter, - UserListFilter, - UserListFilterKind, -} from '../types'; +import { UserListFilterKind } from '../types'; +import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../filters'; import { EntityKindPicker, UserListPicker } from '../components'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 525ce0a146..1b96b1f759 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -26,7 +26,6 @@ import React, { import { useAsyncFn, useDebounce } from 'react-use'; import { catalogApiRef } from '../api'; import { - EntityFilter, EntityKindFilter, EntityLifecycleFilter, EntityOwnerFilter, @@ -34,7 +33,8 @@ import { EntityTextFilter, EntityTypeFilter, UserListFilter, -} from '../types'; +} from '../filters'; +import { EntityFilter } from '../types'; import { reduceCatalogFilters, reduceEntityFilters } from '../utils'; import { useApi } from '@backstage/core-plugin-api'; diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 55d9f9ca95..aeda12c6f8 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -16,13 +16,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '../api'; import { DefaultEntityFilters, useEntityListProvider, } from './useEntityListProvider'; -import { EntityTypeFilter } from '../types'; -import { useApi } from '@backstage/core-plugin-api'; +import { EntityTypeFilter } from '../filters'; type EntityTypeReturn = { loading: boolean; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 62426432a2..4ee613012d 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -17,6 +17,7 @@ export type { CatalogApi } from '@backstage/catalog-client'; export { catalogApiRef } from './api'; export * from './components'; export * from './hooks'; +export * from './filters'; export { catalogRouteRef, entityRoute, diff --git a/plugins/catalog-react/src/types.ts b/plugins/catalog-react/src/types.ts index a41e08024a..68c11d7f38 100644 --- a/plugins/catalog-react/src/types.ts +++ b/plugins/catalog-react/src/types.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -import { - Entity, - RELATION_OWNED_BY, - UserEntity, -} from '@backstage/catalog-model'; -import { getEntityRelations, isOwnerOf } from './utils'; -import { formatEntityRefTitle } from './components/EntityRefLink'; +import { Entity } from '@backstage/catalog-model'; export type EntityFilter = { /** @@ -42,85 +36,4 @@ export type EntityFilter = { filterEntity?: (entity: Entity) => boolean; }; -export class EntityKindFilter implements EntityFilter { - constructor(readonly value: string) {} - - getCatalogFilters(): Record { - return { kind: this.value }; - } -} - -export class EntityTypeFilter implements EntityFilter { - constructor(readonly value: string) {} - - getCatalogFilters(): Record { - return { 'spec.type': this.value }; - } -} - -export class EntityTagFilter implements EntityFilter { - constructor(readonly values: string[]) {} - - filterEntity(entity: Entity): boolean { - return this.values.every(v => (entity.metadata.tags ?? []).includes(v)); - } -} - -// TODO(chaseajen): add unit test for logic -export class EntityTextFilter implements EntityFilter { - constructor(readonly value: string) {} - - filterEntity(entity: Entity): boolean { - const upperCaseValue = this.value.toLocaleUpperCase('en-US'); - - return ( - `${entity.metadata.title}` - .toLocaleUpperCase('en-US') - .includes(upperCaseValue) || - entity.metadata.tags - ?.join('') - .toLocaleUpperCase('en-US') - .indexOf(upperCaseValue) !== -1 - ); - } -} - -export class EntityOwnerFilter implements EntityFilter { - constructor(readonly values: string[]) {} - - filterEntity(entity: Entity): boolean { - return this.values.some(v => - getEntityRelations(entity, RELATION_OWNED_BY).some( - o => formatEntityRefTitle(o, { defaultKind: 'group' }) === 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( - readonly value: UserListFilterKind, - readonly user: UserEntity | undefined, - readonly isStarredEntity: (entity: Entity) => boolean, - ) {} - - filterEntity(entity: Entity): boolean { - switch (this.value) { - case 'owned': - return this.user !== undefined && isOwnerOf(this.user, entity); - case 'starred': - return this.isStarredEntity(entity); - default: - return true; - } - } -}