[ES|QL] Adds a help menu popover (#190579)

## Summary

Closes https://github.com/elastic/kibana/issues/190539

Adds a help menu button for the ES|QL mode

<img width="1548" alt="image"
src="https://github.com/user-attachments/assets/f8dde898-a1bf-4441-ae21-053e8290a5a6">


### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
Co-authored-by: Drew Tate <andrew.tate@elastic.co>
This commit is contained in:
Stratoula Kalafateli 2024-08-19 23:33:57 +02:00 committed by GitHub
parent 9524bbcdc7
commit a0474aec39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 199 additions and 93 deletions

View file

@ -506,6 +506,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
percolate: `${ELASTICSEARCH_DOCS}query-dsl-percolate-query.html`,
queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`,
queryESQL: `${ELASTICSEARCH_DOCS}esql.html`,
queryESQLExamples: `${ELASTICSEARCH_DOCS}esql-examples.html`,
},
search: {
sessions: `${KIBANA_DOCS}search-sessions.html`,

View file

@ -377,6 +377,7 @@ export interface DocLinks {
readonly percolate: string;
readonly queryDsl: string;
readonly queryESQL: string;
readonly queryESQLExamples: string;
};
readonly date: {
readonly dateMath: string;

View file

@ -17,7 +17,7 @@ import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/d
import { ChangeDataView } from './change_dataview';
import { DataViewSelector } from './data_view_selector';
import { dataViewMock, dataViewMockEsql } from './mocks/dataview';
import { DataViewPickerPropsExtended } from './data_view_picker';
import { DataViewPickerProps } from './data_view_picker';
describe('DataView component', () => {
const createMockWebStorage = () => ({
@ -43,7 +43,7 @@ describe('DataView component', () => {
};
function wrapDataViewComponentInContext(
testProps: DataViewPickerPropsExtended,
testProps: DataViewPickerProps,
storageValue: boolean,
uiSettingValue: boolean = false
) {
@ -75,7 +75,7 @@ describe('DataView component', () => {
</I18nProvider>
);
}
let props: DataViewPickerPropsExtended;
let props: DataViewPickerProps;
beforeEach(() => {
props = {
currentDataViewId: 'dataview-1',

View file

@ -24,10 +24,9 @@ import {
EuiButtonEmpty,
} from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getLanguageDisplayName } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { IUnifiedSearchPluginServices } from '../types';
import { type DataViewPickerPropsExtended } from './data_view_picker';
import { type DataViewPickerProps } from './data_view_picker';
import type { DataViewListItemEnhanced } from './dataview_list';
import adhoc from './assets/adhoc.svg';
import { changeDataViewStyles } from './change_dataview.styles';
@ -53,18 +52,13 @@ export function ChangeDataView({
onDataViewCreated,
trigger,
selectableProps,
textBasedLanguage,
isDisabled,
onEditDataView,
onCreateDefaultAdHocDataView,
}: DataViewPickerPropsExtended) {
}: DataViewPickerProps) {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
const [dataViewsList, setDataViewsList] = useState<DataViewListItemEnhanced[]>([]);
const [triggerLabel, setTriggerLabel] = useState('');
const [isTextBasedLangSelected, setIsTextBasedLangSelected] = useState(
Boolean(textBasedLanguage)
);
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { application, data, dataViews, dataViewEditor } = kibana.services;
@ -91,20 +85,6 @@ export function ChangeDataView({
fetchDataViews();
}, [data, currentDataViewId, adHocDataViews, savedDataViews]);
useEffect(() => {
if (textBasedLanguage) {
setTriggerLabel(getLanguageDisplayName(textBasedLanguage));
} else {
setTriggerLabel(trigger.label);
}
}, [textBasedLanguage, trigger.label]);
useEffect(() => {
if (Boolean(textBasedLanguage) !== isTextBasedLangSelected) {
setIsTextBasedLangSelected(Boolean(textBasedLanguage));
}
}, [isTextBasedLangSelected, textBasedLanguage]);
const isAdHocSelected = useMemo(() => {
return adHocDataViews?.some((dataView) => dataView.id === currentDataViewId);
}, [adHocDataViews, currentDataViewId]);
@ -121,14 +101,14 @@ export function ChangeDataView({
color={isMissingCurrent ? 'danger' : 'text'}
iconSide="right"
iconType="arrowDown"
title={triggerLabel}
title={trigger.label}
disabled={isDisabled}
textProps={{ className: 'eui-textTruncate' }}
{...rest}
>
<>
{/* we don't want to display the adHoc icon on text based mode */}
{isAdHocSelected && !isTextBasedLangSelected && (
{isAdHocSelected && (
<EuiIcon
type={adhoc}
color="primary"
@ -137,7 +117,7 @@ export function ChangeDataView({
`}
/>
)}
{triggerLabel}
{trigger.label}
</>
</EuiButtonEmpty>
);
@ -256,45 +236,43 @@ export function ChangeDataView({
return (
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
{!isTextBasedLangSelected && (
<>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
<EuiFlexItem
grow={false}
css={css`
padding: 11px;
border-radius: ${euiTheme.border.radius.small} 0 0 ${euiTheme.border.radius.small};
background-color: ${euiTheme.colors.lightestShade};
border: ${euiTheme.border.thin};
border-right: 0;
`}
>
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.switcherLabelTitle', {
defaultMessage: 'Data view',
})}
</EuiFlexItem>
<EuiPopover
panelClassName="changeDataViewPopover"
button={createTrigger()}
panelProps={{
['data-test-subj']: 'changeDataViewPopover',
}}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
panelPaddingSize="none"
initialFocus={`#${searchListInputId}`}
display="block"
buffer={8}
>
<div css={styles.popoverContent}>
<EuiContextMenuPanel size="s" items={getPanelItems()} />
</div>
</EuiPopover>
</EuiFlexGroup>
</EuiFlexItem>
</>
)}
<>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
<EuiFlexItem
grow={false}
css={css`
padding: 11px;
border-radius: ${euiTheme.border.radius.small} 0 0 ${euiTheme.border.radius.small};
background-color: ${euiTheme.colors.lightestShade};
border: ${euiTheme.border.thin};
border-right: 0;
`}
>
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.switcherLabelTitle', {
defaultMessage: 'Data view',
})}
</EuiFlexItem>
<EuiPopover
panelClassName="changeDataViewPopover"
button={createTrigger()}
panelProps={{
['data-test-subj']: 'changeDataViewPopover',
}}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
panelPaddingSize="none"
initialFocus={`#${searchListInputId}`}
display="block"
buffer={8}
>
<div css={styles.popoverContent}>
<EuiContextMenuPanel size="s" items={getPanelItems()} />
</div>
</EuiPopover>
</EuiFlexGroup>
</EuiFlexItem>
</>
</EuiFlexGroup>
);
}

