Merge branch 'backstage:master' into master

This commit is contained in:
Bogdan Nechyporenko
2022-09-19 09:02:43 +02:00
committed by GitHub
25 changed files with 2838 additions and 336 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
Adds skipMetricsLookup to the kubernetes-backend schema
+283
View File
@@ -0,0 +1,283 @@
---
'@backstage/plugin-search-react': minor
---
The `<SearchResult/>` component now accepts a optional `query` prop to request results from the search api:
> Note: If a query prop is not defined, the results will by default be consumed from the context.
Example:
```jsx
import React, { useState, useCallback } from 'react';
import { Grid, List, Paper } from '@material-ui/core';
import { Page, Header, Content, Lifecycle } from '@backstage/core-components';
import {
DefaultResultListItem,
SearchBarBase,
SearchResult,
} from '@backstage/plugin-search-react';
const SearchPage = () => {
const [query, setQuery] = useState({
term: '',
types: [],
filters: {},
});
const handleChange = useCallback(
(term: string) => {
setQuery(prevQuery => ({ ...prevQuery, term }));
},
[setQuery],
);
return (
<Page themeId="home">
<Header title="Search" subtitle={<Lifecycle alpha />} />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper>
<SearchBarBase debounceTime={100} onChange={handleChange} />
</Paper>
</Grid>
<Grid item xs>
<SearchResult query={query}>
{({ results }) => (
<List>
{results.map(({ document }) => (
<DefaultResultListItem
key={document.location}
result={document}
/>
))}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
};
```
Additionally, a search page can also be composed using these two new results layout components:
```jsx
// Example rendering results as list
<SearchResult>
{({ results }) => (
<SearchResultListLayout
resultItems={results}
renderResultItem={({ type, document }) => {
switch (type) {
case 'custom-result-item':
return (
<CustomResultListItem key={document.location} result={document} />
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
}}
/>
)}
</SearchResult>
```
```jsx
// Example rendering results as groups
<SearchResult>
{({ results }) => (
<>
<SearchResultGroupLayout
icon={<CustomIcon />}
title="Custom"
link="See all custom results"
resultItems={results.filter(
({ type }) => type === 'custom-result-item',
)}
renderResultItem={({ document }) => (
<CustomResultListItem key={document.location} result={document} />
)}
/>
<SearchResultGroupLayout
icon={<DefaultIcon />}
title="Default"
resultItems={results.filter(
({ type }) => type !== 'custom-result-item',
)}
renderResultItem={({ document }) => (
<DefaultResultListItem key={document.location} result={document} />
)}
/>
</>
)}
</SearchResult>
```
A `SearchResultList` and `SearchResultGroup` components were also created for users who have search pages with multiple queries, both are specializations of `SearchResult` and also accept a `query` as a prop as well:
```jsx
// Example using the <SearchResultList />
const SearchPage = () => {
const query = {
term: 'example',
};
return (
<SearchResultList
query={query}
renderResultItem={({ type, document, highlight, rank }) => {
switch (type) {
case 'custom':
return (
<CustomResultListItem
key={document.location}
icon={<CatalogIcon />}
result={document}
highlight={highlight}
rank={rank}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
}}
/>
);
};
```
```jsx
// Example using the <SearchResultGroup /> for creating a component that search and group software catalog results
import React, { useState, useCallback } from 'react';
import { MenuItem } from '@material-ui/core';
import { JsonValue } from '@backstage/types';
import { CatalogIcon } from '@backstage/core-components';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
import {
SearchResultGroup,
SearchResultGroupTextFilterField,
SearchResultGroupSelectFilterField,
} from @backstage/plugin-search-react;
import { SearchQuery } from '@backstage/plugin-search-common';
const CatalogResultsGroup = () => {
const [query, setQuery] = useState<Partial<SearchQuery>>({
types: ['software-catalog'],
});
const filterOptions = [
{
label: 'Lifecycle',
value: 'lifecycle',
},
{
label: 'Owner',
value: 'owner',
},
];
const handleFilterAdd = useCallback(
(key: string) => () => {
setQuery(prevQuery => {
const { filters: prevFilters, ...rest } = prevQuery;
const newFilters = { ...prevFilters, [key]: undefined };
return { ...rest, filters: newFilters };
});
},
[],
);
const handleFilterChange = useCallback(
(key: string) => (value: JsonValue) => {
setQuery(prevQuery => {
const { filters: prevFilters, ...rest } = prevQuery;
const newFilters = { ...prevFilters, [key]: value };
return { ...rest, filters: newFilters };
});
},
[],
);
const handleFilterDelete = useCallback(
(key: string) => () => {
setQuery(prevQuery => {
const { filters: prevFilters, ...rest } = prevQuery;
const newFilters = { ...prevFilters };
delete newFilters[key];
return { ...rest, filters: newFilters };
});
},
[],
);
return (
<SearchResultGroup
query={query}
icon={<CatalogIcon />}
title="Software Catalog"
link="See all software catalog results"
filterOptions={filterOptions}
renderFilterOption={({ label, value }) => (
<MenuItem key={value} onClick={handleFilterAdd(value)}>
{label}
</MenuItem>
)}
renderFilterField={(key: string) => {
switch (key) {
case 'lifecycle':
return (
<SearchResultGroupSelectFilterField
key={key}
label="Lifecycle"
value={query.filters?.lifecycle}
onChange={handleFilterChange('lifecycle')}
onDelete={handleFilterDelete('lifecycle')}
>
<MenuItem value="production">Production</MenuItem>
<MenuItem value="experimental">Experimental</MenuItem>
</SearchResultGroupSelectFilterField>
);
case 'owner':
return (
<SearchResultGroupTextFilterField
key={key}
label="Owner"
value={query.filters?.owner}
onChange={handleFilterChange('owner')}
onDelete={handleFilterDelete('owner')}
/>
);
default:
return null;
}
}
renderResultItem={({ document, highlight, rank }) => (
<CatalogSearchResultListItem
key={document.location}
result={document}
highlight={highlight}
rank={rank}
/>
)}
/>
);
};
```
+1 -1
View File
@@ -202,7 +202,7 @@ _You can do this by using the [Adopter form](https://form.typeform.com/to/zcOaKi
| [Skillz](https://skillz.com/) | [Peiman Jafari](https://github.com/peimanja) | Internal developers portal for technical documentations, components ownership and relationship, software templates and integrations with internal tools |
| [Telus](https://www.telus.com/en/) | [Leo Li](mailto:leo.li@telus.com), [Laurent Robichaud](mailto:laurent.robichaud@telus.com), [Seb Barre](https://github.com/sbarre) | Simplifying the developer experience through centralized team member portals. Our current focus includes the adoption of Tech Docs, Software Catalog, Software Templates, the plethora of plugins, and contributing features back to Backstage. 🤖 |
| [Fidelity Investments](https://fidelity.com) | [Ankita Upadhyay](mailto:ankita.upadhyay@fmr.com) | Getting started with the adoption for Monorepo projects |
| [Verisk](https://verisk.com) | [Callen Barton](mailto:cbarton@verisk.com) | Developer portal to quickly create and deploy microservices. |
| [Verisk](https://verisk.com) | [Callen Barton](mailto:#xw_architecture@verisk.com), [Kevin Johnson](mailto:#xw_architecture@verisk.com) | Developer portal to quickly create and deploy microservices. |
| [iodigital](https://iodigital.com) | [Jan-Willem Mulder](mailto:jan-willem.mulder@iodigital.com) | Internal developer portal for discovery of applications, projects and teams. Using several plugins like the Software Catalog and Tech Insights for promoting best practices and supporting our SDLC toolchain |
| [Fanatics](https://www.fanaticsinc.com/) | [Rory Scott](mailto:rscott@fanatics.com) | Internal Portal consolidating documentation, making it easier to manage applications, internal developer community platform, and self-service cloud infrastructure + pipelines. |
| [Appfolio](https://appfolio.com) | [Andy Vaughn](mailto:andy.vaughn@appfolio.com) | Internal software catalog, tech radar, documentation portal to disambiguate software and domain ownership, foster exploration of available developer platform services and tools, improve communication, democratize documentation and knowledge sharing, and coordinate the software lifecycle; all in service of a best-in-class developer experience. |
+1 -1
View File
@@ -65,7 +65,7 @@ the local `auth.environment` setting will be selected.
> and access to a Backstage Identity Token, which can be passed to backend
> plugins.
Using an authentication provide for sign-in is something you need to configure
Using an authentication provider for sign-in is something you need to configure
both in the frontend app, as well as the `auth` backend plugin. For information
on how to configure the backend app, see [Sign-in Identities and Resolvers](./identity-resolver.md).
The rest of this section will focus on how to configure sign-in for the frontend app.
@@ -156,15 +156,16 @@ export default async function createPlugin(
Here is a list of Open Source custom actions that you can add to your Backstage
scaffolder backend:
| Name | Package | Owner |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
| Utility actions | [scaffolder-backend-module-utils](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-utils) | [Roadie](https://roadie.io) |
| AWS cli actions | [scaffolder-backend-module-aws](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-aws) | [Roadie](https://roadie.io) |
| Scaffolder .NET Actions | [plugin-scaffolder-dotnet-backend](https://www.npmjs.com/package/@plusultra/plugin-scaffolder-dotnet-backend) | [Alef Carlos](https://github.com/alefcarlos) |
| Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) |
| Name | Package | Owner |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| Yeoman | [plugin-scaffolder-backend-module-yeoman](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-yeoman) | [Backstage](https://backstage.io) |
| Cookiecutter | [plugin-scaffolder-backend-module-cookiecutter](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-cookiecutter) | [Backstage](https://backstage.io) |
| Rails | [plugin-scaffolder-backend-module-rails](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend-module-rails) | [Backstage](https://backstage.io) |
| HTTP requests | [scaffolder-backend-module-http-request](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-http-request) | [Roadie](https://roadie.io) |
| Utility actions | [scaffolder-backend-module-utils](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-utils) | [Roadie](https://roadie.io) |
| AWS cli actions | [scaffolder-backend-module-aws](https://www.npmjs.com/package/@roadiehq/scaffolder-backend-module-aws) | [Roadie](https://roadie.io) |
| Scaffolder .NET Actions | [plugin-scaffolder-dotnet-backend](https://www.npmjs.com/package/@plusultra/plugin-scaffolder-dotnet-backend) | [Alef Carlos](https://github.com/alefcarlos) |
| Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) |
| Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) |
Have fun! 🚀
+4
View File
@@ -40,6 +40,8 @@ export interface Config {
region?: string;
/** @visibility frontend */
skipTLSVerify?: boolean;
/** @visibility frontend */
skipMetricsLookup?: boolean;
}
| {
/** @visibility frontend */
@@ -62,6 +64,8 @@ export interface Config {
oidcTokenProvider?: string;
/** @visibility frontend */
skipTLSVerify?: boolean;
/** @visibility frontend */
skipMetricsLookup?: boolean;
}>;
}
>;
+130 -6
View File
@@ -11,7 +11,10 @@ import { AutocompleteProps } from '@material-ui/lab';
import { ForwardRefExoticComponent } from 'react';
import { InputBaseProps } from '@material-ui/core';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { LinkProps } from '@backstage/core-components';
import { ListItemTextProps } from '@material-ui/core';
import { ListProps } from '@material-ui/core';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { ReactElement } from 'react';
@@ -21,6 +24,7 @@ import { SearchDocument } from '@backstage/plugin-search-common';
import { SearchQuery } from '@backstage/plugin-search-common';
import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common';
import { SearchResultSet } from '@backstage/plugin-search-common';
import { TypographyProps } from '@material-ui/core';
// @public (undocumented)
export const AutocompleteFilter: (
@@ -205,22 +209,142 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & {
debug?: boolean;
};
// @public (undocumented)
// @public
export const SearchResult: (props: SearchResultProps) => JSX.Element;
// @public
export const SearchResultComponent: ({
children,
}: SearchResultProps) => JSX.Element;
export const SearchResultApi: (props: SearchResultApiProps) => JSX.Element;
// @public
export type SearchResultApiProps = SearchResultContextProps & {
query: Partial<SearchQuery>;
};
// @public
export const SearchResultComponent: (props: SearchResultProps) => JSX.Element;
// @public
export const SearchResultContext: (
props: SearchResultContextProps,
) => JSX.Element;
// @public
export type SearchResultContextProps = {
children: (state: AsyncState<SearchResultSet>) => JSX.Element;
};
// @public
export function SearchResultGroup<FilterOption>(
props: SearchResultGroupProps<FilterOption>,
): JSX.Element;
// @public
export const SearchResultGroupFilterFieldLayout: (
props: SearchResultGroupFilterFieldLayoutProps,
) => JSX.Element;
// @public
export type SearchResultGroupFilterFieldLayoutProps = PropsWithChildren<{
label: string;
value?: JsonValue;
onDelete: () => void;
}>;
// @public
export type SearchResultGroupFilterFieldPropsWith<T> = T &
SearchResultGroupFilterFieldLayoutProps & {
onChange: (value: JsonValue) => void;
};
// @public
export function SearchResultGroupLayout<FilterOption>(
props: SearchResultGroupLayoutProps<FilterOption>,
): JSX.Element;
// @public
export type SearchResultGroupLayoutProps<FilterOption> = ListProps & {
icon: JSX.Element;
title: ReactNode;
titleProps?: Partial<TypographyProps>;
link?: ReactNode;
linkProps?: Partial<LinkProps>;
filterOptions?: FilterOption[];
renderFilterOption?: (filterOption: FilterOption) => JSX.Element;
filterFields?: string[];
renderFilterField?: (key: string) => JSX.Element | null;
resultItems?: SearchResult_2[];
renderResultItem?: (resultItem: SearchResult_2) => JSX.Element;
error?: Error;
loading?: boolean;
};
// @public
export type SearchResultGroupProps<FilterOption> = Omit<
SearchResultGroupLayoutProps<FilterOption>,
'loading' | 'error' | 'resultItems' | 'filterFields'
> & {
query: Partial<SearchQuery>;
};
// @public
export const SearchResultGroupSelectFilterField: (
props: SearchResultGroupSelectFilterFieldProps,
) => JSX.Element;
// @public
export type SearchResultGroupSelectFilterFieldProps =
SearchResultGroupFilterFieldPropsWith<{
children: ReactNode;
}>;
// @public
export const SearchResultGroupTextFilterField: (
props: SearchResultGroupTextFilterFieldProps,
) => JSX.Element;
// @public
export type SearchResultGroupTextFilterFieldProps =
SearchResultGroupFilterFieldPropsWith<{}>;
// @public
export const SearchResultList: (props: SearchResultListProps) => JSX.Element;
// @public
export const SearchResultListLayout: (
props: SearchResultListLayoutProps,
) => JSX.Element;
// @public
export type SearchResultListLayoutProps = ListProps & {
resultItems?: SearchResult_2[];
renderResultItem?: (resultItem: SearchResult_2) => JSX.Element;
error?: Error;
loading?: boolean;
};
// @public
export type SearchResultListProps = Omit<
SearchResultListLayoutProps,
'loading' | 'error' | 'resultItems'
> & {
query: Partial<SearchQuery>;
};
// @public (undocumented)
export const SearchResultPager: () => JSX.Element;
// @public
export type SearchResultProps = {
children: (results: { results: SearchResult_2[] }) => JSX.Element;
export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & {
children: (resultSet: SearchResultSet) => JSX.Element;
};
// @public
export const SearchResultState: (props: SearchResultStateProps) => JSX.Element;
// @public
export type SearchResultStateProps = SearchResultContextProps &
Partial<SearchResultApiProps>;
// @public (undocumented)
export const SelectFilter: (props: SearchFilterComponentProps) => JSX.Element;
+1
View File
@@ -40,6 +40,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"qs": "^6.9.4",
"react-use": "^17.3.2"
},
"peerDependencies": {
@@ -15,16 +15,24 @@
*/
import React, { ComponentType } from 'react';
import { List, ListItem } from '@material-ui/core';
import { MemoryRouter } from 'react-router';
import { List, ListItem } from '@material-ui/core';
import DefaultIcon from '@material-ui/icons/InsertDriveFile';
import CustomIcon from '@material-ui/icons/NoteAdd';
import { Link } from '@backstage/core-components';
import { TestApiProvider } from '@backstage/test-utils';
import { SearchDocument } from '@backstage/plugin-search-common';
import { searchApiRef, MockSearchApi } from '../../api';
import { SearchContextProvider } from '../../context';
import { DefaultResultListItem } from '../DefaultResultListItem';
import { SearchResultListLayout } from '../SearchResultList';
import { SearchResult } from './SearchResult';
import { SearchResultGroupLayout } from '../SearchResultGroup';
const mockResults = {
results: [
@@ -55,15 +63,15 @@ const mockResults = {
],
};
const searchApiMock = new MockSearchApi(mockResults);
export default {
title: 'Plugins/Search/SearchResult',
component: SearchResult,
decorators: [
(Story: ComponentType<{}>) => (
<MemoryRouter>
<TestApiProvider
apis={[[searchApiRef, new MockSearchApi(mockResults)]]}
>
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchContextProvider>
<Story />
</SearchContextProvider>
@@ -73,6 +81,17 @@ export default {
],
};
const CustomResultListItem = (props: { result: SearchDocument }) => {
const { result } = props;
return (
<ListItem>
<Link to={result.location}>
{result.title} - {result.text}
</Link>
</ListItem>
);
};
export const Default = () => {
return (
<SearchResult>
@@ -82,18 +101,17 @@ export const Default = () => {
switch (type) {
case 'custom-result-item':
return (
<DefaultResultListItem
<CustomResultListItem
key={document.location}
result={document}
/>
);
default:
return (
<ListItem>
<Link to={document.location}>
{document.title} - {document.text}
</Link>
</ListItem>
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
})}
@@ -102,3 +120,101 @@ export const Default = () => {
</SearchResult>
);
};
export const WithQuery = () => {
const query = {
term: 'documentation',
};
return (
<SearchResult query={query}>
{({ results }) => (
<List>
{results.map(({ type, document }) => {
switch (type) {
case 'custom-result-item':
return (
<CustomResultListItem
key={document.location}
result={document}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
})}
</List>
)}
</SearchResult>
);
};
export const ListLayout = () => {
return (
<SearchResult>
{({ results }) => (
<SearchResultListLayout
resultItems={results}
renderResultItem={({ type, document }) => {
switch (type) {
case 'custom-result-item':
return (
<CustomResultListItem
key={document.location}
result={document}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
}}
/>
)}
</SearchResult>
);
};
export const GroupLayout = () => {
return (
<SearchResult>
{({ results }) => (
<>
<SearchResultGroupLayout
icon={<CustomIcon />}
title="Custom"
link="See all custom results"
resultItems={results.filter(
({ type }) => type === 'custom-result-item',
)}
renderResultItem={({ document }) => (
<CustomResultListItem key={document.location} result={document} />
)}
/>
<SearchResultGroupLayout
icon={<DefaultIcon />}
title="Default"
resultItems={results.filter(
({ type }) => type !== 'custom-result-item',
)}
renderResultItem={({ document }) => (
<DefaultResultListItem
key={document.location}
result={document}
/>
)}
/>
</>
)}
</SearchResult>
);
};
@@ -15,69 +15,215 @@
*/
import React from 'react';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
import {
EmptyState,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { AnalyticsContext } from '@backstage/core-plugin-api';
import { SearchResult } from '@backstage/plugin-search-common';
import { AnalyticsContext, useApi } from '@backstage/core-plugin-api';
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
import { useSearch } from '../../context';
import { searchApiRef } from '../../api';
/**
* Props for {@link SearchResultComponent}
*
* Props for {@link SearchResultContext}
* @public
*/
export type SearchResultProps = {
children: (results: { results: SearchResult[] }) => JSX.Element;
export type SearchResultContextProps = {
/**
* A child function that receives an asynchronous result set and returns a react element.
*/
children: (state: AsyncState<SearchResultSet>) => JSX.Element;
};
/**
* A component returning the search result.
*
* Provides context-based results to a child function.
* @param props - see {@link SearchResultContextProps}.
* @example
* ```
* <SearchResultContext>
* {({ loading, error, value }) => (
* <List>
* {value?.map(({ document }) => (
* <DefaultSearchResultListItem
* key={document.location}
* result={document}
* />
* ))}
* </List>
* )}
* </SearchResultContext>
* ```
* @public
*/
export const SearchResultComponent = ({ children }: SearchResultProps) => {
const {
result: { loading, error, value },
} = useSearch();
if (loading) {
return <Progress />;
}
if (error) {
return (
<ResponseErrorPanel
title="Error encountered while fetching search results"
error={error}
/>
);
}
if (!value?.results.length) {
return <EmptyState missing="data" title="Sorry, no results were found" />;
}
return <>{children({ results: value.results })}</>;
export const SearchResultContext = (props: SearchResultContextProps) => {
const { children } = props;
const context = useSearch();
const state = context.result;
return children(state);
};
/**
* Props for {@link SearchResultApi}
* @public
*/
const HigherOrderSearchResult = (props: SearchResultProps) => {
return (
<AnalyticsContext
attributes={{
pluginId: 'search',
extension: 'SearchResult',
}}
>
<SearchResultComponent {...props} />
</AnalyticsContext>
export type SearchResultApiProps = SearchResultContextProps & {
query: Partial<SearchQuery>;
};
/**
* Request results through the search api and provide them to a child function.
* @param props - see {@link SearchResultApiProps}.
* @example
* ```
* <SearchResultApi>
* {({ loading, error, value }) => (
* <List>
* {value?.map(({ document }) => (
* <DefaultSearchResultListItem
* key={document.location}
* result={document}
* />
* ))}
* </List>
* )}
* </SearchResultApi>
* ```
* @public
*/
export const SearchResultApi = (props: SearchResultApiProps) => {
const { query, children } = props;
const searchApi = useApi(searchApiRef);
const state = useAsync(
() =>
searchApi.query({
term: query.term ?? '',
types: query.types ?? [],
filters: query.filters ?? {},
pageCursor: query.pageCursor,
}),
[query],
);
return children(state);
};
/**
* Props for {@link SearchResultState}
* @public
*/
export type SearchResultStateProps = SearchResultContextProps &
Partial<SearchResultApiProps>;
/**
* Call a child render function passing a search state as an argument.
* @remarks By default, results are taken from context, but when a "query" prop is set, results are requested from the search api.
* @param props - see {@link SearchResultStateProps}.
* @example
* Consuming results from context:
* ```
* <SearchResultState>
* {({ loading, error, value }) => (
* <List>
* {value?.map(({ document }) => (
* <DefaultSearchResultListItem
* key={document.location}
* result={document}
* />
* ))}
* </List>
* )}
* </SearchResultState>
* ```
* @example
* Requesting results using the search api:
* ```
* <SearchResultState query={{ term: 'documentation' }}>
* {({ loading, error, value }) => (
* <List>
* {value?.map(({ document }) => (
* <DefaultSearchResultListItem
* key={document.location}
* result={document}
* />
* ))}
* </List>
* )}
* </SearchResultState>
* ```
* @public
*/
export const SearchResultState = (props: SearchResultStateProps) => {
const { query, children } = props;
return query ? (
<SearchResultApi query={query}>{children}</SearchResultApi>
) : (
<SearchResultContext>{children}</SearchResultContext>
);
};
export { HigherOrderSearchResult as SearchResult };
/**
* Props for {@link SearchResult}
* @public
*/
export type SearchResultProps = Pick<SearchResultStateProps, 'query'> & {
children: (resultSet: SearchResultSet) => JSX.Element;
};
/**
* Renders results from a parent search context or api.
* @remarks default components for loading, error and empty variants are returned.
* @param props - see {@link SearchResultProps}.
* @public
*/
export const SearchResultComponent = (props: SearchResultProps) => {
const { query, children } = props;
return (
<SearchResultState query={query}>
{({ loading, error, value }) => {
if (loading) {
return <Progress />;
}
if (error) {
return (
<ResponseErrorPanel
title="Error encountered while fetching search results"
error={error}
/>
);
}
if (!value?.results.length) {
return (
<EmptyState missing="data" title="Sorry, no results were found" />
);
}
return children(value);
}}
</SearchResultState>
);
};
/**
* A component returning the search result from a parent search context or api.
* @param props - see {@link SearchResultProps}.
* @public
*/
export const SearchResult = (props: SearchResultProps) => (
<AnalyticsContext
attributes={{
pluginId: 'search',
extension: 'SearchResult',
}}
>
<SearchResultComponent {...props} />
</AnalyticsContext>
);
@@ -14,5 +14,17 @@
* limitations under the License.
*/
export { SearchResult, SearchResultComponent } from './SearchResult';
export type { SearchResultProps } from './SearchResult';
export {
SearchResult,
SearchResultApi,
SearchResultContext,
SearchResultState,
SearchResultComponent,
} from './SearchResult';
export type {
SearchResultProps,
SearchResultApiProps,
SearchResultContextProps,
SearchResultStateProps,
} from './SearchResult';
@@ -0,0 +1,326 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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, { ComponentType, useCallback, useState } from 'react';
import {
Grid,
ListItem,
ListItemIcon,
ListItemText,
MenuItem,
} from '@material-ui/core';
import DocsIcon from '@material-ui/icons/InsertDriveFile';
import { JsonValue } from '@backstage/types';
import { Link } from '@backstage/core-components';
import { createRouteRef } from '@backstage/core-plugin-api';
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
import { searchApiRef, MockSearchApi } from '../../api';
import {
SearchResultGroup,
SearchResultGroupTextFilterField,
SearchResultGroupSelectFilterField,
} from './SearchResultGroup';
const routeRef = createRouteRef({
id: 'storybook.search.results.group.route',
});
const searchApiMock = new MockSearchApi({
results: [
{
type: 'techdocs',
document: {
location: 'search/search-result1',
title: 'Search Result 1',
text: 'Some text from the search result 1',
},
},
{
type: 'custom',
document: {
location: 'search/search-result2',
title: 'Search Result 2',
text: 'Some text from the search result 2',
},
},
],
});
export default {
title: 'Plugins/Search/SearchResultGroup',
component: SearchResultGroup,
decorators: [
(Story: ComponentType<{}>) =>
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<Grid container direction="row">
<Grid item xs={12}>
<Story />
</Grid>
</Grid>
</TestApiProvider>,
{ mountedRoutes: { '/': routeRef } },
),
],
};
export const Default = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Documentation"
/>
);
};
export const Loading = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<TestApiProvider
apis={[
[searchApiRef, { query: () => new Promise<SearchResultSet>(() => {}) }],
]}
>
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Documentation"
/>
</TestApiProvider>
);
};
export const WithError = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<TestApiProvider
apis={[
[
searchApiRef,
{
query: () =>
new Promise<SearchResultSet>(() => {
throw new Error();
}),
},
],
]}
>
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Documentation"
/>
</TestApiProvider>
);
};
export const WithCustomTitle = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['custom'],
});
return (
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Custom"
titleProps={{ color: 'secondary' }}
/>
);
};
export const WithCustomLink = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['custom'],
});
return (
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Custom"
link="See all custom results"
linkProps={{ to: '/custom' }}
/>
);
};
export const WithFilters = () => {
const [query, setQuery] = useState<Partial<SearchQuery>>({
types: ['software-catalog'],
});
const filterOptions = [
{
label: 'Lifecycle',
value: 'lifecycle',
},
{
label: 'Owner',
value: 'owner',
},
];
const handleFilterAdd = useCallback(
(key: string) => () => {
setQuery(prevQuery => {
const { filters: prevFilters, ...rest } = prevQuery;
const newFilters = { ...prevFilters, [key]: undefined };
return { ...rest, filters: newFilters };
});
},
[],
);
const handleFilterChange = useCallback(
(key: string) => (value: JsonValue) => {
setQuery(prevQuery => {
const { filters: prevFilters, ...rest } = prevQuery;
const newFilters = { ...prevFilters, [key]: value };
return { ...rest, filters: newFilters };
});
},
[],
);
const handleFilterDelete = useCallback(
(key: string) => () => {
setQuery(prevQuery => {
const { filters: prevFilters, ...rest } = prevQuery;
const newFilters = { ...prevFilters };
delete newFilters[key];
return { ...rest, filters: newFilters };
});
},
[],
);
return (
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Documentation"
filterOptions={filterOptions}
renderFilterOption={option => (
<MenuItem key={option.value} onClick={handleFilterAdd(option.value)}>
{option.label}
</MenuItem>
)}
renderFilterField={(key: string) => {
switch (key) {
case 'lifecycle':
return (
<SearchResultGroupSelectFilterField
key={key}
label="Lifecycle"
value={query.filters?.lifecycle}
onChange={handleFilterChange('lifecycle')}
onDelete={handleFilterDelete('lifecycle')}
>
<MenuItem value="production">Production</MenuItem>
<MenuItem value="experimental">Experimental</MenuItem>
</SearchResultGroupSelectFilterField>
);
case 'owner':
return (
<SearchResultGroupTextFilterField
key={key}
label="Owner"
value={query.filters?.owner}
onChange={handleFilterChange('owner')}
onDelete={handleFilterDelete('owner')}
/>
);
default:
return null;
}
}}
/>
);
};
export const WithNoResults = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Documentation"
/>
</TestApiProvider>
);
};
const CustomResultListItem = (props: any) => {
const { icon, result } = props;
return (
<Link to={result.location}>
<ListItem alignItems="flex-start" divider>
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText
primary={result.title}
primaryTypographyProps={{ variant: 'h6' }}
secondary={result.text}
/>
</ListItem>
</Link>
);
};
export const WithCustomResultItem = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['custom'],
});
return (
<SearchResultGroup
query={query}
icon={<DocsIcon />}
title="Custom"
link="See all custom results"
renderResultItem={({ document, highlight, rank }) => (
<CustomResultListItem
key={document.location}
result={document}
highlight={highlight}
rank={rank}
/>
)}
/>
);
};
@@ -0,0 +1,316 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MenuItem } from '@material-ui/core';
import DocsIcon from '@material-ui/icons/InsertDriveFile';
import {
TestApiProvider,
renderWithEffects,
wrapInTestApp,
} from '@backstage/test-utils';
import { searchApiRef } from '../../api';
import {
SearchResultGroup,
SearchResultGroupSelectFilterField,
SearchResultGroupTextFilterField,
} from './SearchResultGroup';
const query = jest.fn().mockResolvedValue({ results: [] });
const searchApiMock = { query };
describe('SearchResultGroup', () => {
const results = [
{
type: 'techdocs',
document: {
location: 'search/search-result1',
title: 'Search Result 1',
text: 'Some text from the search result 1',
},
},
{
type: 'techdocs',
document: {
location: 'search/search-result2',
title: 'Search Result 2',
text: 'Some text from the search result 2',
},
},
];
beforeEach(() => {
jest.clearAllMocks();
});
it('Renders without exploding', async () => {
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
/>
</TestApiProvider>,
),
);
expect(screen.getByTitle('Docs icon')).toBeInTheDocument();
expect(screen.getByText('Documentation')).toBeInTheDocument();
expect(query).toHaveBeenCalledWith({
filters: {},
pageCursor: undefined,
term: '',
types: ['techdocs'],
});
});
it('Defines a default link', async () => {
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
/>
</TestApiProvider>,
),
);
const link = screen.getByText('See all', { exact: false });
expect(link).toHaveAttribute('href', encodeURI('/search?types[]=techdocs'));
});
it('Defines a default render result item', async () => {
query.mockResolvedValueOnce({
results,
});
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
/>
</TestApiProvider>,
),
);
expect(screen.getByText('Search Result 1')).toBeInTheDocument();
expect(
screen.getByText('Some text from the search result 1'),
).toBeInTheDocument();
expect(screen.getByText('Search Result 2')).toBeInTheDocument();
expect(
screen.getByText('Some text from the search result 2'),
).toBeInTheDocument();
});
it('Could be customized with no results text', async () => {
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
/>
</TestApiProvider>,
),
);
expect(
screen.getByText('Sorry, no results were found'),
).toBeInTheDocument();
});
it('Could be customized with filters', async () => {
query.mockResolvedValueOnce({
results,
});
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
filterOptions={['lifecycle', 'owner']}
/>
</TestApiProvider>,
),
);
await userEvent.click(screen.getByText('Add filter', { exact: false }));
await waitFor(() => {
expect(screen.getByText('lifecycle')).toBeInTheDocument();
});
expect(screen.getByText('owner')).toBeInTheDocument();
});
it('Could have a text search filter field', async () => {
query.mockResolvedValueOnce({
results,
});
const handleFilterChange = jest.fn();
const handleFilterDelete = jest.fn();
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{
types: ['techdocs'],
filters: { owner: null },
}}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
filterOptions={['owner']}
renderFilterField={(key: string) =>
key === 'owner' ? (
<SearchResultGroupTextFilterField
key={key}
label="Owner"
onChange={handleFilterChange}
onDelete={handleFilterDelete}
/>
) : null
}
/>
</TestApiProvider>,
),
);
await userEvent.click(screen.getByText('Add filter', { exact: false }));
await userEvent.click(screen.getByText('owner'));
await userEvent.type(
screen.getByRole('textbox'),
'{backspace}{backspace}{backspace}{backspace}techdocs-core',
);
await waitFor(() => {
expect(screen.getByText('techdocs-core')).toBeInTheDocument();
});
});
it('Could have a select search filter field', async () => {
query.mockResolvedValueOnce({
results,
});
const handleFilterChange = jest.fn();
const handleFilterDelete = jest.fn();
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{
types: ['techdocs'],
filters: { lifecycle: null },
}}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
filterOptions={['lifecycle']}
renderFilterField={(key: string) =>
key === 'lifecycle' ? (
<SearchResultGroupSelectFilterField
key={key}
label="Lifecycle"
onChange={handleFilterChange}
onDelete={handleFilterDelete}
>
<MenuItem value="production">Production</MenuItem>
<MenuItem value="experimental">Experimental</MenuItem>
</SearchResultGroupSelectFilterField>
) : null
}
/>
</TestApiProvider>,
),
);
await userEvent.click(screen.getByText('Add filter', { exact: false }));
await userEvent.click(screen.getByText('lifecycle'));
await userEvent.click(screen.getByText('None'));
await userEvent.click(screen.getByText('Experimental'));
await waitFor(() => {
expect(handleFilterChange).toHaveBeenCalledWith('experimental');
});
});
it('Shows a progress bar when loading results', async () => {
query.mockReturnValueOnce(new Promise(() => {}));
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
/>
</TestApiProvider>,
),
);
await waitFor(() => {
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
});
it('Shows an error panel when results rendering fails', async () => {
query.mockRejectedValueOnce(new Error());
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultGroup
query={{ types: ['techdocs'] }}
icon={<DocsIcon titleAccess="Docs icon" />}
title="Documentation"
/>
</TestApiProvider>,
),
);
await waitFor(() => {
expect(
screen.getByText(
'Error: Error encountered while fetching search results',
),
).toBeInTheDocument();
});
});
});
@@ -0,0 +1,512 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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, {
ChangeEvent,
PropsWithChildren,
ReactNode,
useCallback,
useState,
} from 'react';
import qs from 'qs';
import {
makeStyles,
Theme,
List,
ListSubheader,
ListItem,
ListProps,
Menu,
MenuItem,
InputBase,
Select,
Chip,
Typography,
TypographyProps,
} from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add';
import ArrowRightIcon from '@material-ui/icons/ArrowForwardIos';
import { JsonValue } from '@backstage/types';
import {
EmptyState,
Link,
LinkProps,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { AnalyticsContext } from '@backstage/core-plugin-api';
import { SearchQuery, SearchResult } from '@backstage/plugin-search-common';
import { DefaultResultListItem } from '../DefaultResultListItem';
import { SearchResultState } from '../SearchResult';
const useStyles = makeStyles((theme: Theme) => ({
listSubheader: {
display: 'flex',
alignItems: 'center',
},
listSubheaderName: {
marginLeft: theme.spacing(1),
textTransform: 'uppercase',
},
listSubheaderChip: {
color: theme.palette.text.secondary,
margin: theme.spacing(0, 0, 0, 1.5),
},
listSubheaderFilter: {
display: 'flex',
color: theme.palette.text.secondary,
margin: theme.spacing(0, 0, 0, 1.5),
},
listSubheaderLink: {
marginLeft: 'auto',
display: 'flex',
alignItems: 'center',
},
listSubheaderLinkIcon: {
fontSize: 'inherit',
marginLeft: theme.spacing(0.5),
},
}));
/**
* Props for {@link SearchResultGroupFilterFieldLayout}
* @public
*/
export type SearchResultGroupFilterFieldLayoutProps = PropsWithChildren<{
label: string;
value?: JsonValue;
onDelete: () => void;
}>;
/**
* Default layout for a search group filter field.
* @param props - See {@link SearchResultGroupFilterFieldLayoutProps}.
* @public
*/
export const SearchResultGroupFilterFieldLayout = (
props: SearchResultGroupFilterFieldLayoutProps,
) => {
const classes = useStyles();
const { label, children, ...rest } = props;
return (
<Chip
{...rest}
className={classes.listSubheaderFilter}
variant="outlined"
label={
<>
{label}: {children}
</>
}
/>
);
};
const NullIcon = () => null;
/**
* Common props for a result group filter field.
* @public
*/
export type SearchResultGroupFilterFieldPropsWith<T> = T &
SearchResultGroupFilterFieldLayoutProps & {
onChange: (value: JsonValue) => void;
};
const useSearchResultGroupTextFilterStyles = makeStyles((theme: Theme) => ({
root: {
fontSize: 'inherit',
'&:focus': {
outline: 'none',
background: theme.palette.common.white,
},
'&:not(:focus)': {
cursor: 'pointer',
color: theme.palette.primary.main,
'&:hover': {
textDecoration: 'underline',
},
},
},
}));
/**
* Props for {@link SearchResultGroupTextFilterField}.
* @public
*/
export type SearchResultGroupTextFilterFieldProps =
SearchResultGroupFilterFieldPropsWith<{}>;
/**
* A text field that can be used as filter on search result groups.
* @param props - See {@link SearchResultGroupTextFilterFieldProps}.
* @example
* ```
* <SearchResultGroupTextFilterField
* id="lifecycle"
* label="Lifecycle"
* value={value}
* onChange={handleChangeFilter}
* onDelete={handleDeleteFilter}
* />
* ```
* @public
*/
export const SearchResultGroupTextFilterField = (
props: SearchResultGroupTextFilterFieldProps,
) => {
const classes = useSearchResultGroupTextFilterStyles();
const { label, value = 'None', onChange, onDelete } = props;
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
},
[onChange],
);
return (
<SearchResultGroupFilterFieldLayout label={label} onDelete={onDelete}>
<Typography
role="textbox"
component="span"
className={classes.root}
onChange={handleChange}
contentEditable
suppressContentEditableWarning
>
{value}
</Typography>
</SearchResultGroupFilterFieldLayout>
);
};
const useSearchResultGroupSelectFilterStyles = makeStyles((theme: Theme) => ({
root: {
fontSize: 'inherit',
'&:not(:focus)': {
cursor: 'pointer',
color: theme.palette.primary.main,
'&:hover': {
textDecoration: 'underline',
},
},
'&:focus': {
outline: 'none',
},
'&>div:first-child': {
padding: 0,
},
},
}));
/**
* Props for {@link SearchResultGroupTextFilterField}.
* @public
*/
export type SearchResultGroupSelectFilterFieldProps =
SearchResultGroupFilterFieldPropsWith<{
children: ReactNode;
}>;
/**
* A select field that can be used as filter on search result groups.
* @param props - See {@link SearchResultGroupSelectFilterFieldProps}.
* @example
* ```
* <SearchResultGroupSelectFilterField
* id="lifecycle"
* label="Lifecycle"
* value={filters.lifecycle}
* onChange={handleChangeFilter}
* onDelete={handleDeleteFilter}
* >
* <MenuItem value="experimental">Experimental</MenuItem>
* <MenuItem value="production">Production</MenuItem>
* </SearchResultGroupSelectFilterField>
* ```
* @public
*/
export const SearchResultGroupSelectFilterField = (
props: SearchResultGroupSelectFilterFieldProps,
) => {
const classes = useSearchResultGroupSelectFilterStyles();
const { label, value = 'none', onChange, onDelete, children } = props;
const handleChange = useCallback(
(e: ChangeEvent<{ value: unknown }>) => {
onChange(e.target.value as JsonValue);
},
[onChange],
);
return (
<SearchResultGroupFilterFieldLayout label={label} onDelete={onDelete}>
<Select
className={classes.root}
value={value}
onChange={handleChange}
input={<InputBase />}
IconComponent={NullIcon}
>
<MenuItem value="none">None</MenuItem>
{children}
</Select>
</SearchResultGroupFilterFieldLayout>
);
};
/**
* Props for {@link SearchResultGroupLayout}
* @public
*/
export type SearchResultGroupLayoutProps<FilterOption> = ListProps & {
/**
* Icon that representing a result group.
*/
icon: JSX.Element;
/**
* The results group title content, it could be a text or an element.
*/
title: ReactNode;
/**
* Props for the results group title.
*/
titleProps?: Partial<TypographyProps>;
/**
* The results group link content, it could be a text or an element.
*/
link?: ReactNode;
/**
* Props for the results group link, the "to" prop defaults to "/search".
*/
linkProps?: Partial<LinkProps>;
/**
* A generic filter options that is rendered on the "Add filter" dropdown.
*/
filterOptions?: FilterOption[];
/**
* Function to customize how filter options are rendered.
* @remarks Defaults to a menu item where its value and label bounds to the option string.
*/
renderFilterOption?: (filterOption: FilterOption) => JSX.Element;
/**
* A list of search filter keys, also known as filter field names.
*/
filterFields?: string[];
/**
* Function to customize how filter chips are rendered.
*/
renderFilterField?: (key: string) => JSX.Element | null;
/**
* Search results to be rendered as a group.
*/
resultItems?: SearchResult[];
/**
* Function to customize how result items are rendered.
*/
renderResultItem?: (resultItem: SearchResult) => JSX.Element;
/**
* If defined, will render a default error panel.
*/
error?: Error;
/**
* If defined, will render a default loading progress.
*/
loading?: boolean;
};
/**
* Default layout for rendering search results in a group.
* @param props - See {@link SearchResultGroupLayoutProps}.
* @public
*/
export function SearchResultGroupLayout<FilterOption>(
props: SearchResultGroupLayoutProps<FilterOption>,
) {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const {
loading,
error,
icon,
title,
titleProps = {},
link,
linkProps = {},
filterOptions,
renderFilterOption,
filterFields,
renderFilterField,
resultItems,
renderResultItem,
...rest
} = props;
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(e.currentTarget);
}, []);
const handleClose = useCallback(() => {
setAnchorEl(null);
}, []);
return (
<List {...rest}>
<ListSubheader className={classes.listSubheader}>
{icon}
<Typography
className={classes.listSubheaderName}
component="strong"
{...titleProps}
>
{title}
</Typography>
{filterOptions ? (
<Chip
className={classes.listSubheaderChip}
component="button"
icon={<AddIcon />}
variant="outlined"
label="Add filter"
aria-controls="filters-menu"
aria-haspopup="true"
onClick={handleClick}
/>
) : null}
{filterOptions ? (
<Menu
id="filters-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
onClick={handleClose}
keepMounted
>
{filterOptions.map(filterOption =>
renderFilterOption ? (
renderFilterOption(filterOption)
) : (
<MenuItem
key={String(filterOption)}
value={String(filterOption)}
>
{filterOption}
</MenuItem>
),
)}
</Menu>
) : null}
{filterFields?.map(
filterField => renderFilterField?.(filterField) ?? null,
)}
<Link className={classes.listSubheaderLink} to="/search" {...linkProps}>
{link ?? (
<>
See all
<ArrowRightIcon className={classes.listSubheaderLinkIcon} />
</>
)}
</Link>
</ListSubheader>
{loading ? <Progress /> : null}
{!loading && error ? (
<ResponseErrorPanel
title="Error encountered while fetching search results"
error={error}
/>
) : null}
{!loading && !error && resultItems?.length
? resultItems.map(resultItem => renderResultItem?.(resultItem) ?? null)
: null}
{!loading && !error && !resultItems?.length ? (
<ListItem>
<EmptyState missing="data" title="Sorry, no results were found" />
</ListItem>
) : null}
</List>
);
}
/**
* Props for {@link SearchResultGroup}.
* @public
*/
export type SearchResultGroupProps<FilterOption> = Omit<
SearchResultGroupLayoutProps<FilterOption>,
'loading' | 'error' | 'resultItems' | 'filterFields'
> & {
/**
* A search query used for requesting the results to be grouped.
*/
query: Partial<SearchQuery>;
};
/**
* Given a query, search for results and render them as a group.
* @param props - See {@link SearchResultGroupProps}.
* @public
*/
export function SearchResultGroup<FilterOption>(
props: SearchResultGroupProps<FilterOption>,
) {
const {
query,
linkProps = {},
renderResultItem = ({ document }) => (
<DefaultResultListItem key={document.location} result={document} />
),
...rest
} = props;
const to = `/search?${qs.stringify(
{
query: query.term,
types: query.types,
filters: query.filters,
pageCursor: query.pageCursor,
},
{ arrayFormat: 'brackets' },
)}`;
return (
<AnalyticsContext
attributes={{
pluginId: 'search',
extension: 'SearchResultGroup',
}}
>
<SearchResultState query={query}>
{({ loading, error, value }) => (
<SearchResultGroupLayout
{...rest}
loading={loading}
error={error}
linkProps={{ to, ...linkProps }}
resultItems={value?.results}
renderResultItem={renderResultItem}
filterFields={Object.keys(query.filters ?? {})}
/>
)}
</SearchResultState>
</AnalyticsContext>
);
}
@@ -0,0 +1,32 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 {
SearchResultGroup,
SearchResultGroupLayout,
SearchResultGroupTextFilterField,
SearchResultGroupSelectFilterField,
SearchResultGroupFilterFieldLayout,
} from './SearchResultGroup';
export type {
SearchResultGroupProps,
SearchResultGroupLayoutProps,
SearchResultGroupFilterFieldPropsWith,
SearchResultGroupFilterFieldLayoutProps,
SearchResultGroupTextFilterFieldProps,
SearchResultGroupSelectFilterFieldProps,
} from './SearchResultGroup';
@@ -0,0 +1,182 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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, { ComponentType, useState } from 'react';
import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
import { createRouteRef } from '@backstage/core-plugin-api';
import { CatalogIcon, Link } from '@backstage/core-components';
import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common';
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
import { searchApiRef, MockSearchApi } from '../../api';
import { SearchResultList } from './SearchResultList';
import { DefaultResultListItem } from '../DefaultResultListItem';
const routeRef = createRouteRef({
id: 'storybook.search.results.list.route',
});
const searchApiMock = new MockSearchApi({
results: [
{
type: 'techdocs',
document: {
location: 'search/search-result1',
title: 'Search Result 1',
text: 'Some text from the search result 1',
},
},
{
type: 'custom',
document: {
location: 'search/search-result2',
title: 'Search Result 2',
text: 'Some text from the search result 2',
},
},
],
});
export default {
title: 'Plugins/Search/SearchResultList',
component: SearchResultList,
decorators: [
(Story: ComponentType<{}>) =>
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<Grid container direction="row">
<Grid item xs={12}>
<Story />
</Grid>
</Grid>
</TestApiProvider>,
{ mountedRoutes: { '/': routeRef } },
),
],
};
export const Default = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return <SearchResultList query={query} />;
};
export const Loading = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<TestApiProvider
apis={[
[searchApiRef, { query: () => new Promise<SearchResultSet>(() => {}) }],
]}
>
<SearchResultList query={query} />
</TestApiProvider>
);
};
export const WithError = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<TestApiProvider
apis={[
[
searchApiRef,
{
query: () =>
new Promise<SearchResultSet>(() => {
throw new Error();
}),
},
],
]}
>
<SearchResultList query={query} />
</TestApiProvider>
);
};
export const WithNoResults = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['techdocs'],
});
return (
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
<SearchResultList query={query} />
</TestApiProvider>
);
};
const CustomResultListItem = (props: any) => {
const { icon, result } = props;
return (
<Link to={result.location}>
<ListItem alignItems="flex-start" divider>
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText
primary={result.title}
primaryTypographyProps={{ variant: 'h6' }}
secondary={result.text}
/>
</ListItem>
</Link>
);
};
export const WithCustomResultItem = () => {
const [query] = useState<Partial<SearchQuery>>({
types: ['custom'],
});
return (
<SearchResultList
query={query}
renderResultItem={({ type, document, highlight, rank }) => {
switch (type) {
case 'custom':
return (
<CustomResultListItem
key={document.location}
icon={<CatalogIcon />}
result={document}
highlight={highlight}
rank={rank}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
}}
/>
);
};
@@ -0,0 +1,146 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { screen, waitFor } from '@testing-library/react';
import {
TestApiProvider,
renderWithEffects,
wrapInTestApp,
} from '@backstage/test-utils';
import { searchApiRef } from '../../api';
import { SearchResultList } from './SearchResultList';
const query = jest.fn().mockResolvedValue({ results: [] });
const searchApiMock = { query };
describe('SearchResultList', () => {
const results = [
{
type: 'techdocs',
document: {
location: 'search/search-result1',
title: 'Search Result 1',
text: 'Some text from the search result 1',
},
},
{
type: 'techdocs',
document: {
location: 'search/search-result2',
title: 'Search Result 2',
text: 'Some text from the search result 2',
},
},
];
beforeEach(() => {
jest.clearAllMocks();
});
it('Renders without exploding', async () => {
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultList
query={{
types: ['techdocs'],
}}
/>
</TestApiProvider>,
),
);
expect(query).toHaveBeenCalledWith({
filters: {},
pageCursor: undefined,
term: '',
types: ['techdocs'],
});
});
it('Defines a default render result item', async () => {
query.mockResolvedValueOnce({
results,
});
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultList
query={{
types: ['techdocs'],
}}
/>
</TestApiProvider>,
),
);
expect(screen.getByText('Search Result 1')).toBeInTheDocument();
expect(
screen.getByText('Some text from the search result 1'),
).toBeInTheDocument();
expect(screen.getByText('Search Result 2')).toBeInTheDocument();
expect(
screen.getByText('Some text from the search result 2'),
).toBeInTheDocument();
});
it('Shows a progress bar when loading results', async () => {
query.mockReturnValueOnce(new Promise(() => {}));
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultList
query={{
types: ['techdocs'],
}}
/>
</TestApiProvider>,
),
);
await waitFor(() => {
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
});
it('Shows an error panel when results rendering fails', async () => {
query.mockRejectedValueOnce(new Error());
await renderWithEffects(
wrapInTestApp(
<TestApiProvider apis={[[searchApiRef, searchApiMock]]}>
<SearchResultList
query={{
types: ['techdocs'],
}}
/>
</TestApiProvider>,
),
);
await waitFor(() => {
expect(
screen.getByText(
'Error: Error encountered while fetching search results',
),
).toBeInTheDocument();
});
});
});
@@ -0,0 +1,130 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { List, ListProps } from '@material-ui/core';
import {
EmptyState,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { AnalyticsContext } from '@backstage/core-plugin-api';
import { SearchQuery, SearchResult } from '@backstage/plugin-search-common';
import { DefaultResultListItem } from '../DefaultResultListItem';
import { SearchResultState } from '../SearchResult';
/**
* Props for {@link SearchResultListLayout}
* @public
*/
export type SearchResultListLayoutProps = ListProps & {
/**
* Search results to be rendered as a list.
*/
resultItems?: SearchResult[];
/**
* Function to customize how result items are rendered.
*/
renderResultItem?: (resultItem: SearchResult) => JSX.Element;
/**
* If defined, will render a default error panel.
*/
error?: Error;
/**
* If defined, will render a default loading progress.
*/
loading?: boolean;
};
/**
* Default layout for rendering search results in a list.
* @param props - See {@link SearchResultListLayoutProps}.
* @public
*/
export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
const { loading, error, resultItems, renderResultItem, ...rest } = props;
return (
<List {...rest}>
{loading ? <Progress /> : null}
{!loading && error ? (
<ResponseErrorPanel
title="Error encountered while fetching search results"
error={error}
/>
) : null}
{!loading && !error && resultItems?.length
? resultItems.map(resultItem => renderResultItem?.(resultItem) ?? null)
: null}
{!loading && !error && !resultItems?.length ? (
<EmptyState missing="data" title="Sorry, no results were found" />
) : null}
</List>
);
};
/**
* Props for {@link SearchResultList}.
* @public
*/
export type SearchResultListProps = Omit<
SearchResultListLayoutProps,
'loading' | 'error' | 'resultItems'
> & {
/**
* A search query used for requesting the results to be listed.
*/
query: Partial<SearchQuery>;
};
/**
* Given a query, search for results and render them as a list.
* @param props - See {@link SearchResultListProps}.
* @public
*/
export const SearchResultList = (props: SearchResultListProps) => {
const {
query,
renderResultItem = ({ document }) => (
<DefaultResultListItem key={document.location} result={document} />
),
...rest
} = props;
return (
<AnalyticsContext
attributes={{
pluginId: 'search',
extension: 'SearchResultList',
}}
>
<SearchResultState query={query}>
{({ loading, error, value }) => (
<SearchResultListLayout
{...rest}
loading={loading}
error={error}
resultItems={value?.results}
renderResultItem={renderResultItem}
/>
)}
</SearchResultState>
</AnalyticsContext>
);
};
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { SearchResultList, SearchResultListLayout } from './SearchResultList';
export type {
SearchResultListProps,
SearchResultListLayoutProps,
} from './SearchResultList';
@@ -43,7 +43,7 @@ export const SearchResultPager = () => {
}
return (
<nav arial-label="pagination navigation" className={classes.root}>
<nav aria-label="pagination navigation" className={classes.root}>
<Button
aria-label="previous page"
disabled={!fetchPreviousPage}
@@ -20,4 +20,6 @@ export * from './SearchAutocomplete';
export * from './SearchFilter';
export * from './SearchResult';
export * from './SearchResultPager';
export * from './SearchResultList';
export * from './SearchResultGroup';
export * from './DefaultResultListItem';
@@ -14,13 +14,6 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/types';
import { useApi, AnalyticsContext } from '@backstage/core-plugin-api';
import { SearchResultSet } from '@backstage/plugin-search-common';
import {
createVersionedContext,
createVersionedValueMap,
} from '@backstage/version-bridge';
import React, {
PropsWithChildren,
useCallback,
@@ -30,6 +23,15 @@ import React, {
} from 'react';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
import usePrevious from 'react-use/lib/usePrevious';
import {
createVersionedContext,
createVersionedValueMap,
} from '@backstage/version-bridge';
import { JsonObject } from '@backstage/types';
import { AnalyticsContext, useApi } from '@backstage/core-plugin-api';
import { SearchResultSet } from '@backstage/plugin-search-common';
import { searchApiRef } from '../api';
/**
@@ -104,12 +106,13 @@ const useSearchContextValue = (
initialValue: SearchContextState = searchInitialState,
) => {
const searchApi = useApi(searchApiRef);
const [term, setTerm] = useState<string>(initialValue.term);
const [types, setTypes] = useState<string[]>(initialValue.types);
const [filters, setFilters] = useState<JsonObject>(initialValue.filters);
const [pageCursor, setPageCursor] = useState<string | undefined>(
initialValue.pageCursor,
);
const [filters, setFilters] = useState<JsonObject>(initialValue.filters);
const [term, setTerm] = useState<string>(initialValue.term);
const [types, setTypes] = useState<string[]>(initialValue.types);
const prevTerm = usePrevious(term);
@@ -121,7 +124,7 @@ const useSearchContextValue = (
pageCursor,
types,
}),
[term, filters, types, pageCursor],
[term, types, filters, pageCursor],
);
const hasNextPage =
@@ -19,6 +19,7 @@ export {
useSearch,
useSearchContextCheck,
} from './SearchContext';
export type {
SearchContextProviderProps,
SearchContextState,
+357 -238
View File
@@ -1812,17 +1812,17 @@ __metadata:
linkType: hard
"@storybook/addon-a11y@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/addon-a11y@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/addon-a11y@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/channels": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/theming": 6.5.10
"@storybook/theming": 6.5.12
axe-core: ^4.2.0
core-js: ^3.8.2
global: ^4.4.0
@@ -1839,21 +1839,21 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: 8264ea473b6c8d9be3dd0f6f62849043e2834dfc78030c886ed9e75d4f6bd2617dc8a80a4a223f5d4282e32c8e1614428ce78e6c8eb734f7834877c666a53048
checksum: f93f3c4f4dd9f2f8cfc79200d6201a385d19a3d1bb71ed4b253347db2628f7f414d3479d5545302383a8ff7580bfa5f542ddc9c22ee95ff1abc005e152193580
languageName: node
linkType: hard
"@storybook/addon-actions@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/addon-actions@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/addon-actions@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/theming": 6.5.10
"@storybook/theming": 6.5.12
core-js: ^3.8.2
fast-deep-equal: ^3.1.3
global: ^4.4.0
@@ -1874,23 +1874,23 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: b864ceb0ec9aef76c438cfd55977946619954e07b2b822205e5209e3901cc9ae669babc9304026e48e3717e075212c9e5175d62fd63183cf696e3e196f1f6dd8
checksum: 94f433a6b0956e4301e5b46c68eb56f6a9b01b5ec314099611d584542369d1aec4878a5353052f963e462b1b5f3ce74070f0d03eadf30e275b0b88ef7b713dd8
languageName: node
linkType: hard
"@storybook/addon-controls@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/addon-controls@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/addon-controls@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/node-logger": 6.5.10
"@storybook/store": 6.5.10
"@storybook/theming": 6.5.10
"@storybook/node-logger": 6.5.12
"@storybook/store": 6.5.12
"@storybook/theming": 6.5.12
core-js: ^3.8.2
lodash: ^4.17.21
ts-dedent: ^2.0.0
@@ -1902,19 +1902,19 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: 3c8152e4a4be960a7376ab1b1dc405fb3b6eeab367684766330cfb260519420f693d19b46225fd66976b4fa16e2e888585bfa571436507b2bf10f9905dfa968e
checksum: 27ee396ae4ab411b1bd99eacb0ebe747aa36300dc3b787d48eb685a446e64e33ff7df3b8713943132b6dbe3c78af3d2d6115b4da4c6196ec351d843690ab8a55
languageName: node
linkType: hard
"@storybook/addon-links@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/addon-links@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/addon-links@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/router": 6.5.10
"@storybook/router": 6.5.12
"@types/qs": ^6.9.5
core-js: ^3.8.2
global: ^4.4.0
@@ -1930,21 +1930,21 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: 5ffecdc7f1aac3d9f08ad443a05977da260f6cfbe9f9207bb9c6890dd797eb0304e41527cf70c6c9c68f69f98569ef89f5463bec57209814ff57471c1f0592d6
checksum: 9e1394bc2fed9019537f3fee9f60abc4ea866d1688df72a435052dd07f25a8bac848d6acc2beef92af0cebc3715e9db9ae1bd95c87311a8b9fad7dee988bbd1d
languageName: node
linkType: hard
"@storybook/addon-storysource@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/addon-storysource@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/addon-storysource@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/router": 6.5.10
"@storybook/source-loader": 6.5.10
"@storybook/theming": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/router": 6.5.12
"@storybook/source-loader": 6.5.12
"@storybook/theming": 6.5.12
core-js: ^3.8.2
estraverse: ^5.2.0
loader-utils: ^2.0.0
@@ -1959,11 +1959,11 @@ __metadata:
optional: true
react-dom:
optional: true
checksum: 20dde7aa87cb2255e5cbe4992be622610ae55e32032b6dddb490f490ed35e600f38b0de48de6556dad85e88963ce2a5513e5b09ac21d40a83e3cb5fae8952a90
checksum: 7ebaeffc1646229bebf7c9e918c225c73838b8972c00937e60928a5988440b3e6bf19225cf5f1d21c80aef59b2e20c6f473f6b5c386f5cebf7c9780d74b6ef18
languageName: node
linkType: hard
"@storybook/addons@npm:6.5.10, @storybook/addons@npm:^6.5.9":
"@storybook/addons@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/addons@npm:6.5.10"
dependencies:
@@ -1985,6 +1985,28 @@ __metadata:
languageName: node
linkType: hard
"@storybook/addons@npm:6.5.12, @storybook/addons@npm:^6.0.0, @storybook/addons@npm:^6.5.9":
version: 6.5.12
resolution: "@storybook/addons@npm:6.5.12"
dependencies:
"@storybook/api": 6.5.12
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/router": 6.5.12
"@storybook/theming": 6.5.12
"@types/webpack-env": ^1.16.0
core-js: ^3.8.2
global: ^4.4.0
regenerator-runtime: ^0.13.7
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: c6242a80c7355544eb309603e77fdc3787d78ad983aba931f00812aeba75cc2cbd0e98c1ac0ce01441b58fabcdb671a9a799358f4bd6511cab289bc030d91f61
languageName: node
linkType: hard
"@storybook/api@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/api@npm:6.5.10"
@@ -2013,27 +2035,55 @@ __metadata:
languageName: node
linkType: hard
"@storybook/builder-webpack4@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/builder-webpack4@npm:6.5.10"
"@storybook/api@npm:6.5.12, @storybook/api@npm:^6.0.0":
version: 6.5.12
resolution: "@storybook/api@npm:6.5.12"
dependencies:
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/router": 6.5.12
"@storybook/semver": ^7.3.2
"@storybook/theming": 6.5.12
core-js: ^3.8.2
fast-deep-equal: ^3.1.3
global: ^4.4.0
lodash: ^4.17.21
memoizerific: ^1.11.3
regenerator-runtime: ^0.13.7
store2: ^2.12.0
telejson: ^6.0.8
ts-dedent: ^2.0.0
util-deprecate: ^1.0.2
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 3982cea5aaf851ccc19ff97ef82b7590d47839f0ebee28399e3b9381578edc130b4e46fe36431c62fa281949b2e0d5da2b1feafab0c2d24f70c4097d800b2679
languageName: node
linkType: hard
"@storybook/builder-webpack4@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/builder-webpack4@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/channel-postmessage": 6.5.10
"@storybook/channels": 6.5.10
"@storybook/client-api": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/node-logger": 6.5.10
"@storybook/preview-web": 6.5.10
"@storybook/router": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/channel-postmessage": 6.5.12
"@storybook/channels": 6.5.12
"@storybook/client-api": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/node-logger": 6.5.12
"@storybook/preview-web": 6.5.12
"@storybook/router": 6.5.12
"@storybook/semver": ^7.3.2
"@storybook/store": 6.5.10
"@storybook/theming": 6.5.10
"@storybook/ui": 6.5.10
"@storybook/store": 6.5.12
"@storybook/theming": 6.5.12
"@storybook/ui": 6.5.12
"@types/node": ^14.0.10 || ^16.0.0
"@types/webpack": ^4.41.26
autoprefixer: ^9.8.6
@@ -2070,30 +2120,30 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 26921bbc477b8cc69a9515996f4e4a4b79ba43f783dab96930067c48ca6d127397ab7a461c25e3120468b99cad1cb641fbb85a0cd6ecf25661e2da2c182a97e6
checksum: 3cb72ade60fc0767480c424cd5da6659027c35759852198936530c555d7fa1ae18326b3d696def06a54a55f7f26a6eb0246e675ea566c5bbfc63db54e0ad8880
languageName: node
linkType: hard
"@storybook/builder-webpack5@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/builder-webpack5@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/builder-webpack5@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/channel-postmessage": 6.5.10
"@storybook/channels": 6.5.10
"@storybook/client-api": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/node-logger": 6.5.10
"@storybook/preview-web": 6.5.10
"@storybook/router": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/channel-postmessage": 6.5.12
"@storybook/channels": 6.5.12
"@storybook/client-api": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/node-logger": 6.5.12
"@storybook/preview-web": 6.5.12
"@storybook/router": 6.5.12
"@storybook/semver": ^7.3.2
"@storybook/store": 6.5.10
"@storybook/theming": 6.5.10
"@storybook/store": 6.5.12
"@storybook/theming": 6.5.12
"@types/node": ^14.0.10 || ^16.0.0
babel-loader: ^8.0.0
babel-plugin-named-exports-order: ^0.0.2
@@ -2122,35 +2172,35 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: a2a0d7cbdcf2d1d53ec8db9c17433522525f54c9115b5ed9b576e528dd164f9036ebad0b850a00178a9b60dd9f5b8f62af4e6b19977ffe857442c8b5483349de
checksum: 60387a186defc3b40ceae8c41376da5de65d52cc652a203bce8ef8733e54bec552143440ce5fb466916a87b0ef67b98932a96985f3ee371a3f74a9049277092f
languageName: node
linkType: hard
"@storybook/channel-postmessage@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/channel-postmessage@npm:6.5.10"
"@storybook/channel-postmessage@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/channel-postmessage@npm:6.5.12"
dependencies:
"@storybook/channels": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
core-js: ^3.8.2
global: ^4.4.0
qs: ^6.10.0
telejson: ^6.0.8
checksum: c0bb9cccb8071b6d68ba879f23a9eb52ee9da5563f93a235f2496838a691c7e3f7ed81e550f924bbdc305357e5a81d4b409254c7ce4d5bed53920f0ea357d4f6
checksum: c225f848f4774e8159b9fd8bd904520ab2755f46ac6ef5a8ed7193b5cd79856e0bb797d10adfa0a1db9b9df075c41b4487d195cc451fb65b04737db70e5db6db
languageName: node
linkType: hard
"@storybook/channel-websocket@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/channel-websocket@npm:6.5.10"
"@storybook/channel-websocket@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/channel-websocket@npm:6.5.12"
dependencies:
"@storybook/channels": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
core-js: ^3.8.2
global: ^4.4.0
telejson: ^6.0.8
checksum: e8c6df2ae02a7a257f0503cd489a2e787a419d23bc1c077868520db0ad61642001655543d594c4903f346112dc27698acfc9501c07d02bb2027d2fbb9f98f1eb
checksum: 03d4ed3f2b67daceea06325c8705b95013b65d014dfda769a923b2c1c890d1eca1fe8eea09e3386cc572c738ce43bc8951225ebd92de4205ad80438bb9e36ebe
languageName: node
linkType: hard
@@ -2165,17 +2215,28 @@ __metadata:
languageName: node
linkType: hard
"@storybook/client-api@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/client-api@npm:6.5.10"
"@storybook/channels@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/channels@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/channel-postmessage": 6.5.10
"@storybook/channels": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core-events": 6.5.10
core-js: ^3.8.2
ts-dedent: ^2.0.0
util-deprecate: ^1.0.2
checksum: e6b240a6c62a68a485bf8f4db536df0504cfcbe9685654e5a5712b833917b9a620e91994bf2283a420e413511c967e92ead522c98ad7e6c0e88b3830ddfd4e30
languageName: node
linkType: hard
"@storybook/client-api@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/client-api@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.12
"@storybook/channel-postmessage": 6.5.12
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/store": 6.5.10
"@storybook/store": 6.5.12
"@types/qs": ^6.9.5
"@types/webpack-env": ^1.16.0
core-js: ^3.8.2
@@ -2192,7 +2253,7 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: c939abed09fc71b91a2813b4d817a00f717dcef6c51444b091ad3676dd0e904673252dbb9e027192e87db72aeb950a76438c5ae5829471fe18c053887049f151
checksum: 6a103cdf1c0499e238e6a652f192b3287b7bce2a96c194c48854d1e6d8e470568aa36307aa49650e8322965295b10d601c4a087e2cb7e3515bb9ba8281aeda35
languageName: node
linkType: hard
@@ -2206,13 +2267,23 @@ __metadata:
languageName: node
linkType: hard
"@storybook/components@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/components@npm:6.5.10"
"@storybook/client-logger@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/client-logger@npm:6.5.12"
dependencies:
"@storybook/client-logger": 6.5.10
core-js: ^3.8.2
global: ^4.4.0
checksum: bd11bc25115f9b4a965e378d7dac28f9152038173ab5debb1e116a7aba69c814752d2c8aa4092dd1fc3f60cd99d4896c9e74d5e6f3c85768e7633adaf5bd2bf2
languageName: node
linkType: hard
"@storybook/components@npm:6.5.12, @storybook/components@npm:^6.0.0":
version: 6.5.12
resolution: "@storybook/components@npm:6.5.12"
dependencies:
"@storybook/client-logger": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/theming": 6.5.10
"@storybook/theming": 6.5.12
core-js: ^3.8.2
memoizerific: ^1.11.3
qs: ^6.10.0
@@ -2221,24 +2292,24 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: ee0d520048296a4312b3018759a6b01fcc2c3fa867c64dd938e0c5ae6e4d907f599286323855128901420cd45955890e8cdb767c7b381be75d67729d89ca368a
checksum: fa469ae615d9146df7e23f01b85731d27e6400e2d94035db172deb1f61903d86c121d858558dd12307ecc6344d21b496db020731e73eff6ace3f82672b953a93
languageName: node
linkType: hard
"@storybook/core-client@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/core-client@npm:6.5.10"
"@storybook/core-client@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/core-client@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/channel-postmessage": 6.5.10
"@storybook/channel-websocket": 6.5.10
"@storybook/client-api": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/channel-postmessage": 6.5.12
"@storybook/channel-websocket": 6.5.12
"@storybook/client-api": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/preview-web": 6.5.10
"@storybook/store": 6.5.10
"@storybook/ui": 6.5.10
"@storybook/preview-web": 6.5.12
"@storybook/store": 6.5.12
"@storybook/ui": 6.5.12
airbnb-js-shims: ^2.2.1
ansi-to-html: ^0.6.11
core-js: ^3.8.2
@@ -2256,13 +2327,13 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: c8bc4b41af51664461716dab87a176e7ac408f75568ff884b80435bbfce197ba7dd607ba83a2b36bdfbc90236e2e88848089a3ae27732a60b210fbd60ed3597c
checksum: 4fb567964a6c15526ee6ee882e20d72c650ad0c74504ee2a058c13856efd25a0c9c7f666d36c6b4e70a75ece73c1ed812f554bbbf87771cd6171cd09ebf31410
languageName: node
linkType: hard
"@storybook/core-common@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/core-common@npm:6.5.10"
"@storybook/core-common@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/core-common@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@babel/plugin-proposal-class-properties": ^7.12.1
@@ -2286,7 +2357,7 @@ __metadata:
"@babel/preset-react": ^7.12.10
"@babel/preset-typescript": ^7.12.7
"@babel/register": ^7.12.1
"@storybook/node-logger": 6.5.10
"@storybook/node-logger": 6.5.12
"@storybook/semver": ^7.3.2
"@types/node": ^14.0.10 || ^16.0.0
"@types/pretty-hrtime": ^1.0.0
@@ -2320,7 +2391,7 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: b3b95214a427c1ff34464c1638219fd34aa8a98b60541ec3e13d84b095be79773e5de64c958903da877e6ec52b88ff05dd9a8cd7ab0fde548ffa0db762a4ea4e
checksum: d12b276718d3bb527084135882abc35fcdc4690896579b9f5e0417236a0d02f2791424ac62a891a0353af635be10c22db7dfe04d0db644ab0757ae3981f0ff1a
languageName: node
linkType: hard
@@ -2333,22 +2404,31 @@ __metadata:
languageName: node
linkType: hard
"@storybook/core-server@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/core-server@npm:6.5.10"
"@storybook/core-events@npm:6.5.12, @storybook/core-events@npm:^6.0.0":
version: 6.5.12
resolution: "@storybook/core-events@npm:6.5.12"
dependencies:
core-js: ^3.8.2
checksum: 82a4b9cb2a8599f3916db84b08b4cfbde8f56cb96a7afe641b3f144676fc7dc5a705e65f8844430b36ee6e6e14d6b2cb741622a8b39411276682219df5e04271
languageName: node
linkType: hard
"@storybook/core-server@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/core-server@npm:6.5.12"
dependencies:
"@discoveryjs/json-ext": ^0.5.3
"@storybook/builder-webpack4": 6.5.10
"@storybook/core-client": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/builder-webpack4": 6.5.12
"@storybook/core-client": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/csf-tools": 6.5.10
"@storybook/manager-webpack4": 6.5.10
"@storybook/node-logger": 6.5.10
"@storybook/csf-tools": 6.5.12
"@storybook/manager-webpack4": 6.5.12
"@storybook/node-logger": 6.5.12
"@storybook/semver": ^7.3.2
"@storybook/store": 6.5.10
"@storybook/telemetry": 6.5.10
"@storybook/store": 6.5.12
"@storybook/telemetry": 6.5.12
"@types/node": ^14.0.10 || ^16.0.0
"@types/node-fetch": ^2.5.7
"@types/pretty-hrtime": ^1.0.0
@@ -2392,16 +2472,16 @@ __metadata:
optional: true
typescript:
optional: true
checksum: 0359f8cf68e2a207d07ec631d0615c30991c78bcbe3ebe50cb8df8dd5159ab939d52789b84a50e074c027c253f74f813f745b4a002a5cf945de50a0069e0e758
checksum: 1e7e8de948012eb126f30261d23a552e18e671ad7796c46051e17be545c37a70c72437cbb24249d2e88b3525ed6a4b7205ddeeb167aca6b9478df85b128a57d3
languageName: node
linkType: hard
"@storybook/core@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/core@npm:6.5.10"
"@storybook/core@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/core@npm:6.5.12"
dependencies:
"@storybook/core-client": 6.5.10
"@storybook/core-server": 6.5.10
"@storybook/core-client": 6.5.12
"@storybook/core-server": 6.5.12
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -2413,13 +2493,13 @@ __metadata:
optional: true
typescript:
optional: true
checksum: ee80fa596cfc305138089757b1f095a0b44ed403ff1db727e99190a5d04cca84614faa816ed881b60c7ed91a4d268ee91632cb7bcaa7f2a2127424acd66a2c96
checksum: 82606be8f89ad34a662d366e64d85af5a61270e2cff4dab8c3c3b0ec17ae3e34e560b54a1e2dd3fdd52eb072f8101b409e762f8581e65abcf07097e768baaaf0
languageName: node
linkType: hard
"@storybook/csf-tools@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/csf-tools@npm:6.5.10"
"@storybook/csf-tools@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/csf-tools@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@babel/generator": ^7.12.11
@@ -2440,7 +2520,7 @@ __metadata:
peerDependenciesMeta:
"@storybook/mdx2-csf":
optional: true
checksum: 9bb4b61822760520c91da78b734a05c1f5145ad2e91f73cfe03aa900a6f40fd455c1fc2c3b1529a97a5e33246efb44462c68ad72b8dbf8f0b1811b7491411267
checksum: 21da554c88f22ee583cd1956cf440506212d9e8727c7f0a493a92804e58b83d3fcfa18d3081a9fd1b5e8da07a1cfbee15bfa638a13a8fad585eac04fe26f5112
languageName: node
linkType: hard
@@ -2453,18 +2533,18 @@ __metadata:
languageName: node
linkType: hard
"@storybook/docs-tools@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/docs-tools@npm:6.5.10"
"@storybook/docs-tools@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/docs-tools@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/store": 6.5.10
"@storybook/store": 6.5.12
core-js: ^3.8.2
doctrine: ^3.0.0
lodash: ^4.17.21
regenerator-runtime: ^0.13.7
checksum: 7fe14992ba94c31879964001a192f338bd53399a9582c598ab1681a05efb30999059329b1f7a4cbd33947778f17e6929d0f983cf54c36cb9e371414044f5dd89
checksum: 9433b0bc74e739f37d4be857e366d74e56566cdf27f72f462eb09a6e84713aadc8c768e075fbe7a062be104bb1536c14b8dea1e890917ca94b1580e71dbe60be
languageName: node
linkType: hard
@@ -2481,19 +2561,19 @@ __metadata:
languageName: node
linkType: hard
"@storybook/manager-webpack4@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/manager-webpack4@npm:6.5.10"
"@storybook/manager-webpack4@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/manager-webpack4@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@babel/plugin-transform-template-literals": ^7.12.1
"@babel/preset-react": ^7.12.10
"@storybook/addons": 6.5.10
"@storybook/core-client": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/node-logger": 6.5.10
"@storybook/theming": 6.5.10
"@storybook/ui": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/core-client": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/node-logger": 6.5.12
"@storybook/theming": 6.5.12
"@storybook/ui": 6.5.12
"@types/node": ^14.0.10 || ^16.0.0
"@types/webpack": ^4.41.26
babel-loader: ^8.0.0
@@ -2526,23 +2606,23 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 954f93dded7a2294cdbd7c7df93fcf1addb34d5daf46d58d8bf64d0a7e83664e34f32cd905b2b2ec771a551869e497c12f8d659a358ac079d815ca02baede6e6
checksum: 89c6ab508a930def13403275201e1e7efd667a25f0e04a6a4fe83e83d8a77065309f11c6ba183bb412072b80580e232365789d2317ae9c1e92df92c4061752be
languageName: node
linkType: hard
"@storybook/manager-webpack5@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/manager-webpack5@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/manager-webpack5@npm:6.5.12"
dependencies:
"@babel/core": ^7.12.10
"@babel/plugin-transform-template-literals": ^7.12.1
"@babel/preset-react": ^7.12.10
"@storybook/addons": 6.5.10
"@storybook/core-client": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/node-logger": 6.5.10
"@storybook/theming": 6.5.10
"@storybook/ui": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/core-client": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/node-logger": 6.5.12
"@storybook/theming": 6.5.12
"@storybook/ui": 6.5.12
"@types/node": ^14.0.10 || ^16.0.0
babel-loader: ^8.0.0
case-sensitive-paths-webpack-plugin: ^2.3.0
@@ -2572,7 +2652,7 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 0e7542ed57cd5bef81374e88f96b4572bdea2cf0f4d09282c9794cb46641f23e085c2d6de65e224b358605e01655679e9a1933756ce728c41d1936865aa14e8a
checksum: 701768cee510e9de024259c88ebd85ebc212d37d9f913fc7a1e13ab8751d7fb579bb99e494a439d6d5388c02e204eea8c8d91d1490b138105b414df8a384b7ae
languageName: node
linkType: hard
@@ -2595,29 +2675,29 @@ __metadata:
languageName: node
linkType: hard
"@storybook/node-logger@npm:6.5.10, @storybook/node-logger@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/node-logger@npm:6.5.10"
"@storybook/node-logger@npm:6.5.12, @storybook/node-logger@npm:^6.5.9":
version: 6.5.12
resolution: "@storybook/node-logger@npm:6.5.12"
dependencies:
"@types/npmlog": ^4.1.2
chalk: ^4.1.0
core-js: ^3.8.2
npmlog: ^5.0.1
pretty-hrtime: ^1.0.3
checksum: 684eddeadccb632dd0aa7d2bca62a374f71a15f07037788ee82f4d57e18ce7616304e5d8084b96dff742fe2b810843c44f26d53d4ff8f7d0706cdd81d0060fee
checksum: 7589477486a25e67d9119e9c363e8bde23e52601043a506ac0d28f4d353f3a228face79b40a2eb0cc0c7c8b05ed084336fef5dcc3213ed4484527c6631eafeb0
languageName: node
linkType: hard
"@storybook/preview-web@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/preview-web@npm:6.5.10"
"@storybook/preview-web@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/preview-web@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/channel-postmessage": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/channel-postmessage": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/store": 6.5.10
"@storybook/store": 6.5.12
ansi-to-html: ^0.6.11
core-js: ^3.8.2
global: ^4.4.0
@@ -2631,7 +2711,7 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: ad4ee244101a5b9bac68373e99c95e86f3cf397d5e1093d871ad880a439dac737297bd85f4148a0b6dd3f7550e994125b4f434e113359ce9208c71cefaeab195
checksum: e11671fd136042a0ac19be6749f40bcdf858adb6fbc36998eb2c1737fc622c15df233eb04ff3cf555a61a31d32e5218cf9ff3ff9b941b457a8159a8ca54ab2e8
languageName: node
linkType: hard
@@ -2654,22 +2734,22 @@ __metadata:
linkType: hard
"@storybook/react@npm:^6.5.9":
version: 6.5.10
resolution: "@storybook/react@npm:6.5.10"
version: 6.5.12
resolution: "@storybook/react@npm:6.5.12"
dependencies:
"@babel/preset-flow": ^7.12.1
"@babel/preset-react": ^7.12.10
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.3
"@storybook/addons": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core": 6.5.12
"@storybook/core-common": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
"@storybook/docs-tools": 6.5.10
"@storybook/node-logger": 6.5.10
"@storybook/docs-tools": 6.5.12
"@storybook/node-logger": 6.5.12
"@storybook/react-docgen-typescript-plugin": 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0
"@storybook/semver": ^7.3.2
"@storybook/store": 6.5.10
"@storybook/store": 6.5.12
"@types/estree": ^0.0.51
"@types/node": ^14.14.20 || ^16.0.0
"@types/webpack-env": ^1.16.0
@@ -2714,7 +2794,7 @@ __metadata:
build-storybook: bin/build.js
start-storybook: bin/index.js
storybook-server: bin/index.js
checksum: 4459ee91ec8aa0159d51e9fae2d0a7bde1be4ff6fd84ae0c1a414feed2d1797e7c8b9f38b256177e3b451142ebbb64a26ac1960a15a5689ae25dbffd3fb1a7b2
checksum: 7b762f5b0db2b94d3e492ed45a7566009dc9ff008c9b29db278d6404e1c7c9f419a0114090bf23fc5b3e193a83d74e21f0fe6ba04105eb30f59be1d4c87d652d
languageName: node
linkType: hard
@@ -2734,6 +2814,22 @@ __metadata:
languageName: node
linkType: hard
"@storybook/router@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/router@npm:6.5.12"
dependencies:
"@storybook/client-logger": 6.5.12
core-js: ^3.8.2
memoizerific: ^1.11.3
qs: ^6.10.0
regenerator-runtime: ^0.13.7
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 545f4b767021b88f82eac69b9356fa5fa3a5866285c3a34fa762abc5743e3280895858aa5c820195d95a04c6768299191d4dd788a7d9fd3f17ff1d8236c0ba75
languageName: node
linkType: hard
"@storybook/semver@npm:^7.3.2":
version: 7.3.2
resolution: "@storybook/semver@npm:7.3.2"
@@ -2746,12 +2842,12 @@ __metadata:
languageName: node
linkType: hard
"@storybook/source-loader@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/source-loader@npm:6.5.10"
"@storybook/source-loader@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/source-loader@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
core-js: ^3.8.2
estraverse: ^5.2.0
@@ -2763,17 +2859,17 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 77d7a0255cace96fc9953518fe54162ce4b2167b53eb744f498cf2098ba4af8074d75f572940621675303043b69e2281e8a5479ce2d331d47aa86c189cdd53bb
checksum: ad6b0774877678d8495e7afaeb820adbc2d1b091df5678da39123eca875cc30fe41f013c2db0a4c5f7614529dc46be00cb165484e65a960d3fe7657b04cf418f
languageName: node
linkType: hard
"@storybook/store@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/store@npm:6.5.10"
"@storybook/store@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/store@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/addons": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/csf": 0.0.2--canary.4566f4d.1
core-js: ^3.8.2
fast-deep-equal: ^3.1.3
@@ -2789,16 +2885,16 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: cd8628f9bca4fe021dbf915ac2fad02baccf6fb06568fbe8192d268d7eaaed0f96c5b42c43f9883a1528a9b46e98c7d1d06aa8c0fad4c103f5f99325979a6b89
checksum: 7fab43471c692cda33e9cbb7abf8a932bf922bd01bf26c56a3dd909f57ff0f26a80c6456a41c5be01191bb818536f16f5c98617d263b49a60e432b0acca07949
languageName: node
linkType: hard
"@storybook/telemetry@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/telemetry@npm:6.5.10"
"@storybook/telemetry@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/telemetry@npm:6.5.12"
dependencies:
"@storybook/client-logger": 6.5.10
"@storybook/core-common": 6.5.10
"@storybook/client-logger": 6.5.12
"@storybook/core-common": 6.5.12
chalk: ^4.1.0
core-js: ^3.8.2
detect-package-manager: ^2.0.1
@@ -2809,7 +2905,7 @@ __metadata:
nanoid: ^3.3.1
read-pkg-up: ^7.0.1
regenerator-runtime: ^0.13.7
checksum: 774acc7f5d91b855be3ec1e2ae5a13b61e3eb9db2c2284ee54d788a701e637a86d4ca14597a027d32555f74392e4c99f47e886bc7729a7222e4e8159c492e054
checksum: fe465e31e20bc271b1b066a1c1fc4ea8b7cdca1858bc875e19d7ad4e7988c0cff084ce822dc0657ecdefafa02a416dd239753c9c197f6758b39d9260964d631c
languageName: node
linkType: hard
@@ -2841,19 +2937,34 @@ __metadata:
languageName: node
linkType: hard
"@storybook/ui@npm:6.5.10":
version: 6.5.10
resolution: "@storybook/ui@npm:6.5.10"
"@storybook/theming@npm:6.5.12, @storybook/theming@npm:^6.0.0":
version: 6.5.12
resolution: "@storybook/theming@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.10
"@storybook/api": 6.5.10
"@storybook/channels": 6.5.10
"@storybook/client-logger": 6.5.10
"@storybook/components": 6.5.10
"@storybook/core-events": 6.5.10
"@storybook/router": 6.5.10
"@storybook/client-logger": 6.5.12
core-js: ^3.8.2
memoizerific: ^1.11.3
regenerator-runtime: ^0.13.7
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: a982ebf88c7e1e21127febd17feebf26ac8d655f0c868bf110cbcaaef87eedb257300087618c525cb654808b590dc4b7b98dd6fec92fd76a040441d86c4b8289
languageName: node
linkType: hard
"@storybook/ui@npm:6.5.12":
version: 6.5.12
resolution: "@storybook/ui@npm:6.5.12"
dependencies:
"@storybook/addons": 6.5.12
"@storybook/api": 6.5.12
"@storybook/channels": 6.5.12
"@storybook/client-logger": 6.5.12
"@storybook/components": 6.5.12
"@storybook/core-events": 6.5.12
"@storybook/router": 6.5.12
"@storybook/semver": ^7.3.2
"@storybook/theming": 6.5.10
"@storybook/theming": 6.5.12
core-js: ^3.8.2
memoizerific: ^1.11.3
qs: ^6.10.0
@@ -2862,7 +2973,7 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: fc0180fc183a41b5da5a530aa8e22fd84b1934602b01bdc90b3a9865794161ffaad520e7904daf603d9fd1797dca304c4df1c5360cc1894c4971dbbca463e5dd
checksum: 026ddc42d00773ad711824a5374b040b589e8a87b8500db4d6a8cb67263eba0789d6e6afe7fd8e0fcb798380eba210b4225af197f064d3e88ec0bda97a83b28b
languageName: node
linkType: hard
@@ -10585,18 +10696,26 @@ __metadata:
linkType: hard
"storybook-dark-mode@npm:^1.1.0":
version: 1.1.0
resolution: "storybook-dark-mode@npm:1.1.0"
version: 1.1.2
resolution: "storybook-dark-mode@npm:1.1.2"
dependencies:
fast-deep-equal: ^3.0.0
memoizerific: ^1.11.3
peerDependencies:
"@storybook/addons": ^6.0.0
"@storybook/api": ^6.0.0
"@storybook/components": ^6.0.0
"@storybook/core-events": ^6.0.0
"@storybook/theming": ^6.0.0
checksum: e1d7abbb96d1cdbe9cdba1e20aabffa6878f170e348958cc328de4183027c2052f44f6d78f82a45039f803349c7a4ec19988d42898030b2720dc591192f520e6
fast-deep-equal: ^3.0.0
global: ^4.4.0
memoizerific: ^1.11.3
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
checksum: c1522be5d7b52315e7343840bc5b6ed771e671814ccc0f63fbaa3a9f47d25c5cc0e0f5d0cdb5ac3e75e356e93d66ced8722ad2b0f7306b4113489e21cf8b07ff
languageName: node
linkType: hard
+34 -15
View File
@@ -6633,6 +6633,7 @@ __metadata:
"@testing-library/react": ^12.1.3
"@testing-library/react-hooks": ^8.0.0
"@testing-library/user-event": ^14.0.0
qs: ^6.9.4
react-use: ^17.3.2
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
@@ -8128,11 +8129,11 @@ __metadata:
linkType: hard
"@google-cloud/container@npm:^4.0.0":
version: 4.1.2
resolution: "@google-cloud/container@npm:4.1.2"
version: 4.1.3
resolution: "@google-cloud/container@npm:4.1.3"
dependencies:
google-gax: ^3.3.0
checksum: deb1b88732b2dd3dba5f7bea48fd672636fedb0d3bd894e29c8d097e087de3bfd30b53668e3686f8d879b3312be2e86d5a65cd1ba6bf630ba1c2bbe892e8bbb8
checksum: d9554ed344e065ec16b9d281eeecd2796bf5c90149358e724596c6e2bc97c6b9ea16a9dbcf4805e29003e4bcaf3aaa85c70609b7b9b1db0fbae6c9018e55b6d8
languageName: node
linkType: hard
@@ -11247,13 +11248,20 @@ __metadata:
languageName: node
linkType: hard
"@microsoft/tsdoc@npm:0.14.1, @microsoft/tsdoc@npm:^0.14.1":
"@microsoft/tsdoc@npm:0.14.1":
version: 0.14.1
resolution: "@microsoft/tsdoc@npm:0.14.1"
checksum: e4ad038ccff2cd96e0d53ee42e2136f0f5a925b16cfda14261f1c2eb55ba0088a0e3b08ff819b476ddc69b2242a391925fab7f6ae2afabb19b96f87e19c114fc
languageName: node
linkType: hard
"@microsoft/tsdoc@npm:^0.14.1":
version: 0.14.2
resolution: "@microsoft/tsdoc@npm:0.14.2"
checksum: b167c89e916ba73ee20b9c9d5dba6aa3a0de25ed3d50050e8a344dca7cd43cb2e1059bd515c820369b6e708901dd3fda476a42bc643ca74a35671ce77f724a3a
languageName: node
linkType: hard
"@mswjs/cookies@npm:^0.2.0":
version: 0.2.0
resolution: "@mswjs/cookies@npm:0.2.0"
@@ -14039,7 +14047,7 @@ __metadata:
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.5":
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18":
version: 4.17.29
resolution: "@types/express-serve-static-core@npm:4.17.29"
dependencies:
@@ -14061,6 +14069,17 @@ __metadata:
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:^4.17.5":
version: 4.17.31
resolution: "@types/express-serve-static-core@npm:4.17.31"
dependencies:
"@types/node": "*"
"@types/qs": "*"
"@types/range-parser": "*"
checksum: 009bfbe1070837454a1056aa710d0390ee5fb8c05dfe5a1691cc3e2ca88dc256f80e1ca27cb51a978681631d2f6431bfc9ec352ea46dd0c6eb183d0170bde5df
languageName: node
linkType: hard
"@types/express-session@npm:^1.17.2":
version: 1.17.5
resolution: "@types/express-session@npm:1.17.5"
@@ -14464,9 +14483,9 @@ __metadata:
linkType: hard
"@types/marked@npm:^4.0.0":
version: 4.0.6
resolution: "@types/marked@npm:4.0.6"
checksum: 223f7d8e9481287aa0274df573dd56df6cb11a1090649fabd63fd97f900ed318e730d9dfaff598aa75d8946a212c1d8959a421c5cb46d90c5a6f734a1e4a7d78
version: 4.0.7
resolution: "@types/marked@npm:4.0.7"
checksum: 4907b6a606578cd864bad429aca3c234591e6ed56bd141c575140487269b825a480ace9a85e4d003d1de1f007004c9d9b2fe600038ded5bba75aef59118e58d5
languageName: node
linkType: hard
@@ -22111,8 +22130,8 @@ __metadata:
linkType: hard
"eslint-plugin-react@npm:^7.28.0":
version: 7.31.1
resolution: "eslint-plugin-react@npm:7.31.1"
version: 7.31.8
resolution: "eslint-plugin-react@npm:7.31.8"
dependencies:
array-includes: ^3.1.5
array.prototype.flatmap: ^1.3.0
@@ -22130,7 +22149,7 @@ __metadata:
string.prototype.matchall: ^4.0.7
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
checksum: 6217d4c4e36c8fea24facd0cdcf22b2fd38a3603db94ec7c0a6f430046c8564b6c6884e0a9d4a4b8766201f66e8b18af594002210421bf9b6623b1fc32e15a3a
checksum: 0683e2a624a4df6f08264a3f6bc614a81e8f961c83173bdf2d8d3523f84ed5d234cddc976dbc6815913e007c5984df742ba61be0c0592b27c3daabe0f68165a3
languageName: node
linkType: hard
@@ -31058,13 +31077,13 @@ __metadata:
linkType: hard
"nodemon@npm:^2.0.2":
version: 2.0.19
resolution: "nodemon@npm:2.0.19"
version: 2.0.20
resolution: "nodemon@npm:2.0.20"
dependencies:
chokidar: ^3.5.2
debug: ^3.2.7
ignore-by-default: ^1.0.1
minimatch: ^3.0.4
minimatch: ^3.1.2
pstree.remy: ^1.1.8
semver: ^5.7.1
simple-update-notifier: ^1.0.7
@@ -31073,7 +31092,7 @@ __metadata:
undefsafe: ^2.0.5
bin:
nodemon: bin/nodemon.js
checksum: c6cf89435a8945693fac2701285eb1f539b5003d943a1be89a9ffbfc9d0275aa7779f85a9eee509e9f19a988d53ce293266d8b35b91010e36ad9e78683f8eb07
checksum: 9fe858682414fe703179f4fe36c86e71f40d2693b5345c09803d7b191816a6589c5df8f1f9873bffee92893880183b95a031c86340e46b364ef1b0b7f619edbf
languageName: node
linkType: hard