diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md
new file mode 100644
index 0000000000..a2eee15e7c
--- /dev/null
+++ b/.changeset/search-en-jul-hemma.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Introduces a `` component, which operates on the same part of a search query as the `` 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.
diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx
index 54f59d1a6e..e8d590bc92 100644
--- a/packages/app/src/components/search/SearchPage.tsx
+++ b/packages/app/src/components/search/SearchPage.tsx
@@ -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 = () => {
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ ]}
+ />
-
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)
diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx
new file mode 100644
index 0000000000..c83f3d88d7
--- /dev/null
+++ b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.stories.tsx
@@ -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([]);
+
+ return (
+ {} } as any}
+ >
+ ,
+ },
+ { value: 'techdocs', name: 'Documentation', icon: },
+ ]}
+ />
+
+ );
+};
diff --git a/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx
new file mode 100644
index 0000000000..e054e0d028
--- /dev/null
+++ b/plugins/search/src/components/SearchTypeFacet/SearchTypeFacet.tsx
@@ -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: ,
+ },
+ ...givenTypes,
+ ];
+ const selected = types[0] || '';
+
+ return (
+
+
+
+
+ }
+ IconButtonProps={{ size: 'small' }}
+ >
+ {expanded
+ ? 'Collapse'
+ : definedTypes.filter(t => t.value === selected)[0]!.name}
+
+
+
+ {definedTypes.map(type => (
+
+
+
+
+ {cloneElement(type.icon, {
+ className: classes.listItemIcon,
+ })}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/plugins/search/src/components/SearchTypeFacet/index.ts b/plugins/search/src/components/SearchTypeFacet/index.ts
new file mode 100644
index 0000000000..f98c6f8326
--- /dev/null
+++ b/plugins/search/src/components/SearchTypeFacet/index.ts
@@ -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';
diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/index.tsx
index e413e9505d..82cc7bf762 100644
--- a/plugins/search/src/components/index.tsx
+++ b/plugins/search/src/components/index.tsx
@@ -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';
diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts
index 5bfdcf4318..8f760f42db 100644
--- a/plugins/search/src/index.ts
+++ b/plugins/search/src/index.ts
@@ -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,