<SearchPageNext /> -> <SearchPage />, with upgrade instructions and deprecation warnings.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -43,7 +43,7 @@ import { GraphiQLPage } from '@backstage/plugin-graphiql';
|
||||
import { LighthousePage } from '@backstage/plugin-lighthouse';
|
||||
import { NewRelicPage } from '@backstage/plugin-newrelic';
|
||||
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
import { SearchPage, SearchPageNext } from '@backstage/plugin-search';
|
||||
import { SearchPage } from '@backstage/plugin-search';
|
||||
import { TechRadarPage } from '@backstage/plugin-tech-radar';
|
||||
import { TechdocsPage } from '@backstage/plugin-techdocs';
|
||||
import { UserSettingsPage } from '@backstage/plugin-user-settings';
|
||||
@@ -119,8 +119,7 @@ const routes = (
|
||||
<Route path="/api-docs" element={<ApiExplorerPage />} />
|
||||
<Route path="/gcp-projects" element={<GcpProjectsPage />} />
|
||||
<Route path="/newrelic" element={<NewRelicPage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
<Route path="/search-next" element={<SearchPageNext />}>
|
||||
<Route path="/search" element={<SearchPage />}>
|
||||
{searchPage}
|
||||
</Route>
|
||||
<Route path="/cost-insights" element={<CostInsightsPage />} />
|
||||
|
||||
+14
-7
@@ -16,17 +16,17 @@
|
||||
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { useLocation, Outlet } from 'react-router';
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
|
||||
import { useSearch, SearchContextProvider } from '../SearchContext';
|
||||
import { SearchPageNext } from './';
|
||||
import { SearchPage } from './';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
search: '',
|
||||
}),
|
||||
Outlet: jest.fn().mockReturnValue(null),
|
||||
useOutlet: jest.fn().mockReturnValue('Route Children'),
|
||||
}));
|
||||
|
||||
jest.mock('../SearchContext', () => ({
|
||||
@@ -68,7 +68,7 @@ describe('SearchPage', () => {
|
||||
});
|
||||
|
||||
// When we render the page...
|
||||
await renderInTestApp(<SearchPageNext />);
|
||||
await renderInTestApp(<SearchPage />);
|
||||
|
||||
// Then search context should be initialized with these values...
|
||||
const calls = (SearchContextProvider as jest.Mock).mock.calls[0];
|
||||
@@ -80,9 +80,16 @@ describe('SearchPage', () => {
|
||||
});
|
||||
|
||||
it('renders provided router element', async () => {
|
||||
await renderInTestApp(<SearchPageNext />);
|
||||
const { getByText } = await renderInTestApp(<SearchPage />);
|
||||
|
||||
expect(Outlet).toHaveBeenCalled();
|
||||
expect(getByText('Route Children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders upgrade error whe no router children are provided', async () => {
|
||||
(useOutlet as jest.Mock).mockReturnValueOnce(null);
|
||||
const { getByText } = await renderInTestApp(<SearchPage />);
|
||||
|
||||
expect(getByText('Error: No Search Layout Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('replaces window history with expected query parameters', async () => {
|
||||
@@ -96,7 +103,7 @@ describe('SearchPage', () => {
|
||||
'?query=bieber&types[]=software-catalog&pageCursor=page2-or-something&filters[anyKey]=anyValue',
|
||||
);
|
||||
|
||||
await renderInTestApp(<SearchPageNext />);
|
||||
await renderInTestApp(<SearchPage />);
|
||||
|
||||
const calls = (window.history.replaceState as jest.Mock).mock.calls[0];
|
||||
expect(calls[2]).toContain(expectedLocation);
|
||||
@@ -13,55 +13,81 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Content, Header, Page, useQueryParamState } from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { SearchBar } from '../SearchBar';
|
||||
import { SearchResult } from '../SearchResult';
|
||||
|
||||
export const SearchPage = () => {
|
||||
const [queryString, setQueryString] = useQueryParamState<string>('query');
|
||||
const [searchQuery, setSearchQuery] = useState(queryString ?? '');
|
||||
|
||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
setSearchQuery(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => setSearchQuery(queryString ?? ''), [queryString]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
setQueryString(searchQuery);
|
||||
},
|
||||
200,
|
||||
[searchQuery],
|
||||
);
|
||||
|
||||
const handleClearSearchBar = () => {
|
||||
setSearchQuery('');
|
||||
};
|
||||
import React from 'react';
|
||||
import qs from 'qs';
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
import { SearchContextProvider, useSearch } from '../SearchContext';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { Content, Header, Page, Link, WarningPanel } from '@backstage/core';
|
||||
|
||||
const UpdateInstructions = () => {
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title="Search" />
|
||||
<Content>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<SearchBar
|
||||
handleSearch={handleSearch}
|
||||
handleClearSearchBar={handleClearSearchBar}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<SearchResult
|
||||
searchQuery={(queryString ?? '').toLocaleLowerCase('en-US')}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="No Search Layout Found"
|
||||
message={
|
||||
<>
|
||||
As of v0.4.0 of the Backstage Search Plugin, a search layout must
|
||||
be provided by the App. For detailed instructions, check the{' '}
|
||||
<Link to="https://backstage.io/docs/features/search/getting-started">
|
||||
getting started guide
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export const UrlUpdater = () => {
|
||||
const { term, types, pageCursor, filters } = useSearch();
|
||||
|
||||
const newParams = qs.stringify(
|
||||
{
|
||||
query: term,
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
},
|
||||
{ arrayFormat: 'brackets' },
|
||||
);
|
||||
const newUrl = `${window.location.pathname}?${newParams}`;
|
||||
|
||||
// We directly manipulate window history here in order to not re-render
|
||||
// infinitely (state => location => state => etc). The intention of this
|
||||
// code is just to ensure the right query/filters are loaded when a user
|
||||
// clicks the "back" button after clicking a result.
|
||||
window.history.replaceState(null, document.title, newUrl);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const SearchPage = () => {
|
||||
const location = useLocation();
|
||||
const outlet = useOutlet();
|
||||
const query = qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {};
|
||||
const filters = (query.filters as JsonObject) || {};
|
||||
const queryString = (query.query as string) || '';
|
||||
const pageCursor = (query.pageCursor as string) || '';
|
||||
const types = (query.types as string[]) || [];
|
||||
|
||||
const initialState = {
|
||||
term: queryString || '',
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<UrlUpdater />
|
||||
{outlet || <UpdateInstructions />}
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import qs from 'qs';
|
||||
import { Outlet, useLocation } from 'react-router';
|
||||
import { SearchContextProvider, useSearch } from '../SearchContext';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
|
||||
export const UrlUpdater = () => {
|
||||
const { term, types, pageCursor, filters } = useSearch();
|
||||
|
||||
const newParams = qs.stringify(
|
||||
{
|
||||
query: term,
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
},
|
||||
{ arrayFormat: 'brackets' },
|
||||
);
|
||||
const newUrl = `${window.location.pathname}?${newParams}`;
|
||||
|
||||
// We directly manipulate window history here in order to not re-render
|
||||
// infinitely (state => location => state => etc). The intention of this
|
||||
// code is just to ensure the right query/filters are loaded when a user
|
||||
// clicks the "back" button after clicking a result.
|
||||
window.history.replaceState(null, document.title, newUrl);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const SearchPageNext = () => {
|
||||
const location = useLocation();
|
||||
const query = qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {};
|
||||
const filters = (query.filters as JsonObject) || {};
|
||||
const queryString = (query.query as string) || '';
|
||||
const pageCursor = (query.pageCursor as string) || '';
|
||||
const types = (query.types as string[]) || [];
|
||||
|
||||
const initialState = {
|
||||
term: queryString || '',
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<UrlUpdater />
|
||||
<Outlet />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { SearchPageNext } from './SearchPageNext';
|
||||
@@ -19,7 +19,6 @@ export * from './SearchFilterNext';
|
||||
export * from './SearchBar';
|
||||
export * from './SearchBarNext';
|
||||
export * from './SearchPage';
|
||||
export * from './SearchPageNext';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultNext';
|
||||
export * from './DefaultResultListItem';
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
} from '@backstage/core';
|
||||
import { SearchClient, searchApiRef } from './apis';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { SearchPage as SearchPageComponent } from './components/SearchPage';
|
||||
import { SearchPageNext as SearchPageNextComponent } from './components/SearchPageNext';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/search',
|
||||
@@ -47,10 +45,6 @@ export const searchPlugin = createPlugin({
|
||||
},
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, SearchPageComponent);
|
||||
router.addRoute(rootNextRouteRef, SearchPageNextComponent);
|
||||
},
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
nextRoot: rootNextRouteRef,
|
||||
@@ -64,10 +58,15 @@ export const SearchPage = searchPlugin.provide(
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated This component was used for rapid prototyping of the Backstage
|
||||
* Search platform. Now that the API has stabilized, you should use the
|
||||
* <SearchPage /> component instead. This component will be removed in an
|
||||
* upcoming release.
|
||||
*/
|
||||
export const SearchPageNext = searchPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () =>
|
||||
import('./components/SearchPageNext').then(m => m.SearchPageNext),
|
||||
component: () => import('./components/SearchPage').then(m => m.SearchPage),
|
||||
mountPoint: rootNextRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user