mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
9524bbcdc7
commit
a0474aec39
9 changed files with 199 additions and 93 deletions
|
@ -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`,
|
||||
|
|
|
@ -377,6 +377,7 @@ export interface DocLinks {
|
|||
readonly percolate: string;
|
||||
readonly queryDsl: string;
|
||||
readonly queryESQL: string;
|
||||
readonly queryESQLExamples: string;
|
||||
};
|
||||
readonly date: {
|
||||
readonly dateMath: string;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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%' }}
|
||||
|
|
|
@ -94,6 +94,7 @@ function wrapSearchBarInContext(testProps: any) {
|
|||
notifications: startMock.notifications,
|
||||
http: startMock.http,
|
||||
theme: startMock.theme,
|
||||
docLinks: startMock.docLinks,
|
||||
storage: createMockStorage(),
|
||||
data: {
|
||||
query: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue