Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-esql-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
isSingleSource,
type ESQLSourceKind,
ensureApproximationLicense,
ESQLValuesPreview,
} from './src';

export { ENABLE_ESQL, GROUP_NOT_SET_VALUE } from './constants';
2 changes: 2 additions & 0 deletions src/platform/packages/shared/kbn-esql-utils/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ dependsOn:
- '@kbn/esql-types'
- '@kbn/search-types'
- '@kbn/expressions-plugin'
- '@kbn/field-formats-common'
- '@kbn/field-types'
- '@kbn/es-types'
- '@kbn/i18n'
- '@kbn/i18n-react'
- '@kbn/datemath'
- '@kbn/es-query'
- '@kbn/code-editor'
Expand Down
2 changes: 2 additions & 0 deletions src/platform/packages/shared/kbn-esql-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,5 @@ export { injectWhereClauseAfterSourceCommand } from './utils/inject_where_after_
export * from './utils/callbacks';

export { ensureApproximationLicense } from './utils/ensure_approximation_license';

export { ESQLValuesPreview } from './utils/controls/esql_values_preview';
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import React, { useCallback, useState } from 'react';
import type { EuiSelectableOption } from '@elastic/eui';
import { EuiButtonEmpty, EuiPopover, EuiSelectable } from '@elastic/eui';
import { css } from '@emotion/react';
import type { ESQLColumn } from '@kbn/es-types';
import { DataControlEditorStrings } from '../data_control_constants';
import { i18n } from '@kbn/i18n';

const SELECT_COLUMN_LABEL = i18n.translate('esqlUtils.valuesPreview.selectAColumnText', {
defaultMessage: 'Select a column',
});

