Enable Filtering of the Catalog Page (#1143)

* feat(catalog/Filters): Add ability to render a React component as the count. Good for loading states and async data rendering for counts.

* chore(Catalog/filters): Updating the table so that we use loading states of the table rather than the panel

* chore(Catalog/filters): added some nice things for enabling the filtering with some nice count componeents

* feat(Catalog/filters): Fetch the correct data and added in enum types to make some nice resolvers

* chore(Catalog/filters): Use the new enum type here

* chore(Catalog/filters): Removing the unused import to fix lintig

* chore(Catalog/filters): Addressing some PR comments

* feat(catalog/filters): Making WebStorage return the same instance for the same bucket for subscriptions

* chore(core/Storage): fixing some issues with different instances of the storage and adding tests for it

* chore(catalog/filters): fixing some tests and trying to remove some of the act warnings in the tests
This commit is contained in:
Ben Lambert
2020-06-09 13:28:40 +02:00
committed by GitHub
parent 4d7b28e89c
commit 00d33f93e4
22 changed files with 400 additions and 78 deletions
@@ -15,6 +15,7 @@
*/
import { WebStorage } from './WebStorage';
import { CreateStorageApiOptions, StorageApi } from '../../definitions';
describe('WebStorage Storage API', () => {
const mockErrorApi = { post: jest.fn(), error$: jest.fn() };
const createWebStorage = (
@@ -161,4 +162,12 @@ describe('WebStorage Storage API', () => {
}),
);
});
it('should return a singleton for the same namespace and same bucket', async () => {
const rootStorage = createWebStorage({
namespace: '/Test/Mock/Thing/Thing ',
});
expect(rootStorage.forBucket('test')).toBe(rootStorage.forBucket('test'));
});
});
@@ -22,6 +22,8 @@ import {
import { Observable } from '../../../types';
import ObservableImpl from 'zen-observable';
const buckets = new Map<string, WebStorage>();
export class WebStorage implements StorageApi {
constructor(
private readonly namespace: string,
@@ -46,7 +48,11 @@ export class WebStorage implements StorageApi {
}
forBucket(name: string): WebStorage {
return new WebStorage(`${this.namespace}/${name}`, this.errorApi);
const bucketPath = `${this.namespace}/${name}`;
if (!buckets.has(bucketPath)) {
buckets.set(bucketPath, new WebStorage(bucketPath, this.errorApi));
}
return buckets.get(bucketPath)!;
}
async set<T>(key: string, data: T): Promise<void> {
@@ -25,7 +25,7 @@ import {
storageApiRef,
StorageApi,
WebStorage,
} from '@backstage/core';
} from '@backstage/core-api';
export default {
title: 'DismissableBanner',
@@ -25,7 +25,7 @@ import {
CreateStorageApiOptions,
StorageApi,
WebStorage,
} from '@backstage/core';
} from '@backstage/core-api';
describe('<DismissableBanner />', () => {
let apis: ApiRegistry;
@@ -15,7 +15,7 @@
*/
import React, { FC, ReactNode, useState, useEffect } from 'react';
import { useApi, storageApiRef } from '@backstage/core';
import { useApi, storageApiRef } from '@backstage/core-api';
import { useObservable } from 'react-use';
import classNames from 'classnames';
import { makeStyles, Theme } from '@material-ui/core';
+3 -1
View File
@@ -42,11 +42,13 @@
"@backstage/test-utils": "^0.1.1-alpha.6",
"@testing-library/jest-dom": "^5.7.0",
"@testing-library/react": "^9.3.2",
"@testing-library/react-hooks": "^3.3.0",
"@testing-library/user-event": "^10.2.4",
"@types/jest": "^25.2.2",
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "^5.0.4",
"jest-fetch-mock": "^3.0.3"
"jest-fetch-mock": "^3.0.3",
"react-test-renderer": "^16.13.1"
},
"files": [
"dist/**/*.{js,d.ts}"
@@ -0,0 +1,32 @@
/*
* 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 React from 'react';
import { useApi } from '@backstage/core';
import { catalogApiRef } from '../../api/types';
import { useAsync } from 'react-use';
import { CircularProgress, useTheme } from '@material-ui/core';
export const AllServicesCount: React.FC<{}> = () => {
const theme = useTheme();
const catalogApi = useApi(catalogApiRef);
const { value, loading } = useAsync(() => catalogApi.getEntities());
if (loading) {
return <CircularProgress size={theme.spacing(2)} />;
}
return <span>{value?.length ?? '-'}</span>;
};
@@ -18,6 +18,7 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter';
import { FilterGroupItem } from '../../types';
describe('Catalog Filter', () => {
it('should render the different groups', async () => {
@@ -40,11 +41,11 @@ describe('Catalog Filter', () => {
name: 'Test Group 1',
items: [
{
id: 'first',
id: FilterGroupItem.ALL,
label: 'First Label',
},
{
id: 'second',
id: FilterGroupItem.STARRED,
label: 'Second Label',
},
],
@@ -67,12 +68,12 @@ describe('Catalog Filter', () => {
name: 'Test Group 1',
items: [
{
id: 'first',
id: FilterGroupItem.ALL,
label: 'First Label',
count: 100,
},
{
id: 'second',
id: FilterGroupItem.STARRED,
label: 'Second Label',
count: 400,
},
@@ -96,12 +97,12 @@ describe('Catalog Filter', () => {
name: 'Test Group 1',
items: [
{
id: 'first',
id: FilterGroupItem.ALL,
label: 'First Label',
count: 100,
},
{
id: 'second',
id: FilterGroupItem.STARRED,
label: 'Second Label',
count: 400,
},
@@ -128,4 +129,29 @@ describe('Catalog Filter', () => {
expect(onSelectedChangeHandler).toHaveBeenCalledWith(item);
});
it('should render a component when a function is passed to the count component', async () => {
const mockGroups: CatalogFilterGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: FilterGroupItem.ALL,
label: 'First Label',
count: () => <b>BACKSTAGE!</b>,
},
{
id: FilterGroupItem.STARRED,
label: 'Second Label',
count: 400,
},
],
},
];
const { findByText } = render(
wrapInTestApp(<CatalogFilter groups={mockGroups} />),
);
expect(await findByText('BACKSTAGE!')).toBeInTheDocument();
});
});
@@ -25,13 +25,12 @@ import {
makeStyles,
} from '@material-ui/core';
import type { IconComponent } from '@backstage/core';
import { FilterGroupItem } from '../../types';
export type CatalogFilterItem = {
id: string;
id: FilterGroupItem;
label: string;
icon?: IconComponent;
count?: number;
loading?: boolean;
count?: number | React.FC;
};
export type CatalogFilterGroup = {
@@ -105,7 +104,11 @@ export const CatalogFilter: React.FC<CatalogFilterProps> = ({
{item.label}
</Typography>
</ListItemText>
{item.count}
{typeof item.count === 'function' ? (
<item.count />
) : (
item.count
)}
</MenuItem>
))}
</List>
@@ -0,0 +1,33 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { StarredCount } from './StarredCount';
import * as Hooks from '../../hooks/useStarredEntites';
describe('Starred Count', () => {
it('should render the count returned from the hook', async () => {
jest.spyOn(Hooks, 'useStarredEntities').mockReturnValue({
starredEntities: new Set(['id1', 'id2', 'id3', 'id4']),
});
const { findByText } = render(wrapInTestApp(<StarredCount />));
expect(await findByText('4')).toBeInTheDocument();
});
});
@@ -0,0 +1,22 @@
/*
* 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 React from 'react';
import { useStarredEntities } from '../../hooks/useStarredEntites';
export const StarredCount: React.FC<{}> = () => {
const { starredEntities } = useStarredEntities();
return <span>{starredEntities.size}</span>;
};
@@ -18,47 +18,34 @@ import React from 'react';
import { render } from '@testing-library/react';
import CatalogPage from './CatalogPage';
import {
ApiProvider,
ApiRegistry,
CreateStorageApiOptions,
ApiProvider,
errorApiRef,
storageApiRef,
StorageApi,
WebStorage,
} from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { wrapInTestApp, MockErrorApi } from '@backstage/test-utils';
import { catalogApiRef } from '../..';
import { CatalogApi } from '../../api/types';
import { Entity } from '@backstage/catalog-model';
const errorApi = { post: () => {} };
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve([
{
metadata: {
name: 'Entity1',
},
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
},
] as Entity[]),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
const mockWebStorageErrorApi = { post: jest.fn(), error$: jest.fn() };
const createWebStorage = (
args?: Partial<CreateStorageApiOptions>,
): StorageApi => {
return WebStorage.create({
errorApi: mockWebStorageErrorApi,
...args,
});
};
const storageApi = createWebStorage();
describe('CatalogPage', () => {
const mockErrorApi = new MockErrorApi();
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve([
{
metadata: {
name: 'Entity1',
},
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
},
] as Entity[]),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
// this test right now causes some red lines in the log output when running tests
// related to some theme issues in mui-table
// https://github.com/mbrn/material-table/issues/1293
@@ -67,9 +54,9 @@ describe('CatalogPage', () => {
wrapInTestApp(
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[errorApiRef, mockErrorApi],
[catalogApiRef, catalogApi],
[storageApiRef, storageApi],
[storageApiRef, new WebStorage('@mock', mockErrorApi)],
])}
>
<CatalogPage />
@@ -27,19 +27,22 @@ import {
useApi,
} from '@backstage/core';
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
import { Button, Link, makeStyles, Typography } from '@material-ui/core';
import { Button, makeStyles, Typography, Link } from '@material-ui/core';
import GitHub from '@material-ui/icons/GitHub';
import React, { FC, useCallback, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { useAsync } from 'react-use';
import { catalogApiRef } from '../..';
import { Component } from '../../data/component';
import { defaultFilter, filterGroups } from '../../data/filters';
import { defaultFilter, filterGroups, dataResolvers } from '../../data/filters';
import { entityToComponent, findLocationForEntityMeta } from '../../data/utils';
import {
CatalogFilter,
CatalogFilterItem,
} from '../CatalogFilter/CatalogFilter';
import { useStarredEntities } from '../../hooks/useStarredEntites';
import CatalogTable from '../CatalogTable/CatalogTable';
const useStyles = makeStyles(theme => ({
@@ -57,15 +60,20 @@ const useStyles = makeStyles(theme => ({
const CatalogPage: FC<{}> = () => {
const catalogApi = useApi(catalogApiRef);
const { value, error, loading } = useAsync(() => catalogApi.getEntities());
const { starredEntities } = useStarredEntities();
const [selectedFilter, setSelectedFilter] = useState<CatalogFilterItem>(
defaultFilter,
);
const { value, error, loading } = useAsync(
() => dataResolvers[selectedFilter.id]({ catalogApi, starredEntities }),
[selectedFilter.id],
);
const onFilterSelected = useCallback(
selected => setSelectedFilter(selected),
[],
);
const styles = useStyles();
const actions = [
@@ -41,16 +41,6 @@ const components: Component[] = [
];
describe('CatalogTable component', () => {
it('should render loading when loading prop it set to true', async () => {
const rendered = render(
wrapInTestApp(
<CatalogTable titlePreamble="Owned" components={[]} loading />,
),
);
const progress = await rendered.findByTestId('progress');
expect(progress).toBeInTheDocument();
});
it('should render error message when error is passed in props', async () => {
const rendered = render(
wrapInTestApp(
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Progress, Table, TableColumn } from '@backstage/core';
import { Table, TableColumn } from '@backstage/core';
import { Link } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React, { FC } from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { Component } from '../../data/component';
import { entityRoute } from '../../routes';
const columns: TableColumn[] = [
@@ -60,9 +61,7 @@ const CatalogTable: FC<CatalogTableProps> = ({
titlePreamble,
actions,
}) => {
if (loading) {
return <Progress />;
} else if (error) {
if (error) {
return (
<div>
<Alert severity="error">
@@ -74,8 +73,14 @@ const CatalogTable: FC<CatalogTableProps> = ({
return (
<Table
isLoading={loading}
columns={columns}
options={{ paging: false, actionsColumnIndex: -1 }}
options={{
paging: false,
actionsColumnIndex: -1,
loadingType: 'linear',
showEmptyDataSourceMessage: !loading,
}}
title={`${titlePreamble} (${(components && components.length) || 0})`}
data={components}
actions={actions}
@@ -22,7 +22,7 @@ describe('ComponentContextMenu', () => {
it('should call onUnregisterComponent on button click', async () => {
await act(async () => {
const mockCallback = jest.fn();
const menu = await render(
const menu = render(
<ComponentContextMenu onUnregisterComponent={mockCallback} />,
);
const button = await menu.findByTestId('menu-button');
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import ComponentPage from './ComponentPage';
import { render } from '@testing-library/react';
import { render, wait } from '@testing-library/react';
import * as React from 'react';
import { wrapInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
@@ -38,7 +38,7 @@ const errorApi = { post: () => {} };
describe('ComponentPage', () => {
it('should redirect to component table page when name is not provided', async () => {
const props = getTestProps('');
await render(
render(
wrapInTestApp(
<ApiProvider
apis={ApiRegistry.from([
@@ -55,6 +55,9 @@ describe('ComponentPage', () => {
</ApiProvider>,
),
);
expect(props.history.push).toHaveBeenCalledWith('/catalog');
await wait(() =>
expect(props.history.push).toHaveBeenCalledWith('/catalog'),
);
});
});
+33 -6
View File
@@ -19,21 +19,26 @@ import {
} from '../components/CatalogFilter/CatalogFilter';
import SettingsIcon from '@material-ui/icons/Settings';
import StarIcon from '@material-ui/icons/Star';
import { StarredCount } from '../components/CatalogFilter/StarredCount';
import { AllServicesCount } from '../components/CatalogFilter/AllServicesCount';
import { FilterGroupItem } from '../types';
import { CatalogApi } from '../..';
import { Entity } from '@backstage/catalog-model';
export const filterGroups: CatalogFilterGroup[] = [
{
name: 'Personal',
items: [
{
id: 'owned',
id: FilterGroupItem.OWNED,
label: 'Owned',
count: 123,
count: 0,
icon: SettingsIcon,
},
{
id: 'starred',
id: FilterGroupItem.STARRED,
label: 'Starred',
count: 10,
count: StarredCount,
icon: StarIcon,
},
],
@@ -43,12 +48,34 @@ export const filterGroups: CatalogFilterGroup[] = [
name: 'Company',
items: [
{
id: 'all',
id: FilterGroupItem.ALL,
label: 'All Services',
count: 123,
count: AllServicesCount,
},
],
},
];
type ResolverFunction = ({
catalogApi,
starredEntities,
}: {
catalogApi: CatalogApi;
starredEntities: Set<string>;
}) => Promise<Entity[]>;
export const dataResolvers: Record<FilterGroupItem, ResolverFunction> = {
[FilterGroupItem.OWNED]: async () => [],
[FilterGroupItem.ALL]: async ({ catalogApi }) => {
return catalogApi.getEntities();
},
[FilterGroupItem.STARRED]: async ({ catalogApi, starredEntities }) => {
const allEntities = await catalogApi.getEntities();
return allEntities.filter(entity =>
starredEntities.has(entity.metadata.name),
);
},
};
export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0];
@@ -0,0 +1,43 @@
/*
* 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 { useState, useEffect } from 'react';
import { useApi, storageApiRef } from '@backstage/core';
import { useObservable } from 'react-use';
export const useStarredEntities = () => {
const storageApi = useApi(storageApiRef);
const settingsStore = storageApi.forBucket('settings');
const rawStarredItems = settingsStore.get<string[]>('starredEntities') ?? [];
const [starredEntities, setStarredEntities] = useState(
new Set(rawStarredItems),
);
const observedItems = useObservable(
settingsStore.observe$<string[]>('starredEntities'),
);
useEffect(() => {
if (observedItems?.newValue) {
const currentValue = observedItems?.newValue ?? [];
setStarredEntities(new Set(currentValue));
}
}, [observedItems?.newValue]);
return {
starredEntities,
};
};
@@ -0,0 +1,80 @@
/*
* 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 React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { useStarredEntities } from './useStarredEntites';
import {
ApiProvider,
ApiRegistry,
storageApiRef,
WebStorage,
StorageApi,
} from '@backstage/core';
import { MockErrorApi } from '@backstage/test-utils';
describe('useStarredEntities', () => {
let mockStorage: StorageApi | undefined;
const wrapper: React.FC<{}> = ({ children }) => {
return (
<ApiProvider apis={ApiRegistry.with(storageApiRef, mockStorage)}>
{children}
</ApiProvider>
);
};
beforeEach(() => {
mockStorage = new WebStorage('@backstage', new MockErrorApi()).forBucket(
Date.now().toString(), // TODO(blam): need something that changes every test run for now until the MockStorage is implemented
);
});
it('should return an empty set for when there is no items in storage', async () => {
const { result } = renderHook(() => useStarredEntities(), { wrapper });
expect(result.current.starredEntities.size).toBe(0);
});
it('should return a set with the current items when there is items in storage', async () => {
const expectedIds = ['i', 'am', 'some', 'test', 'ids'];
const store = mockStorage?.forBucket('settings');
await store?.set('starredEntities', expectedIds);
const { result } = renderHook(() => useStarredEntities(), { wrapper });
for (const item of expectedIds) {
expect(result.current.starredEntities.has(item)).toBeTruthy();
}
});
it('should listen to changes when the storage is set elsewhere', async () => {
const store = mockStorage?.forBucket('settings');
const { result, waitForNextUpdate } = renderHook(
() => useStarredEntities(),
{ wrapper },
);
expect(result.current.starredEntities.size).toBe(0);
expect(result.current.starredEntities.has('something')).toBeFalsy();
// Make this happen after awaiting for the next update so we can
// catch when the hook re-renders with the latest data
setTimeout(() => store?.set('starredEntities', ['something']), 1);
await waitForNextUpdate();
expect(result.current.starredEntities.size).toBe(1);
expect(result.current.starredEntities.has('something')).toBeTruthy();
});
});
+6
View File
@@ -157,3 +157,9 @@ export type KindParser = {
envelope: DescriptorEnvelope,
): Promise<DescriptorEnvelope | undefined>;
};
export enum FilterGroupItem {
ALL = 'ALL',
STARRED = 'STARRED',
OWNED = 'OWNED',
}
+40
View File
@@ -891,6 +891,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.5.4":
version "7.10.2"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@@ -3279,6 +3286,14 @@
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react-hooks@^3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-3.3.0.tgz#dc217bfce8e7c34a99c811d73d23feef957b7c1d"
integrity sha512-rE9geI1+HJ6jqXkzzJ6abREbeud6bLF8OmF+Vyc7gBoPwZAEVBYjbC1up5nNoVfYBhO5HUwdD4u9mTehAUeiyw==
dependencies:
"@babel/runtime" "^7.5.4"
"@types/testing-library__react-hooks" "^3.0.0"
"@testing-library/react@^9.3.2":
version "9.5.0"
resolved "https://registry.npmjs.org/@testing-library/react/-/react-9.5.0.tgz#71531655a7890b61e77a1b39452fbedf0472ca5e"
@@ -3910,6 +3925,13 @@
dependencies:
"@types/react" "*"
"@types/react-test-renderer@*":
version "16.9.2"
resolved "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5"
integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw==
dependencies:
"@types/react" "*"
"@types/react-textarea-autosize@^4.3.3":
version "4.3.5"
resolved "https://registry.npmjs.org/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz#6c4d2753fa1864c98c0b2b517f67bb1f6e4c46de"
@@ -4067,6 +4089,14 @@
dependencies:
"@types/jest" "*"
"@types/testing-library__react-hooks@^3.0.0":
version "3.2.0"
resolved "https://registry.npmjs.org/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897"
integrity sha512-dE8iMTuR5lzB+MqnxlzORlXzXyCL0EKfzH0w/lau20OpkHD37EaWjZDz0iNG8b71iEtxT4XKGmSKAGVEqk46mw==
dependencies:
"@types/react" "*"
"@types/react-test-renderer" "*"
"@types/testing-library__react@^9.1.2":
version "9.1.3"
resolved "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.3.tgz#35eca61cc6ea923543796f16034882a1603d7302"
@@ -15617,6 +15647,16 @@ react-syntax-highlighter@^12.2.1:
prismjs "^1.8.4"
refractor "^2.4.1"
react-test-renderer@^16.13.1:
version "16.13.1"
resolved "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
react-is "^16.8.6"
scheduler "^0.19.1"
react-textarea-autosize@^7.1.0:
version "7.1.2"
resolved "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-7.1.2.tgz#70fdb333ef86bcca72717e25e623e90c336e2cda"