mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `7.17`: - [[Global Search] Limit characters for global search bar (#186560)](https://github.com/elastic/kibana/pull/186560) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Rachel Shen","email":"rshen@elastic.co"},"sourceCommit":{"committedDate":"2024-07-11T14:50:20Z","message":"[Global Search] Limit characters for global search bar (#186560)\n\n## Summary\r\n\r\nThis PR limits the number of characters that can be input into the\r\nglobal search bar. The character limit can be specified with the config\r\nvalue `xpack.global_search_bar.input_max_limit` with a default of\r\n`1000`. When an input that exceeds the configured character limit is\r\nprovided a descriptive visual notice is displayed to the user.\r\n\r\n## Visual \r\n<img width=\"662\" alt=\"Screenshot 2024-07-04 at 19 28 39\"\r\nsrc=\"cf30f589
-fe65-40a9-b9c8-ce0f235d206e\">\r\n\r\n\r\n## How to test\r\n\r\n- run the following command below in the browser console, which would\r\ncreate a string that exceeds the configured default search character\r\nlimit and copy it to your clipboard\r\n```ts\r\n\tcopy(Array.from(new Array(1001)).reduce((acc) => acc+'a', ''))\r\n```\r\n- open up kibana, simply paste the value that should exist in your\r\nclipboard in the global search input field and you should be presented\r\nwith a result similar to the image above.\r\n\r\n---------\r\n\r\nCo-authored-by: Eyo Okon Eyo <eyo.eyo@elastic.co>","sha":"f57d9c5f01e8e7cdf338251ce031d597d8fd56a4","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","auto-backport","backport:prev-MAJOR","backport:all-open"],"number":186560,"url":"https://github.com/elastic/kibana/pull/186560","mergeCommit":{"message":"[Global Search] Limit characters for global search bar (#186560)\n\n## Summary\r\n\r\nThis PR limits the number of characters that can be input into the\r\nglobal search bar. The character limit can be specified with the config\r\nvalue `xpack.global_search_bar.input_max_limit` with a default of\r\n`1000`. When an input that exceeds the configured character limit is\r\nprovided a descriptive visual notice is displayed to the user.\r\n\r\n## Visual \r\n<img width=\"662\" alt=\"Screenshot 2024-07-04 at 19 28 39\"\r\nsrc=\"cf30f589
-fe65-40a9-b9c8-ce0f235d206e\">\r\n\r\n\r\n## How to test\r\n\r\n- run the following command below in the browser console, which would\r\ncreate a string that exceeds the configured default search character\r\nlimit and copy it to your clipboard\r\n```ts\r\n\tcopy(Array.from(new Array(1001)).reduce((acc) => acc+'a', ''))\r\n```\r\n- open up kibana, simply paste the value that should exist in your\r\nclipboard in the global search input field and you should be presented\r\nwith a result similar to the image above.\r\n\r\n---------\r\n\r\nCo-authored-by: Eyo Okon Eyo <eyo.eyo@elastic.co>","sha":"f57d9c5f01e8e7cdf338251ce031d597d8fd56a4"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[]}] BACKPORT--> Co-authored-by: Rachel Shen <rshen@elastic.co>
This commit is contained in:
parent
4950f7136e
commit
735fb1b0d4
10 changed files with 201 additions and 42 deletions
|
@ -192,6 +192,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
|
||||
'xpack.fleet.agents.enabled (boolean)',
|
||||
'xpack.global_search.search_timeout (duration)',
|
||||
'xpack.global_search_bar.input_max_limit (number)',
|
||||
'xpack.graph.canEditDrillDownUrls (boolean)',
|
||||
'xpack.graph.savePolicy (alternatives)',
|
||||
'xpack.ilm.ui.enabled (boolean)',
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
},
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"server": false,
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["globalSearch"],
|
||||
"optionalPlugins": ["usageCollection", "savedObjectsTagging"],
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiImage, EuiText, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
interface PopoverPlaceholderProps {
|
||||
basePath: string;
|
||||
customPlaceholderMessage?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PopoverPlaceholder: FC<PopoverPlaceholderProps> = ({
|
||||
basePath,
|
||||
customPlaceholderMessage,
|
||||
}) => {
|
||||
const { colorMode } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
style={{ minHeight: 300 }}
|
||||
data-test-subj="nav-search-no-results"
|
||||
direction="column"
|
||||
gutterSize="xs"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiImage
|
||||
alt={i18n.translate('xpack.globalSearchBar.searchBar.noResultsImageAlt', {
|
||||
defaultMessage: 'Illustration of black hole',
|
||||
})}
|
||||
size="fullWidth"
|
||||
url={`${basePath}illustration_product_no_search_results_${
|
||||
colorMode === 'DARK' ? 'dark' : 'light'
|
||||
}.svg`}
|
||||
/>
|
||||
|
||||
{customPlaceholderMessage ?? (
|
||||
<>
|
||||
<EuiText size="m">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.globalSearchBar.searchBar.noResultsHeading"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.globalSearchBar.searchBar.noResults"
|
||||
defaultMessage="Try searching for applications, dashboards, visualizations, and more."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -42,6 +42,9 @@ const createResult = (result: Result): GlobalSearchResult => {
|
|||
const createBatch = (...results: Result[]): GlobalSearchBatchedResults => ({
|
||||
results: results.map(createResult),
|
||||
});
|
||||
|
||||
const searchCharLimit = 1000;
|
||||
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
describe('SearchBar', () => {
|
||||
|
@ -96,7 +99,7 @@ describe('SearchBar', () => {
|
|||
render(
|
||||
<IntlProvider locale="en">
|
||||
<SearchBar
|
||||
globalSearch={searchService}
|
||||
globalSearch={{ ...searchService, searchCharLimit }}
|
||||
navigateToUrl={applications.navigateToUrl}
|
||||
basePathUrl={basePathUrl}
|
||||
darkMode={darkMode}
|
||||
|
@ -124,7 +127,7 @@ describe('SearchBar', () => {
|
|||
render(
|
||||
<IntlProvider locale="en">
|
||||
<SearchBar
|
||||
globalSearch={searchService}
|
||||
globalSearch={{ ...searchService, searchCharLimit }}
|
||||
navigateToUrl={applications.navigateToUrl}
|
||||
basePathUrl={basePathUrl}
|
||||
darkMode={darkMode}
|
||||
|
@ -156,7 +159,7 @@ describe('SearchBar', () => {
|
|||
render(
|
||||
<IntlProvider locale="en">
|
||||
<SearchBar
|
||||
globalSearch={searchService}
|
||||
globalSearch={{ ...searchService, searchCharLimit }}
|
||||
navigateToUrl={applications.navigateToUrl}
|
||||
basePathUrl={basePathUrl}
|
||||
darkMode={darkMode}
|
||||
|
|
|
@ -11,13 +11,12 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiHeaderSectionItemButton,
|
||||
EuiIcon,
|
||||
EuiImage,
|
||||
EuiSelectableMessage,
|
||||
EuiSelectableTemplateSitewide,
|
||||
EuiSelectableTemplateSitewideOption,
|
||||
EuiText,
|
||||
EuiBadge,
|
||||
euiSelectableTemplateSitewideRenderOptions,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -36,10 +35,11 @@ import {
|
|||
import { SavedObjectTaggingPluginStart, Tag } from '../../../saved_objects_tagging/public';
|
||||
import { parseSearchParams } from '../search_syntax';
|
||||
import { getSuggestions, SearchSuggestion } from '../suggestions';
|
||||
import { PopoverPlaceholder } from './popover_placeholder';
|
||||
import './search_bar.scss';
|
||||
|
||||
interface Props {
|
||||
globalSearch: GlobalSearchPluginStart;
|
||||
globalSearch: GlobalSearchPluginStart & { searchCharLimit: number };
|
||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||
trackUiMetric: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
taggingApi?: SavedObjectTaggingPluginStart;
|
||||
|
@ -88,6 +88,43 @@ const TagListWrapper = ({ children }: { children: ReactNode }) => (
|
|||
</ul>
|
||||
);
|
||||
|
||||
const NoMatchesMessage = (props: { basePathUrl: string }) => {
|
||||
return <PopoverPlaceholder basePath={props.basePathUrl} />;
|
||||
};
|
||||
|
||||
const SearchCharLimitExceededMessage = (props: { basePathUrl: string }) => {
|
||||
const charLimitMessage = (
|
||||
<>
|
||||
<EuiText size="m">
|
||||
<p data-test-subj="searchCharLimitExceededMessageHeading">
|
||||
<FormattedMessage
|
||||
id="xpack.globalSearchBar.searchBar.searchCharLimitExceededHeading"
|
||||
defaultMessage="Search character limit exceeded"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.globalSearchBar.searchBar.searchCharLimitExceeded"
|
||||
defaultMessage="Try searching for applications, dashboards, visualizations, and more."
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<PopoverPlaceholder basePath={props.basePathUrl} customPlaceholderMessage={charLimitMessage} />
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyMessage = () => (
|
||||
<EuiFlexGroup direction="column" justifyContent="center" style={{ minHeight: '300px' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
const buildListItem = ({ color, name, id }: Tag) => {
|
||||
return (
|
||||
<li className="kbnSearchOption__tagsListItem" key={id}>
|
||||
|
@ -189,6 +226,7 @@ export function SearchBar({
|
|||
const [options, _setOptions] = useState<EuiSelectableTemplateSitewideOption[]>([]);
|
||||
const [searchableTypes, setSearchableTypes] = useState<string[]>([]);
|
||||
const UNKNOWN_TAG_ID = '__unknown__';
|
||||
const [searchCharLimitExceeded, setSearchCharLimitExceeded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoad) {
|
||||
|
@ -244,6 +282,14 @@ export function SearchBar({
|
|||
searchSubscription.current = null;
|
||||
}
|
||||
|
||||
if (searchValue.length > globalSearch.searchCharLimit) {
|
||||
// setting this will display cause a more descriptive error message to be displayed to the user
|
||||
setSearchCharLimitExceeded(true);
|
||||
return setOptions([], []);
|
||||
} else {
|
||||
setSearchCharLimitExceeded(false);
|
||||
}
|
||||
|
||||
const suggestions = loadSuggestions(searchValue);
|
||||
|
||||
let aggregatedResults: GlobalSearchResult[] = [];
|
||||
|
@ -357,34 +403,6 @@ export function SearchBar({
|
|||
}
|
||||
};
|
||||
|
||||
const emptyMessage = (
|
||||
<EuiSelectableMessage style={{ minHeight: 300 }} data-test-subj="nav-search-no-results">
|
||||
<EuiImage
|
||||
alt={i18n.translate('xpack.globalSearchBar.searchBar.noResultsImageAlt', {
|
||||
defaultMessage: 'Illustration of black hole',
|
||||
})}
|
||||
size="fullWidth"
|
||||
url={`${basePathUrl}illustration_product_no_search_results_${
|
||||
darkMode ? 'dark' : 'light'
|
||||
}.svg`}
|
||||
/>
|
||||
<EuiText size="m">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.globalSearchBar.searchBar.noResultsHeading"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.globalSearchBar.searchBar.noResults"
|
||||
defaultMessage="Try searching for applications, dashboards, visualizations, and more."
|
||||
/>
|
||||
</p>
|
||||
</EuiSelectableMessage>
|
||||
);
|
||||
|
||||
useEvent('keydown', onKeyDown);
|
||||
|
||||
return (
|
||||
|
@ -422,14 +440,20 @@ export function SearchBar({
|
|||
setInitialLoad(true);
|
||||
},
|
||||
}}
|
||||
emptyMessage={<EmptyMessage />}
|
||||
noMatchesMessage={
|
||||
searchCharLimitExceeded ? (
|
||||
<SearchCharLimitExceededMessage basePathUrl={basePathUrl} />
|
||||
) : (
|
||||
<NoMatchesMessage basePathUrl={basePathUrl} />
|
||||
)
|
||||
}
|
||||
popoverProps={{
|
||||
'data-test-subj': 'nav-search-popover',
|
||||
panelClassName: 'navSearch__panel',
|
||||
repositionOnScroll: true,
|
||||
buttonRef: setButtonRef,
|
||||
}}
|
||||
emptyMessage={emptyMessage}
|
||||
noMatchesMessage={emptyMessage}
|
||||
popoverFooter={
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
import { PluginInitializer } from 'src/core/public';
|
||||
import { GlobalSearchBarPlugin } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<{}, {}, {}, {}> = () => new GlobalSearchBarPlugin();
|
||||
export const plugin: PluginInitializer<{}, {}, {}, {}> = (initializerContext) =>
|
||||
new GlobalSearchBarPlugin(initializerContext);
|
||||
|
|
|
@ -10,19 +10,29 @@ import ReactDOM from 'react-dom';
|
|||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ApplicationStart } from 'kibana/public';
|
||||
import { CoreStart, Plugin } from 'src/core/public';
|
||||
import { CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
|
||||
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
|
||||
import { GlobalSearchPluginStart } from '../../global_search/public';
|
||||
import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public';
|
||||
import { SearchBar } from './components/search_bar';
|
||||
|
||||
export interface GlobalSearchBarPluginStartDeps {
|
||||
globalSearch: GlobalSearchPluginStart;
|
||||
globalSearch: GlobalSearchPluginStart & { searchCharLimit: number };
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export interface GlobalSearchBarConfigType {
|
||||
input_max_limit: number;
|
||||
}
|
||||
|
||||
export class GlobalSearchBarPlugin implements Plugin<{}, {}> {
|
||||
private config: GlobalSearchBarConfigType;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.get<GlobalSearchBarConfigType>();
|
||||
}
|
||||
|
||||
public setup() {
|
||||
return {};
|
||||
}
|
||||
|
@ -40,7 +50,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> {
|
|||
mount: (container) =>
|
||||
this.mount({
|
||||
container,
|
||||
globalSearch,
|
||||
globalSearch: { ...globalSearch, searchCharLimit: this.config.input_max_limit },
|
||||
savedObjectsTagging,
|
||||
navigateToUrl: core.application.navigateToUrl,
|
||||
basePathUrl: core.http.basePath.prepend('/plugins/globalSearchBar/assets/'),
|
||||
|
@ -61,7 +71,7 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> {
|
|||
trackUiMetric,
|
||||
}: {
|
||||
container: HTMLElement;
|
||||
globalSearch: GlobalSearchPluginStart;
|
||||
globalSearch: GlobalSearchBarPluginStartDeps['globalSearch'];
|
||||
savedObjectsTagging?: SavedObjectTaggingPluginStart;
|
||||
navigateToUrl: ApplicationStart['navigateToUrl'];
|
||||
basePathUrl: string;
|
||||
|
|
24
x-pack/plugins/global_search_bar/server/config.ts
Normal file
24
x-pack/plugins/global_search_bar/server/config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginConfigDescriptor } from 'src/core/server';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
input_max_limit: schema.number({
|
||||
defaultValue: 1000,
|
||||
}),
|
||||
});
|
||||
|
||||
export type ConfigSchema = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<ConfigSchema> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
input_max_limit: true,
|
||||
},
|
||||
};
|
12
x-pack/plugins/global_search_bar/server/index.ts
Normal file
12
x-pack/plugins/global_search_bar/server/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GlobalSearchBarPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new GlobalSearchBarPlugin();
|
||||
|
||||
export { config } from './config';
|
18
x-pack/plugins/global_search_bar/server/plugin.ts
Normal file
18
x-pack/plugins/global_search_bar/server/plugin.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Plugin } from 'src/core/server';
|
||||
|
||||
export class GlobalSearchBarPlugin implements Plugin {
|
||||
setup() {
|
||||
return {};
|
||||
}
|
||||
|
||||
start() {
|
||||
return {};
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue