[ES|QL] Increase discoverability (#188898)

## Summary

Closes https://github.com/elastic/kibana/issues/184691
Closes https://github.com/elastic/kibana/issues/189029
Closes https://github.com/elastic/kibana/issues/166085

This PR is mostly a redesign of the unified search with the dataview
picker and the ES|QL editor

### Unified search
- We removed the ES|QL switch from the dataview picker
- A lot of cleanup

<img width="2502" alt="image"
src="https://github.com/user-attachments/assets/2afca7ce-c7d5-4300-93c9-2c2b77434fd8">

### ES|QL Editor
- The biggest change is the elimination of the compact mode

<img width="1256" alt="image"
src="https://github.com/user-attachments/assets/a0b96796-e086-4397-b8c9-c270e445a034">

### Discover
- Moved the transition modal to Discover
- Added a new menu item (Try ES|QL, Switch to classic)
- A small redesign of the transition modal

<img width="858" alt="image"
src="https://github.com/user-attachments/assets/7fdba235-e0ed-46c2-9e29-e3ae586c019c">


### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] 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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2024-08-05 17:40:06 +02:00 committed by GitHub
parent 22287545aa
commit f2d7a28134
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 1049 additions and 1816 deletions

View file

@ -6,3 +6,4 @@
* Side Public License, v 1.
*/
export const ENABLE_ESQL = 'enableESQL';
export const FEEDBACK_LINK = 'https://ela.st/esql-feedback';

View file

@ -27,4 +27,4 @@ export {
TextBasedLanguages,
} from './src';
export { ENABLE_ESQL } from './constants';
export { ENABLE_ESQL, FEEDBACK_LINK } from './constants';

View file

@ -7,7 +7,7 @@ It can be used in every application that would like to add an in-app documentati
- A details page
```
<LanguageDocumentationPopover language={language} sections={documentationSections} />
<LanguageDocumentationPopover language={language} sections={documentationSections} onHelpMenuVisibilityChange={onHelpMenuVisibilityChange} isHelpMenuOpen={isHelpMenuOpen} />
```
The properties are typed as:

View file

@ -66,5 +66,7 @@ storiesOf('Language documentation popover', module).add('default', () => (
language="Test"
sections={sections}
buttonProps={{ color: 'text' }}
isHelpMenuOpen={true}
onHelpMenuVisibilityChange={() => {}}
/>
));

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiPopover,
@ -21,6 +21,8 @@ import {
interface DocumentationPopoverProps {
language: string;
isHelpMenuOpen: boolean;
onHelpMenuVisibilityChange: (status: boolean) => void;
sections?: LanguageDocumentationSections;
buttonProps?: Omit<EuiButtonIconProps, 'iconType'>;
searchInDescription?: boolean;
@ -33,24 +35,28 @@ function DocumentationPopover({
buttonProps,
searchInDescription,
linkToDocumentation,
isHelpMenuOpen,
onHelpMenuVisibilityChange,
}: DocumentationPopoverProps) {
const [isHelpOpen, setIsHelpOpen] = useState<boolean>(false);
const toggleDocumentationPopover = useCallback(() => {
setIsHelpOpen(!isHelpOpen);
}, [isHelpOpen]);
onHelpMenuVisibilityChange?.(!isHelpMenuOpen);
}, [isHelpMenuOpen, onHelpMenuVisibilityChange]);
useEffect(() => {
onHelpMenuVisibilityChange(isHelpMenuOpen ?? false);
}, [isHelpMenuOpen, onHelpMenuVisibilityChange]);
return (
<EuiOutsideClickDetector
onOutsideClick={() => {
setIsHelpOpen(false);
onHelpMenuVisibilityChange?.(false);
}}
>
<EuiPopover
panelClassName="documentation__docs--overlay"
panelPaddingSize="none"
isOpen={isHelpOpen}
closePopover={() => setIsHelpOpen(false)}
isOpen={isHelpMenuOpen}
closePopover={() => onHelpMenuVisibilityChange(false)}
button={
<EuiToolTip
position="top"
@ -62,7 +68,7 @@ function DocumentationPopover({
})}
>
<EuiButtonIcon
iconType="iInCircle"
iconType="documentation"
onClick={toggleDocumentationPopover}
{...buttonProps}
/>

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
export type { TextBasedLanguagesEditorProps } from './src/text_based_languages_editor';
export type { TextBasedLanguagesEditorProps } from './src/types';
export { fetchFieldsFromESQL } from './src/fetch_fields_from_esql';
import { TextBasedLanguagesEditor } from './src/text_based_languages_editor';

View file

@ -28,16 +28,15 @@ The TextBasedLanguagesEditor component is a reusable component and can be used t
<Canvas>
<Story
name='compact mode'
name='expanded mode'
args={
{
query: { esql: 'from dataview | keep field1, field2' },
isCodeEditorExpanded:false,
'data-test-subj':'test-id'
}
}
argTypes={
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }, expandCodeEditor: { action: 'expanded' }}
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }}
}
>
{Template.bind({})}
@ -52,7 +51,6 @@ When there are errors to the query the UI displays the errors to the editor:
args={
{
query: { esql: 'from dataview | keep field1, field2' },
isCodeEditorExpanded:false,
'data-test-subj':'test-id',
errors: [
new Error(
@ -62,69 +60,7 @@ When there are errors to the query the UI displays the errors to the editor:
}
}
argTypes={
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }, expandCodeEditor: { action: 'expanded' }}
}
>
{Template.bind({})}
</Story>
</Canvas>
When there the query is long and the editor is on the compact view:
<Canvas>
<Story
name='with long query'
args={
{
query: { esql: 'from dataview | keep field1, field2, field 3, field 4, field 5 | where field5 > 5 | stats var = avg(field3)' },
isCodeEditorExpanded:false,
'data-test-subj':'test-id',
}
}
argTypes={
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }, expandCodeEditor: { action: 'expanded' }}
}
>
{Template.bind({})}
</Story>
</Canvas>
The editor also works on the expanded mode:
<Canvas>
<Story
name='on expanded mode'
args={
{
query: { esql: 'from dataview | keep field1, field2' },
isCodeEditorExpanded:true,
'data-test-subj':'test-id',
}
}
argTypes={
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }, expandCodeEditor: { action: 'expanded' }}
}
>
{Template.bind({})}
</Story>
</Canvas>
The editor also works on the expanded mode with the minimize button hidden:
<Canvas>
<Story
name='on expanded mode with hidden the minimize button'
args={
{
query: { esql: 'from dataview | keep field1, field2' },
isCodeEditorExpanded:true,
hideMinimizeButton: true,
'data-test-subj':'test-id',
}
}
argTypes={
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }, expandCodeEditor: { action: 'expanded' }}
{ onTextLangQueryChange: { action: 'changed' }, onTextLangQuerySubmit: { action: 'submitted' }}
}
>
{Template.bind({})}
@ -135,4 +71,4 @@ The editor also works on the expanded mode with the minimize button hidden:
The component exposes the following properties:
<ArgsTable story="compact mode"/>
<ArgsTable story="expanded mode"/>

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiText,
@ -17,13 +17,12 @@ import {
EuiPopoverTitle,
EuiDescriptionList,
EuiDescriptionListDescription,
EuiBadge,
} from '@elastic/eui';
import { css, Interpolation, Theme } from '@emotion/react';
import { css } from '@emotion/react';
import { css as classNameCss } from '@emotion/css';
import type { MonacoMessage } from './helpers';
import type { MonacoMessage } from '../helpers';
export const getConstsByType = (type: 'error' | 'warning', count: number) => {
const getConstsByType = (type: 'error' | 'warning', count: number) => {
if (type === 'error') {
return {
color: 'danger',
@ -49,7 +48,7 @@ export const getConstsByType = (type: 'error' | 'warning', count: number) => {
}
};
export function ErrorsWarningsContent({
function ErrorsWarningsContent({
items,
type,
onErrorClick,
@ -100,48 +99,6 @@ export function ErrorsWarningsContent({
);
}
export function ErrorsWarningsCompactViewPopover({
items,
type,
onErrorClick,
popoverCSS,
}: {
items: MonacoMessage[];
type: 'error' | 'warning';
onErrorClick: (error: MonacoMessage) => void;
popoverCSS: Interpolation<Theme>;
}) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const { color, message } = getConstsByType(type, items.length);
return (
<EuiPopover
button={
<EuiBadge
color={color}
onClick={() => setIsPopoverOpen(true)}
onClickAriaLabel={message}
iconType={type}
iconSide="left"
data-test-subj={`TextBasedLangEditor-inline-${type}-badge`}
title={message}
css={css`
cursor: pointer;
`}
>
{items.length}
</EuiBadge>
}
css={popoverCSS}
ownFocus={false}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
data-test-subj={`TextBasedLangEditor-inline-${type}-popover`}
>
<ErrorsWarningsContent items={items} type={type} onErrorClick={onErrorClick} />
</EuiPopover>
);
}
export function ErrorsWarningsFooterPopover({
isPopoverOpen,
items,

View file

@ -0,0 +1,70 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiFlexItem, EuiIcon, useEuiTheme, EuiLink, EuiToolTip } from '@elastic/eui';
import { css } from '@emotion/react';
import { FEEDBACK_LINK } from '@kbn/esql-utils';
export function SubmitFeedbackComponent({ isSpaceReduced }: { isSpaceReduced?: boolean }) {
const { euiTheme } = useEuiTheme();
return (
<>
{isSpaceReduced && (
<EuiFlexItem grow={false}>
<EuiLink
href={FEEDBACK_LINK}
external={false}
target="_blank"
data-test-subj="TextBasedLangEditor-feedback-link"
>
<EuiToolTip
position="top"
content={i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.feedback', {
defaultMessage: 'Feedback',
})}
>
<EuiIcon
type="editorComment"
color="primary"
size="m"
css={css`
margin-right: ${euiTheme.size.s};
`}
/>
</EuiToolTip>
</EuiLink>
</EuiFlexItem>
)}
{!isSpaceReduced && (
<>
<EuiFlexItem grow={false}>
<EuiIcon type="editorComment" color="primary" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
href={FEEDBACK_LINK}
external={false}
target="_blank"
css={css`
font-size: 12px;
margin-right: ${euiTheme.size.m};
`}
data-test-subj="TextBasedLangEditor-feedback-link"
>
{i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.submitFeedback', {
defaultMessage: 'Submit feedback',
})}
</EuiLink>
</EuiFlexItem>
</>
)}
</>
);
}

View file

@ -6,92 +6,25 @@
* Side Public License, v 1.
*/
import React, { memo, useState, useCallback } from 'react';
import React, { memo, useState, useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
useEuiTheme,
EuiLink,
EuiCode,
EuiButtonIcon,
EuiToolTip,
} from '@elastic/eui';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCode } from '@elastic/eui';
import { Interpolation, Theme, css } from '@emotion/react';
import type { MonacoMessage } from './helpers';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
LanguageDocumentationPopover,
type LanguageDocumentationSections,
} from '@kbn/language-documentation-popover';
import { type MonacoMessage, getDocumentationSections } from '../helpers';
import { ErrorsWarningsFooterPopover } from './errors_warnings_popover';
import { QueryHistoryAction, QueryHistory } from './query_history';
import { SubmitFeedbackComponent } from './feedback_component';
import { QueryWrapComponent } from './query_wrap_component';
import type { TextBasedEditorDeps } from '../types';
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
const COMMAND_KEY = isMac ? '⌘' : '^';
const FEEDBACK_LINK = 'https://ela.st/esql-feedback';
export function SubmitFeedbackComponent({ isSpaceReduced }: { isSpaceReduced?: boolean }) {
const { euiTheme } = useEuiTheme();
return (
<>
{isSpaceReduced && (
<EuiFlexItem grow={false}>
<EuiLink
href={FEEDBACK_LINK}
external={false}
target="_blank"
data-test-subj="TextBasedLangEditor-feedback-link"
>
<EuiToolTip
position="top"
content={i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.submitFeedback',
{
defaultMessage: 'Submit feedback',
}
)}
>
<EuiIcon
type="editorComment"
color="primary"
size="m"
css={css`
margin-right: ${euiTheme.size.s};
`}
/>
</EuiToolTip>
</EuiLink>
</EuiFlexItem>
)}
{!isSpaceReduced && (
<>
<EuiFlexItem grow={false}>
<EuiIcon type="editorComment" color="primary" size="s" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
href={FEEDBACK_LINK}
external={false}
target="_blank"
css={css`
font-size: 12px;
margin-right: ${euiTheme.size.m};
`}
data-test-subj="TextBasedLangEditor-feedback-link"
>
{isSpaceReduced
? i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.feedback', {
defaultMessage: 'Feedback',
})
: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.submitFeedback', {
defaultMessage: 'Submit feedback',
})}
</EuiLink>
</EuiFlexItem>
</>
)}
</>
);
}
interface EditorFooterProps {
lines: number;
@ -99,6 +32,7 @@ interface EditorFooterProps {
bottomContainer: Interpolation<Theme>;
historyContainer: Interpolation<Theme>;
};
code: string;
errors?: MonacoMessage[];
warnings?: MonacoMessage[];
detectedTimestamp?: string;
@ -107,18 +41,16 @@ interface EditorFooterProps {
updateQuery: (qs: string) => void;
isHistoryOpen: boolean;
setIsHistoryOpen: (status: boolean) => void;
isHelpMenuOpen: boolean;
setIsHelpMenuOpen: (status: boolean) => void;
measuredContainerWidth: number;
hideRunQueryText?: boolean;
disableSubmitAction?: boolean;
editorIsInline?: boolean;
isSpaceReduced?: boolean;
isLoading?: boolean;
allowQueryCancellation?: boolean;
hideTimeFilterInfo?: boolean;
hideQueryHistory?: boolean;
refetchHistoryItems?: boolean;
isInCompactMode?: boolean;
queryHasChanged?: boolean;
}
export const EditorFooter = memo(function EditorFooter({
@ -131,22 +63,27 @@ export const EditorFooter = memo(function EditorFooter({
runQuery,
updateQuery,
hideRunQueryText,
disableSubmitAction,
editorIsInline,
isSpaceReduced,
isLoading,
allowQueryCancellation,
hideTimeFilterInfo,
isHistoryOpen,
setIsHistoryOpen,
hideQueryHistory,
refetchHistoryItems,
isInCompactMode,
queryHasChanged,
measuredContainerWidth,
code,
isHelpMenuOpen,
setIsHelpMenuOpen,
}: EditorFooterProps) {
const kibana = useKibana<TextBasedEditorDeps>();
const { docLinks } = kibana.services;
const [isErrorPopoverOpen, setIsErrorPopoverOpen] = useState(false);
const [isWarningPopoverOpen, setIsWarningPopoverOpen] = useState(false);
const [documentationSections, setDocumentationSections] =
useState<LanguageDocumentationSections>();
const onUpdateAndSubmit = useCallback(
(qs: string) => {
// update the query first
@ -161,6 +98,16 @@ export const EditorFooter = memo(function EditorFooter({
[runQuery, updateQuery]
);
useEffect(() => {
async function getDocumentation() {
const sections = await getDocumentationSections('esql');
setDocumentationSections(sections);
}
if (!documentationSections) {
getDocumentation();
}
}, [documentationSections]);
return (
<EuiFlexGroup
gutterSize="none"
@ -180,6 +127,7 @@ export const EditorFooter = memo(function EditorFooter({
>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
<QueryWrapComponent code={code} updateQuery={updateQuery} />
<EuiFlexItem grow={false} style={{ marginRight: '8px' }}>
<EuiText
size="xs"
@ -300,6 +248,29 @@ export const EditorFooter = memo(function EditorFooter({
</EuiFlexGroup>
</EuiFlexItem>
)}
{documentationSections && !editorIsInline && (
<EuiFlexItem grow={false}>
<LanguageDocumentationPopover
language="ES|QL"
sections={documentationSections}
searchInDescription
linkToDocumentation={docLinks?.links?.query?.queryESQL ?? ''}
buttonProps={{
color: 'text',
size: 'xs',
'data-test-subj': 'TextBasedLangEditor-documentation',
'aria-label': i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.documentationLabel',
{
defaultMessage: 'Documentation',
}
),
}}
isHelpMenuOpen={isHelpMenuOpen}
onHelpMenuVisibilityChange={setIsHelpMenuOpen}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{Boolean(editorIsInline) && (
@ -314,49 +285,29 @@ export const EditorFooter = memo(function EditorFooter({
isSpaceReduced={true}
/>
)}
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.runQuery',
{
defaultMessage: 'Run query',
}
)}
>
<EuiButtonIcon
display="base"
color={queryHasChanged ? 'success' : 'primary'}
onClick={runQuery}
iconType={
allowQueryCancellation && isLoading
? 'cross'
: queryHasChanged
? 'play'
: 'refresh'
}
size="s"
isLoading={isLoading && !allowQueryCancellation}
isDisabled={Boolean(disableSubmitAction && !allowQueryCancellation)}
data-test-subj="TextBasedLangEditor-run-query-button"
aria-label={
allowQueryCancellation && isLoading
? i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.cancel',
{
defaultMessage: 'Cancel',
}
)
: i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.runQuery',
{
defaultMessage: 'Run query',
}
)
}
{documentationSections && (
<EuiFlexItem grow={false}>
<LanguageDocumentationPopover
language="ES|QL"
sections={documentationSections}
searchInDescription
linkToDocumentation={docLinks?.links?.query?.queryESQL ?? ''}
buttonProps={{
color: 'text',
size: 'xs',
'data-test-subj': 'TextBasedLangEditor-documentation',
'aria-label': i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.documentationLabel',
{
defaultMessage: 'Documentation',
}
),
}}
isHelpMenuOpen={isHelpMenuOpen}
onHelpMenuVisibilityChange={setIsHelpMenuOpen}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</>

View file

@ -9,8 +9,8 @@ import React from 'react';
import { QueryHistoryAction, getTableColumns, QueryHistory, QueryColumn } from './query_history';
import { render, screen } from '@testing-library/react';
jest.mock('./history_local_storage', () => {
const module = jest.requireActual('./history_local_storage');
jest.mock('../history_local_storage', () => {
const module = jest.requireActual('../history_local_storage');
return {
...module,
getHistoryItems: () => [

View file

@ -24,7 +24,7 @@ import {
euiScrollBarStyles,
} from '@elastic/eui';
import { css, Interpolation, Theme } from '@emotion/react';
import { type QueryHistoryItem, getHistoryItems } from './history_local_storage';
import { type QueryHistoryItem, getHistoryItems } from '../history_local_storage';
import { getReducedSpaceStyling, swapArrayElements } from './query_history_helpers';
const CONTAINER_MAX_HEIGHT_EXPANDED = 190;

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
import type { QueryHistoryItem } from './history_local_storage';
import type { QueryHistoryItem } from '../history_local_storage';
export const getReducedSpaceStyling = () => {
return `

View file

@ -0,0 +1,74 @@
/*
* 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 } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexItem, EuiToolTip, EuiButtonIcon } from '@elastic/eui';
import { getWrappedInPipesCode } from '../helpers';
export function QueryWrapComponent({
code,
updateQuery,
}: {
code: string;
updateQuery: (qs: string) => void;
}) {
const isWrappedInPipes = useMemo(() => {
const pipes = code.split('|');
const pipesWithNewLine = code?.split('\n|');
return pipes?.length === pipesWithNewLine?.length;
}, [code]);
return (
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={
isWrappedInPipes
? i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel',
{
defaultMessage: 'Remove line breaks on pipes',
}
)
: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel', {
defaultMessage: 'Add line breaks on pipes',
})
}
>
<EuiButtonIcon
iconType={isWrappedInPipes ? 'pipeNoBreaks' : 'pipeBreaks'}
color="text"
size="xs"
data-test-subj="TextBasedLangEditor-toggleWordWrap"
aria-label={
isWrappedInPipes
? i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel',
{
defaultMessage: 'Remove line breaks on pipes',
}
)
: i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel',
{
defaultMessage: 'Add line breaks on pipes',
}
)
}
onClick={() => {
const updatedCode = getWrappedInPipesCode(code, isWrappedInPipes);
if (code !== updatedCode) {
updateQuery(updatedCode);
}
}}
/>
</EuiToolTip>
</EuiFlexItem>
);
}

View file

@ -9,7 +9,6 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import {
parseErrors,
parseWarning,
getInlineEditorText,
getWrappedInPipesCode,
getIndicesList,
getRemoteIndicesList,
@ -209,33 +208,6 @@ describe('helpers', function () {
});
});
describe('getInlineEditorText', function () {
it('should return the entire query if it is one liner', function () {
const text = getInlineEditorText('FROM index1 | keep field1, field2 | order field1', false);
expect(text).toEqual(text);
});
it('should return the query on one line with extra space if is multiliner', function () {
const text = getInlineEditorText(
'FROM index1 | keep field1, field2\n| keep field1, field2 | order field1',
true
);
expect(text).toEqual(
'FROM index1 | keep field1, field2 | keep field1, field2 | order field1'
);
});
it('should return the query on one line with extra spaces removed if is multiliner', function () {
const text = getInlineEditorText(
'FROM index1 | keep field1, field2\n| keep field1, field2 \n | order field1',
true
);
expect(text).toEqual(
'FROM index1 | keep field1, field2 | keep field1, field2 | order field1'
);
});
});
describe('getWrappedInPipesCode', function () {
it('should return the code wrapped', function () {
const code = getWrappedInPipesCode('FROM index1 | keep field1, field2 | order field1', false);

View file

@ -195,10 +195,6 @@ export const getDocumentationSections = async (language: string) => {
}
};
export const getInlineEditorText = (queryString: string, isMultiLine: boolean) => {
return isMultiLine ? queryString.replace(/\r?\n|\r/g, ' ').replace(/ +/g, ' ') : queryString;
};
export const getWrappedInPipesCode = (code: string, isWrapped: boolean): string => {
const pipes = code?.split('|');
const codeNoLines = pipes?.map((pipe) => {

View file

@ -1,14 +1,8 @@
/* Editor styles for any layout mode */
/* NOTE: Much of this is overriding Monaco styles so the specificity is intentional */
// Radius for both the main container and the margin (container for line numbers)
.TextBasedLangEditor .monaco-editor, .TextBasedLangEditor .monaco-editor .margin, .TextBasedLangEditor .monaco-editor .overflow-guard {
border-top-left-radius: $euiBorderRadius;
border-bottom-left-radius: $euiBorderRadius;
}
.TextBasedLangEditor .monaco-editor .monaco-hover {
display: none !important;
display: block !important;
}
.TextBasedLangEditor .monaco-editor .margin-view-overlays .line-numbers {
@ -35,29 +29,15 @@
@include euiTextBreakWord;
}
/* For compact mode */
// All scrollable containers (e.g. main container and suggest menu)
.TextBasedLangEditor--compact .monaco-editor .monaco-scrollable-element {
.TextBasedLangEditor .monaco-editor .monaco-scrollable-element {
margin-left: $euiSizeS;
}
// Suggest menu in compact mode
.TextBasedLangEditor--compact .monaco-editor .monaco-list .monaco-scrollable-element {
.TextBasedLangEditor .monaco-editor .monaco-list .monaco-scrollable-element {
margin-left: 0;
.monaco-list-row.focused {
border-radius: $euiBorderRadius;
}
}
/* For expanded mode */
.TextBasedLangEditor--expanded .monaco-editor .monaco-hover {
display: block !important;
}
.TextBasedLangEditor--expanded .monaco-editor, .TextBasedLangEditor--expanded .monaco-editor .margin, .TextBasedLangEditor--expanded .monaco-editor .overflow-guard {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}

View file

@ -7,61 +7,37 @@
*/
import type { EuiThemeComputed } from '@elastic/eui';
export const EDITOR_INITIAL_HEIGHT = 38;
export const EDITOR_INITIAL_HEIGHT_EXPANDED = 140;
export const EDITOR_INITIAL_HEIGHT = 80;
export const EDITOR_INITIAL_HEIGHT_INLINE_EDITING = 140;
export const EDITOR_MIN_HEIGHT = 40;
export const EDITOR_MAX_HEIGHT = 400;
export const textBasedLanguageEditorStyles = (
euiTheme: EuiThemeComputed,
isCompactFocused: boolean,
editorHeight: number,
isCodeEditorExpanded: boolean,
hasErrors: boolean,
hasWarning: boolean,
isCodeEditorExpandedFocused: boolean,
hasReference: boolean,
editorIsInline: boolean,
historyIsOpen: boolean,
hideHeaderWhenExpanded: boolean
hasOutline: boolean
) => {
const bottomContainerBorderColor = hasErrors ? euiTheme.colors.danger : euiTheme.colors.primary;
const showHeader = hideHeaderWhenExpanded === true && isCodeEditorExpanded;
let position = isCompactFocused ? ('absolute' as const) : ('relative' as const);
if (isCodeEditorExpanded) {
position = 'relative';
}
return {
editorContainer: {
position,
position: 'relative' as const,
left: 0,
right: 0,
zIndex: isCompactFocused ? 4 : 0,
zIndex: 4,
height: `${editorHeight}px`,
border: isCompactFocused ? euiTheme.border.thin : 'none',
borderLeft: editorIsInline || !isCompactFocused ? 'none' : euiTheme.border.thin,
borderRight: editorIsInline || !isCompactFocused ? 'none' : euiTheme.border.thin,
borderTopLeftRadius: isCodeEditorExpanded ? 0 : euiTheme.border.radius.medium,
borderBottom: isCodeEditorExpanded
? 'none'
: isCompactFocused
? euiTheme.border.thin
: 'none',
},
resizableContainer: {
display: 'flex',
width: isCodeEditorExpanded ? '100%' : `calc(100% - ${hasReference ? 80 : 40}px)`,
alignItems: isCompactFocused ? 'flex-start' : 'center',
border: !isCompactFocused ? euiTheme.border.thin : 'none',
borderTopLeftRadius: isCodeEditorExpanded ? 0 : euiTheme.border.radius.medium,
borderBottomLeftRadius: isCodeEditorExpanded ? 0 : euiTheme.border.radius.medium,
borderBottomWidth: hasErrors ? '2px' : '1px',
borderBottomColor: hasErrors ? euiTheme.colors.danger : euiTheme.colors.lightShade,
borderRight: isCodeEditorExpanded ? euiTheme.border.thin : 'none',
...(isCodeEditorExpanded && { overflow: 'hidden' }),
width: '100%',
alignItems: 'flex-start',
border: hasOutline ? euiTheme.border.thin : 'none',
borderBottom: 'none',
overflow: 'hidden',
},
linesBadge: {
position: 'absolute' as const,
@ -78,51 +54,44 @@ export const textBasedLanguageEditorStyles = (
transform: 'translate(0, -50%)',
},
bottomContainer: {
borderLeft: editorIsInline ? 'none' : euiTheme.border.thin,
borderRight: editorIsInline ? 'none' : euiTheme.border.thin,
borderTop:
isCodeEditorExpanded && !isCodeEditorExpandedFocused
? hasErrors
? `2px solid ${euiTheme.colors.danger}`
: euiTheme.border.thin
: `2px solid ${bottomContainerBorderColor}`,
borderBottom: editorIsInline ? 'none' : euiTheme.border.thin,
borderTop: !isCodeEditorExpandedFocused
? hasErrors
? `2px solid ${euiTheme.colors.danger}`
: `2px solid ${euiTheme.colors.lightestShade}`
: `2px solid ${bottomContainerBorderColor}`,
backgroundColor: euiTheme.colors.lightestShade,
paddingLeft: euiTheme.size.base,
paddingRight: euiTheme.size.base,
paddingLeft: euiTheme.size.xs,
paddingRight: euiTheme.size.xs,
paddingTop: editorIsInline ? euiTheme.size.s : euiTheme.size.xs,
paddingBottom: editorIsInline ? euiTheme.size.s : euiTheme.size.xs,
width: isCodeEditorExpanded ? '100%' : 'calc(100% + 2px)',
width: '100%',
position: 'relative' as const,
marginTop: 0,
marginLeft: isCodeEditorExpanded ? 0 : -1,
marginLeft: 0,
marginBottom: 0,
borderBottomLeftRadius: editorIsInline || historyIsOpen ? 0 : euiTheme.border.radius.medium,
borderBottomRightRadius: editorIsInline || historyIsOpen ? 0 : euiTheme.border.radius.medium,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
historyContainer: {
border: euiTheme.border.thin,
borderTop: 'none',
borderLeft: editorIsInline ? 'none' : euiTheme.border.thin,
borderRight: editorIsInline ? 'none' : euiTheme.border.thin,
border: 'none',
backgroundColor: euiTheme.colors.lightestShade,
width: '100%',
position: 'relative' as const,
marginTop: 0,
marginLeft: 0,
marginBottom: 0,
borderBottomLeftRadius: editorIsInline ? 0 : euiTheme.border.radius.medium,
borderBottomRightRadius: editorIsInline ? 0 : euiTheme.border.radius.medium,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
topContainer: {
border: editorIsInline ? 'none' : euiTheme.border.thin,
borderTopLeftRadius: editorIsInline ? 0 : euiTheme.border.radius.medium,
borderTopRightRadius: editorIsInline ? 0 : euiTheme.border.radius.medium,
border: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
backgroundColor: euiTheme.colors.lightestShade,
paddingLeft: euiTheme.size.s,
paddingRight: euiTheme.size.s,
paddingTop: showHeader ? euiTheme.size.s : euiTheme.size.xs,
paddingBottom: showHeader ? euiTheme.size.s : euiTheme.size.xs,
paddingTop: euiTheme.size.s,
paddingBottom: euiTheme.size.s,
width: '100%',
position: 'relative' as const,
marginLeft: 0,

View file

@ -12,10 +12,8 @@ import { IUiSettingsClient } from '@kbn/core/public';
import { mountWithIntl as mount } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import {
TextBasedLanguagesEditor,
TextBasedLanguagesEditorProps,
} from './text_based_languages_editor';
import { TextBasedLanguagesEditor } from './text_based_languages_editor';
import type { TextBasedLanguagesEditorProps } from './types';
import { ReactWrapper } from 'enzyme';
jest.mock('./helpers', () => {
@ -63,10 +61,8 @@ describe('TextBasedLanguagesEditor', () => {
beforeEach(() => {
props = {
query: { esql: 'from test' },
isCodeEditorExpanded: false,
onTextLangQueryChange: jest.fn(),
onTextLangQuerySubmit: jest.fn(),
expandCodeEditor: jest.fn(),
};
});
it('should render the editor component', async () => {
@ -74,13 +70,6 @@ describe('TextBasedLanguagesEditor', () => {
expect(component.find('[data-test-subj="TextBasedLangEditor"]').length).not.toBe(0);
});
it('should render the lines badge for the inline mode by default', async () => {
const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
expect(
component.find('[data-test-subj="TextBasedLangEditor-inline-lines-badge"]').length
).not.toBe(0);
});
it('should render the date info with no @timestamp found', async () => {
const newProps = {
...props,
@ -163,60 +152,6 @@ describe('TextBasedLanguagesEditor', () => {
).toBe(0);
});
it('should render the errors badge for the inline mode by default if errors are provided', async () => {
const newProps = {
...props,
errors: [new Error('error1')],
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const errorBadge = component.find('[data-test-subj="TextBasedLangEditor-inline-error-badge"]');
expect(errorBadge.length).not.toBe(0);
errorBadge.at(0).simulate('click');
expect(
component.find('[data-test-subj="TextBasedLangEditor-inline-error-popover"]').length
).not.toBe(0);
});
it('should render the warnings badge for the inline mode by default if warning are provided', async () => {
const newProps = {
...props,
warning: 'Line 1: 20: Warning',
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
const warningBadge = component.find(
'[data-test-subj="TextBasedLangEditor-inline-warning-badge"]'
);
expect(warningBadge.length).not.toBe(0);
warningBadge.at(0).simulate('click');
expect(
component.find('[data-test-subj="TextBasedLangEditor-inline-warning-popover"]').length
).not.toBe(0);
});
it('should render the correct buttons for the inline code editor mode', async () => {
let component: ReactWrapper;
await act(async () => {
component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
});
component!.update();
expect(component!.find('[data-test-subj="TextBasedLangEditor-expand"]').length).not.toBe(0);
expect(
component!.find('[data-test-subj="TextBasedLangEditor-inline-documentation"]').length
).not.toBe(0);
});
it('should call the expand editor function when expand button is clicked', async () => {
const expandCodeEditorSpy = jest.fn();
const newProps = {
...props,
expandCodeEditor: expandCodeEditorSpy,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
findTestSubject(component, 'TextBasedLangEditor-expand').simulate('click');
expect(expandCodeEditorSpy).toHaveBeenCalled();
});
it('should render the correct buttons for the expanded code editor mode', async () => {
const newProps = {
...props,
@ -230,46 +165,11 @@ describe('TextBasedLanguagesEditor', () => {
expect(
component!.find('[data-test-subj="TextBasedLangEditor-toggleWordWrap"]').length
).not.toBe(0);
expect(component!.find('[data-test-subj="TextBasedLangEditor-minimize"]').length).not.toBe(0);
expect(component!.find('[data-test-subj="TextBasedLangEditor-documentation"]').length).not.toBe(
0
);
});
it('should not render the minimize button for the expanded code editor mode if the prop is set to true', async () => {
const newProps = {
...props,
isCodeEditorExpanded: true,
hideMinimizeButton: true,
};
let component: ReactWrapper;
await act(async () => {
component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
});
component!.update();
await act(async () => {
expect(
component.find('[data-test-subj="TextBasedLangEditor-toggleWordWrap"]').length
).not.toBe(0);
expect(component.find('[data-test-subj="TextBasedLangEditor-minimize"]').length).toBe(0);
expect(
component.find('[data-test-subj="TextBasedLangEditor-documentation"]').length
).not.toBe(0);
});
});
it('should call the expand editor function when minimize button is clicked', async () => {
const expandCodeEditorSpy = jest.fn();
const newProps = {
...props,
isCodeEditorExpanded: true,
expandCodeEditor: expandCodeEditorSpy,
};
const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
findTestSubject(component, 'TextBasedLangEditor-minimize').simulate('click');
expect(expandCodeEditorSpy).toHaveBeenCalled();
});
it('should render the resize for the expanded code editor mode', async () => {
const newProps = {
...props,

View file

@ -7,45 +7,33 @@
*/
import {
EuiBadge,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiOutsideClickDetector,
EuiToolTip,
useEuiTheme,
EuiDatePicker,
EuiToolTip,
EuiButton,
type EuiButtonColor,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { CodeEditor, CodeEditorProps } from '@kbn/code-editor';
import type { CoreStart } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import { getAggregateQueryMode, getLanguageDisplayName } from '@kbn/es-query';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { i18n } from '@kbn/i18n';
import type { IndexManagementPluginSetup } from '@kbn/index-management';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
LanguageDocumentationPopover,
type LanguageDocumentationSections,
} from '@kbn/language-documentation-popover';
import { ESQLLang, ESQL_LANG_ID, ESQL_THEME_ID, monaco, type ESQLCallbacks } from '@kbn/monaco';
import classNames from 'classnames';
import memoize from 'lodash/memoize';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { css } from '@emotion/react';
import { EditorFooter } from './editor_footer';
import { ErrorsWarningsCompactViewPopover } from './errors_warnings_popover';
import { fetchFieldsFromESQL } from './fetch_fields_from_esql';
import {
clearCacheWhenOld,
getDocumentationSections,
getESQLSources,
getInlineEditorText,
getWrappedInPipesCode,
parseErrors,
parseWarning,
useDebounceWithOptions,
@ -55,112 +43,31 @@ import { addQueriesToCache, updateCachedQueries } from './history_local_storage'
import { ResizableButton } from './resizable_button';
import {
EDITOR_INITIAL_HEIGHT,
EDITOR_INITIAL_HEIGHT_EXPANDED,
EDITOR_INITIAL_HEIGHT_INLINE_EDITING,
EDITOR_MAX_HEIGHT,
EDITOR_MIN_HEIGHT,
textBasedLanguageEditorStyles,
} from './text_based_languages_editor.styles';
import { getRateLimitedColumnsWithMetadata } from './ecs_metadata_helper';
import type { TextBasedLanguagesEditorProps, TextBasedEditorDeps } from './types';
import './overwrite.scss';
export interface TextBasedLanguagesEditorProps {
/** The aggregate type query */
query: AggregateQuery;
/** Callback running everytime the query changes */
onTextLangQueryChange: (query: AggregateQuery) => void;
/** Callback running when the user submits the query */
onTextLangQuerySubmit: (
query?: AggregateQuery,
abortController?: AbortController
) => Promise<void>;
/** Can be used to expand/minimize the editor */
expandCodeEditor: (status: boolean) => void;
/** If it is true, the editor initializes with height EDITOR_INITIAL_HEIGHT_EXPANDED */
isCodeEditorExpanded: boolean;
/** If it is true, the editor displays the message @timestamp found
* The text based queries are relying on adhoc dataviews which
* can have an @timestamp timefield or nothing
*/
detectedTimestamp?: string;
/** Array of errors */
errors?: Error[];
/** Warning string as it comes from ES */
warning?: string;
/** Disables the editor and displays loading icon in run button
* It is also used for hiding the history component if it is not defined
*/
isLoading?: boolean;
/** Disables the editor */
isDisabled?: boolean;
/** Indicator if the editor is on dark mode */
isDarkMode?: boolean;
dataTestSubj?: string;
/** If true it hides the minimize button and the user can't return to the minimized version
* Useful when the application doesn't want to give this capability
*/
hideMinimizeButton?: boolean;
/** Hide the Run query information which appears on the footer*/
hideRunQueryText?: boolean;
/** This is used for applications (such as the inline editing flyout in dashboards)
* which want to add the editor without being part of the Unified search component
* It renders a submit query button inside the editor
*/
editorIsInline?: boolean;
/** Disables the submit query action*/
disableSubmitAction?: boolean;
/** when set to true enables query cancellation **/
allowQueryCancellation?: boolean;
/** hide @timestamp info **/
hideTimeFilterInfo?: boolean;
/** hide query history **/
hideQueryHistory?: boolean;
/** hide header buttons when editor is expanded */
hideHeaderWhenExpanded?: boolean;
}
interface TextBasedEditorDeps {
core: CoreStart;
dataViews: DataViewsPublicPluginStart;
expressions: ExpressionsStart;
indexManagementApiService?: IndexManagementPluginSetup['apiService'];
fieldsMetadata?: FieldsMetadataPublicStart;
}
const MAX_COMPACT_VIEW_LENGTH = 250;
const FONT_WIDTH = 8;
const EDITOR_ONE_LINER_UNUSED_SPACE = 180;
const EDITOR_ONE_LINER_UNUSED_SPACE_WITH_ERRORS = 220;
const KEYCODE_ARROW_UP = 38;
const KEYCODE_ARROW_DOWN = 40;
// for editor width smaller than this value we want to start hiding some text
const BREAKPOINT_WIDTH = 540;
let clickedOutside = false;
let initialRender = true;
let updateLinesFromModel = false;
let lines = 1;
let isDatePickerOpen = false;
export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
query,
onTextLangQueryChange,
onTextLangQuerySubmit,
expandCodeEditor,
isCodeEditorExpanded,
detectedTimestamp,
errors: serverErrors,
warning: serverWarning,
isLoading,
isDisabled,
isDarkMode,
hideMinimizeButton,
hideRunQueryText,
editorIsInline,
disableSubmitAction,
@ -168,41 +75,30 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
allowQueryCancellation,
hideTimeFilterInfo,
hideQueryHistory,
hideHeaderWhenExpanded,
hasOutline,
}: TextBasedLanguagesEditorProps) {
const popoverRef = useRef<HTMLDivElement>(null);
const datePickerOpenStatusRef = useRef<boolean>(false);
const { euiTheme } = useEuiTheme();
const language = getAggregateQueryMode(query);
const queryString: string = query[language] ?? '';
const kibana = useKibana<TextBasedEditorDeps>();
const {
dataViews,
expressions,
indexManagementApiService,
application,
docLinks,
core,
fieldsMetadata,
} = kibana.services;
const { dataViews, expressions, indexManagementApiService, application, core, fieldsMetadata } =
kibana.services;
const timeZone = core?.uiSettings?.get('dateFormat:tz');
const [code, setCode] = useState<string>(queryString ?? '');
const [codeOneLiner, setCodeOneLiner] = useState<string | null>(null);
const [code, setCode] = useState<string>(query.esql ?? '');
// To make server side errors less "sticky", register the state of the code when submitting
const [codeWhenSubmitted, setCodeStateOnSubmission] = useState(code);
const [editorHeight, setEditorHeight] = useState(
isCodeEditorExpanded ? EDITOR_INITIAL_HEIGHT_EXPANDED : EDITOR_INITIAL_HEIGHT
editorIsInline ? EDITOR_INITIAL_HEIGHT_INLINE_EDITING : EDITOR_INITIAL_HEIGHT
);
const [popoverPosition, setPopoverPosition] = useState<{ top?: number; left?: number }>({});
const [timePickerDate, setTimePickerDate] = useState(moment());
const [measuredEditorWidth, setMeasuredEditorWidth] = useState(0);
const [measuredContentWidth, setMeasuredContentWidth] = useState(0);
const isSpaceReduced = Boolean(editorIsInline) && measuredEditorWidth < BREAKPOINT_WIDTH;
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [showLineNumbers, setShowLineNumbers] = useState(isCodeEditorExpanded);
const [isCompactFocused, setIsCompactFocused] = useState(isCodeEditorExpanded);
const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false);
const [isLanguagePopoverOpen, setIsLanguagePopoverOpen] = useState(false);
const [isQueryLoading, setIsQueryLoading] = useState(true);
const [abortController, setAbortController] = useState(new AbortController());
// contains both client side validation and server messages
@ -230,10 +126,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const onQueryUpdate = useCallback(
(value: string) => {
setCode(value);
onTextLangQueryChange({ [language]: value } as AggregateQuery);
onTextLangQueryChange({ esql: value } as AggregateQuery);
},
[language, onTextLangQueryChange]
[onTextLangQueryChange]
);
const onQuerySubmit = useCallback(() => {
@ -249,16 +144,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
if (currentValue != null) {
setCodeStateOnSubmission(currentValue);
}
onTextLangQuerySubmit({ [language]: currentValue } as AggregateQuery, abc);
onTextLangQuerySubmit({ esql: currentValue } as AggregateQuery, abc);
}
}, [
isQueryLoading,
isLoading,
allowQueryCancellation,
abortController,
onTextLangQuerySubmit,
language,
]);
}, [isQueryLoading, isLoading, allowQueryCancellation, abortController, onTextLangQuerySubmit]);
const onCommentLine = useCallback(() => {
const currentSelection = editor1?.current?.getSelection();
@ -290,8 +178,13 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
if (!isLoading) setIsQueryLoading(false);
}, [isLoading]);
const [documentationSections, setDocumentationSections] =
useState<LanguageDocumentationSections>();
useEffect(() => {
if (editor1.current) {
if (code !== query.esql) {
setCode(query.esql);
}
}
}, [code, query.esql]);
const toggleHistory = useCallback((status: boolean) => {
setIsHistoryOpen(status);
@ -310,7 +203,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const absoluteLeft = editorLeft + (editorPosition?.left ?? 0);
setPopoverPosition({ top: absoluteTop, left: absoluteLeft });
isDatePickerOpen = true;
datePickerOpenStatusRef.current = true;
popoverRef.current?.focus();
}
}, []);
@ -330,28 +223,17 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const styles = textBasedLanguageEditorStyles(
euiTheme,
isCompactFocused,
editorHeight,
isCodeEditorExpanded,
Boolean(editorMessages.errors.length),
Boolean(editorMessages.warnings.length),
isCodeEditorExpandedFocused,
Boolean(documentationSections),
Boolean(editorIsInline),
isHistoryOpen,
!!hideHeaderWhenExpanded
Boolean(hasOutline)
);
const isDark = isDarkMode;
const editorModel = useRef<monaco.editor.ITextModel>();
const editor1 = useRef<monaco.editor.IStandaloneCodeEditor>();
const containerRef = useRef<HTMLElement>(null);
const editorClassName = classNames('TextBasedLangEditor', {
'TextBasedLangEditor--expanded': isCodeEditorExpanded,
'TextBasedLangEditor--compact': isCompactFocused,
'TextBasedLangEditor--initial': !isCompactFocused,
});
// When the editor is on full size mode, the user can resize the height of the editor.
const onMouseDownResizeHandler = useCallback(
(mouseDownEvent) => {
@ -389,36 +271,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
[editorHeight]
);
const restoreInitialMode = () => {
setIsCodeEditorExpandedFocused(false);
if (isCodeEditorExpanded) return;
setEditorHeight(EDITOR_INITIAL_HEIGHT);
setIsCompactFocused(false);
setShowLineNumbers(false);
updateLinesFromModel = false;
clickedOutside = true;
if (editor1.current) {
const contentWidth = editor1.current.getLayoutInfo().width;
calculateVisibleCode(contentWidth, true);
editor1.current.layout({ width: contentWidth, height: EDITOR_INITIAL_HEIGHT });
}
};
const updateHeight = useCallback((editor: monaco.editor.IStandaloneCodeEditor) => {
if (clickedOutside || initialRender) return;
const contentHeight = Math.min(MAX_COMPACT_VIEW_LENGTH, editor.getContentHeight());
setEditorHeight(contentHeight);
editor.layout({ width: editor.getLayoutInfo().width, height: contentHeight });
}, []);
const onEditorFocus = useCallback(() => {
setIsCompactFocused(true);
setIsCodeEditorExpandedFocused(true);
setShowLineNumbers(true);
setCodeOneLiner(null);
clickedOutside = false;
initialRender = false;
updateLinesFromModel = true;
}, []);
const { cache: esqlFieldsCache, memoizedFieldsFromESQL } = useMemo(() => {
@ -449,7 +303,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const esqlCallbacks: ESQLCallbacks = useMemo(() => {
const callbacks: ESQLCallbacks = {
getSources: async () => {
clearCacheWhenOld(dataSourcesCache, queryString);
clearCacheWhenOld(dataSourcesCache, query.esql);
const sources = await memoizedSources(dataViews, core).result;
return sources;
},
@ -494,7 +348,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
};
return callbacks;
}, [
queryString,
query.esql,
memoizedSources,
dataSourcesCache,
dataViews,
@ -507,15 +361,43 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
fieldsMetadata,
]);
const queryRunButtonProperties = useMemo(() => {
if (allowQueryCancellation && isLoading) {
return {
label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.cancel', {
defaultMessage: 'Cancel',
}),
iconType: 'cross',
color: 'text',
};
}
if (code !== codeWhenSubmitted) {
return {
label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.runQuery', {
defaultMessage: 'Run query',
}),
iconType: 'play',
color: 'success',
};
}
return {
label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.refreshLabel', {
defaultMessage: 'Refresh',
}),
iconType: 'refresh',
color: 'primary',
};
}, [allowQueryCancellation, code, codeWhenSubmitted, isLoading]);
const parseMessages = useCallback(async () => {
if (editorModel.current) {
return await ESQLLang.validate(editorModel.current, queryString, esqlCallbacks);
return await ESQLLang.validate(editorModel.current, code, esqlCallbacks);
}
return {
errors: [],
warnings: [],
};
}, [esqlCallbacks, queryString]);
}, [esqlCallbacks, code]);
const clientParserStatus = clientParserMessages.errors?.length
? 'error'
@ -535,24 +417,24 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
};
if (isQueryLoading || isLoading) {
addQueriesToCache({
queryString,
queryString: code,
timeZone,
});
validateQuery();
setRefetchHistoryItems(false);
} else {
updateCachedQueries({
queryString,
queryString: code,
status: clientParserStatus,
});
setRefetchHistoryItems(true);
}
}, [clientParserStatus, isLoading, isQueryLoading, parseMessages, queryString, timeZone]);
}, [clientParserStatus, isLoading, isQueryLoading, parseMessages, code, timeZone]);
const queryValidation = useCallback(
async ({ active }: { active: boolean }) => {
if (!editorModel.current || language !== 'esql' || editorModel.current.isDisposed()) return;
if (!editorModel.current || editorModel.current.isDisposed()) return;
monaco.editor.setModelMarkers(editorModel.current, 'Unified search', []);
const { warnings: parserWarnings, errors: parserErrors } = await parseMessages();
const markers = [];
@ -566,7 +448,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
return;
}
},
[language, parseMessages]
[parseMessages]
);
useDebounceWithOptions(
@ -602,18 +484,15 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
);
const suggestionProvider = useMemo(
() => (language === 'esql' ? ESQLLang.getSuggestionProvider?.(esqlCallbacks) : undefined),
[language, esqlCallbacks]
() => ESQLLang.getSuggestionProvider?.(esqlCallbacks),
[esqlCallbacks]
);
const hoverProvider = useMemo(
() => (language === 'esql' ? ESQLLang.getHoverProvider?.(esqlCallbacks) : undefined),
[language, esqlCallbacks]
);
const hoverProvider = useMemo(() => ESQLLang.getHoverProvider?.(esqlCallbacks), [esqlCallbacks]);
const codeActionProvider = useMemo(
() => (language === 'esql' ? ESQLLang.getCodeActionProvider?.(esqlCallbacks) : undefined),
[language, esqlCallbacks]
() => ESQLLang.getCodeActionProvider?.(esqlCallbacks),
[esqlCallbacks]
);
const onErrorClick = useCallback(({ startLineNumber, startColumn }: MonacoMessage) => {
@ -639,46 +518,11 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
};
}, []);
const calculateVisibleCode = useCallback(
(width: number, force?: boolean) => {
const containerWidth = containerRef.current?.offsetWidth;
if (containerWidth && (!isCompactFocused || force)) {
const hasLines = /\r|\n/.exec(queryString);
if (hasLines && !updateLinesFromModel) {
lines = queryString.split(/\r|\n/).length;
}
const text = getInlineEditorText(queryString, Boolean(hasLines));
const queryLength = text.length;
const unusedSpace =
editorMessages.errors.length || editorMessages.warnings.length
? EDITOR_ONE_LINER_UNUSED_SPACE_WITH_ERRORS
: EDITOR_ONE_LINER_UNUSED_SPACE;
const charactersAlowed = Math.floor((width - unusedSpace) / FONT_WIDTH);
if (queryLength > charactersAlowed) {
const shortedCode = text.substring(0, charactersAlowed) + '...';
setCodeOneLiner(shortedCode);
} else {
const shortedCode = text;
setCodeOneLiner(shortedCode);
}
}
},
[isCompactFocused, queryString, editorMessages]
);
// When the layout changes, and the editor is not focused, we want to
// recalculate the visible code so it fills up the available space. We
// use a ref because editorDidMount is only called once, and the reference
// to the state becomes stale after re-renders.
const onLayoutChange = (layoutInfoEvent: monaco.editor.EditorLayoutInfo) => {
if (layoutInfoEvent.contentWidth !== measuredContentWidth) {
const nextMeasuredWidth = layoutInfoEvent.contentWidth;
setMeasuredContentWidth(nextMeasuredWidth);
if (!isCodeEditorExpandedFocused && !isCompactFocused) {
calculateVisibleCode(nextMeasuredWidth, true);
}
}
if (layoutInfoEvent.width !== measuredEditorWidth) {
setMeasuredEditorWidth(layoutInfoEvent.width);
}
@ -688,38 +532,6 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
onLayoutChangeRef.current = onLayoutChange;
useEffect(() => {
if (editor1.current && !isCompactFocused) {
if (code !== queryString) {
setCode(queryString);
calculateVisibleCode(editor1.current.getLayoutInfo().width);
}
}
}, [calculateVisibleCode, code, isCompactFocused, queryString]);
useEffect(() => {
// make sure to always update the code in expanded editor when query prop changes
if (isCodeEditorExpanded && editor1.current?.getValue() !== queryString) {
setCode(queryString);
}
}, [isCodeEditorExpanded, queryString]);
const isWrappedInPipes = useMemo(() => {
const pipes = code?.split('|');
const pipesWithNewLine = code?.split('\n|');
return pipes?.length === pipesWithNewLine?.length;
}, [code]);
useEffect(() => {
async function getDocumentation() {
const sections = await getDocumentationSections(language);
setDocumentationSections(sections);
}
if (!documentationSections) {
getDocumentation();
}
}, [language, documentationSections]);
const codeEditorOptions: CodeEditorProps['options'] = {
accessibilitySupport: 'off',
autoIndent: 'none',
@ -734,7 +546,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
enabled: false,
},
lineDecorationsWidth: 12,
lineNumbers: showLineNumbers ? 'on' : 'off',
lineNumbers: 'on',
lineNumbersMinChars: 3,
minimap: { enabled: false },
overviewRulerLanes: 0,
@ -744,204 +556,78 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
bottom: 8,
},
quickSuggestions: true,
readOnly:
isDisabled || Boolean(!isCompactFocused && codeOneLiner && codeOneLiner.includes('...')),
renderLineHighlight: !isCodeEditorExpanded ? 'none' : 'line',
readOnly: isDisabled,
renderLineHighlight: 'line',
renderLineHighlightOnlyWhenFocus: true,
scrollbar: {
horizontal: 'hidden',
vertical: 'auto',
},
scrollBeyondLastLine: false,
theme: language === 'esql' ? ESQL_THEME_ID : isDark ? 'vs-dark' : 'vs',
theme: ESQL_THEME_ID,
wordWrap: 'on',
wrappingIndent: 'none',
};
if (isCompactFocused) {
codeEditorOptions.overviewRulerLanes = 4;
codeEditorOptions.hideCursorInOverviewRuler = false;
codeEditorOptions.overviewRulerBorder = true;
}
const editorPanel = (
<>
{isCodeEditorExpanded && !hideHeaderWhenExpanded && (
{Boolean(editorIsInline) && (
<EuiFlexGroup
gutterSize="s"
justifyContent="spaceBetween"
css={styles.topContainer}
gutterSize="none"
responsive={false}
justifyContent="flexEnd"
css={css`
padding: ${euiTheme.size.s};
`}
>
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={
isWrappedInPipes
? i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel',
{
defaultMessage: 'Remove line breaks on pipes',
}
)
: i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel',
{
defaultMessage: 'Add line breaks on pipes',
}
)
}
>
<EuiButtonIcon
iconType={isWrappedInPipes ? 'pipeNoBreaks' : 'pipeBreaks'}
color="text"
size="xs"
data-test-subj="TextBasedLangEditor-toggleWordWrap"
aria-label={
isWrappedInPipes
? i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel',
{
defaultMessage: 'Remove line breaks on pipes',
}
)
: i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel',
{
defaultMessage: 'Add line breaks on pipes',
}
)
}
onClick={() => {
const updatedCode = getWrappedInPipesCode(code, isWrappedInPipes);
if (code !== updatedCode) {
setCode(updatedCode);
onTextLangQueryChange({ [language]: updatedCode } as AggregateQuery);
}
}}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
{documentationSections && (
<EuiFlexItem grow={false}>
<LanguageDocumentationPopover
language={getLanguageDisplayName(String(language))}
sections={documentationSections}
searchInDescription
linkToDocumentation={
language === 'esql' ? docLinks?.links?.query?.queryESQL : ''
}
buttonProps={{
color: 'text',
size: 'xs',
'data-test-subj': 'TextBasedLangEditor-documentation',
'aria-label': i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.documentationLabel',
{
defaultMessage: 'Documentation',
}
),
}}
/>
</EuiFlexItem>
)}
</EuiFlexItem>
{!Boolean(hideMinimizeButton) && (
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip',
{
defaultMessage: 'Compact query editor',
}
)}
>
<EuiButtonIcon
iconType="minimize"
color="text"
aria-label={i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor',
{
defaultMessage: 'Minimize editor',
}
)}
data-test-subj="TextBasedLangEditor-minimize"
size="xs"
onClick={() => {
expandCodeEditor(false);
updateLinesFromModel = false;
}}
/>
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiToolTip
position="top"
content={i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.runQuery', {
defaultMessage: 'Run query',
})}
>
<EuiButton
color={queryRunButtonProperties.color as EuiButtonColor}
onClick={onQuerySubmit}
iconType={queryRunButtonProperties.iconType}
size="s"
isLoading={isLoading && !allowQueryCancellation}
isDisabled={Boolean(disableSubmitAction && !allowQueryCancellation)}
data-test-subj="TextBasedLangEditor-run-query-button"
aria-label={queryRunButtonProperties.label}
>
{queryRunButtonProperties.label}
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup gutterSize="none" responsive={false} ref={containerRef}>
<EuiOutsideClickDetector
onOutsideClick={() => {
restoreInitialMode();
setIsCodeEditorExpandedFocused(false);
}}
>
<div css={styles.resizableContainer}>
<EuiFlexItem
data-test-subj={dataTestSubj ?? 'TextBasedLangEditor'}
className={editorClassName}
className="TextBasedLangEditor"
css={css`
max-width: 100%;
position: relative;
`}
>
<div css={styles.editorContainer}>
{!isCompactFocused && (
<EuiBadge
color={euiTheme.colors.lightShade}
css={styles.linesBadge}
data-test-subj="TextBasedLangEditor-inline-lines-badge"
>
{i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.lineCount', {
defaultMessage: '{count} {count, plural, one {line} other {lines}}',
values: { count: lines },
})}
</EuiBadge>
)}
{!isCompactFocused && editorMessages.errors.length > 0 && (
<ErrorsWarningsCompactViewPopover
items={editorMessages.errors}
type="error"
onErrorClick={onErrorClick}
popoverCSS={styles.errorsBadge}
/>
)}
{!isCompactFocused &&
editorMessages.warnings.length > 0 &&
editorMessages.errors.length === 0 && (
<ErrorsWarningsCompactViewPopover
items={editorMessages.warnings}
type="warning"
onErrorClick={onErrorClick}
popoverCSS={styles.errorsBadge}
/>
)}
<CodeEditor
languageId={ESQL_LANG_ID}
value={codeOneLiner || code}
value={code}
options={codeEditorOptions}
width="100%"
suggestionProvider={suggestionProvider}
hoverProvider={{
provideHover: (model, position, token) => {
if (isCompactFocused || !hoverProvider?.provideHover) {
if (!hoverProvider?.provideHover) {
return { contents: [] };
}
return hoverProvider?.provideHover(model, position, token);
@ -955,10 +641,6 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
if (model) {
editorModel.current = model;
}
if (isCodeEditorExpanded) {
lines = model?.getLineCount() || 1;
}
// this is fixing a bug between the EUIPopover and the monaco editor
// when the user clicks the editor, we force it to focus and the onDidFocusEditorText
// to fire, the timeout is needed because otherwise it refocuses on the popover icon
@ -968,7 +650,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
setTimeout(() => {
editor.focus();
}, 100);
if (isDatePickerOpen) {
if (datePickerOpenStatusRef.current) {
setPopoverPosition({});
}
});
@ -996,156 +678,45 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
);
setMeasuredEditorWidth(editor.getLayoutInfo().width);
setMeasuredContentWidth(editor.getContentWidth());
editor.onDidLayoutChange((layoutInfoEvent) => {
onLayoutChangeRef.current(layoutInfoEvent);
});
if (!isCodeEditorExpanded) {
editor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged) {
updateHeight(editor);
}
});
}
}}
/>
{isCompactFocused && !isCodeEditorExpanded && (
<EditorFooter
lines={lines}
styles={{
bottomContainer: styles.bottomContainer,
historyContainer: styles.historyContainer,
}}
{...editorMessages}
onErrorClick={onErrorClick}
runQuery={onQuerySubmit}
updateQuery={onQueryUpdate}
detectedTimestamp={detectedTimestamp}
editorIsInline={editorIsInline}
disableSubmitAction={disableSubmitAction}
hideRunQueryText={hideRunQueryText}
isSpaceReduced={isSpaceReduced}
isLoading={isQueryLoading}
allowQueryCancellation={allowQueryCancellation}
hideTimeFilterInfo={hideTimeFilterInfo}
isHistoryOpen={isHistoryOpen}
setIsHistoryOpen={toggleHistory}
measuredContainerWidth={measuredEditorWidth}
hideQueryHistory={hideHistoryComponent}
refetchHistoryItems={refetchHistoryItems}
isInCompactMode={true}
queryHasChanged={code !== codeWhenSubmitted}
/>
)}
</div>
</EuiFlexItem>
</div>
</EuiOutsideClickDetector>
{!isCodeEditorExpanded && (
<EuiFlexItem grow={false}>
<EuiFlexGroup responsive={false} gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
{documentationSections && (
<EuiFlexItem grow={false}>
<LanguageDocumentationPopover
language={
String(language) === 'esql' ? 'ES|QL' : String(language).toUpperCase()
}
linkToDocumentation={
language === 'esql' ? docLinks?.links?.query?.queryESQL : ''
}
searchInDescription
sections={documentationSections}
buttonProps={{
display: 'empty',
'data-test-subj': 'TextBasedLangEditor-inline-documentation',
'aria-label': i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.documentationLabel',
{
defaultMessage: 'Documentation',
}
),
size: 'm',
css: {
borderRadius: 0,
backgroundColor: isDark ? euiTheme.colors.lightestShade : '#e9edf3',
border: '1px solid rgb(17 43 134 / 10%) !important',
transform: 'none !important',
},
}}
/>
</EuiFlexItem>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={i18n.translate(
'textBasedEditor.query.textBasedLanguagesEditor.expandTooltip',
{
defaultMessage: 'Expand query editor',
}
)}
>
<EuiButtonIcon
display="empty"
iconType="expand"
size="m"
aria-label="Expand"
onClick={() => expandCodeEditor(true)}
data-test-subj="TextBasedLangEditor-expand"
css={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
backgroundColor: isDark ? euiTheme.colors.lightestShade : '#e9edf3',
border: '1px solid rgb(17 43 134 / 10%) !important',
borderLeft: 'transparent !important',
transform: 'none !important',
}}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
{isCodeEditorExpanded && (
<EditorFooter
lines={lines}
styles={{
bottomContainer: styles.bottomContainer,
historyContainer: styles.historyContainer,
}}
onErrorClick={onErrorClick}
runQuery={onQuerySubmit}
updateQuery={onQueryUpdate}
detectedTimestamp={detectedTimestamp}
hideRunQueryText={hideRunQueryText}
editorIsInline={editorIsInline}
disableSubmitAction={disableSubmitAction}
isSpaceReduced={isSpaceReduced}
isLoading={isQueryLoading}
allowQueryCancellation={allowQueryCancellation}
hideTimeFilterInfo={hideTimeFilterInfo}
{...editorMessages}
isHistoryOpen={isHistoryOpen}
setIsHistoryOpen={toggleHistory}
measuredContainerWidth={measuredEditorWidth}
hideQueryHistory={hideHistoryComponent}
refetchHistoryItems={refetchHistoryItems}
queryHasChanged={code !== codeWhenSubmitted}
/>
)}
{isCodeEditorExpanded && (
<ResizableButton
onMouseDownResizeHandler={onMouseDownResizeHandler}
onKeyDownResizeHandler={onKeyDownResizeHandler}
editorIsInline={editorIsInline}
/>
)}
<EditorFooter
lines={editorModel.current?.getLineCount() || 1}
styles={{
bottomContainer: styles.bottomContainer,
historyContainer: styles.historyContainer,
}}
code={code}
onErrorClick={onErrorClick}
runQuery={onQuerySubmit}
updateQuery={onQueryUpdate}
detectedTimestamp={detectedTimestamp}
hideRunQueryText={hideRunQueryText}
editorIsInline={editorIsInline}
isSpaceReduced={isSpaceReduced}
hideTimeFilterInfo={hideTimeFilterInfo}
{...editorMessages}
isHistoryOpen={isHistoryOpen}
setIsHistoryOpen={toggleHistory}
measuredContainerWidth={measuredEditorWidth}
hideQueryHistory={hideHistoryComponent}
refetchHistoryItems={refetchHistoryItems}
isHelpMenuOpen={isLanguagePopoverOpen}
setIsHelpMenuOpen={setIsLanguagePopoverOpen}
/>
<ResizableButton
onMouseDownResizeHandler={onMouseDownResizeHandler}
onKeyDownResizeHandler={onKeyDownResizeHandler}
editorIsInline={editorIsInline}
/>
{createPortal(
Object.keys(popoverPosition).length !== 0 && popoverPosition.constructor === Object && (
<div
@ -1193,7 +764,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
},
]);
setPopoverPosition({});
isDatePickerOpen = false;
datePickerOpenStatusRef.current = false;
}
}}
inline

View file

@ -0,0 +1,70 @@
/*
* 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 type { CoreStart } from '@kbn/core/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { IndexManagementPluginSetup } from '@kbn/index-management';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
export interface TextBasedLanguagesEditorProps {
/** The aggregate type query */
query: AggregateQuery;
/** Callback running everytime the query changes */
onTextLangQueryChange: (query: AggregateQuery) => void;
/** Callback running when the user submits the query */
onTextLangQuerySubmit: (
query?: AggregateQuery,
abortController?: AbortController
) => Promise<void>;
/** If it is true, the editor displays the message @timestamp found
* The text based queries are relying on adhoc dataviews which
* can have an @timestamp timefield or nothing
*/
detectedTimestamp?: string;
/** Array of errors */
errors?: Error[];
/** Warning string as it comes from ES */
warning?: string;
/** Disables the editor and displays loading icon in run button
* It is also used for hiding the history component if it is not defined
*/
isLoading?: boolean;
/** Disables the editor */
isDisabled?: boolean;
dataTestSubj?: string;
/** Hide the Run query information which appears on the footer*/
hideRunQueryText?: boolean;
/** This is used for applications (such as the inline editing flyout in dashboards)
* which want to add the editor without being part of the Unified search component
* It renders a submit query button inside the editor
*/
editorIsInline?: boolean;
/** Disables the submit query action*/
disableSubmitAction?: boolean;
/** when set to true enables query cancellation **/
allowQueryCancellation?: boolean;
/** hide @timestamp info **/
hideTimeFilterInfo?: boolean;
/** hide query history **/
hideQueryHistory?: boolean;
/** adds border in the editor **/
hasOutline?: boolean;
}
export interface TextBasedEditorDeps {
core: CoreStart;
dataViews: DataViewsPublicPluginStart;
expressions: ExpressionsStart;
indexManagementApiService?: IndexManagementPluginSetup['apiService'];
fieldsMetadata?: FieldsMetadataPublicStart;
}

View file

@ -28,6 +28,7 @@
"@kbn/shared-ux-markdown",
"@kbn/fields-metadata-plugin",
"@kbn/esql-validation-autocomplete",
"@kbn/esql-utils",
],
"exclude": [
"target/**/*",

View file

@ -21,3 +21,6 @@ export enum VIEW_MODE {
export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => {
return parseInt(uiSettings.get(SAMPLE_ROWS_PER_PAGE_SETTING), 10) || DEFAULT_ROWS_PER_PAGE;
};
// local storage key for the ES|QL to Dataviews transition modal
export const ESQL_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal';

View file

@ -11,11 +11,8 @@ import { type DataView, DataViewType } from '@kbn/data-views-plugin/public';
import { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import { TextBasedLanguages } from '@kbn/esql-utils';
import {
useSavedSearch,
useSavedSearchHasChanged,
useSavedSearchInitial,
} from '../../state_management/discover_state_provider';
import { useSavedSearchInitial } from '../../state_management/discover_state_provider';
import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import type { DiscoverStateContainer } from '../../state_management/discover_state';
@ -25,6 +22,7 @@ import { addLog } from '../../../../utils/add_log';
import { useAppStateSelector } from '../../state_management/discover_app_state_container';
import { useDiscoverTopNav } from './use_discover_topnav';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { ESQLToDataViewTransitionModal } from './esql_dataview_transition';
export interface DiscoverTopNavProps {
savedQuery?: string;
@ -59,6 +57,9 @@ export const DiscoverTopNav = ({
const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews);
const dataView = useInternalStateSelector((state) => state.dataView!);
const savedDataViews = useInternalStateSelector((state) => state.savedDataViews);
const isESQLToDataViewTransitionModalVisible = useInternalStateSelector(
(state) => state.isESQLToDataViewTransitionModalVisible
);
const savedSearch = useSavedSearchInitial();
const isEsqlMode = useIsEsqlMode();
const showDatePicker = useMemo(() => {
@ -150,17 +151,34 @@ export const DiscoverTopNav = ({
}
};
const onEsqlSavedAndExit = useCallback(
({ onSave, onCancel }) => {
onSaveSearch({
savedSearch: stateContainer.savedSearchState.getState(),
services,
state: stateContainer,
onClose: onCancel,
onSaveCb: onSave,
});
const onESQLToDataViewTransitionModalClose = useCallback(
(shouldDismissModal?: boolean, needsSave?: boolean) => {
if (shouldDismissModal) {
services.storage.set(ESQL_TRANSITION_MODAL_KEY, true);
}
stateContainer.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(false);
// the user dismissed the modal, we don't need to save the search or switch to the data view mode
if (needsSave == null) {
return;
}
if (needsSave) {
onSaveSearch({
savedSearch: stateContainer.savedSearchState.getState(),
services,
state: stateContainer,
onClose: () =>
stateContainer.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(
false
),
onSaveCb: () => {
stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? '');
},
});
} else {
stateContainer.actions.transitionFromESQLToDataView(dataView.id ?? '');
}
},
[services, stateContainer]
[dataView.id, services, stateContainer]
);
const { topNavBadges, topNavMenu } = useDiscoverTopNav({ stateContainer });
@ -181,8 +199,6 @@ export const DiscoverTopNav = ({
topNavMenu,
]);
const savedSearchId = useSavedSearch().id;
const savedSearchHasChanged = useSavedSearchHasChanged();
const dataViewPickerProps: DataViewPickerProps = useMemo(() => {
const isESQLModeEnabled = uiSettings.get(ENABLE_ESQL);
const supportedTextBasedLanguages: DataViewPickerProps['textBasedLanguages'] = isESQLModeEnabled
@ -201,7 +217,6 @@ export const DiscoverTopNav = ({
onCreateDefaultAdHocDataView: stateContainer.actions.createAndAppendAdHocDataView,
onChangeDataView: stateContainer.actions.onChangeDataView,
textBasedLanguages: supportedTextBasedLanguages,
shouldShowTextBasedLanguageTransitionModal: !savedSearchId || savedSearchHasChanged,
adHocDataViews,
savedDataViews,
onEditDataView,
@ -213,8 +228,6 @@ export const DiscoverTopNav = ({
dataView,
onEditDataView,
savedDataViews,
savedSearchHasChanged,
savedSearchId,
stateContainer,
uiSettings,
]);
@ -230,40 +243,44 @@ export const DiscoverTopNav = ({
!!searchBarCustomization?.CustomDataViewPicker || !!searchBarCustomization?.hideDataViewPicker;
return (
<SearchBar
{...topNavProps}
appName="discover"
indexPatterns={[dataView]}
onQuerySubmit={stateContainer.actions.onUpdateQuery}
onCancel={onCancelClick}
isLoading={isLoading}
onSavedQueryIdChange={updateSavedQueryId}
query={query}
savedQueryId={savedQuery}
screenTitle={savedSearch.title}
showDatePicker={showDatePicker}
saveQueryMenuVisibility={
services.capabilities.discover.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
showSearchBar={true}
useDefaultBehaviors={true}
dataViewPickerOverride={
searchBarCustomization?.CustomDataViewPicker ? (
<searchBarCustomization.CustomDataViewPicker />
) : undefined
}
dataViewPickerComponentProps={
shouldHideDefaultDataviewPicker ? undefined : dataViewPickerProps
}
displayStyle="detached"
textBasedLanguageModeErrors={esqlModeErrors ? [esqlModeErrors] : undefined}
textBasedLanguageModeWarning={esqlModeWarning}
onTextBasedSavedAndExit={onEsqlSavedAndExit}
prependFilterBar={
searchBarCustomization?.PrependFilterBar ? (
<searchBarCustomization.PrependFilterBar />
) : undefined
}
/>
<>
<SearchBar
{...topNavProps}
appName="discover"
indexPatterns={[dataView]}
onQuerySubmit={stateContainer.actions.onUpdateQuery}
onCancel={onCancelClick}
isLoading={isLoading}
onSavedQueryIdChange={updateSavedQueryId}
query={query}
savedQueryId={savedQuery}
screenTitle={savedSearch.title}
showDatePicker={showDatePicker}
saveQueryMenuVisibility={
services.capabilities.discover.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
}
showSearchBar={true}
useDefaultBehaviors={true}
dataViewPickerOverride={
searchBarCustomization?.CustomDataViewPicker ? (
<searchBarCustomization.CustomDataViewPicker />
) : undefined
}
dataViewPickerComponentProps={
shouldHideDefaultDataviewPicker ? undefined : dataViewPickerProps
}
displayStyle="detached"
textBasedLanguageModeErrors={esqlModeErrors ? [esqlModeErrors] : undefined}
textBasedLanguageModeWarning={esqlModeWarning}
prependFilterBar={
searchBarCustomization?.PrependFilterBar ? (
<searchBarCustomization.PrependFilterBar />
) : undefined
}
/>
{isESQLToDataViewTransitionModalVisible && (
<ESQLToDataViewTransitionModal onClose={onESQLToDataViewTransitionModalClose} />
)}
</>
);
};

View file

@ -0,0 +1,118 @@
/*
* 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, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FEEDBACK_LINK } from '@kbn/esql-utils';
import {
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiButton,
EuiButtonEmpty,
EuiText,
EuiCheckbox,
EuiFlexItem,
EuiFlexGroup,
EuiLink,
EuiHorizontalRule,
} from '@elastic/eui';
export interface ESQLToDataViewTransitionModalProps {
onClose: (dismissFlag?: boolean, needsSave?: boolean) => void;
}
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
export default function ESQLToDataViewTransitionModal({
onClose,
}: ESQLToDataViewTransitionModalProps) {
const [dismissModalChecked, setDismissModalChecked] = useState(false);
const onTransitionModalDismiss = useCallback((e) => {
setDismissModalChecked(e.target.checked);
}, []);
return (
<EuiModal
onClose={() => onClose()}
style={{ width: 700 }}
data-test-subj="discover-esql-to-dataview-modal"
>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate('discover.esqlToDataViewTransitionModal.title', {
defaultMessage: 'Unsaved changes',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText size="m">
{i18n.translate('discover.esqlToDataviewTransitionModalBody', {
defaultMessage:
'Switching data views removes the current ES|QL query. Save this search to avoid losing work.',
})}
</EuiText>
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiLink external href={FEEDBACK_LINK} target="_blank">
{i18n.translate('discover.esqlToDataViewTransitionModal.feedbackLink', {
defaultMessage: 'Submit ES|QL feedback',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />
</EuiModalBody>
<EuiModalFooter css={{ paddingBlockStart: 0 }}>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiCheckbox
id="dismiss-text-based-languages-transition-modal"
label={i18n.translate('discover.esqlToDataViewTransitionModal.dismissButtonLabel', {
defaultMessage: "Don't ask me again",
})}
checked={dismissModalChecked}
onChange={onTransitionModalDismiss}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={() => onClose(dismissModalChecked, false)}
color="danger"
iconType="trash"
data-test-subj="discover-esql-to-dataview-no-save-btn"
>
{i18n.translate('discover.esqlToDataViewTransitionModal.closeButtonLabel', {
defaultMessage: 'Discard and switch',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => onClose(dismissModalChecked, true)}
fill
color="primary"
iconType="save"
data-test-subj="discover-esql-to-dataview-save-btn"
>
{i18n.translate('discover.esqlToDataViewTransitionModal.saveButtonLabel', {
defaultMessage: 'Save and switch',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);
}

View file

@ -0,0 +1,21 @@
/*
* 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 type { ESQLToDataViewTransitionModalProps } from './esql_dataview_transition_modal';
const Fallback = () => <div />;
const LazyESQLToDataViewTransitionModal = React.lazy(
() => import('./esql_dataview_transition_modal')
);
export const ESQLToDataViewTransitionModal = (props: ESQLToDataViewTransitionModalProps) => (
<React.Suspense fallback={<Fallback />}>
<LazyESQLToDataViewTransitionModal {...props} />
</React.Suspense>
);

View file

@ -17,6 +17,9 @@ const services = {
save: true,
},
},
uiSettings: {
get: jest.fn(() => true),
},
} as unknown as DiscoverServices;
const state = {} as unknown as DiscoverStateContainer;
@ -30,9 +33,20 @@ test('getTopNavLinks result', () => {
isEsqlMode: false,
adHocDataViews: [],
topNavCustomization: undefined,
shouldShowESQLToDataViewTransitionModal: false,
});
expect(topNavLinks).toMatchInlineSnapshot(`
Array [
Object {
"color": "text",
"emphasize": true,
"fill": false,
"iconType": "editorRedo",
"id": "esql",
"label": "Try ES|QL",
"run": [Function],
"testId": "select-text-based-language-btn",
},
Object {
"description": "New Search",
"id": "new",
@ -83,9 +97,20 @@ test('getTopNavLinks result for ES|QL mode', () => {
isEsqlMode: true,
adHocDataViews: [],
topNavCustomization: undefined,
shouldShowESQLToDataViewTransitionModal: false,
});
expect(topNavLinks).toMatchInlineSnapshot(`
Array [
Object {
"color": "text",
"emphasize": true,
"fill": false,
"iconType": "editorRedo",
"id": "esql",
"label": "Switch to classic",
"run": [Function],
"testId": "switch-to-dataviews",
},
Object {
"description": "New Search",
"id": "new",

View file

@ -11,7 +11,10 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import type { TopNavMenuData } from '@kbn/navigation-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { omit } from 'lodash';
import { METRIC_TYPE } from '@kbn/analytics';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import type { DiscoverAppLocatorParams } from '../../../../../common';
import { ESQL_TRANSITION_MODAL_KEY } from '../../../../../common/constants';
import { showOpenSearchPanel } from './show_open_search_panel';
import { getSharingData, showPublicUrlSwitch } from '../../../../utils/get_sharing_data';
import { DiscoverServices } from '../../../../build_services';
@ -31,6 +34,7 @@ export const getTopNavLinks = ({
isEsqlMode,
adHocDataViews,
topNavCustomization,
shouldShowESQLToDataViewTransitionModal,
}: {
dataView: DataView | undefined;
services: DiscoverServices;
@ -39,6 +43,7 @@ export const getTopNavLinks = ({
isEsqlMode: boolean;
adHocDataViews: DataView[];
topNavCustomization: TopNavCustomization | undefined;
shouldShowESQLToDataViewTransitionModal: boolean;
}): TopNavMenuData[] => {
const alerts = {
id: 'alerts',
@ -60,6 +65,48 @@ export const getTopNavLinks = ({
testId: 'discoverAlertsButton',
};
/**
* Switches from ES|QL to classic mode and vice versa
*/
const esqLDataViewTransitionToggle = {
id: 'esql',
label: isEsqlMode
? i18n.translate('discover.localMenu.switchToClassicTitle', {
defaultMessage: 'Switch to classic',
})
: i18n.translate('discover.localMenu.tryESQLTitle', {
defaultMessage: 'Try ES|QL',
}),
emphasize: true,
iconType: 'editorRedo',
fill: false,
color: 'text',
run: () => {
if (dataView) {
if (isEsqlMode) {
services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:back_to_classic_clicked`);
/**
* Display the transition modal if:
* - the user has not dismissed the modal
* - the user has opened and applied changes to the saved search
*/
if (
shouldShowESQLToDataViewTransitionModal &&
!services.storage.get(ESQL_TRANSITION_MODAL_KEY)
) {
state.internalState.transitions.setIsESQLToDataViewTransitionModalVisible(true);
} else {
state.actions.transitionFromESQLToDataView(dataView.id ?? '');
}
} else {
state.actions.transitionFromDataViewToESQL(dataView);
services.trackUiMetric?.(METRIC_TYPE.CLICK, `esql:try_btn_clicked`);
}
}
},
testId: isEsqlMode ? 'switch-to-dataviews' : 'select-text-based-language-btn',
};
const newSearch = {
id: 'new',
label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', {
@ -226,6 +273,10 @@ export const getTopNavLinks = ({
const defaultMenu = topNavCustomization?.defaultMenu;
const entries = [...(topNavCustomization?.getMenuItems?.() ?? [])];
if (services.uiSettings.get(ENABLE_ESQL)) {
entries.push({ data: esqLDataViewTransitionToggle, order: 0 });
}
if (!defaultMenu?.newItem?.disabled) {
entries.push({ data: newSearch, order: defaultMenu?.newItem?.order ?? 100 });
}

View file

@ -13,6 +13,10 @@ import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { useInspector } from '../../hooks/use_inspector';
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
import {
useSavedSearch,
useSavedSearchHasChanged,
} from '../../state_management/discover_state_provider';
import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { getTopNavBadges } from './get_top_nav_badges';
import { getTopNavLinks } from './get_top_nav_links';
@ -39,7 +43,9 @@ export const useDiscoverTopNav = ({
}),
[stateContainer, services, hasUnsavedChanges, topNavCustomization]
);
const savedSearchId = useSavedSearch().id;
const savedSearchHasChanged = useSavedSearchHasChanged();
const shouldShowESQLToDataViewTransitionModal = !savedSearchId || savedSearchHasChanged;
const dataView = useInternalStateSelector((state) => state.dataView);
const adHocDataViews = useInternalStateSelector((state) => state.adHocDataViews);
const isEsqlMode = useIsEsqlMode();
@ -58,6 +64,7 @@ export const useDiscoverTopNav = ({
isEsqlMode,
adHocDataViews,
topNavCustomization,
shouldShowESQLToDataViewTransitionModal,
}),
[
adHocDataViews,
@ -67,6 +74,7 @@ export const useDiscoverTopNav = ({
services,
stateContainer,
topNavCustomization,
shouldShowESQLToDataViewTransitionModal,
]
);

View file

@ -24,6 +24,7 @@ export interface InternalState {
expandedDoc: DataTableRecord | undefined;
customFilters: Filter[];
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saved search saving
isESQLToDataViewTransitionModalVisible?: boolean;
resetDefaultProfileState: { columns: boolean; rowHeight: boolean };
}
@ -49,6 +50,9 @@ export interface InternalStateTransitions {
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined
) => InternalState;
resetOnSavedSearchChange: (state: InternalState) => () => InternalState;
setIsESQLToDataViewTransitionModalVisible: (
state: InternalState
) => (isVisible: boolean) => InternalState;
setResetDefaultProfileState: (
state: InternalState
) => (resetDefaultProfileState: InternalState['resetDefaultProfileState']) => InternalState;
@ -85,6 +89,11 @@ export function getInternalStateContainer() {
...prevState,
isDataViewLoading: loading,
}),
setIsESQLToDataViewTransitionModalVisible:
(prevState: InternalState) => (isVisible: boolean) => ({
...prevState,
isESQLToDataViewTransitionModalVisible: isVisible,
}),
setSavedDataViews: (prevState: InternalState) => (nextDataViewList: DataViewListItem[]) => ({
...prevState,
savedDataViews: nextDataViewList,

View file

@ -753,6 +753,28 @@ describe('Test discover state actions', () => {
expect(persistedDataViewId).toBe(nextSavedSearch?.searchSource.getField('index')!.id);
});
test('transitionFromDataViewToESQL', async () => {
const savedSearchWithQuery = copySavedSearch(savedSearchMock);
const query = { query: "foo: 'bar'", language: 'kuery' };
savedSearchWithQuery.searchSource.setField('query', query);
const { state } = await getState('/', { savedSearch: savedSearchWithQuery });
await state.actions.transitionFromDataViewToESQL(dataViewMock);
expect(state.appState.getState().query).toStrictEqual({
esql: 'FROM the-data-view-title | LIMIT 10',
});
});
test('transitionFromESQLToDataView', async () => {
const savedSearchWithQuery = copySavedSearch(savedSearchMock);
const query = {
esql: 'FROM the-data-view-title | LIMIT 10',
};
savedSearchWithQuery.searchSource.setField('query', query);
const { state } = await getState('/', { savedSearch: savedSearchWithQuery });
await state.actions.transitionFromESQLToDataView('the-data-view-id');
expect(state.appState.getState().query).toStrictEqual({ query: '', language: 'kuery' });
});
test('onChangeDataView', async () => {
const { state, getCurrentUrl } = await getState('/', { savedSearch: savedSearchMock });
const { actions, savedSearchState, dataState } = state;

View file

@ -23,6 +23,7 @@ import { DataView, DataViewSpec, DataViewType } from '@kbn/data-views-plugin/pub
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { v4 as uuidv4 } from 'uuid';
import { merge } from 'rxjs';
import { getInitialESQLQuery } from '@kbn/esql-utils';
import { AggregateQuery, Query, TimeRange } from '@kbn/es-query';
import { loadSavedSearch as loadSavedSearchFn } from './utils/load_saved_search';
import { restoreStateFromSavedSearch } from '../../../services/saved_searches/restore_from_saved_search';
@ -174,6 +175,16 @@ export interface DiscoverStateContainer {
* @param dataView
*/
onDataViewEdited: (dataView: DataView) => Promise<void>;
/**
* Triggered when transitioning from ESQL to Dataview
* Clean ups the ES|QL query and moves to the dataview mode
*/
transitionFromESQLToDataView: (dataViewId: string) => void;
/**
* Triggered when transitioning from ESQL to Dataview
* Clean ups the ES|QL query and moves to the dataview mode
*/
transitionFromDataViewToESQL: (dataView: DataView) => void;
/**
* Triggered when a saved search is opened in the savedObject finder
* @param savedSearchId
@ -354,6 +365,31 @@ export function getDiscoverStateContainer({
}
};
const transitionFromESQLToDataView = (dataViewId: string) => {
appStateContainer.update({
query: {
language: 'kuery',
query: '',
},
columns: [],
dataSource: {
type: DataSourceType.DataView,
dataViewId,
},
});
};
const transitionFromDataViewToESQL = (dataView: DataView) => {
const queryString = getInitialESQLQuery(dataView);
appStateContainer.update({
query: { esql: queryString },
dataSource: {
type: DataSourceType.Esql,
},
columns: [],
});
};
const onDataViewCreated = async (nextDataView: DataView) => {
if (!nextDataView.isPersisted()) {
internalStateContainer.transitions.appendAdHocDataViews(nextDataView);
@ -549,6 +585,8 @@ export function getDiscoverStateContainer({
onDataViewCreated,
onDataViewEdited,
onOpenSavedSearch,
transitionFromESQLToDataView,
transitionFromDataViewToESQL,
onUpdateQuery,
setDataView,
undoSavedSearchChanges,

View file

@ -4,7 +4,6 @@
The editor accepts the following properties:
- query: This is the **AggregateQuery** query. i.e. (`{esql: from index1 | limit 10}`)
- onTextLangQueryChange: callback that is called every time the query is updated
- expandCodeEditor: flag that opens the editor on the expanded mode
- errors: array of `Error`.
- warning: A string for visualizing warnings
- onTextLangQuerySubmit: callback that is called when the user submits the query
@ -17,8 +16,6 @@ import { TextBasedLangEditor } from '@kbn/esql/public';
<TextBasedLangEditor
query={query}
onTextLangQueryChange={onTextLangQueryChange}
expandCodeEditor={(status: boolean) => setCodeEditorIsExpanded(status)}
isCodeEditorExpanded={codeEditorIsExpandedFlag}
errors={props.textBasedLanguageModeErrors}
isDisabled={false}
onTextLangQuerySubmit={onTextLangQuerySubmit}

View file

@ -30,7 +30,7 @@ export const TextBasedLangEditor = (props: TextBasedLanguagesEditorProps) => {
...deps,
}}
>
<TextBasedLanguagesEditor {...props} isDarkMode={deps.darkMode} />
<TextBasedLanguagesEditor {...props} />
</KibanaContextProvider>
);
};

View file

@ -17,7 +17,6 @@ export let core: CoreStart;
interface ServiceDeps {
core: CoreStart;
darkMode: boolean;
dataViews: DataViewsPublicPluginStart;
expressions: ExpressionsStart;
indexManagementApiService?: IndexManagementPluginSetup['apiService'];
@ -45,14 +44,11 @@ export const setKibanaServices = (
fieldsMetadata?: FieldsMetadataPublicStart
) => {
core = kibanaCore;
core.theme.theme$.subscribe(({ darkMode }) => {
servicesReady$.next({
core,
darkMode,
dataViews,
expressions,
indexManagementApiService: indexManagement?.apiService,
fieldsMetadata,
});
servicesReady$.next({
core,
dataViews,
expressions,
indexManagementApiService: indexManagement?.apiService,
fieldsMetadata,
});
};

View file

@ -2,6 +2,7 @@
exports[`TopNavMenu Should render emphasized item which should be clickable 1`] = `
<EuiButton
color="primary"
fill={true}
iconSide="right"
iconType="beaker"

View file

@ -22,6 +22,8 @@ export interface TopNavMenuData {
tooltip?: string | (() => string | undefined);
badge?: EuiBetaBadgeProps;
emphasize?: boolean;
fill?: boolean;
color?: string;
isLoading?: boolean;
iconType?: string;
iconSide?: EuiButtonProps['iconSide'];

View file

@ -8,7 +8,7 @@
import { upperFirst, isFunction } from 'lodash';
import React, { MouseEvent } from 'react';
import { EuiToolTip, EuiButton, EuiHeaderLink, EuiBetaBadge } from '@elastic/eui';
import { EuiToolTip, EuiButton, EuiHeaderLink, EuiBetaBadge, EuiButtonColor } from '@elastic/eui';
import { TopNavMenuData } from './top_nav_menu_data';
export function TopNavMenuItem(props: TopNavMenuData) {
@ -48,6 +48,8 @@ export function TopNavMenuItem(props: TopNavMenuData) {
iconSide: props.iconSide,
'data-test-subj': props.testId,
className: props.className,
color: (props.color ?? 'primary') as EuiButtonColor,
fill: props.fill ?? true,
};
// If the item specified a href, then override the suppress the onClick
@ -58,11 +60,11 @@ export function TopNavMenuItem(props: TopNavMenuData) {
: {};
const btn = props.emphasize ? (
<EuiButton size="s" {...commonButtonProps} fill>
<EuiButton size="s" {...commonButtonProps}>
{getButtonContainer()}
</EuiButton>
) : (
<EuiHeaderLink size="s" color="primary" {...commonButtonProps} {...overrideProps}>
<EuiHeaderLink size="s" {...commonButtonProps} {...overrideProps}>
{getButtonContainer()}
</EuiHeaderLink>
);

View file

@ -566,7 +566,7 @@ storiesOf('SearchBar', module)
],
} as unknown as SearchBarProps)
)
.add('with dataviewPicker with ESQL', () =>
.add('with dataviewPicker with ES|QL', () =>
wrapSearchBarInContext({
dataViewPickerComponentProps: {
currentDataViewId: '1234',
@ -582,13 +582,13 @@ storiesOf('SearchBar', module)
},
} as SearchBarProps)
)
.add('with dataviewPicker with ESQL and ESQL query', () =>
.add('with dataviewPicker with ES|QL and ES|QL query', () =>
wrapSearchBarInContext({
dataViewPickerComponentProps: {
currentDataViewId: '1234',
trigger: {
'data-test-subj': 'dataView-switch-link',
label: 'ESQL',
label: 'ES|QL',
title: 'ESQL',
},
onChangeDataView: action('onChangeDataView'),
@ -599,13 +599,13 @@ storiesOf('SearchBar', module)
query: { esql: 'from dataview | project field1, field2' },
} as unknown as SearchBarProps<Query>)
)
.add('with dataviewPicker with ESQL and large ESQL query', () =>
.add('with dataviewPicker with ES|QL and large ES|QL query', () =>
wrapSearchBarInContext({
dataViewPickerComponentProps: {
currentDataViewId: '1234',
trigger: {
'data-test-subj': 'dataView-switch-link',
label: 'ESQL',
label: 'ES|QL',
title: 'ESQL',
},
onChangeDataView: action('onChangeDataView'),
@ -618,13 +618,13 @@ storiesOf('SearchBar', module)
},
} as unknown as SearchBarProps<Query>)
)
.add('with dataviewPicker with ESQL and errors in ESQL query', () =>
.add('with dataviewPicker with ES|QL and errors in ES|QL query', () =>
wrapSearchBarInContext({
dataViewPickerComponentProps: {
currentDataViewId: '1234',
trigger: {
'data-test-subj': 'dataView-switch-link',
label: 'ESQL',
label: 'ES|QL',
title: 'ESQL',
},
onChangeDataView: action('onChangeDataView'),

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { EuiThemeComputed } from '@elastic/eui';
import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count';
import { DataViewListItemEnhanced } from './dataview_list';
@ -14,13 +14,18 @@ const MIN_WIDTH = 300;
export const changeDataViewStyles = ({
fullWidth,
dataViewsList,
theme,
}: {
fullWidth?: boolean;
dataViewsList: DataViewListItemEnhanced[];
theme: EuiThemeComputed;
}) => {
return {
trigger: {
maxWidth: fullWidth ? undefined : MIN_WIDTH,
border: theme.border.thin,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
popoverContent: {
width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { minWidth: MIN_WIDTH }),

View file

@ -14,13 +14,10 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { TextBasedLanguages } from '@kbn/esql-utils';
import { ChangeDataView } from './change_dataview';
import { DataViewSelector } from './data_view_selector';
import { dataViewMock, dataViewMockEsql } from './mocks/dataview';
import { DataViewPickerPropsExtended } from './data_view_picker';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('DataView component', () => {
const createMockWebStorage = () => ({
@ -89,7 +86,6 @@ describe('DataView component', () => {
'data-test-subj': 'dataview-trigger',
},
onChangeDataView: jest.fn(),
onTextLangQuerySubmit: jest.fn(),
};
});
@ -135,39 +131,6 @@ describe('DataView component', () => {
expect(addDataViewSpy).toHaveBeenCalled();
});
it('should render the text based languages panels if languages are given', async () => {
const component = mount(
wrapDataViewComponentInContext(
{
...props,
textBasedLanguages: [TextBasedLanguages.ESQL],
textBasedLanguage: TextBasedLanguages.ESQL,
},
false
)
);
findTestSubject(component, 'dataview-trigger').simulate('click');
const text = component.find('[data-test-subj="select-text-based-language-panel"]');
expect(text.length).not.toBe(0);
});
it('should cleanup the query is on text based mode and add new dataview', async () => {
const component = mount(
wrapDataViewComponentInContext(
{
...props,
onDataViewCreated: jest.fn(),
textBasedLanguages: [TextBasedLanguages.ESQL],
textBasedLanguage: TextBasedLanguages.ESQL,
},
false
)
);
findTestSubject(component, 'dataview-trigger').simulate('click');
component.find('[data-test-subj="dataview-create-new"]').first().simulate('click');
expect(props.onTextLangQuerySubmit).toHaveBeenCalled();
});
it('should properly handle ad hoc data views', async () => {
const component = mount(
wrapDataViewComponentInContext(
@ -233,44 +196,4 @@ describe('DataView component', () => {
},
]);
});
describe('test based language switch warning icon', () => {
beforeAll(() => {
// Enzyme doesn't clean the DOM between tests, so we need to do it manually
document.body.innerHTML = '';
});
it('should show text based language switch warning icon', () => {
render(
wrapDataViewComponentInContext(
{
...props,
onDataViewCreated: jest.fn(),
textBasedLanguages: [TextBasedLanguages.ESQL],
textBasedLanguage: TextBasedLanguages.ESQL,
},
false
)
);
userEvent.click(screen.getByTestId('dataview-trigger'));
expect(screen.queryByTestId('textBasedLang-warning')).toBeInTheDocument();
});
it('should not show text based language switch warning icon when shouldShowTextBasedLanguageTransitionModal is false', () => {
render(
wrapDataViewComponentInContext(
{
...props,
onDataViewCreated: jest.fn(),
textBasedLanguages: [TextBasedLanguages.ESQL],
textBasedLanguage: TextBasedLanguages.ESQL,
shouldShowTextBasedLanguageTransitionModal: false,
},
false
)
);
userEvent.click(screen.getByTestId('dataview-trigger'));
expect(screen.queryByTestId('textBasedLang-warning')).not.toBeInTheDocument();
});
});
});

View file

@ -7,13 +7,11 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { css } from '@emotion/react';
import {
EuiPopover,
EuiPanel,
EuiHorizontalRule,
EuiButton,
EuiContextMenuPanel,
EuiContextMenuItem,
useEuiTheme,
@ -24,37 +22,17 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiToolTip,
} from '@elastic/eui';
import { METRIC_TYPE } from '@kbn/analytics';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { AggregateQuery, getLanguageDisplayName } from '@kbn/es-query';
import { getInitialESQLQuery } from '@kbn/esql-utils';
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 { DataViewListItemEnhanced } from './dataview_list';
import type { TextBasedLanguagesTransitionModalProps } from './text_languages_transition_modal';
import adhoc from './assets/adhoc.svg';
import { changeDataViewStyles } from './change_dataview.styles';
import { DataViewSelector } from './data_view_selector';
// local storage key for the text based languages transition modal
const TEXT_LANG_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal';
const Fallback = () => <div />;
const LazyTextBasedLanguagesTransitionModal = React.lazy(
() => import('./text_languages_transition_modal')
);
export const TextBasedLanguagesTransitionModal = (
props: TextBasedLanguagesTransitionModalProps
) => (
<React.Suspense fallback={<Fallback />}>
<LazyTextBasedLanguagesTransitionModal {...props} />
</React.Suspense>
);
const mapAdHocDataView = (adHocDataView: DataView): DataViewListItemEnhanced => {
return {
title: adHocDataView.title,
@ -75,11 +53,7 @@ export function ChangeDataView({
onDataViewCreated,
trigger,
selectableProps,
textBasedLanguages,
onSaveTextLanguageQuery,
onTextLangQuerySubmit,
textBasedLanguage,
shouldShowTextBasedLanguageTransitionModal = true,
isDisabled,
onEditDataView,
onCreateDefaultAdHocDataView,
@ -91,19 +65,15 @@ export function ChangeDataView({
const [isTextBasedLangSelected, setIsTextBasedLangSelected] = useState(
Boolean(textBasedLanguage)
);
const [isTextLangTransitionModalVisible, setIsTextLangTransitionModalVisible] = useState(false);
const [selectedDataView, setSelectedDataView] = useState<DataView | undefined>(undefined);
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { application, data, storage, dataViews, dataViewEditor, appName, usageCollection } =
kibana.services;
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
const { application, data, dataViews, dataViewEditor } = kibana.services;
const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth, dataViewsList });
const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() =>
Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY))
);
const styles = changeDataViewStyles({
fullWidth: trigger.fullWidth,
dataViewsList,
theme: euiTheme,
});
// Create a reusable id to ensure search input is the first focused item in the popover even though it's not the first item
const searchListInputId = useGeneratedHtmlId({ prefix: 'dataviewPickerListSearchInput' });
@ -112,18 +82,14 @@ export function ChangeDataView({
const fetchDataViews = async () => {
const savedDataViewRefs: DataViewListItemEnhanced[] = savedDataViews
? savedDataViews
: await data.dataViews.getIdsWithTitle();
: (await data.dataViews.getIdsWithTitle()) ?? [];
const adHocDataViewRefs: DataViewListItemEnhanced[] =
adHocDataViews?.map(mapAdHocDataView) ?? [];
setDataViewsList(savedDataViewRefs.concat(adHocDataViewRefs));
if (currentDataViewId) {
const currentDataview = await data.dataViews.get(currentDataViewId, false);
setSelectedDataView(currentDataview);
}
};
fetchDataViews();
}, [data, currentDataViewId, adHocDataViews, savedDataViews, isTextBasedLangSelected]);
}, [data, currentDataViewId, adHocDataViews, savedDataViews]);
useEffect(() => {
if (textBasedLanguage) {
@ -146,17 +112,16 @@ export function ChangeDataView({
const createTrigger = function () {
const { label, title, 'data-test-subj': dataTestSubj, fullWidth, ...rest } = trigger;
return (
<EuiButton
<EuiButtonEmpty
css={styles.trigger}
data-test-subj={dataTestSubj}
onClick={() => {
setPopoverIsOpen(!isPopoverOpen);
}}
color={isMissingCurrent ? 'danger' : 'primary'}
color={isMissingCurrent ? 'danger' : 'text'}
iconSide="right"
iconType="arrowDown"
title={triggerLabel}
fullWidth={fullWidth}
disabled={isDisabled}
textProps={{ className: 'eui-textTruncate' }}
{...rest}
@ -174,13 +139,13 @@ export function ChangeDataView({
)}
{triggerLabel}
</>
</EuiButton>
</EuiButtonEmpty>
);
};
const getPanelItems = () => {
const panelItems: EuiContextMenuPanelProps['items'] = [];
if (onAddField && !isTextBasedLangSelected) {
if (onAddField) {
panelItems.push(
<EuiContextMenuItem
key="add"
@ -242,29 +207,6 @@ export function ChangeDataView({
>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
{isTextBasedLangSelected && shouldShowTextBasedLanguageTransitionModal ? (
<EuiToolTip
position="top"
content={i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning',
{
defaultMessage:
"Switching data views removes the current {textBasedLanguage} query. Save this search to ensure you don't lose work.",
values: {
textBasedLanguage: getLanguageDisplayName(textBasedLanguage),
},
}
)}
>
<EuiIcon
type="warning"
color="warning"
data-test-subj="textBasedLang-warning"
/>
</EuiToolTip>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<h5>
@ -281,16 +223,6 @@ export function ChangeDataView({
onClick={() => {
setPopoverIsOpen(false);
onDataViewCreated();
// go to dataview mode
if (isTextBasedLangSelected) {
setIsTextBasedLangSelected(false);
// clean up the Text based language query
onTextLangQuerySubmit?.({
language: 'kuery',
query: '',
});
setTriggerLabel(trigger.label);
}
}}
size="xs"
iconType="plusInCircleFilled"
@ -309,167 +241,60 @@ export function ChangeDataView({
searchListInputId={searchListInputId}
dataViewsList={dataViewsList}
selectableProps={selectableProps}
isTextBasedLangSelected={isTextBasedLangSelected}
setPopoverIsOpen={setPopoverIsOpen}
onChangeDataView={async (newId) => {
const currentDataview = await data.dataViews.get(newId, false);
setSelectedDataView(currentDataview);
setPopoverIsOpen(false);
if (isTextBasedLangSelected) {
const showTransitionModal =
!isTextLangTransitionModalDismissed && shouldShowTextBasedLanguageTransitionModal;
if (showTransitionModal) {
setIsTextLangTransitionModalVisible(true);
} else {
setIsTextBasedLangSelected(false);
// clean up the Text based language query
onTextLangQuerySubmit?.({
language: 'kuery',
query: '',
});
onChangeDataView(newId);
setTriggerLabel(trigger.label);
}
} else {
onChangeDataView(newId);
}
onChangeDataView(newId);
}}
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
/>
</React.Fragment>
);
if (textBasedLanguages?.length) {
panelItems.push(
<EuiHorizontalRule margin="none" key="textbasedLanguages-divider" />,
<EuiPanel color="transparent" paddingSize="none" key="try-esql">
<EuiButton
css={css`
border-top-right-radius: unset;
border-top-left-radius: unset;
`}
color="success"
size="s"
fullWidth
onClick={() => {
if (selectedDataView) {
onTextBasedSubmit({
esql: getInitialESQLQuery(selectedDataView),
});
}
}}
data-test-subj="select-text-based-language-panel"
contentProps={{
css: {
justifyContent: 'flex-start',
paddingLeft: '26px',
},
}}
>
{i18n.translate('unifiedSearch.query.queryBar.textBasedLanguagesTryLabel', {
defaultMessage: 'Language: ES|QL',
})}
</EuiButton>
</EuiPanel>
);
}
return panelItems;
};
let modal;
const onTransitionModalDismiss = useCallback(() => {
storage.set(TEXT_LANG_TRANSITION_MODAL_KEY, true);
setIsTextLangTransitionModalDismissed(true);
}, [storage]);
const onTextBasedSubmit = useCallback(
(q: AggregateQuery) => {
onTextLangQuerySubmit?.(q);
setPopoverIsOpen(false);
reportUiCounter?.(METRIC_TYPE.CLICK, `esql:unified_search_clicked`);
},
[onTextLangQuerySubmit, reportUiCounter]
);
const cleanup = useCallback(
(shouldDismissModal: boolean) => {
setIsTextLangTransitionModalVisible(false);
setIsTextBasedLangSelected(false);
// clean up the Text based language query
onTextLangQuerySubmit?.({
language: 'kuery',
query: '',
});
if (selectedDataView?.id) {
onChangeDataView(selectedDataView?.id);
}
setTriggerLabel(trigger.label);
if (shouldDismissModal) {
onTransitionModalDismiss();
}
},
[
onChangeDataView,
onTextLangQuerySubmit,
onTransitionModalDismiss,
selectedDataView?.id,
trigger.label,
]
);
const onModalClose = useCallback(
(shouldDismissModal: boolean, needsSave?: boolean) => {
if (Boolean(needsSave)) {
setIsTextLangTransitionModalVisible(false);
onSaveTextLanguageQuery?.({
onSave: () => {
cleanup(shouldDismissModal);
},
onCancel: () => {
setIsTextLangTransitionModalVisible(false);
},
});
} else {
cleanup(shouldDismissModal);
}
},
[cleanup, onSaveTextLanguageQuery]
);
if (isTextLangTransitionModalVisible && !isTextLangTransitionModalDismissed) {
modal = (
<TextBasedLanguagesTransitionModal
closeModal={onModalClose}
setIsTextLangTransitionModalVisible={setIsTextLangTransitionModalVisible}
textBasedLanguage={textBasedLanguage}
/>
);
}
return (
<>
<EuiPopover
panelClassName="changeDataViewPopover"
button={createTrigger()}
panelProps={{
['data-test-subj']: 'changeDataViewPopover',
}}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
panelPaddingSize="none"
initialFocus={!isTextBasedLangSelected ? `#${searchListInputId}` : undefined}
display="block"
buffer={8}
>
<div css={styles.popoverContent}>
<EuiContextMenuPanel size="s" items={getPanelItems()} />
</div>
</EuiPopover>
{modal}
</>
<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>
</>
)}
</EuiFlexGroup>
);
}

View file

@ -9,7 +9,6 @@
import React from 'react';
import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui';
import type { DataView, DataViewListItem, DataViewSpec } from '@kbn/data-views-plugin/public';
import type { AggregateQuery, Query } from '@kbn/es-query';
import { TextBasedLanguages } from '@kbn/esql-utils';
import { ChangeDataView } from './change_dataview';
@ -18,11 +17,6 @@ export type ChangeDataViewTriggerProps = EuiButtonProps & {
title?: string;
};
export interface OnSaveTextLanguageQueryProps {
onSave: () => void;
onCancel: () => void;
}
/** @public */
export interface DataViewPickerProps {
/**
@ -76,16 +70,6 @@ export interface DataViewPickerProps {
* will be available.
*/
textBasedLanguages?: TextBasedLanguages[];
/**
* Callback that is called when the user clicks the Save and switch transition modal button
*/
onSaveTextLanguageQuery?: ({ onSave, onCancel }: OnSaveTextLanguageQueryProps) => void;
/**
* Determines if the text based language transition
* modal should be shown when switching data views
*/
shouldShowTextBasedLanguageTransitionModal?: boolean;
/**
* Makes the picker disabled by disabling the popover trigger
*/
@ -93,10 +77,6 @@ export interface DataViewPickerProps {
}
export interface DataViewPickerPropsExtended extends DataViewPickerProps {
/**
* Callback that is called when the user clicks the submit button
*/
onTextLangQuerySubmit?: (query?: Query | AggregateQuery) => void;
/**
* Text based language that is currently selected; depends on the query
*/
@ -115,10 +95,7 @@ export const DataViewPicker = ({
trigger,
selectableProps,
textBasedLanguages,
onSaveTextLanguageQuery,
onTextLangQuerySubmit,
textBasedLanguage,
shouldShowTextBasedLanguageTransitionModal,
onCreateDefaultAdHocDataView,
isDisabled,
}: DataViewPickerPropsExtended) => {
@ -136,10 +113,7 @@ export const DataViewPicker = ({
savedDataViews={savedDataViews}
selectableProps={selectableProps}
textBasedLanguages={textBasedLanguages}
onSaveTextLanguageQuery={onSaveTextLanguageQuery}
onTextLangQuerySubmit={onTextLangQuerySubmit}
textBasedLanguage={textBasedLanguage}
shouldShowTextBasedLanguageTransitionModal={shouldShowTextBasedLanguageTransitionModal}
isDisabled={isDisabled}
/>
);

View file

@ -19,7 +19,6 @@ export interface DataViewSelectorProps {
searchListInputId?: string;
dataViewsList: DataViewListItem[];
selectableProps?: EuiSelectableProps;
isTextBasedLangSelected: boolean;
setPopoverIsOpen: (isOpen: boolean) => void;
onChangeDataView: (dataViewId: string) => void;
onCreateDefaultAdHocDataView?: (dataViewSpec: DataViewSpec) => void;
@ -30,7 +29,6 @@ export const DataViewSelector = ({
searchListInputId,
dataViewsList,
selectableProps,
isTextBasedLangSelected,
setPopoverIsOpen,
onChangeDataView,
onCreateDefaultAdHocDataView,
@ -83,7 +81,6 @@ export const DataViewSelector = ({
},
}}
searchListInputId={searchListInputId}
isTextBasedLangSelected={isTextBasedLangSelected}
/>
<ExploreMatchingButton
noDataViewMatches={noDataViewMatches}

View file

@ -54,7 +54,6 @@ describe('DataView list component', () => {
currentDataViewId: 'dataview-1',
onChangeDataView: changeDataViewSpy,
dataViewsList: list,
isTextBasedLangSelected: false,
};
});
@ -75,17 +74,8 @@ describe('DataView list component', () => {
]);
});
it('should render a warning icon if a text based language is selected', () => {
const component = shallow(<DataViewsList {...props} isTextBasedLangSelected />);
expect(getDataViewPickerOptions(component)!.map((option: any) => option.append)).not.toBeNull();
});
describe('ad hoc data views', () => {
const runAdHocDataViewTest = (
esqlMode: boolean,
esqlDataViews: DataViewListItemEnhanced[] = []
) => {
const runAdHocDataViewTest = (esqlDataViews: DataViewListItemEnhanced[] = []) => {
const dataViewList = [
...list,
{
@ -95,9 +85,7 @@ describe('DataView list component', () => {
},
...esqlDataViews,
];
const component = shallow(
<DataViewsList {...props} dataViewsList={dataViewList} isTextBasedLangSelected={esqlMode} />
);
const component = shallow(<DataViewsList {...props} dataViewsList={dataViewList} />);
expect(getDataViewPickerOptions(component)!.map((option: any) => option.label)).toEqual([
'dataview-1',
'dataview-2',
@ -114,20 +102,12 @@ describe('DataView list component', () => {
},
];
it('should show ad hoc data views for text based mode', () => {
runAdHocDataViewTest(true);
});
it('should show ad hoc data views for data view mode', () => {
runAdHocDataViewTest(false);
});
it('should not show ES|QL ad hoc data views for text based mode', () => {
runAdHocDataViewTest(true, esqlDataViews);
runAdHocDataViewTest();
});
it('should not show ES|QL ad hoc data views for data view mode', () => {
runAdHocDataViewTest(false, esqlDataViews);
runAdHocDataViewTest(esqlDataViews);
});
});
});

View file

@ -66,7 +66,6 @@ export interface DataViewListItemEnhanced extends DataViewListItem {
export interface DataViewsListProps {
dataViewsList: DataViewListItemEnhanced[];
onChangeDataView: (newId: string) => void;
isTextBasedLangSelected?: boolean;
currentDataViewId?: string;
selectableProps?: EuiSelectableProps;
searchListInputId?: string;
@ -75,7 +74,6 @@ export interface DataViewsListProps {
export function DataViewsList({
dataViewsList,
onChangeDataView,
isTextBasedLangSelected,
currentDataViewId,
selectableProps,
searchListInputId,
@ -135,7 +133,7 @@ export function DataViewsList({
key: id,
label: name ? name : title,
value: id,
checked: id === currentDataViewId && !Boolean(isTextBasedLangSelected) ? 'on' : undefined,
checked: id === currentDataViewId ? 'on' : undefined,
append: isAdhoc ? (
<EuiBadge color="hollow" data-test-subj={`dataViewItemTempBadge-${name}`}>
{strings.editorAndPopover.adhoc.getTemporaryDataviewLabel()}

View file

@ -9,7 +9,7 @@
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export type { DataViewPickerProps, OnSaveTextLanguageQueryProps } from './data_view_picker';
export type { DataViewPickerProps } from './data_view_picker';
/**
* The Lazily-loaded `DataViewsList` component. Consumers should use `React.Suspense` or

View file

@ -1,127 +0,0 @@
/*
* 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, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { getLanguageDisplayName } from '@kbn/es-query';
import {
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiButton,
EuiText,
EuiCheckbox,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
export interface TextBasedLanguagesTransitionModalProps {
closeModal: (dismissFlag: boolean, needsSave?: boolean) => void;
setIsTextLangTransitionModalVisible: (flag: boolean) => void;
textBasedLanguage?: string;
}
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
export default function TextBasedLanguagesTransitionModal({
closeModal,
setIsTextLangTransitionModalVisible,
textBasedLanguage,
}: TextBasedLanguagesTransitionModalProps) {
const [dismissModalChecked, setDismissModalChecked] = useState(false);
const onTransitionModalDismiss = useCallback((e) => {
setDismissModalChecked(e.target.checked);
}, []);
const language = getLanguageDisplayName(textBasedLanguage);
return (
<EuiModal
onClose={() => setIsTextLangTransitionModalVisible(false)}
style={{ width: 700 }}
data-test-subj="unifiedSearch_switch_modal"
>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle',
{
defaultMessage: 'Your query will be removed',
}
)}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText size="m">
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody',
{
defaultMessage:
"Switching data views removes the current {language} query. Save this search to ensure you don't lose work.",
values: { language },
}
)}
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiCheckbox
id="dismiss-text-based-languages-transition-modal"
label={i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton',
{
defaultMessage: "Don't show this warning again",
}
)}
checked={dismissModalChecked}
onChange={onTransitionModalDismiss}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => closeModal(dismissModalChecked)}
color="warning"
iconType="merge"
data-test-subj="unifiedSearch_switch_noSave"
>
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton',
{
defaultMessage: 'Switch without saving',
}
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => closeModal(dismissModalChecked, true)}
fill
color="success"
iconType="save"
data-test-subj="unifiedSearch_switch_andSave"
>
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton',
{
defaultMessage: 'Save and switch',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);
}

View file

@ -22,7 +22,7 @@ function createEditor() {
uiSettings: { get: () => {} },
}}
>
<TextBasedLanguagesEditor {...props} isDarkMode={false} />
<TextBasedLanguagesEditor {...props} />
</KibanaContextProvider>
);
};

View file

@ -8,6 +8,7 @@
import dateMath from '@kbn/datemath';
import classNames from 'classnames';
import { css } from '@emotion/react';
import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import useObservable from 'react-use/lib/useObservable';
@ -35,6 +36,7 @@ import {
EuiToolTip,
EuiButton,
EuiButtonIcon,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public';
@ -47,11 +49,7 @@ import QueryStringInputUI from './query_string_input';
import { NoDataPopover } from './no_data_popover';
import { shallowEqual } from '../utils/shallow_equal';
import { AddFilterPopover } from './add_filter_popover';
import {
DataViewPicker,
DataViewPickerProps,
OnSaveTextLanguageQueryProps,
} from '../dataview_picker';
import { DataViewPicker, DataViewPickerProps } from '../dataview_picker';
import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group';
import type {
@ -167,7 +165,6 @@ export interface QueryBarTopRowProps<QT extends Query | AggregateQuery = Query>
dataViewPickerComponentProps?: DataViewPickerProps;
textBasedLanguageModeErrors?: Error[];
textBasedLanguageModeWarning?: string;
onTextBasedSavedAndExit?: ({ onSave }: OnSaveTextLanguageQueryProps) => void;
filterBar?: React.ReactNode;
showDatePickerAsBadge?: boolean;
showSubmitButton?: boolean;
@ -185,6 +182,7 @@ export interface QueryBarTopRowProps<QT extends Query | AggregateQuery = Query>
onTextLangQueryChange: (query: AggregateQuery) => void;
submitOnBlur?: boolean;
renderQueryInputAppend?: () => React.ReactNode;
disableExternalPadding?: boolean;
}
export const SharingMetaFields = React.memo(function SharingMetaFields({
@ -232,7 +230,6 @@ export const QueryBarTopRow = React.memo(
) {
const isMobile = useIsWithinBreakpoints(['xs', 's']);
const [isXXLarge, setIsXXLarge] = useState<boolean>(false);
const [codeEditorIsExpanded, setCodeEditorIsExpanded] = useState<boolean>(false);
const submitButtonStyle: QueryBarTopRowProps['submitButtonStyle'] =
props.submitButtonStyle ?? 'auto';
const submitButtonIconOnly =
@ -466,7 +463,10 @@ export const QueryBarTopRow = React.memo(
}
function shouldShowDatePickerAsBadge(): boolean {
return Boolean(props.showDatePickerAsBadge) && !shouldRenderQueryInput();
return (
(Boolean(props.showDatePickerAsBadge) && !shouldRenderQueryInput()) ||
Boolean(isQueryLangSelected && props.query && isOfAggregateQueryType(props.query))
);
}
function renderDatePicker() {
@ -632,9 +632,7 @@ export const QueryBarTopRow = React.memo(
<DataViewPicker
{...props.dataViewPickerComponentProps}
trigger={{ fullWidth: isMobile, ...props.dataViewPickerComponentProps.trigger }}
onTextLangQuerySubmit={props.onTextLangQuerySubmit}
textBasedLanguage={textBasedLanguage}
onSaveTextLanguageQuery={props.onTextBasedSavedAndExit}
isDisabled={props.isDisabled}
/>
</EuiFlexItem>
@ -734,8 +732,6 @@ export const QueryBarTopRow = React.memo(
<TextBasedLangEditor
query={props.query}
onTextLangQueryChange={props.onTextLangQueryChange}
expandCodeEditor={(status: boolean) => setCodeEditorIsExpanded(status)}
isCodeEditorExpanded={codeEditorIsExpanded}
errors={props.textBasedLanguageModeErrors}
warning={props.textBasedLanguageModeWarning}
detectedTimestamp={detectedTimestamp}
@ -753,7 +749,7 @@ export const QueryBarTopRow = React.memo(
)
);
}
const { euiTheme } = useEuiTheme();
const isScreenshotMode = props.isScreenshotMode === true;
return (
@ -770,6 +766,11 @@ export const QueryBarTopRow = React.memo(
direction={isMobile && !shouldShowDatePickerAsBadge() ? 'column' : 'row'}
responsive={false}
gutterSize="s"
css={css`
padding: ${isQueryLangSelected && !props.disableExternalPadding
? euiTheme.size.s
: 0};
`}
justifyContent={shouldShowDatePickerAsBadge() ? 'flexStart' : 'flexEnd'}
wrap
>
@ -778,18 +779,14 @@ export const QueryBarTopRow = React.memo(
grow={!shouldShowDatePickerAsBadge()}
style={{ minWidth: shouldShowDatePickerAsBadge() ? 'auto' : 320, maxWidth: '100%' }}
>
{!isQueryLangSelected
? renderQueryInput()
: !codeEditorIsExpanded
? renderTextLangEditor()
: null}
{!isQueryLangSelected ? renderQueryInput() : null}
</EuiFlexItem>
{props.renderQueryInputAppend?.()}
{shouldShowDatePickerAsBadge() && props.filterBar}
{renderUpdateButton()}
</EuiFlexGroup>
{!shouldShowDatePickerAsBadge() && props.filterBar}
{codeEditorIsExpanded && renderTextLangEditor()}
{renderTextLangEditor()}
</>
)}
</>

View file

@ -261,7 +261,6 @@ export function createSearchBar({
dataViewPickerComponentProps={props.dataViewPickerComponentProps}
textBasedLanguageModeErrors={props.textBasedLanguageModeErrors}
textBasedLanguageModeWarning={props.textBasedLanguageModeWarning}
onTextBasedSavedAndExit={props.onTextBasedSavedAndExit}
displayStyle={props.displayStyle}
isScreenshotMode={isScreenshotMode}
dataTestSubj={props.dataTestSubj}

View file

@ -9,10 +9,10 @@
import { UseEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => {
export const searchBarStyles = ({ euiTheme }: UseEuiTheme, isESQLQuery: boolean) => {
return {
uniSearchBar: css`
padding: ${euiTheme.size.s};
padding: ${isESQLQuery ? 0 : euiTheme.size.s};
position: relative;
`,
detached: css`
@ -21,6 +21,10 @@ export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => {
inPage: css`
padding: 0;
`,
withBorders: css`
border: ${euiTheme.border.thin};
border-bottom: none;
`,
hidden: css`
display: none;
`,

View file

@ -16,7 +16,14 @@ import { get, isEqual } from 'lodash';
import memoizeOne from 'memoize-one';
import { METRIC_TYPE } from '@kbn/analytics';
import { Query, Filter, TimeRange, AggregateQuery, isOfQueryType } from '@kbn/es-query';
import {
type Query,
type Filter,
type TimeRange,
type AggregateQuery,
isOfQueryType,
isOfAggregateQueryType,
} from '@kbn/es-query';
import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public';
import type {
TimeHistoryContract,
@ -32,7 +39,7 @@ import type { IUnifiedSearchPluginServices } from '../types';
import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form';
import { SavedQueryManagementList } from '../saved_query_management';
import { QueryBarMenu, QueryBarMenuProps } from '../query_string_input/query_bar_menu';
import type { DataViewPickerProps, OnSaveTextLanguageQueryProps } from '../dataview_picker';
import type { DataViewPickerProps } from '../dataview_picker';
import QueryBarTopRow, { QueryBarTopRowProps } from '../query_string_input/query_bar_top_row';
import { FilterBar, FilterItems } from '../filter_bar';
import type {
@ -108,13 +115,12 @@ export interface SearchBarOwnProps<QT extends AggregateQuery | Query = Query> {
disableQueryLanguageSwitcher?: boolean;
// defines padding and border; use 'inPage' to avoid any padding or border;
// use 'detached' if the searchBar appears at the very top of the view, without any wrapper
displayStyle?: 'inPage' | 'detached';
displayStyle?: 'inPage' | 'detached' | 'withBorders';
// super update button background fill control
fillSubmitButton?: boolean;
dataViewPickerComponentProps?: DataViewPickerProps;
textBasedLanguageModeErrors?: Error[];
textBasedLanguageModeWarning?: string;
onTextBasedSavedAndExit?: ({ onSave }: OnSaveTextLanguageQueryProps) => void;
showSubmitButton?: boolean;
submitButtonStyle?: QueryBarTopRowProps['submitButtonStyle'];
// defines size of suggestions query popover
@ -480,9 +486,10 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
}
public render() {
const { theme } = this.props;
const { theme, query } = this.props;
const isESQLQuery = isOfAggregateQueryType(query);
const isScreenshotMode = this.props.isScreenshotMode === true;
const styles = searchBarStyles(theme);
const styles = searchBarStyles(theme, isESQLQuery);
const cssStyles = [
styles.uniSearchBar,
this.props.displayStyle && styles[this.props.displayStyle],
@ -640,7 +647,6 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
dataViewPickerComponentProps={this.props.dataViewPickerComponentProps}
textBasedLanguageModeErrors={this.props.textBasedLanguageModeErrors}
textBasedLanguageModeWarning={this.props.textBasedLanguageModeWarning}
onTextBasedSavedAndExit={this.props.onTextBasedSavedAndExit}
showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()}
filterBar={filterBar}
suggestionsSize={this.props.suggestionsSize}
@ -650,6 +656,7 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
submitOnBlur={this.props.submitOnBlur}
suggestionsAbstraction={this.props.suggestionsAbstraction}
renderQueryInputAppend={this.props.renderQueryInputAppend}
disableExternalPadding={this.props.displayStyle === 'withBorders'}
/>
</div>
);

View file

@ -253,9 +253,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.selectTextBaseLang();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern('logstash-*', false);
await testSubjects.click('switch-to-dataviews');
await retry.try(async () => {
await testSubjects.existOrFail('unifiedSearch_switch_modal');
await testSubjects.existOrFail('discover-esql-to-dataview-modal');
});
});
@ -266,19 +266,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern('logstash-*', false);
await testSubjects.click('switch-to-dataviews');
await retry.try(async () => {
await testSubjects.existOrFail('unifiedSearch_switch_modal');
await testSubjects.existOrFail('discover-esql-to-dataview-modal');
});
await find.clickByCssSelector(
'[data-test-subj="unifiedSearch_switch_modal"] .euiModal__closeIcon'
'[data-test-subj="discover-esql-to-dataview-modal"] .euiModal__closeIcon'
);
await retry.try(async () => {
await testSubjects.missingOrFail('unifiedSearch_switch_modal');
await testSubjects.missingOrFail('discover-esql-to-dataview-modal');
});
await PageObjects.discover.saveSearch('esql_test');
await PageObjects.discover.selectIndexPattern('logstash-*');
await testSubjects.missingOrFail('unifiedSearch_switch_modal');
await testSubjects.click('switch-to-dataviews');
await testSubjects.missingOrFail('discover-esql-to-dataview-modal');
});
it('should show switch modal when switching to a data view while a saved search with unsaved changes is open', async () => {
@ -291,9 +291,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern('logstash-*', false);
await testSubjects.click('switch-to-dataviews');
await retry.try(async () => {
await testSubjects.existOrFail('unifiedSearch_switch_modal');
await testSubjects.existOrFail('discover-esql-to-dataview-modal');
});
});
});
@ -339,7 +339,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
const historyItems = await esql.getHistoryItems();
log.debug(historyItems);
@ -362,7 +361,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
const historyItems = await esql.getHistoryItems();
log.debug(historyItems);
@ -379,7 +377,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
// click a history item
await esql.clickHistoryItem(1);
@ -405,7 +402,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
const historyItem = await esql.getHistoryItem(0);
await historyItem.findByTestSubject('TextBasedLangEditor-queryHistory-error');
@ -597,7 +593,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await dataGrid.clickCellFilterForButtonExcludingControlColumns(0, 1);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
@ -630,7 +625,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await dataGrid.clickCellFilterForButtonExcludingControlColumns(0, 1);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();

View file

@ -66,13 +66,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should modify the time range when the histogram is brushed', async function () {
await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished();
// this is the number of renderings of the histogram needed when new data is fetched
let renderingCountInc = 1;
const prevRenderingCount = await elasticChart.getVisualizationRenderingCount();
await queryBar.submitQuery();
await retry.waitFor('chart rendering complete', async () => {
const actualCount = await elasticChart.getVisualizationRenderingCount();
const expectedCount = prevRenderingCount + renderingCountInc;
const expectedCount = prevRenderingCount;
log.debug(`renderings before brushing - actual: ${actualCount} expected: ${expectedCount}`);
return actualCount === expectedCount;
});
@ -85,7 +82,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.brushHistogram();
await PageObjects.discover.waitUntilSearchingHasFinished();
renderingCountInc = 2;
const renderingCountInc = 2;
await retry.waitFor('chart rendering complete after being brushed', async () => {
const actualCount = await elasticChart.getVisualizationRenderingCount();
const expectedCount = prevRenderingCount + renderingCountInc * 2;

View file

@ -489,10 +489,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
(await PageObjects.unifiedFieldList.getSidebarSectionFieldNames('selected')).join(', ')
).to.be('countB, geo.dest');
await PageObjects.unifiedSearch.switchToDataViewMode();
await PageObjects.unifiedSearch.switchDataView(
'discover-dataView-switch-link',
'logstash-*',
true
'logstash-*'
);
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -169,7 +169,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('bytes', '0');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`bytes\`==0`
@ -190,7 +189,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('extension.raw', 'css');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`extension.raw\`=="css"`
@ -212,7 +210,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('clientip', '216.126.255.31');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`clientip\`::string=="216.126.255.31"`
@ -238,7 +235,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should not have stats for a date field yet but create an is not null filter', async () => {
await PageObjects.unifiedFieldList.clickFieldListItem('@timestamp');
await PageObjects.unifiedFieldList.clickFieldListExistsFilter('@timestamp');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`@timestamp\` is not null`
@ -274,7 +270,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('extension', 'css');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`extension\`=="css"`
@ -314,7 +309,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await PageObjects.unifiedFieldList.clickFieldListPlusFilter('avg(bytes)', '5453');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* | sort @timestamp desc | limit 50 | stats avg(bytes) by geo.dest | limit 3\n| WHERE \`avg(bytes)\`==5453`
@ -343,7 +337,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await PageObjects.unifiedFieldList.clickFieldListMinusFilter('enabled', 'true');
await testSubjects.click('TextBasedLangEditor-expand');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(`row enabled = true\n| WHERE \`enabled\`!=true`);
await PageObjects.unifiedFieldList.closeFieldPopover();

View file

@ -571,9 +571,10 @@ export class DiscoverPageObject extends FtrService {
}
public async selectTextBaseLang() {
await this.testSubjects.click('discover-dataView-switch-link');
await this.testSubjects.click('select-text-based-language-panel');
await this.header.waitUntilLoadingHasFinished();
if (await this.testSubjects.exists('select-text-based-language-btn')) {
await this.testSubjects.click('select-text-based-language-btn');
await this.header.waitUntilLoadingHasFinished();
}
}
public async removeHeaderColumn(name: string) {

View file

@ -13,21 +13,13 @@ export class UnifiedSearchPageObject extends FtrService {
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly find = this.ctx.getService('find');
public async switchDataView(
switchButtonSelector: string,
dataViewTitle: string,
transitionFromTextBasedLanguages?: boolean
) {
public async switchDataView(switchButtonSelector: string, dataViewTitle: string) {
await this.testSubjects.click(switchButtonSelector);
const indexPatternSwitcher = await this.testSubjects.find('indexPattern-switcher', 500);
await this.testSubjects.setValue('indexPattern-switcher--input', dataViewTitle);
await (await indexPatternSwitcher.findByCssSelector(`[title="${dataViewTitle}"]`)).click();
if (Boolean(transitionFromTextBasedLanguages)) {
await this.testSubjects.click('unifiedSearch_switch_noSave');
}
await this.retry.waitFor(
'wait for updating switcher',
async () => (await this.getSelectedDataView(switchButtonSelector)) === dataViewTitle
@ -50,4 +42,9 @@ export class UnifiedSearchPageObject extends FtrService {
`[data-test-subj="text-based-languages-switcher"] [title="${language}"]`
);
}
public async switchToDataViewMode() {
await this.testSubjects.click('switch-to-dataviews');
await this.testSubjects.click('discover-esql-to-dataview-no-save-btn');
}
}

View file

@ -54,7 +54,6 @@ export interface IndexDataVisualizerESQLProps {
getAdditionalLinks?: GetAdditionalLinks;
}
const DEFAULT_ESQL_QUERY = { esql: '' };
const expandCodeEditor = () => true;
export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVisualizerProps) => {
const { services } = useDataVisualizerKibana();
const { data } = services;
@ -264,10 +263,7 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
query={localQuery}
onTextLangQueryChange={onTextLangQueryChange}
onTextLangQuerySubmit={onTextLangQuerySubmit}
expandCodeEditor={expandCodeEditor}
isCodeEditorExpanded={true}
detectedTimestamp={currentDataView?.timeFieldName}
hideMinimizeButton={true}
hideRunQueryText={false}
isLoading={queryHistoryStatus ?? false}
/>

View file

@ -9,8 +9,6 @@ import { TextBasedLangEditor } from '@kbn/esql/public';
import { EuiFlexItem } from '@elastic/eui';
import type { AggregateQuery } from '@kbn/es-query';
const expandCodeEditor = (status: boolean) => {};
interface FieldStatsESQLEditorProps {
canEditTextBasedQuery?: boolean;
query: AggregateQuery;
@ -47,9 +45,6 @@ export const FieldStatsESQLEditor = ({
setQuery(q);
prevQuery.current = q;
}}
expandCodeEditor={expandCodeEditor}
isCodeEditorExpanded
hideMinimizeButton
editorIsInline
hideRunQueryText
onTextLangQuerySubmit={onTextLangQuerySubmit}

View file

@ -471,14 +471,6 @@ export function App({
[dataViews, uiActions, http, notifications, uiSettings, initialContext, dispatch]
);
const onTextBasedSavedAndExit = useCallback(async ({ onSave, onCancel: _onCancel }) => {
setIsSaveModalVisible(true);
setShouldCloseAndSaveTextBasedQuery(true);
saveAndExit.current = () => {
onSave();
};
}, []);
// remember latest URL based on the configuration
// url_panel_content has a similar logic
const shareURLCache = useRef({ params: '', url: '' });
@ -571,7 +563,6 @@ export function App({
topNavMenuEntryGenerators={topNavMenuEntryGenerators}
initialContext={initialContext}
indexPatternService={indexPatternService}
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
getUserMessages={getUserMessages}
shortUrlService={shortUrlService}
startServices={coreStart}

View file

@ -281,7 +281,6 @@ export const LensTopNavMenu = ({
initialContext,
indexPatternService,
currentDoc,
onTextBasedSavedAndExit,
getUserMessages,
shortUrlService,
isCurrentStateDirty,
@ -1112,7 +1111,6 @@ export const LensTopNavMenu = ({
)
}
textBasedLanguageModeErrors={textBasedLanguageModeErrors}
onTextBasedSavedAndExit={onTextBasedSavedAndExit}
showFilterBar={true}
data-test-subj="lnsApp_topNav"
screenTitle={'lens'}

View file

@ -479,8 +479,6 @@ export function LensEditConfigurationFlyout({
setQuery(q);
prevQuery.current = q;
}}
expandCodeEditor={(status: boolean) => {}}
isCodeEditorExpanded
detectedTimestamp={adHocDataViews?.[0]?.timeFieldName}
hideTimeFilterInfo={hideTimeFilterInfo}
errors={errors}
@ -492,7 +490,6 @@ export function LensEditConfigurationFlyout({
})
: undefined
}
hideMinimizeButton
editorIsInline
hideRunQueryText
onTextLangQuerySubmit={async (q, a) => {

View file

@ -126,7 +126,6 @@ export interface LensTopNavMenuProps {
initialContext?: VisualizeFieldContext | VisualizeEditorContext;
currentDoc: Document | undefined;
indexPatternService: IndexPatternServiceAPI;
onTextBasedSavedAndExit: ({ onSave }: { onSave: () => void }) => Promise<void>;
getUserMessages: UserMessagesGetter;
shortUrlService: (params: LensAppLocatorParams) => Promise<string>;
isCurrentStateDirty: boolean;

View file

@ -857,6 +857,8 @@ export function FormulaEditor({
}
),
}}
isHelpMenuOpen={isHelpOpen}
onHelpMenuVisibilityChange={setIsHelpOpen}
/>
)}
</EuiFlexItem>

View file

@ -89,11 +89,6 @@ export function ESQLEditor(props: Props) {
}}
errors={error ? [error] : undefined}
warning={warning}
expandCodeEditor={(status: boolean) => {
// never called because hideMinimizeButton hides UI
}}
isCodeEditorExpanded
hideMinimizeButton
editorIsInline
hideRunQueryText
isLoading={isLoading}

View file

@ -24,13 +24,11 @@ const emptyPreview = css`
export function AddObservationUI({ onWidgetAdd, timeRange, filters }: Props) {
const [isOpen, setIsOpen] = React.useState(false);
const [isExpanded, setIsExpanded] = React.useState(false);
const [query, setQuery] = React.useState({ esql: '' });
const [submittedQuery, setSubmittedQuery] = React.useState({ esql: '' });
const [isPreviewOpen, setIsPreviewOpen] = React.useState(false);
const resetState = () => {
setIsExpanded(false);
setIsPreviewOpen(false);
setQuery({ esql: '' });
setSubmittedQuery({ esql: '' });
@ -83,11 +81,6 @@ export function AddObservationUI({ onWidgetAdd, timeRange, filters }: Props) {
}}
errors={undefined}
warning={undefined}
expandCodeEditor={(expanded: boolean) => {
setIsExpanded(() => expanded);
}}
isCodeEditorExpanded={isExpanded}
hideMinimizeButton={false}
editorIsInline={false}
hideRunQueryText
isLoading={false}

View file

@ -180,7 +180,7 @@ export const QueryBar = memo<QueryBarComponentProps>(
timeHistory={timeHistory}
dataTestSubj={dataTestSubj}
savedQuery={savedQuery}
displayStyle={displayStyle}
displayStyle={isEsql ? 'withBorders' : displayStyle}
isDisabled={isDisabled}
/>
);

View file

@ -207,7 +207,6 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
setPopoverIsOpen={setDataViewPopoverOpen}
onChangeDataView={onChangeDataView}
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
isTextBasedLangSelected={false}
/>
{createDataView ? (
<EuiPopoverFooter paddingSize="none">

View file

@ -194,13 +194,11 @@ export const EsqlQueryExpression: React.FC<
setParam('esqlQuery', q);
refreshTimeFields(q);
}}
expandCodeEditor={() => true}
isCodeEditorExpanded={true}
onTextLangQuerySubmit={async () => {}}
detectedTimestamp={detectedTimestamp}
hideMinimizeButton={true}
hideRunQueryText={true}
isLoading={isLoading}
hasOutline
/>
</EuiFormRow>
<EuiSpacer />

View file

@ -6633,14 +6633,11 @@
"textBasedEditor.query.textBasedLanguagesEditor.errorCount": "{count} {count, plural, one {erreur} other {erreurs}}",
"textBasedEditor.query.textBasedLanguagesEditor.errorsTitle": "Erreurs",
"textBasedEditor.query.textBasedLanguagesEditor.esql": "ES|QL",
"textBasedEditor.query.textBasedLanguagesEditor.expandTooltip": "Développer léditeur de requête",
"textBasedEditor.query.textBasedLanguagesEditor.feedback": "Commentaires",
"textBasedEditor.query.textBasedLanguagesEditor.functions": "Fonctions",
"textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "Les fonctions sont compatibles avec \"ROW\" (Ligne), \"EVAL\" (Évaluation) et \"WHERE\" (Où).",
"textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, one {ligne} other {lignes}}",
"textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "Ligne {lineNumber}",
"textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor": "Réduire l'éditeur",
"textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip": "Réduire léditeur de requête",
"textBasedEditor.query.textBasedLanguagesEditor.operators": "Opérateurs",
"textBasedEditor.query.textBasedLanguagesEditor.operatorsDocumentationESQLDescription": "ES|QL est compatible avec les opérateurs suivants :",
"textBasedEditor.query.textBasedLanguagesEditor.processingCommands": "Traitement des commandes",
@ -7222,12 +7219,11 @@
"unifiedSearch.query.queryBar.indexPattern.findFilterSet": "Trouver une requête",
"unifiedSearch.query.queryBar.indexPattern.manageFieldButton": "Gérer cette vue de données",
"unifiedSearch.query.queryBar.indexPattern.temporaryDataviewLabel": "Temporaire",
"unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning": "Modifier la vue de données supprime la requête {textBasedLanguage} en cours. Sauvegardez cette recherche pour ne pas perdre de travail.",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody": "Modifier la vue de données supprime la requête {language} en cours. Sauvegardez cette recherche pour ne pas perdre de travail.",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton": "Basculer sans sauvegarder",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton": "Ne plus afficher cet avertissement",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton": "Sauvegarder et basculer",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle": "Votre requête sera supprimée",
"discover.esqlToDataviewTransitionModalBody": "Modifier la vue de données supprime la requête ES|QL en cours. Sauvegardez cette recherche pour ne pas perdre de travail.",
"discover.esqlToDataViewTransitionModal.closeButtonLabel": "Basculer sans sauvegarder",
"discover.esqlToDataViewTransitionModal.dismissButtonLabel": "Ne plus afficher cet avertissement",
"discover.esqlToDataViewTransitionModal.saveButtonLabel": "Sauvegarder et basculer",
"discover.esqlToDataViewTransitionModal.title": "Votre requête sera supprimée",
"unifiedSearch.query.queryBar.kqlLanguageName": "KQL",
"unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "documents",
"unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "Ne plus afficher",
@ -7238,7 +7234,6 @@
"unifiedSearch.query.queryBar.searchInputPlaceholder": "Filtrer vos données à l'aide de la syntaxe {language}",
"unifiedSearch.query.queryBar.searchInputPlaceholderForText": "Filtrer vos données",
"unifiedSearch.query.queryBar.syntaxOptionsTitle": "Options de syntaxe",
"unifiedSearch.query.queryBar.textBasedLanguagesTryLabel": "Essayer ES|QL",
"unifiedSearch.query.queryBar.textBasedNonTimestampWarning": "La sélection de plage de données pour les requêtes en {language} requiert la présence d'un champ @timestamp dans l'ensemble de données.",
"unifiedSearch.queryBarTopRow.datePicker.disabledLabel": "Tout le temps",
"unifiedSearch.queryBarTopRow.submitButton.cancel": "Annuler",

View file

@ -6609,14 +6609,11 @@
"textBasedEditor.query.textBasedLanguagesEditor.errorCount": "{count} {count, plural, other {# 件のエラー}}",
"textBasedEditor.query.textBasedLanguagesEditor.errorsTitle": "エラー",
"textBasedEditor.query.textBasedLanguagesEditor.esql": "ES|QL",
"textBasedEditor.query.textBasedLanguagesEditor.expandTooltip": "クエリエディターを展開",
"textBasedEditor.query.textBasedLanguagesEditor.feedback": "フィードバック",
"textBasedEditor.query.textBasedLanguagesEditor.functions": "関数",
"textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "関数はROW、EVAL、WHEREでサポートされています。",
"textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, other {行}}",
"textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "行{lineNumber}",
"textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor": "エディターを最小化",
"textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip": "クエリエディターを縮小",
"textBasedEditor.query.textBasedLanguagesEditor.operators": "演算子",
"textBasedEditor.query.textBasedLanguagesEditor.operatorsDocumentationESQLDescription": "ES|QLは以下の演算子をサポートしています。",
"textBasedEditor.query.textBasedLanguagesEditor.processingCommands": "処理コマンド",
@ -7198,12 +7195,11 @@
"unifiedSearch.query.queryBar.indexPattern.findFilterSet": "クエリを検索",
"unifiedSearch.query.queryBar.indexPattern.manageFieldButton": "このデータビューを管理",
"unifiedSearch.query.queryBar.indexPattern.temporaryDataviewLabel": "一時",
"unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning": "データビューを切り替えると、現在の{textBasedLanguage}クエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody": "データビューを切り替えると、現在の{language}クエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton": "保存せずに切り替え",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton": "次回以降この警告を表示しない",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton": "保存して切り替え",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle": "クエリは削除されます",
"discover.esqlToDataviewTransitionModalBody": "データビューを切り替えると、現在のES|QLクエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。",
"discover.esqlToDataViewTransitionModal.closeButtonLabel": "保存せずに切り替え",
"discover.esqlToDataViewTransitionModal.dismissButtonLabel": "次回以降この警告を表示しない",
"discover.esqlToDataViewTransitionModal.saveButtonLabel": "保存して切り替え",
"discover.esqlToDataViewTransitionModal.title": "クエリは削除されます",
"unifiedSearch.query.queryBar.kqlLanguageName": "KQL",
"unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "ドキュメント",
"unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "今後表示しない",
@ -7214,7 +7210,6 @@
"unifiedSearch.query.queryBar.searchInputPlaceholder": "{language}構文を使用してデータをフィルタリング",
"unifiedSearch.query.queryBar.searchInputPlaceholderForText": "データのフィルタリング",
"unifiedSearch.query.queryBar.syntaxOptionsTitle": "構文オプション",
"unifiedSearch.query.queryBar.textBasedLanguagesTryLabel": "ES|QLを試す",
"unifiedSearch.query.queryBar.textBasedNonTimestampWarning": "{language}クエリの日付範囲選択では、データセットに@timestampフィールドが存在している必要があります。",
"unifiedSearch.queryBarTopRow.datePicker.disabledLabel": "常時",
"unifiedSearch.queryBarTopRow.submitButton.cancel": "キャンセル",

View file

@ -6642,14 +6642,12 @@
"textBasedEditor.query.textBasedLanguagesEditor.errorCount": "{count} 个{count, plural, other {错误}}",
"textBasedEditor.query.textBasedLanguagesEditor.errorsTitle": "错误",
"textBasedEditor.query.textBasedLanguagesEditor.esql": "ES|QL",
"textBasedEditor.query.textBasedLanguagesEditor.expandTooltip": "展开查询编辑器",
"textBasedEditor.query.textBasedLanguagesEditor.feedback": "反馈",
"textBasedEditor.query.textBasedLanguagesEditor.functions": "函数",
"textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "ROW、EVAL 和 WHERE 支持的函数。",
"textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, other {行}}",
"textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "第 {lineNumber} 行",
"textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor": "最小化编辑器",
"textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip": "压缩查询编辑器",
"": "压缩查询编辑器",
"textBasedEditor.query.textBasedLanguagesEditor.operators": "运算符",
"textBasedEditor.query.textBasedLanguagesEditor.operatorsDocumentationESQLDescription": "ES|QL 支持以下运算符:",
"textBasedEditor.query.textBasedLanguagesEditor.processingCommands": "处理命令",
@ -7234,12 +7232,11 @@
"unifiedSearch.query.queryBar.indexPattern.findFilterSet": "查找查询",
"unifiedSearch.query.queryBar.indexPattern.manageFieldButton": "管理此数据视图",
"unifiedSearch.query.queryBar.indexPattern.temporaryDataviewLabel": "临时",
"unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning": "切换数据视图会移除当前的 {textBasedLanguage} 查询。保存此搜索以确保不会丢失工作。",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody": "切换数据视图会移除当前的 {language} 查询。保存此搜索以确保不会丢失工作。",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton": "切换而不保存",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalDismissButton": "不再显示此警告",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton": "保存并切换",
"unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle": "将移除您的查询",
"discover.esqlToDataviewTransitionModalBody": "切换数据视图会移除当前的 ES|QL 查询。保存此搜索以确保不会丢失工作。",
"discover.esqlToDataViewTransitionModal.closeButtonLabel": "切换而不保存",
"discover.esqlToDataViewTransitionModal.dismissButtonLabel": "不再显示此警告",
"discover.esqlToDataViewTransitionModal.saveButtonLabel": "保存并切换",
"discover.esqlToDataViewTransitionModal.title": "将移除您的查询",
"unifiedSearch.query.queryBar.kqlLanguageName": "KQL",
"unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "文档",
"unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "不再显示",
@ -7250,7 +7247,6 @@
"unifiedSearch.query.queryBar.searchInputPlaceholder": "使用 {language} 语法筛选数据",
"unifiedSearch.query.queryBar.searchInputPlaceholderForText": "筛选您的数据",
"unifiedSearch.query.queryBar.syntaxOptionsTitle": "语法选项",
"unifiedSearch.query.queryBar.textBasedLanguagesTryLabel": "尝试 ES|QL",
"unifiedSearch.query.queryBar.textBasedNonTimestampWarning": "{language} 查询的日期范围选择要求数据集中存在 @timestamp 字段。",
"unifiedSearch.queryBarTopRow.datePicker.disabledLabel": "所有时间",
"unifiedSearch.queryBarTopRow.submitButton.cancel": "取消",

View file

@ -167,7 +167,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
expect(await testSubjects.exists('xyVisChart')).to.be(true);
expect(await PageObjects.lens.canRemoveDimension('lnsXY_xDimensionPanel')).to.equal(true);
@ -190,7 +189,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
@ -209,7 +207,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
@ -228,7 +225,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramSaveVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
@ -257,7 +253,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await monacoEditor.setCodeEditorValue('from logstash-* | limit 10');
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
// save the visualization
await testSubjects.click('unifiedHistogramSaveVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
@ -308,7 +303,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramSaveVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
let title = await testSubjects.getAttribute('savedObjectTitle', 'value');

View file

@ -1035,15 +1035,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
/**
* Changes the index pattern in the data panel
*/
async switchDataPanelIndexPattern(
dataViewTitle: string,
transitionFromTextBasedLanguages?: boolean
) {
await PageObjects.unifiedSearch.switchDataView(
'lns-dataView-switch-link',
dataViewTitle,
transitionFromTextBasedLanguages
);
async switchDataPanelIndexPattern(dataViewTitle: string) {
await PageObjects.unifiedSearch.switchDataView('lns-dataView-switch-link', dataViewTitle);
await PageObjects.header.waitUntilLoadingHasFinished();
},

View file

@ -28,7 +28,6 @@ import { getDetails, goBackToRulesTable } from '../../../../tasks/rule_details';
import { expectNumberOfRules } from '../../../../tasks/alerts_detection_rules';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
expandEsqlQueryBar,
fillAboutRuleAndContinue,
fillDefineEsqlRuleAndContinue,
fillScheduleRuleAndContinue,
@ -87,7 +86,6 @@ describe(
it('creates an ES|QL rule', function () {
selectEsqlRuleType();
expandEsqlQueryBar();
fillDefineEsqlRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
@ -109,7 +107,6 @@ describe(
// this test case is important, since field shown in rule override component are coming from ES|QL query, not data view fields API
it('creates an ES|QL rule and overrides its name', function () {
selectEsqlRuleType();
expandEsqlQueryBar();
fillDefineEsqlRuleAndContinue(rule);
fillAboutSpecificEsqlRuleAndContinue({ ...rule, rule_name_override: 'test_id' });
@ -130,7 +127,6 @@ describe(
});
it('shows error when ES|QL query is empty', function () {
selectEsqlRuleType();
expandEsqlQueryBar();
getDefineContinueButton().click();
cy.get(ESQL_QUERY_BAR).contains('ES|QL query is required');
@ -138,7 +134,6 @@ describe(
it('proceeds further once invalid query is fixed', function () {
selectEsqlRuleType();
expandEsqlQueryBar();
getDefineContinueButton().click();
cy.get(ESQL_QUERY_BAR).contains('required');
@ -153,7 +148,6 @@ describe(
it('shows error when non-aggregating ES|QL query does not have metadata operator', function () {
const invalidNonAggregatingQuery = 'from auditbeat* | limit 5';
selectEsqlRuleType();
expandEsqlQueryBar();
fillEsqlQueryBar(invalidNonAggregatingQuery);
getDefineContinueButton().click();
@ -167,7 +161,6 @@ describe(
'from auditbeat* metadata _id, _version, _index | keep agent.* | limit 5';
selectEsqlRuleType();
expandEsqlQueryBar();
fillEsqlQueryBar(invalidNonAggregatingQuery);
getDefineContinueButton().click();
@ -182,7 +175,6 @@ describe(
visit(CREATE_RULE_URL);
selectEsqlRuleType();
expandEsqlQueryBar();
fillEsqlQueryBar(invalidEsqlQuery);
getDefineContinueButton().click();
@ -207,7 +199,6 @@ describe(
workaroundForResizeObserver();
selectEsqlRuleType();
expandEsqlQueryBar();
fillEsqlQueryBar(queryWithCustomFields);
getDefineContinueButton().click();
@ -242,7 +233,6 @@ describe(
workaroundForResizeObserver();
selectEsqlRuleType();
expandEsqlQueryBar();
interceptEsqlQueryFieldsRequest(queryWithCustomFields, 'esqlSuppressionFieldsRequest');
fillEsqlQueryBar(queryWithCustomFields);

View file

@ -30,7 +30,6 @@ import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management';
import { getDetails } from '../../../../tasks/rule_details';
import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
import {
expandEsqlQueryBar,
fillEsqlQueryBar,
fillOverrideEsqlRuleName,
goToAboutStepTab,
@ -68,7 +67,6 @@ describe(
});
it('edits ES|QL rule and checks details page', () => {
expandEsqlQueryBar();
// ensure once edit form opened, correct query is displayed in ES|QL input
cy.get(ESQL_QUERY_BAR).contains(rule.query);
@ -94,7 +92,6 @@ describe(
});
it('adds ES|QL override rule name on edit', () => {
expandEsqlQueryBar();
// ensure once edit form opened, correct query is displayed in ES|QL input
cy.get(ESQL_QUERY_BAR).contains(rule.query);

View file

@ -261,9 +261,6 @@ export const ESQL_QUERY_BAR_INPUT_AREA =
export const ESQL_QUERY_BAR = '[data-test-subj="detectionEngineStepDefineRuleEsqlQueryBar"]';
export const ESQL_QUERY_BAR_EXPAND_BTN =
'[data-test-subj="detectionEngineStepDefineRuleEsqlQueryBar"] [data-test-subj="TextBasedLangEditor-expand"]';
export const NEW_TERMS_INPUT_AREA = '[data-test-subj="newTermsInput"]';
export const NEW_TERMS_HISTORY_SIZE =

View file

@ -15,7 +15,7 @@ export const DISCOVER_DATA_VIEW_SWITCHER = {
INPUT: getDataTestSubjectSelector('indexPattern-switcher--input'),
GET_DATA_VIEW: (title: string) => `.euiSelectableListItem[role=option][title^="${title}"]`,
CREATE_NEW: getDataTestSubjectSelector('dataview-create-new'),
TEXT_BASE_LANG_SWICTHER: getDataTestSubjectSelector('select-text-based-language-panel'),
TEXT_BASE_LANG_SWITCHER: getDataTestSubjectSelector('select-text-based-language-btn'),
};
export const DISCOVER_DATA_VIEW_EDITOR_FLYOUT = {
@ -32,7 +32,6 @@ export const DISCOVER_ESQL_INPUT = `${DISCOVER_CONTAINER} ${getDataTestSubjectSe
export const DISCOVER_ESQL_INPUT_TEXT_CONTAINER = `${DISCOVER_ESQL_INPUT} .view-lines`;
export const DISCOVER_ESQL_INPUT_EXPAND = getDataTestSubjectSelector('TextBasedLangEditor-expand');
export const DISCOVER_ESQL_EDITABLE_INPUT = `${DISCOVER_ESQL_INPUT} textarea`;
export const DISCOVER_ADD_FILTER = `${DISCOVER_CONTAINER} ${getDataTestSubjectSelector(

View file

@ -59,7 +59,6 @@ import {
EQL_TYPE,
ESQL_TYPE,
ESQL_QUERY_BAR,
ESQL_QUERY_BAR_EXPAND_BTN,
ESQL_QUERY_BAR_INPUT_AREA,
FALSE_POSITIVES_INPUT,
IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK,
@ -632,14 +631,6 @@ export const fillEsqlQueryBar = (query: string) => {
typeEsqlQueryBar(query);
};
/**
* expands query bar, so query is not obscured on narrow screens
* and validation message is not covered by input menu tooltip
*/
export const expandEsqlQueryBar = () => {
cy.get(ESQL_QUERY_BAR_EXPAND_BTN).click();
};
export const fillDefineEsqlRuleAndContinue = (rule: EsqlRuleCreateProps) => {
cy.get(ESQL_QUERY_BAR).contains('ES|QL query');
fillEsqlQueryBar(rule.query);

View file

@ -15,7 +15,6 @@ import {
DISCOVER_DATA_VIEW_EDITOR_FLYOUT,
DISCOVER_FIELD_LIST_LOADING,
DISCOVER_ESQL_EDITABLE_INPUT,
DISCOVER_ESQL_INPUT_EXPAND,
} from '../screens/discover';
import { GET_LOCAL_SEARCH_BAR_SUBMIT_BUTTON } from '../screens/search_bar';
import { goToEsqlTab } from './timeline';
@ -28,8 +27,7 @@ export const switchDataViewTo = (dataviewName: string) => {
};
export const switchDataViewToESQL = () => {
openDataViewSwitcher();
cy.get(DISCOVER_DATA_VIEW_SWITCHER.TEXT_BASE_LANG_SWICTHER).trigger('click');
cy.get(DISCOVER_DATA_VIEW_SWITCHER.TEXT_BASE_LANG_SWITCHER).trigger('click');
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('contain.text', 'ES|QL');
};
@ -56,8 +54,6 @@ export const selectCurrentDiscoverEsqlQuery = (
goToEsqlTab();
// eslint-disable-next-line cypress/no-force
cy.get(discoverEsqlInput).click({ force: true });
// eslint-disable-next-line cypress/no-force
cy.get(DISCOVER_ESQL_INPUT_EXPAND).click({ force: true });
fillEsqlQueryBar(Cypress.platform === 'darwin' ? '{cmd+a}' : '{ctrl+a}');
};

View file

@ -260,9 +260,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.selectTextBaseLang();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern('logstash-*', false);
await testSubjects.click('switch-to-dataviews');
await retry.try(async () => {
await testSubjects.existOrFail('unifiedSearch_switch_modal');
await testSubjects.existOrFail('discover-esql-to-dataview-modal');
});
});
@ -273,19 +273,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern('logstash-*', false);
await testSubjects.click('switch-to-dataviews');
await retry.try(async () => {
await testSubjects.existOrFail('unifiedSearch_switch_modal');
await testSubjects.existOrFail('discover-esql-to-dataview-modal');
});
await find.clickByCssSelector(
'[data-test-subj="unifiedSearch_switch_modal"] .euiModal__closeIcon'
'[data-test-subj="discover-esql-to-dataview-modal"] .euiModal__closeIcon'
);
await retry.try(async () => {
await testSubjects.missingOrFail('unifiedSearch_switch_modal');
await testSubjects.missingOrFail('discover-esql-to-dataview-modal');
});
await PageObjects.discover.saveSearch('esql_test');
await PageObjects.discover.selectIndexPattern('logstash-*');
await testSubjects.missingOrFail('unifiedSearch_switch_modal');
await testSubjects.click('switch-to-dataviews');
await testSubjects.missingOrFail('discover-esql-to-dataview-modal');
});
it('should show switch modal when switching to a data view while a saved search with unsaved changes is open', async () => {
@ -298,9 +298,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern('logstash-*', false);
await testSubjects.click('switch-to-dataviews');
await retry.try(async () => {
await testSubjects.existOrFail('unifiedSearch_switch_modal');
await testSubjects.existOrFail('discover-esql-to-dataview-modal');
});
});
});
@ -346,7 +346,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
const historyItems = await esql.getHistoryItems();
log.debug(historyItems);
@ -369,7 +368,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
const historyItems = await esql.getHistoryItems();
log.debug(historyItems);
@ -386,7 +384,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
// click a history item
await esql.clickHistoryItem(1);
@ -412,7 +409,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('TextBasedLangEditor-toggle-query-history-button');
await testSubjects.click('TextBasedLangEditor-queryHistory-runQuery-button');
const historyItem = await esql.getHistoryItem(0);