Merge pull request #18401 from rikez/rikez/search-initial-state-config

feat(search): enable configuration of the Search component via app-config
This commit is contained in:
Camila Belo
2023-08-22 17:21:50 +02:00
committed by GitHub
16 changed files with 631 additions and 216 deletions
+16
View File
@@ -8,6 +8,22 @@ Development is ongoing. You can follow the progress and contribute at the Backst
Run `yarn dev` in the root directory, and then navigate to [/search](http://localhost:3000/search) to check out the plugin.
### Optional Settings
Configure the search query values via `app-config.yaml` to define how it behaves by default.
```yaml
# app-config.yaml
search:
query:
pageLimit: 50
```
Acceptable values for `pageLimit` are `10`, `25`, `50` or `100`.
**NOTE**: Currently this configuration only reflects the initial state of the Search React components. This means that
it defines how it behaves when it is first loaded or reset.
### Areas of Responsibility
This search plugin is primarily responsible for the following:
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2023 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 interface Config {
/** Configuration options for the search plugin */
search?: {
/**
* An object representing the default search query configuration.
* By configuring and modifying the values of this object,
* you can customize the default values of the search queries
* and define how it behaves by default.
*/
query?: {
/**
* A number indicating the maximum number of results to be returned
* per page during pagination.
* @visibility frontend
*/
pageLimit?: 10 | 25 | 50 | 100;
};
};
}
+4 -2
View File
@@ -70,6 +70,8 @@
"msw": "^1.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -36,16 +36,17 @@ jest.mock('react-router-dom', () => ({
}));
describe('SearchModal', () => {
const query = jest.fn().mockResolvedValue({ results: [] });
const configApiMock = new ConfigReader({ app: { title: 'Mock app' } });
const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) };
const apiRegistry = TestApiRegistry.from(
[configApiRef, new ConfigReader({ app: { title: 'Mock app' } })],
[searchApiRef, { query }],
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
);
beforeEach(() => {
query.mockClear();
navigate.mockClear();
searchApiMock.query.mockClear();
});
const toggleModal = jest.fn();
@@ -63,7 +64,7 @@ describe('SearchModal', () => {
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(query).toHaveBeenCalledTimes(1);
expect(searchApiMock.query).toHaveBeenCalledTimes(1);
});
it('Should use parent search context if defined', async () => {
@@ -88,7 +89,7 @@ describe('SearchModal', () => {
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(query).toHaveBeenCalledWith(initialState);
expect(searchApiMock.query).toHaveBeenCalledWith(initialState);
});
it('Should create a local search context if a parent is not defined', async () => {
@@ -104,7 +105,7 @@ describe('SearchModal', () => {
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(query).toHaveBeenCalledWith({
expect(searchApiMock.query).toHaveBeenCalledWith({
term: '',
filters: {},
types: [],
@@ -141,7 +142,7 @@ describe('SearchModal', () => {
},
);
expect(query).toHaveBeenCalledTimes(1);
expect(searchApiMock.query).toHaveBeenCalledTimes(1);
await userEvent.keyboard('{Escape}');
expect(toggleModal).toHaveBeenCalledTimes(1);
});
@@ -198,7 +199,7 @@ describe('SearchModal', () => {
},
);
expect(query).toHaveBeenCalledWith(
expect(searchApiMock.query).toHaveBeenCalledWith(
expect.objectContaining({ term: 'term' }),
);
@@ -230,7 +231,7 @@ describe('SearchModal', () => {
},
);
expect(query).toHaveBeenCalledWith(
expect(searchApiMock.query).toHaveBeenCalledWith(
expect.objectContaining({ term: 'term' }),
);
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { TestApiProvider } from '@backstage/test-utils';
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
import { act, render, waitFor } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
@@ -23,6 +23,7 @@ import {
SearchContextProvider,
} from '@backstage/plugin-search-react';
import { SearchType } from './SearchType';
import { configApiRef } from '@backstage/core-plugin-api';
const setTypesMock = jest.fn();
const setPageCursorMock = jest.fn();
@@ -40,7 +41,15 @@ jest.mock('@backstage/plugin-search-react', () => ({
}));
describe('SearchType.Accordion', () => {
const query = jest.fn();
const configApiMock = new MockConfigApi({
search: {
query: {
pagelimit: 10,
},
},
});
const searchApiMock = { query: jest.fn() };
const expectedLabel = 'Expected Label';
const expectedType = {
@@ -50,12 +59,20 @@ describe('SearchType.Accordion', () => {
};
beforeEach(() => {
query.mockResolvedValue({ results: [], numberOfResults: 1234 });
searchApiMock.query.mockResolvedValue({
results: [],
numberOfResults: 1234,
});
});
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<TestApiProvider
apis={[
[searchApiRef, searchApiMock],
[configApiRef, configApiMock],
]}
>
<SearchContextProvider>{children}</SearchContextProvider>
</TestApiProvider>
);
@@ -146,13 +163,13 @@ describe('SearchType.Accordion', () => {
</Wrapper>,
);
expect(query).toHaveBeenCalledWith({
expect(searchApiMock.query).toHaveBeenCalledWith({
term: 'abc',
types: [],
filters: { foo: 'bar' },
pageLimit: 0,
});
expect(query).toHaveBeenCalledWith({
expect(searchApiMock.query).toHaveBeenCalledWith({
term: 'abc',
types: [expectedType.value],
filters: {},
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { TestApiProvider } from '@backstage/test-utils';
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
import { act, render } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
@@ -23,6 +23,7 @@ import {
searchApiRef,
} from '@backstage/plugin-search-react';
import { SearchType } from './SearchType';
import { configApiRef } from '@backstage/core-plugin-api';
const setTypesMock = jest.fn();
const setPageCursorMock = jest.fn();
@@ -38,7 +39,14 @@ jest.mock('@backstage/plugin-search-react', () => ({
}));
describe('SearchType.Tabs', () => {
const query = jest.fn().mockResolvedValue({});
const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) };
const configApiMock = new MockConfigApi({
search: {
query: {
pageLimit: 100,
},
},
});
const expectedType = {
value: 'expected-type',
@@ -47,7 +55,12 @@ describe('SearchType.Tabs', () => {
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<TestApiProvider apis={[[searchApiRef, { query }]]}>
<TestApiProvider
apis={[
[searchApiRef, searchApiMock],
[configApiRef, configApiMock],
]}
>
<SearchContextProvider>{children}</SearchContextProvider>
</TestApiProvider>
);
@@ -14,17 +14,16 @@
* limitations under the License.
*/
import { useApi } from '@backstage/core-plugin-api';
import { configApiRef } from '@backstage/core-plugin-api';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { SearchContextProvider } from '@backstage/plugin-search-react';
import {
SearchContextProvider,
searchApiRef,
} from '@backstage/plugin-search-react';
import { SearchType } from './SearchType';
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: jest.fn().mockReturnValue({}),
}));
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
describe('SearchType', () => {
const initialState = {
@@ -37,8 +36,15 @@ describe('SearchType', () => {
const values = ['value1', 'value2'];
const typeValues = ['preselected'];
const query = jest.fn().mockResolvedValue({});
(useApi as jest.Mock).mockReturnValue({ query: query });
const configApiMock = new MockConfigApi({
search: {
query: {
pagelimit: 10,
},
},
});
const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) };
afterAll(() => {
jest.resetAllMocks();
@@ -47,9 +53,16 @@ describe('SearchType', () => {
describe('Type Filter', () => {
it('Renders field name and values when provided as props', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchType name={name} values={values} />
</SearchContextProvider>,
<TestApiProvider
apis={[
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
]}
>
<SearchContextProvider initialState={initialState}>
<SearchType name={name} values={values} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
@@ -72,14 +85,21 @@ describe('SearchType', () => {
it('Renders correctly based on type filter state', async () => {
render(
<SearchContextProvider
initialState={{
...initialState,
types: [values[0]],
}}
<TestApiProvider
apis={[
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
]}
>
<SearchType name={name} values={values} />
</SearchContextProvider>,
<SearchContextProvider
initialState={{
...initialState,
types: [values[0]],
}}
>
<SearchType name={name} values={values} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
@@ -103,9 +123,16 @@ describe('SearchType', () => {
it('Renders correctly based on type filter defaultValue', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchType name={name} values={values} defaultValue={values[0]} />
</SearchContextProvider>,
<TestApiProvider
apis={[
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
]}
>
<SearchContextProvider initialState={initialState}>
<SearchType name={name} values={values} defaultValue={values[0]} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
@@ -129,9 +156,16 @@ describe('SearchType', () => {
it('Selecting a value sets type filter state', async () => {
render(
<SearchContextProvider initialState={initialState}>
<SearchType name={name} values={values} />
</SearchContextProvider>,
<TestApiProvider
apis={[
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
]}
>
<SearchContextProvider initialState={initialState}>
<SearchType name={name} values={values} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
@@ -149,7 +183,7 @@ describe('SearchType', () => {
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect(searchApiMock.query).toHaveBeenLastCalledWith(
expect.objectContaining({
types: [values[0]],
}),
@@ -165,14 +199,21 @@ describe('SearchType', () => {
it('Selecting none defaults to empty state', async () => {
render(
<SearchContextProvider
initialState={{
...initialState,
types: typeValues,
}}
<TestApiProvider
apis={[
[configApiRef, configApiMock],
[searchApiRef, searchApiMock],
]}
>
<SearchType name={name} values={values} />
</SearchContextProvider>,
<SearchContextProvider
initialState={{
...initialState,
types: typeValues,
}}
>
<SearchType name={name} values={values} />
</SearchContextProvider>
</TestApiProvider>,
);
await waitFor(() => {
@@ -190,7 +231,7 @@ describe('SearchType', () => {
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(
expect(searchApiMock.query).toHaveBeenLastCalledWith(
expect.objectContaining({
types: [...typeValues, values[0]],
}),
@@ -206,7 +247,9 @@ describe('SearchType', () => {
await userEvent.click(screen.getByRole('option', { name: values[0] }));
await waitFor(() => {
expect(query).toHaveBeenLastCalledWith(expect.objectContaining([]));
expect(searchApiMock.query).toHaveBeenLastCalledWith(
expect.objectContaining([]),
);
});
});
});