View file

@ -76,13 +76,6 @@ export interface DataViewPickerProps {
isDisabled?: boolean;
}
export interface DataViewPickerPropsExtended extends DataViewPickerProps {
/**
* Text based language that is currently selected; depends on the query
*/
textBasedLanguage?: string;
}
export const DataViewPicker = ({
isMissingCurrent,
currentDataViewId,
@ -95,10 +88,9 @@ export const DataViewPicker = ({
trigger,
selectableProps,
textBasedLanguages,
textBasedLanguage,
onCreateDefaultAdHocDataView,
isDisabled,
}: DataViewPickerPropsExtended) => {
}: DataViewPickerProps) => {
return (
<ChangeDataView
isMissingCurrent={isMissingCurrent}
@ -113,7 +105,6 @@ export const DataViewPicker = ({
savedDataViews={savedDataViews}
selectableProps={selectableProps}
textBasedLanguages={textBasedLanguages}
textBasedLanguage={textBasedLanguage}
isDisabled={isDisabled}
/>
);

View file

@ -0,0 +1,43 @@
/*
* 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 { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { coreMock } from '@kbn/core/public/mocks';
import { ESQLMenuPopover } from './esql_menu_popover';
describe('ESQLMenuPopover', () => {
const renderESQLPopover = () => {
const startMock = coreMock.createStart();
const services = {
docLinks: startMock.docLinks,
};
return render(
<KibanaContextProvider services={services}>
<ESQLMenuPopover />{' '}
</KibanaContextProvider>
);
};
it('should render a button', () => {
renderESQLPopover();
expect(screen.getByTestId('esql-menu-button')).toBeInTheDocument();
});
it('should open a menu when the popover is open', () => {
renderESQLPopover();
expect(screen.getByTestId('esql-menu-button')).toBeInTheDocument();
userEvent.click(screen.getByRole('button'));
expect(screen.getByTestId('esql-examples')).toBeInTheDocument();
expect(screen.getByTestId('esql-about')).toBeInTheDocument();
expect(screen.getByTestId('esql-feedback')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,95 @@
/*
* 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, { useMemo, useState } from 'react';
import {
EuiPopover,
EuiButton,
EuiContextMenuPanel,
type EuiContextMenuPanelProps,
EuiContextMenuItem,
EuiHorizontalRule,
} from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import { FEEDBACK_LINK } from '@kbn/esql-utils';
import type { IUnifiedSearchPluginServices } from '../types';
export const ESQLMenuPopover = () => {
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { docLinks } = kibana.services;
const [isESQLMenuPopoverOpen, setIsESQLMenuPopoverOpen] = useState(false);
const esqlPanelItems = useMemo(() => {
const panelItems: EuiContextMenuPanelProps['items'] = [];
panelItems.push(
<EuiContextMenuItem
key="about"
icon="iInCircle"
data-test-subj="esql-about"
target="_blank"
href={docLinks.links.query.queryESQL}
>
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
defaultMessage: 'Documentation',
})}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="examples"
icon="nested"
data-test-subj="esql-examples"
target="_blank"
href={docLinks.links.query.queryESQLExamples}
>
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
defaultMessage: 'Example queries',
})}
</EuiContextMenuItem>,
<EuiHorizontalRule margin="xs" key="dataviewActions-divider" />,
<EuiContextMenuItem
key="feedback"
icon="editorComment"
data-test-subj="esql-feedback"
target="_blank"
href={FEEDBACK_LINK}
>
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
defaultMessage: 'Submit feedback',
})}
</EuiContextMenuItem>
);
return panelItems;
}, [docLinks.links.query.queryESQL, docLinks.links.query.queryESQLExamples]);
return (
<EuiPopover
button={
<EuiButton
color="text"
onClick={() => setIsESQLMenuPopoverOpen(!isESQLMenuPopoverOpen)}
data-test-subj="esql-menu-button"
size="s"
>
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.label', {
defaultMessage: 'ES|QL help',
})}
</EuiButton>
}
panelProps={{
['data-test-subj']: 'esql-menu-popover',
css: { width: 240 },
}}
isOpen={isESQLMenuPopoverOpen}
closePopover={() => setIsESQLMenuPopoverOpen(false)}
panelPaddingSize="s"
display="block"
>
<EuiContextMenuPanel size="s" items={esqlPanelItems} />
</EuiPopover>
);
};

View file

@ -50,6 +50,7 @@ import { NoDataPopover } from './no_data_popover';
import { shallowEqual } from '../utils/shallow_equal';
import { AddFilterPopover } from './add_filter_popover';
import { DataViewPicker, DataViewPickerProps } from '../dataview_picker';
import { ESQLMenuPopover } from './esql_menu_popover';
import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group';
import type {
@ -621,22 +622,17 @@ export const QueryBarTopRow = React.memo(
}
function renderDataViewsPicker() {
if (!props.dataViewPickerComponentProps) return;
let textBasedLanguage;
if (Boolean(isQueryLangSelected)) {
const query = props.query as AggregateQuery;
textBasedLanguage = getAggregateQueryMode(query);
if (props.dataViewPickerComponentProps && !Boolean(isQueryLangSelected)) {
return (
<EuiFlexItem style={{ maxWidth: '100%' }} grow={isMobile}>
<DataViewPicker
{...props.dataViewPickerComponentProps}
trigger={{ fullWidth: isMobile, ...props.dataViewPickerComponentProps.trigger }}
isDisabled={props.isDisabled}
/>
</EuiFlexItem>
);
}
return (
<EuiFlexItem style={{ maxWidth: '100%' }} grow={isMobile}>
<DataViewPicker
{...props.dataViewPickerComponentProps}
trigger={{ fullWidth: isMobile, ...props.dataViewPickerComponentProps.trigger }}
textBasedLanguage={textBasedLanguage}
isDisabled={props.isDisabled}
/>
</EuiFlexItem>
);
}
function renderAddButton() {
@ -770,12 +766,12 @@ export const QueryBarTopRow = React.memo(
padding: ${isQueryLangSelected && !props.disableExternalPadding
? euiTheme.size.s
: 0};
padding-bottom: 0;
`}
justifyContent={shouldShowDatePickerAsBadge() ? 'flexStart' : 'flexEnd'}
wrap
>
{props.dataViewPickerOverride || renderDataViewsPicker()}
{Boolean(isQueryLangSelected) && <ESQLMenuPopover />}
<EuiFlexItem
grow={!shouldShowDatePickerAsBadge()}
style={{ minWidth: shouldShowDatePickerAsBadge() ? 'auto' : 320, maxWidth: '100%' }}

View file

@ -94,6 +94,7 @@ function wrapSearchBarInContext(testProps: any) {
notifications: startMock.notifications,
http: startMock.http,
theme: startMock.theme,
docLinks: startMock.docLinks,
storage: createMockStorage(),
data: {
query: {