Initial SearchTypeFacet implementation.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-12-27 18:40:20 +01:00
parent 8e5d716c0a
commit 8b532a6c02
8 changed files with 301 additions and 7 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-search': patch
---
Introduces a `<SearchTypeFacet />` component, which operates on the same part of a search query as the `<SearchType />` component, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs).
Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtypefacet--default) to see how it can be used.
@@ -14,7 +14,14 @@
* limitations under the License.
*/
import { Content, Header, Lifecycle, Page } from '@backstage/core-components';
import {
CatalogIcon,
Content,
DocsIcon,
Header,
Lifecycle,
Page,
} from '@backstage/core-components';
import { CatalogResultListItem } from '@backstage/plugin-catalog';
import {
DefaultResultListItem,
@@ -22,7 +29,7 @@ import {
SearchFilter,
SearchResult,
SearchResultPager,
SearchType,
SearchTypeFacet,
} from '@backstage/plugin-search';
import { DocsResultListItem } from '@backstage/plugin-techdocs';
import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core';
@@ -39,6 +46,7 @@ const useStyles = makeStyles((theme: Theme) => ({
},
filters: {
padding: theme.spacing(2),
marginTop: theme.spacing(2),
},
}));
@@ -55,12 +63,23 @@ const SearchPage = () => {
</Paper>
</Grid>
<Grid item xs={3}>
<SearchTypeFacet
name="Result Type"
defaultValue="software-catalog"
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
icon: <CatalogIcon />,
},
{
value: 'techdocs',
name: 'Documentation',
icon: <DocsIcon />,
},
]}
/>
<Paper className={classes.filters}>
<SearchType
values={['techdocs', 'software-catalog']}
name="type"
defaultValue="software-catalog"
/>
<SearchFilter.Select
className={classes.filter}
name="kind"
+14
View File
@@ -224,6 +224,20 @@ export const SearchType: ({
defaultValue,
}: SearchTypeProps) => JSX.Element;
// @public
export const SearchTypeFacet: (props: SearchTypeFacetProps) => JSX.Element;
// @public (undocumented)
export type SearchTypeFacetProps = {
name: string;
types: Array<{
value: string;
name: string;
icon: JSX.Element;
}>;
defaultValue?: string;
};
// Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -0,0 +1,49 @@
/*
* Copyright 2021 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, { useState } from 'react';
import CatalogIcon from '@material-ui/icons/MenuBook';
import DocsIcon from '@material-ui/icons/Description';
import { SearchTypeFacet } from '../index';
import { SearchContext } from '../SearchContext';
export default {
title: 'Plugins/Search/SearchTypeFacet',
component: SearchTypeFacet,
};
export const Default = () => {
const [types, setTypes] = useState<string[]>([]);
return (
<SearchContext.Provider
value={{ types, setTypes, setPageCursor: () => {} } as any}
>
<SearchTypeFacet
name="Result Type"
defaultValue="software-catalog"
types={[
{
value: 'software-catalog',
name: 'Software Catalog',
icon: <CatalogIcon />,
},
{ value: 'techdocs', name: 'Documentation', icon: <DocsIcon /> },
]}
/>
</SearchContext.Provider>
);
};
@@ -0,0 +1,184 @@
/*
* Copyright 2021 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,
cloneElement,
Fragment,
useEffect,
useState,
} from 'react';
import { useSearch } from '../SearchContext';
import {
Accordion,
AccordionSummary,
AccordionDetails,
Card,
CardContent,
CardHeader,
Divider,
List,
ListItem,
ListItemIcon,
ListItemText,
makeStyles,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import AllIcon from '@material-ui/icons/FontDownload';
const useStyles = makeStyles(theme => ({
card: {
backgroundColor: 'rgba(0, 0, 0, .11)',
},
cardContent: {
paddingTop: theme.spacing(1),
},
icon: {
color: theme.palette.common.black,
},
list: {
width: '100%',
},
listItemIcon: {
width: '24px',
height: '24px',
},
accordion: {
backgroundColor: theme.palette.background.paper,
},
accordionSummary: {
minHeight: 'auto',
'&.Mui-expanded': {
minHeight: 'auto',
},
},
accordionSummaryContent: {
margin: theme.spacing(2, 0),
'&.Mui-expanded': {
margin: theme.spacing(2, 0),
},
},
accordionDetails: {
padding: theme.spacing(0, 0, 1),
},
}));
/**
* @public
*/
export type SearchTypeFacetProps = {
/* what about this? */
name: string;
types: Array<{
value: string;
name: string;
icon: JSX.Element;
}>;
defaultValue?: string;
};
/**
* A control surface for the search query's "types" property, displayed as a
* single-select collapsible accordion suitable for use in faceted search UIs.
* @public
*/
export const SearchTypeFacet = (props: SearchTypeFacetProps) => {
const classes = useStyles();
const { setPageCursor, setTypes, types } = useSearch();
const [expanded, setExpanded] = useState(true);
const { defaultValue, name, types: givenTypes } = props;
const handleChange = (_event: ChangeEvent<{}>, newExpanded: boolean) =>
setExpanded(newExpanded);
const handleClick = (type: string) => {
return () => {
setTypes(type !== '' ? [type] : []);
setPageCursor('0');
setExpanded(false);
};
};
// Handle any provided defaultValue
useEffect(() => {
if (defaultValue) {
setTypes([defaultValue]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const definedTypes = [
{
value: '',
name: 'All',
icon: <AllIcon />,
},
...givenTypes,
];
const selected = types[0] || '';
return (
<Card className={classes.card}>
<CardHeader title={name} titleTypographyProps={{ variant: 'overline' }} />
<CardContent className={classes.cardContent}>
<Accordion
className={classes.accordion}
expanded={expanded}
onChange={handleChange}
>
<AccordionSummary
classes={{
root: classes.accordionSummary,
content: classes.accordionSummaryContent,
}}
expandIcon={<ExpandMoreIcon className={classes.icon} />}
IconButtonProps={{ size: 'small' }}
>
{expanded
? 'Collapse'
: definedTypes.filter(t => t.value === selected)[0]!.name}
</AccordionSummary>
<AccordionDetails classes={{ root: classes.accordionDetails }}>
<List
className={classes.list}
component="nav"
aria-label="filter by type"
disablePadding
dense
>
{definedTypes.map(type => (
<Fragment key={type.value}>
<Divider />
<ListItem
selected={types.includes(type.value)}
onClick={handleClick(type.value)}
button
>
<ListItemIcon>
{cloneElement(type.icon, {
className: classes.listItemIcon,
})}
</ListItemIcon>
<ListItemText primary={type.name} />
</ListItem>
</Fragment>
))}
</List>
</AccordionDetails>
</Accordion>
</CardContent>
</Card>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2021 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 { SearchTypeFacet } from './SearchTypeFacet';
export type { SearchTypeFacetProps } from './SearchTypeFacet';
+1
View File
@@ -24,6 +24,7 @@ export * from './SearchPage';
export * from './SearchResult';
export * from './SearchResultPager';
export * from './SearchType';
export * from './SearchTypeFacet';
export * from './SidebarSearch';
export * from './SidebarSearchModal';
export * from './HomePageComponent';
+2
View File
@@ -34,6 +34,7 @@ export {
SearchPage as Router,
SearchResultPager,
SearchType,
SearchTypeFacet,
SidebarSearch,
useSearch,
} from './components';
@@ -45,6 +46,7 @@ export type {
FiltersState,
SearchBarProps,
SearchBarBaseProps,
SearchTypeFacetProps,
} from './components';
export {
DefaultResultListItem,