Move SearchBar, SearchBarBase, and SearchTracker from @backstage/plugin-search to @backstage/plugin-search-react and deprecate SearchBar and SearchbarBase in @backstage/plugin-search
Signed-off-by: Anders Näsman <andersn@spotify.com>
This commit is contained in:
committed by
Eric Peterson
parent
193ae37f15
commit
ccbabfd4bd
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2022 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, { ComponentType } from 'react';
|
||||
import { Grid, makeStyles, Paper } from '@material-ui/core';
|
||||
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef, MockSearchApi } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
|
||||
import { SearchBar } from './SearchBar';
|
||||
|
||||
export default {
|
||||
title: 'Plugins/Search/SearchBar',
|
||||
component: SearchBar,
|
||||
decorators: [
|
||||
(Story: ComponentType<{}>) => (
|
||||
<TestApiProvider apis={[[searchApiRef, new MockSearchApi()]]}>
|
||||
<SearchContextProvider>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={12}>
|
||||
<Story />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</SearchContextProvider>
|
||||
</TestApiProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<Paper style={{ padding: '8px 0' }}>
|
||||
<SearchBar />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomPlaceholder = () => {
|
||||
return (
|
||||
<Paper style={{ padding: '8px 0' }}>
|
||||
<SearchBar placeholder="This is a custom placeholder" />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Focused = () => {
|
||||
return (
|
||||
<Paper style={{ padding: '8px 0' }}>
|
||||
{/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
||||
<SearchBar autoFocus />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithoutClearButton = () => {
|
||||
return (
|
||||
<Paper style={{ padding: '8px 0' }}>
|
||||
<SearchBar clearButton={false} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
search: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 0',
|
||||
borderRadius: '50px',
|
||||
margin: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
export const CustomStyles = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.search}>
|
||||
<SearchBar />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Copyright 2022 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, render, waitFor, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api';
|
||||
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
|
||||
import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils';
|
||||
|
||||
import { searchApiRef } from '../../api';
|
||||
import { SearchContextProvider } from '../../context';
|
||||
import { SearchBar } from './SearchBar';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
}));
|
||||
|
||||
describe('SearchBar', () => {
|
||||
const initialState = {
|
||||
term: '',
|
||||
filters: {},
|
||||
types: ['*'],
|
||||
pageCursor: '',
|
||||
};
|
||||
|
||||
const query = jest.fn().mockResolvedValue({});
|
||||
const analyticsApiSpy = new MockAnalyticsApi();
|
||||
let apiRegistry: TestApiRegistry;
|
||||
|
||||
apiRegistry = TestApiRegistry.from(
|
||||
[
|
||||
configApiRef,
|
||||
new ConfigReader({
|
||||
app: { title: 'Mock title' },
|
||||
}),
|
||||
],
|
||||
[searchApiRef, { query }],
|
||||
);
|
||||
|
||||
const name = 'Search';
|
||||
const term = 'term';
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('Renders without exploding', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<SearchBar />
|
||||
</SearchContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText('Search in Mock title'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders with custom placeholder', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={{ ...initialState }}>
|
||||
<SearchBar placeholder="This is a custom placeholder" />
|
||||
</SearchContextProvider>
|
||||
,
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByPlaceholderText('This is a custom placeholder'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders based on initial search', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={{ ...initialState, term }}>
|
||||
<SearchBar />
|
||||
</SearchContextProvider>
|
||||
,
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toHaveValue(term);
|
||||
});
|
||||
});
|
||||
|
||||
it('Updates term state when text is entered', async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
jest.useFakeTimers();
|
||||
const defaultDebounceTime = 200;
|
||||
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<SearchBar />
|
||||
</SearchContextProvider>
|
||||
,
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
const textbox = screen.getByRole('textbox', { name });
|
||||
|
||||
const value = 'value';
|
||||
|
||||
await user.type(textbox, value);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(defaultDebounceTime);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(textbox).toHaveValue(value);
|
||||
});
|
||||
|
||||
expect(query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: value }),
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('Clear button clears term state', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={{ ...initialState, term }}>
|
||||
<SearchBar />
|
||||
</SearchContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toHaveValue(term);
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toHaveValue('');
|
||||
});
|
||||
|
||||
expect(query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: '' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not show clear button', async () => {
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={{ ...initialState, term }}>
|
||||
<SearchBar clearButton={false} />
|
||||
</SearchContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Clear' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Adheres to provided debounceTime', async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
jest.useFakeTimers();
|
||||
|
||||
const debounceTime = 600;
|
||||
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<SearchBar debounceTime={debounceTime} />
|
||||
</SearchContextProvider>
|
||||
,
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const textbox = screen.getByRole('textbox', { name });
|
||||
|
||||
const value = 'value';
|
||||
|
||||
await user.type(textbox, value);
|
||||
|
||||
expect(query).not.toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: value }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(debounceTime);
|
||||
});
|
||||
expect(textbox).toHaveValue(value);
|
||||
|
||||
expect(query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: value }),
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('does not capture analytics event if not enabled in app', async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
jest.useFakeTimers();
|
||||
|
||||
const debounceTime = 600;
|
||||
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<SearchBar debounceTime={debounceTime} />
|
||||
</SearchContextProvider>
|
||||
,
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const textbox = screen.getByRole('textbox', { name });
|
||||
|
||||
const value = 'value';
|
||||
|
||||
await user.type(textbox, value);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(debounceTime);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(textbox).toHaveValue(value));
|
||||
|
||||
expect(analyticsApiSpy.getEvents()).toHaveLength(0);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('captures analytics events if enabled in app', async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
jest.useFakeTimers();
|
||||
|
||||
const debounceTime = 600;
|
||||
|
||||
apiRegistry = TestApiRegistry.from(
|
||||
[analyticsApiRef, analyticsApiSpy],
|
||||
[
|
||||
configApiRef,
|
||||
new ConfigReader({
|
||||
app: {
|
||||
title: 'Mock title',
|
||||
analytics: {
|
||||
ga: {
|
||||
trackingId: 'xyz123',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
[searchApiRef, { query }],
|
||||
);
|
||||
|
||||
render(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<SearchContextProvider
|
||||
initialState={{
|
||||
term: '',
|
||||
types: ['techdocs', 'software-catalog'],
|
||||
filters: {},
|
||||
}}
|
||||
>
|
||||
<SearchBar debounceTime={debounceTime} />
|
||||
</SearchContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const textbox = screen.getByRole('textbox', { name });
|
||||
|
||||
const value = 'value';
|
||||
|
||||
await user.type(textbox, value);
|
||||
|
||||
expect(analyticsApiSpy.getEvents()).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(debounceTime);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(textbox).toHaveValue(value));
|
||||
|
||||
expect(analyticsApiSpy.getEvents()).toHaveLength(1);
|
||||
expect(analyticsApiSpy.getEvents()[0]).toEqual({
|
||||
action: 'search',
|
||||
context: {
|
||||
extension: 'App',
|
||||
pluginId: 'root',
|
||||
routeRef: 'unknown',
|
||||
searchTypes: 'software-catalog,techdocs',
|
||||
},
|
||||
subject: 'value',
|
||||
});
|
||||
|
||||
await user.clear(textbox);
|
||||
|
||||
// make sure new term is captured
|
||||
await user.type(textbox, 'new value');
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(debounceTime);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(textbox).toHaveValue('new value'));
|
||||
|
||||
expect(analyticsApiSpy.getEvents()).toHaveLength(2);
|
||||
expect(analyticsApiSpy.getEvents()[1]).toEqual({
|
||||
action: 'search',
|
||||
context: {
|
||||
extension: 'App',
|
||||
pluginId: 'root',
|
||||
routeRef: 'unknown',
|
||||
searchTypes: 'software-catalog,techdocs',
|
||||
},
|
||||
subject: 'new value',
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2022 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,
|
||||
KeyboardEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import {
|
||||
InputBase,
|
||||
InputBaseProps,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
} from '@material-ui/core';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import ClearButton from '@material-ui/icons/Clear';
|
||||
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
import {
|
||||
SearchContextProvider,
|
||||
useSearch,
|
||||
useSearchContextCheck,
|
||||
} from '../../context';
|
||||
import { TrackSearch } from '../SearchTracker';
|
||||
|
||||
/**
|
||||
* Props for {@link SearchBarBase}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchBarBaseProps = Omit<InputBaseProps, 'onChange'> & {
|
||||
debounceTime?: number;
|
||||
clearButton?: boolean;
|
||||
onClear?: () => void;
|
||||
onSubmit?: () => void;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* All search boxes exported by the search plugin are based on the <SearchBarBase />,
|
||||
* and this one is based on the <InputBase /> component from Material UI.
|
||||
* Recommended if you don't use Search Provider or Search Context.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SearchBarBase = ({
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onSubmit,
|
||||
debounceTime = 200,
|
||||
clearButton = true,
|
||||
fullWidth = true,
|
||||
value: defaultValue,
|
||||
inputProps: defaultInputProps = {},
|
||||
endAdornment: defaultEndAdornment,
|
||||
...props
|
||||
}: SearchBarBaseProps) => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const [value, setValue] = useState<string>(defaultValue as string);
|
||||
const hasSearchContext = useSearchContextCheck();
|
||||
|
||||
useEffect(() => {
|
||||
setValue(prevValue =>
|
||||
prevValue !== defaultValue ? (defaultValue as string) : prevValue,
|
||||
);
|
||||
}, [defaultValue]);
|
||||
|
||||
useDebounce(() => onChange(value), debounceTime, [value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
},
|
||||
[setValue],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (onKeyDown) onKeyDown(e);
|
||||
if (onSubmit && e.key === 'Enter') {
|
||||
onSubmit();
|
||||
}
|
||||
},
|
||||
[onKeyDown, onSubmit],
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
onChange('');
|
||||
}, [onChange]);
|
||||
|
||||
const placeholder = `Search in ${
|
||||
configApi.getOptionalString('app.title') || 'Backstage'
|
||||
}`;
|
||||
|
||||
const startAdornment = (
|
||||
<InputAdornment position="start">
|
||||
<IconButton aria-label="Query" disabled>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
const endAdornment = (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label="Clear" onClick={handleClear}>
|
||||
<ClearButton />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
const searchBar = (
|
||||
<TrackSearch>
|
||||
<InputBase
|
||||
data-testid="search-bar-next"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
startAdornment={startAdornment}
|
||||
endAdornment={clearButton ? endAdornment : defaultEndAdornment}
|
||||
inputProps={{ 'aria-label': 'Search', ...defaultInputProps }}
|
||||
fullWidth={fullWidth}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
/>
|
||||
</TrackSearch>
|
||||
);
|
||||
|
||||
return hasSearchContext ? (
|
||||
searchBar
|
||||
) : (
|
||||
<SearchContextProvider>{searchBar}</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link SearchBar}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SearchBarProps = Partial<SearchBarBaseProps>;
|
||||
|
||||
/**
|
||||
* Recommended search bar when you use the Search Provider or Search Context.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SearchBar = ({ onChange, ...props }: SearchBarProps) => {
|
||||
const { term, setTerm } = useSearch();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
} else {
|
||||
setTerm(newValue);
|
||||
}
|
||||
},
|
||||
[onChange, setTerm],
|
||||
);
|
||||
|
||||
return <SearchBarBase value={term} onChange={handleChange} {...props} />;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2022 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 { SearchBar, SearchBarBase } from './SearchBar';
|
||||
export type { SearchBarProps, SearchBarBaseProps } from './SearchBar';
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2022 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 } from 'react';
|
||||
import { useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { useSearch } from '../../context';
|
||||
|
||||
/**
|
||||
* Capture search event on term change.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TrackSearch = ({ children }: { children: React.ReactChild }) => {
|
||||
const analytics = useAnalytics();
|
||||
const { term } = useSearch();
|
||||
|
||||
useEffect(() => {
|
||||
if (term) {
|
||||
// Capture analytics search event with search term provided as value
|
||||
analytics.captureEvent('search', term);
|
||||
}
|
||||
}, [analytics, term]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2022 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 { TrackSearch } from './SearchTracker';
|
||||
@@ -18,3 +18,5 @@ export * from './HighlightedSearchResultText';
|
||||
export * from './SearchFilter';
|
||||
export * from './SearchResult';
|
||||
export * from './SearchResultPager';
|
||||
export * from './SearchBar';
|
||||
export * from './SearchTracker';
|
||||
|
||||
Reference in New Issue
Block a user