export function ChooseColumnPopover({
columns,
Expand All @@ -26,25 +29,9 @@ export function ChooseColumnPopover({
columns.map((column) => ({ label: column.name }))
);

const onButtonClick = () => setIsPopoverOpen((status) => !status);
const closePopover = () => setIsPopoverOpen(false);

const button = (
<EuiButtonEmpty
css={css`
vertical-align: top;
`}
onClick={onButtonClick}
data-test-subj="chooseColumnBtn"
>
{DataControlEditorStrings.manageControl.dataSource.valuesPreview.getSelectAColumnText()}
</EuiButtonEmpty>
);

const onColumnChange = useCallback(
(newOptions: EuiSelectableOption[]) => {
setOptions(newOptions);

const selectedColumn = newOptions.find((option) => option.checked === 'on');
if (selectedColumn) {
updateQuery(selectedColumn.label);
Expand All @@ -53,22 +40,30 @@ export function ChooseColumnPopover({
[updateQuery]
);

const button = (
<EuiButtonEmpty
style={{ verticalAlign: 'top' }}
onClick={() => setIsPopoverOpen((status) => !status)}
data-test-subj="chooseColumnBtn"
>
{SELECT_COLUMN_LABEL}
</EuiButtonEmpty>
);

return (
<EuiPopover
aria-label={DataControlEditorStrings.manageControl.dataSource.valuesPreview.getColumnsListLabel()}
aria-label={i18n.translate('esqlUtils.valuesPreview.columnsListLabel', {
defaultMessage: 'Columns',
})}
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
closePopover={() => setIsPopoverOpen(false)}
>
<EuiSelectable
aria-label={DataControlEditorStrings.manageControl.dataSource.valuesPreview.getSelectAColumnText()}
aria-label={SELECT_COLUMN_LABEL}
searchable
searchProps={{
'data-test-subj': 'selectableColumnSearch',
}}
listProps={{
'data-test-subj': 'selectableColumnList',
}}
searchProps={{ 'data-test-subj': 'selectableColumnSearch' }}
listProps={{ 'data-test-subj': 'selectableColumnList' }}
singleSelection="always"
options={options}
onChange={onColumnChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,20 @@ import { ESQLValuesPreview } from './esql_values_preview';

const noopProps = {
updateQuery: jest.fn(),
isQueryRunning: false,
queryNeedsRunning: false,
dataSource: 'index',
};

const numericColumn: ESQLColumn = { name: 'bytes', type: 'long' };
const stringColumn: ESQLColumn = { name: 'os', type: 'keyword' };

describe('ESQLValuesPreview', () => {
it('renders min/max stats for numeric columns', () => {
it('renders min/max stats for numeric columns when control type is range slider', () => {
const { getByText, getByTestId } = render(
<I18nProvider>
<ESQLValuesPreview
{...noopProps}
previewOptions={[6, 7, 67]}
previewColumns={[numericColumn]}
values={[6, 7, 67]}
columns={[numericColumn]}
isRangeControl={true}
/>
</I18nProvider>
);
Expand All @@ -39,12 +37,29 @@ describe('ESQLValuesPreview', () => {
expect(getByText('6')).toBeInTheDocument();
expect(getByText('67')).toBeInTheDocument();
});

it('renders a list of values for numeric columns when control type is options list', () => {
const { getByTestId, queryByTestId } = render(
<I18nProvider>
<ESQLValuesPreview
{...noopProps}
values={[6, 7, 67]}
columns={[numericColumn]}
isRangeControl={false}
/>
</I18nProvider>
);

expect(queryByTestId('esqlValuesPreviewRange')).not.toBeInTheDocument();
expect(getByTestId('esqlValuesPreviewStrings')).toBeInTheDocument();
});

it('renders a list of values for string columns', () => {
const { getByTestId } = render(
<I18nProvider>
<ESQLValuesPreview
{...noopProps}
previewOptions={[
values={[
'some',
'BODY',
'once',
Expand All @@ -66,7 +81,7 @@ describe('ESQLValuesPreview', () => {
'on her fore',
'head',
]}
previewColumns={[stringColumn]}
columns={[stringColumn]}
/>
</I18nProvider>
);
Expand All @@ -77,11 +92,7 @@ describe('ESQLValuesPreview', () => {
it('shows the column picker when the query returns multiple columns', () => {
const { getByText, getByTestId } = render(
<I18nProvider>
<ESQLValuesPreview
{...noopProps}
previewOptions={[]}
previewColumns={[numericColumn, stringColumn]}
/>
<ESQLValuesPreview {...noopProps} values={[]} columns={[numericColumn, stringColumn]} />
</I18nProvider>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useMemo } from 'react';
import { EuiBadge, EuiBadgeGroup, EuiCallOut, EuiCode, EuiFlexGrid, EuiStat } from '@elastic/eui';
import { max, min } from 'lodash';
import type { ESQLColumn } from '@kbn/es-types';
import { isNumericType } from '@kbn/esql-language';
import { EMPTY_LABEL } from '@kbn/field-formats-common';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ChooseColumnPopover } from './choose_column_popover';

export const ESQLValuesPreview: React.FC<{
// The raw values returned by the query — shown as badges or a range stat
values: string[] | number[];
// Columns returned by the query — used to detect multi-column errors and determine value type
columns: ESQLColumn[];
// Query execution error; when set, renders an error callout instead of values
error?: Error;
updateQuery: (column: string) => void;
// When true and the column is numeric, renders a min/max range stat instead of badges
isRangeControl?: boolean;
}> = ({ values, error, columns, updateQuery, isRangeControl }) => {
const range = useMemo(() => {
if (!isRangeControl || !isNumericType(columns?.[0]?.type)) return;

const valuesAsNumbers = values.map((v) => Number(v));
return { min: min(valuesAsNumbers), max: max(valuesAsNumbers) };
}, [values, isRangeControl, columns]);

if (error) {
return (
<EuiCallOut
announceOnMount
title={i18n.translate('esqlUtils.valuesPreview.errorTitle', {
defaultMessage: 'Error getting values preview',
})}
color="danger"
iconType="error"
size="s"
>
<p>{error.message}</p>
</EuiCallOut>
);
}

if (columns.length > 1) {
return (
<EuiCallOut
announceOnMount
title={i18n.translate('esqlUtils.valuesPreview.multiColumnErrorTitle', {
defaultMessage: 'Query must return a single column',
})}
color="warning"
iconType="warning"
size="s"
data-test-subj="esqlMoreThanOneColumnCallout"
>
<FormattedMessage
id="esqlUtils.valuesPreview.multiColumnErrorBody"
defaultMessage="Your query is currently returning {totalColumns} columns. Choose a column, or use {statsBy} to narrow your query down."
values={{
totalColumns: columns.length,
statsBy: <EuiCode>STATS BY</EuiCode>,
}}
/>
<ChooseColumnPopover columns={columns} updateQuery={updateQuery} />
</EuiCallOut>
);
}

if (values.length === 0) {
return (
<EuiCallOut
announceOnMount
title={i18n.translate('esqlUtils.valuesPreview.emptyTitle', {
defaultMessage: 'No values returned',
})}
color="warning"
iconType="warning"
size="s"
data-test-subj="esqlNoValuesForControlCallout"
>
<p>
{i18n.translate('esqlUtils.valuesPreview.emptyText', {
defaultMessage: "This query isn't returning any values. Edit it and run it again.",
})}
</p>
</EuiCallOut>
);
}

if (range) {
return (
<EuiFlexGrid columns={2} data-test-subj="esqlValuesPreviewRange">
<EuiStat
titleSize="s"
title={range.min}
description={i18n.translate('esqlUtils.valuesPreview.minText', {
defaultMessage: 'Minimum value',
})}
/>
<EuiStat
titleSize="s"
title={range.max}
description={i18n.translate('esqlUtils.valuesPreview.maxText', {
defaultMessage: 'Maximum value',
})}
/>
</EuiFlexGrid>
);
}

const visibleOptions = values.slice(0, 10);
const hiddenCount = values.length - visibleOptions.length;

return (
<div>
<EuiBadgeGroup data-test-subj="esqlValuesPreviewStrings">
{visibleOptions.map((option, i) => (
<EuiBadge key={`${i}-${option}`}>{option === '' ? EMPTY_LABEL : option}</EuiBadge>
))}
</EuiBadgeGroup>
{hiddenCount > 0 && (
<p>
{i18n.translate('esqlUtils.valuesPreview.hiddenCount', {
defaultMessage: '+{hiddenCount} more',
values: { hiddenCount },
})}
</p>
)}
</div>
);
};
2 changes: 2 additions & 0 deletions src/platform/packages/shared/kbn-esql-utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
"@kbn/esql-types",
"@kbn/search-types",
"@kbn/expressions-plugin",
"@kbn/field-formats-common",
"@kbn/field-types",
"@kbn/es-types",
"@kbn/i18n",
"@kbn/i18n-react",
"@kbn/datemath",
"@kbn/es-query",
"@kbn/code-editor",
Expand Down
1 change: 0 additions & 1 deletion src/platform/plugins/shared/controls/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ dependsOn:
- '@kbn/esql'
- '@kbn/presentation-publishing-schemas'
- '@kbn/controls-renderer'
- '@kbn/field-formats-common'
- '@kbn/ui-callout'
tags:
- plugin
Expand Down
Loading
Loading