[ES|QL] Allows searching in the documentation description (#171916)

## Summary

Allows searching on the ES|QL reference markdown. This means that now
the search will return more results. Examples:

- If I search for keep it will return all the occurences of the word
keep so the user will see the keep command but also all the other
commands that the keep word is used in the examples. I think that this
is very useful as the user can see more than 1 examples of a command
- If I search for date it will return not only the commands that have
the word date but also the commands that allow date in their arguments
- As now it searches also to the description it can also return false
positive results. I think is an accepted drawback.

<img width="1050" alt="image"
src="5de45bcf-c0fc-4fbc-bbdf-bdf25fcb89f6">


Note: I am not allowing this for Lens formulas. I introduced a new
property to disable it. The implementation works for formulas too but we
haven't received any negative feedback so far so I would like to test it
in the ES|QL reference first.

### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2023-11-27 14:30:32 +02:00 committed by GitHub
parent 0942bcea04
commit 037f68852b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 123 additions and 7 deletions

View file

@ -9,6 +9,7 @@
import React from 'react';
import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import { LanguageDocumentationPopoverContent } from './documentation_content';
describe('###Documentation popover content', () => {
@ -24,11 +25,11 @@ describe('###Documentation popover content', () => {
items: [
{
label: 'Section two item 1',
description: <span>Section 2 item 1 description</span>,
description: <Markdown markdown={`## Section two item 1 description `} />,
},
{
label: 'Section two item 2',
description: <span>Section 2 item 2 description</span>,
description: <Markdown markdown={`## Section two item 2 description `} />,
},
],
},
@ -52,7 +53,7 @@ describe('###Documentation popover content', () => {
});
});
test('Documentation component should list all sections that match the search input', () => {
test('Documentation component should list all sections that match the search input when title matches', () => {
const component = mountWithIntl(
<LanguageDocumentationPopoverContent language="test" sections={sections} />
);
@ -69,4 +70,25 @@ describe('###Documentation popover content', () => {
expect(sectionsLabels.length).toBe(1);
expect(sectionsLabels.text()).toEqual('Section one');
});
test('Documentation component should list all sections that match the search input when description matches', () => {
const component = mountWithIntl(
<LanguageDocumentationPopoverContent
language="test"
sections={sections}
searchInDescription
/>
);
const searchBox = component.find('[data-test-subj="language-documentation-navigation-search"]');
act(() => {
searchBox.at(0).prop('onChange')!({
target: { value: 'item 2 description' },
} as React.ChangeEvent<HTMLInputElement>);
});
component.update();
const sectionsLabels = findTestSubject(component, 'language-documentation-navigation-title');
expect(sectionsLabels.length).toBe(1);
});
});

View file

@ -20,6 +20,7 @@ import {
EuiHighlight,
EuiSpacer,
} from '@elastic/eui';
import { elementToString } from '../utils/element_to_string';
import './documentation.scss';
@ -35,9 +36,11 @@ export interface LanguageDocumentationSections {
interface DocumentationProps {
language: string;
sections?: LanguageDocumentationSections;
// if sets to true, allows searching in the markdown description
searchInDescription?: boolean;
}
function DocumentationContent({ language, sections }: DocumentationProps) {
function DocumentationContent({ language, sections, searchInDescription }: DocumentationProps) {
const [selectedSection, setSelectedSection] = useState<string | undefined>();
const scrollTargets = useRef<Record<string, HTMLElement>>({});
@ -55,7 +58,13 @@ function DocumentationContent({ language, sections }: DocumentationProps) {
.map((group) => {
const items = group.items.filter((helpItem) => {
return (
!normalizedSearchText || helpItem.label.toLocaleLowerCase().includes(normalizedSearchText)
!normalizedSearchText ||
helpItem.label.toLocaleLowerCase().includes(normalizedSearchText) ||
// Converting the JSX element to a string first
(searchInDescription &&
elementToString(helpItem.description)
?.toLocaleLowerCase()
.includes(normalizedSearchText))
);
});
return { ...group, items };

View file

@ -17,9 +17,15 @@ interface DocumentationPopoverProps {
language: string;
sections?: LanguageDocumentationSections;
buttonProps?: Omit<EuiButtonIconProps, 'iconType'>;
searchInDescription?: boolean;
}
function DocumentationPopover({ language, sections, buttonProps }: DocumentationPopoverProps) {
function DocumentationPopover({
language,
sections,
buttonProps,
searchInDescription,
}: DocumentationPopoverProps) {
const [isHelpOpen, setIsHelpOpen] = useState<boolean>(false);
const toggleDocumentationPopover = useCallback(() => {
@ -50,7 +56,11 @@ function DocumentationPopover({ language, sections, buttonProps }: Documentation
</EuiToolTip>
}
>
<LanguageDocumentationPopoverContent language={language} sections={sections} />
<LanguageDocumentationPopoverContent
language={language}
sections={sections}
searchInDescription={searchInDescription}
/>
</EuiPopover>
);
}

View file

@ -0,0 +1,37 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import { elementToString } from './element_to_string';
describe('elementToString', () => {
test('Should return empty string if no element is given', () => {
const text = elementToString(undefined);
expect(text).toEqual('');
});
test('Should return empty string if no markdown is passed', () => {
const text = elementToString(<span>Meow</span>);
expect(text).toEqual('');
});
test('Should convert to string if markdown is passed', () => {
const text = elementToString(<Markdown markdown={`## Markdown goes here `} />);
expect(text).toEqual('## Markdown goes here ');
});
test('Should convert to string if children with markdown are passed', () => {
const text = elementToString(
<>
<h1>Meow</h1>
<Markdown markdown={`## Markdown goes here `} />
</>
);
expect(text).toEqual('## Markdown goes here ');
});
});

View file

@ -0,0 +1,35 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { isValidElement } from 'react';
function nonNullable<T>(v: T): v is NonNullable<T> {
return v != null;
}
/**
* Gets the JSX.Element as the input. It returns the markdown as string.
* If the children are not markdown it will return an empty string.
*/
export function elementToString(element?: JSX.Element): string {
if (!element) {
return '';
}
const props = element.props;
if (props && 'markdown' in props) {
return String(props.markdown);
} else if (props && 'children' in props && Array.isArray(props.children)) {
return props.children.reduce((text: string, child: React.ReactNode): string => {
const validChildren = React.Children.toArray(child).filter(nonNullable);
if (isValidElement(child) && validChildren.length > 0) {
return text.concat(elementToString(child));
}
return text;
}, '');
}
return '';
}

View file

@ -14,6 +14,7 @@
"kbn_references": [
"@kbn/i18n",
"@kbn/test-jest-helpers",
"@kbn/kibana-react-plugin",
],
"exclude": [
"target/**/*",

View file

@ -621,6 +621,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
<LanguageDocumentationPopover
language={getLanguageDisplayName(String(language))}
sections={documentationSections}
searchInDescription
buttonProps={{
color: 'text',
size: 's',
@ -825,6 +826,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
language={
String(language) === 'esql' ? 'ES|QL' : String(language).toUpperCase()
}
searchInDescription
sections={documentationSections}
buttonProps={{
display: 'empty',