extract filters and add tests for EntityTextFilter

Co-authored-by: Tim <timbonicus@gmail.com>
Signed-off-by: Chase Rutherford-Jenkins <chaseajen@users.noreply.github.com>
This commit is contained in:
Chase Rutherford-Jenkins
2021-06-16 16:03:01 -07:00
committed by Tim Hansen
parent c019accb18
commit 3809dc025b
20 changed files with 223 additions and 134 deletions
@@ -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('<EntityKindPicker/>', () => {
@@ -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;
@@ -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[] = [
@@ -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 = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
@@ -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[] = [
@@ -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';
@@ -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(
<MockEntityListContextProvider
value={{ entities, updateFilters, filters }}
>
<MockEntityListContextProvider value={{ updateFilters, filters }}>
<EntitySearchBar />
</MockEntityListContextProvider>,
);
@@ -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: {
@@ -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[] = [
@@ -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 = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
@@ -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';
@@ -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';
@@ -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,
+92
View File
@@ -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();
});
});
+108
View File
@@ -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<string, string | string[]> {
return { kind: this.value };
}
}
export class EntityTypeFilter implements EntityFilter {
constructor(readonly value: string) {}
getCatalogFilters(): Record<string, string | string[]> {
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;
}
}
}
@@ -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';
@@ -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';
@@ -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;
+1
View File
@@ -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,
+1 -88
View File
@@ -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<string, string | string[]> {
return { kind: this.value };
}
}
export class EntityTypeFilter implements EntityFilter {
constructor(readonly value: string) {}
getCatalogFilters(): Record<string, string | string[]> {
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;
}
}
}