Add Search Modal component (#7316)
Signed-off-by: rodmachen <rmachen@expediagroup.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Integrated `SidebarSearchModal` component into default-app to use the `SearchModal`.
|
||||
|
||||
The `SidebarSearchModal` component can also be used in other generated apps:
|
||||
|
||||
```diff
|
||||
import {
|
||||
- SidebarSearch,
|
||||
+ SidebarSearchModal
|
||||
} from '@backstage/plugin-search';
|
||||
...
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
- <SidebarSearch />
|
||||
+ <SidebarSearchModal />
|
||||
<SidebarDivider />
|
||||
...
|
||||
```
|
||||
|
||||
If you only want to use the `SearchModal` you can import it from `'@backstage/plugin-search'`:
|
||||
|
||||
```js
|
||||
import { SearchModal } from '@backstage/plugin-search';
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Added `SearchModal` component.
|
||||
|
||||
Now you can import `SearchModal` in your apps:
|
||||
|
||||
```js
|
||||
import { SearchModal } from '@backstage/plugin-search';
|
||||
```
|
||||
|
||||
You can also use the `SidebarSearchModal` component to integrate it into the sidebar of your sample apps:
|
||||
|
||||
```js
|
||||
import { SidebarSearchModal } from '@backstage/plugin-search';
|
||||
```
|
||||
@@ -109,6 +109,25 @@ const routes = (
|
||||
);
|
||||
```
|
||||
|
||||
### Using the Search Modal
|
||||
|
||||
In `Root.tsx`, add the `SidebarSearchModal` component:
|
||||
|
||||
```bash
|
||||
import { SidebarSearchModal } from '@backstage/plugin-search';
|
||||
|
||||
export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarSearchModal />
|
||||
<SidebarDivider />
|
||||
...
|
||||
```
|
||||
|
||||
For more information about using `Root.tsx`, please see
|
||||
[the changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#0315).
|
||||
|
||||
## Adding Search to the Backend
|
||||
|
||||
Add the following plugins into your backend app:
|
||||
|
||||
@@ -29,7 +29,7 @@ import LogoIcon from './LogoIcon';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
|
||||
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
import { SidebarSearch } from '@backstage/plugin-search';
|
||||
import { SidebarSearchModal } from '@backstage/plugin-search';
|
||||
import { Shortcuts } from '@backstage/plugin-shortcuts';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -79,7 +79,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarSearch />
|
||||
<SidebarSearchModal />
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
|
||||
@@ -25,7 +25,7 @@ import LogoFull from './LogoFull';
|
||||
import LogoIcon from './LogoIcon';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
import { SidebarSearch } from '@backstage/plugin-search';
|
||||
import { SidebarSearchModal } from '@backstage/plugin-search';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarPage,
|
||||
@@ -74,7 +74,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarSearch />
|
||||
<SidebarSearchModal />
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
|
||||
@@ -126,6 +126,24 @@ export const SearchFilterNext: {
|
||||
Select(props: Omit<Props_2, 'component'> & Component): JSX.Element;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const SearchModal: ({
|
||||
open,
|
||||
toggleModal,
|
||||
}: SearchModalProps) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface SearchModalProps {
|
||||
// (undocumented)
|
||||
open?: boolean;
|
||||
// (undocumented)
|
||||
toggleModal: () => void;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
@@ -179,6 +197,11 @@ export const SearchType: ({
|
||||
// @public (undocumented)
|
||||
export const SidebarSearch: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SidebarSearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const SidebarSearchModal: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchContextValue" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "useSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { configApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
ConfigReader,
|
||||
} from '@backstage/core-app-api';
|
||||
import { rootRouteRef } from '../../plugin';
|
||||
import { searchApiRef } from '../../apis';
|
||||
|
||||
import { SearchModal } from './SearchModal';
|
||||
|
||||
jest.mock('../SearchContext', () => ({
|
||||
...jest.requireActual('../SearchContext'),
|
||||
useSearch: jest.fn().mockReturnValue({
|
||||
result: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('SearchModal', () => {
|
||||
const query = jest.fn().mockResolvedValue({});
|
||||
|
||||
const apiRegistry = ApiRegistry.from([
|
||||
[configApiRef, new ConfigReader({ app: { title: 'Mock app' } })],
|
||||
[searchApiRef, { query }],
|
||||
]);
|
||||
|
||||
const toggleModal = jest.fn();
|
||||
|
||||
it('Should render the Modal correctly', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchModal open toggleModal={toggleModal} />
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/search': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Calls toggleModal handler', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchModal open toggleModal={toggleModal} />
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/search': rootRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
userEvent.keyboard('{esc}');
|
||||
expect(toggleModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Grid,
|
||||
List,
|
||||
Paper,
|
||||
} from '@material-ui/core';
|
||||
import { Launch } from '@material-ui/icons/';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { SearchBarBase } from '../SearchBar';
|
||||
import { DefaultResultListItem } from '../DefaultResultListItem';
|
||||
import { SearchResult } from '../SearchResult';
|
||||
import { SearchContextProvider, useSearch } from '../SearchContext';
|
||||
import { SearchResultPager } from '../SearchResultPager';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { rootRouteRef } from '../../plugin';
|
||||
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
export interface SearchModalProps {
|
||||
open?: boolean;
|
||||
toggleModal: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
borderRadius: 30,
|
||||
display: 'flex',
|
||||
height: '2.4em',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
},
|
||||
// Reduces default height of the modal, keeping a gap of 128px between the top and bottom of the page.
|
||||
paperFullWidth: { height: 'calc(100% - 128px)' },
|
||||
dialogActionsContainer: { padding: theme.spacing(1, 3) },
|
||||
viewResultsLink: { verticalAlign: '0.5em' },
|
||||
}));
|
||||
|
||||
export const Modal = ({ open = true, toggleModal }: SearchModalProps) => {
|
||||
const getSearchLink = useRouteRef(rootRouteRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const { term, setTerm } = useSearch();
|
||||
const [value, setValue] = useState<string>(term);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(prevValue => (prevValue !== term ? term : prevValue));
|
||||
}, [term]);
|
||||
|
||||
useDebounce(() => setTerm(value), 1000, [value]);
|
||||
|
||||
const handleQuery = (newValue: string) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const handleClear = () => setValue('');
|
||||
|
||||
const handleResultClick = () => {
|
||||
toggleModal();
|
||||
handleClear();
|
||||
};
|
||||
|
||||
const handleKeyPress = () => {
|
||||
handleResultClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
classes={{
|
||||
paperFullWidth: classes.paperFullWidth,
|
||||
}}
|
||||
onClose={toggleModal}
|
||||
aria-labelledby="search-modal-title"
|
||||
open={open}
|
||||
fullWidth
|
||||
maxWidth="lg"
|
||||
>
|
||||
<DialogTitle>
|
||||
<Paper className={classes.container}>
|
||||
<SearchBarBase
|
||||
className={classes.input}
|
||||
value={value}
|
||||
onChange={handleQuery}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</Paper>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
direction="row-reverse"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item>
|
||||
<Link to={`${getSearchLink()}?query=${value}`}>
|
||||
<span className={classes.viewResultsLink}>View Full Results</span>
|
||||
<Launch color="primary" />
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider />
|
||||
<SearchResult>
|
||||
{({ results }) => (
|
||||
<List>
|
||||
{results.map(({ document }) => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={`${document.location}-btn`}
|
||||
onClick={handleResultClick}
|
||||
onKeyPress={handleKeyPress}
|
||||
>
|
||||
<DefaultResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</SearchResult>
|
||||
</DialogContent>
|
||||
<DialogActions className={classes.dialogActionsContainer}>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchResultPager />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchModal = ({ open = true, toggleModal }: SearchModalProps) => {
|
||||
return (
|
||||
<SearchContextProvider>
|
||||
<Modal open={open} toggleModal={toggleModal} />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { SearchModal } from './SearchModal';
|
||||
export type { SearchModalProps } from './SearchModal';
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 SearchIcon from '@material-ui/icons/Search';
|
||||
import { SearchModal } from '../SearchModal';
|
||||
import { SidebarItem } from '@backstage/core-components';
|
||||
|
||||
export const SidebarSearchModal = () => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const toggleModal = (): void => setOpen(prevState => !prevState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarItem
|
||||
className="search-icon"
|
||||
icon={SearchIcon}
|
||||
text="Search"
|
||||
onClick={toggleModal}
|
||||
/>
|
||||
<SearchModal open={open} toggleModal={toggleModal} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { SidebarSearchModal } from './SidebarSearchModal';
|
||||
@@ -19,8 +19,10 @@ export * from './Filters';
|
||||
export * from './SearchBar';
|
||||
export * from './SearchContext';
|
||||
export * from './SearchFilter';
|
||||
export * from './SearchModal';
|
||||
export * from './SearchPage';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultPager';
|
||||
export * from './SearchType';
|
||||
export * from './SidebarSearch';
|
||||
export * from './SidebarSearchModal';
|
||||
|
||||
@@ -28,20 +28,23 @@ export {
|
||||
SearchContextProvider,
|
||||
SearchFilter,
|
||||
SearchFilterNext,
|
||||
SearchModal,
|
||||
SearchPage as Router,
|
||||
SearchResultPager,
|
||||
SearchType,
|
||||
SidebarSearch,
|
||||
useSearch,
|
||||
} from './components';
|
||||
export type { SearchModalProps } from './components';
|
||||
export type { FiltersState } from './components';
|
||||
export {
|
||||
DefaultResultListItem,
|
||||
HomePageSearchBar,
|
||||
SearchBarNext,
|
||||
SearchPage,
|
||||
SearchPageNext,
|
||||
searchPlugin as plugin,
|
||||
searchPlugin,
|
||||
SearchResult,
|
||||
HomePageSearchBar,
|
||||
SidebarSearchModal,
|
||||
} from './plugin';
|
||||
|
||||
@@ -122,6 +122,18 @@ export const SearchResultNext = searchPlugin.provide(
|
||||
}),
|
||||
);
|
||||
|
||||
export const SidebarSearchModal = searchPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'SidebarSearchModal',
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/SidebarSearchModal').then(
|
||||
m => m.SidebarSearchModal,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const DefaultResultListItem = searchPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'DefaultResultListItem',
|
||||
|
||||
Reference in New Issue
Block a user