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:
committed by
Tim Hansen
parent
c019accb18
commit
3809dc025b
@@ -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;
|
||||
|
||||
+1
-1
@@ -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[] = [
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user