Skip to content

Commit

Permalink
[8.x] [ES|QL] Supports custom formatters in charts (#201540) (#203717)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL] Supports custom formatters in charts
(#201540)](#201540)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Peter
Pisljar","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-11T04:45:05Z","message":"[ES|QL]
Supports custom formatters in charts
(#201540)","sha":"168e67d50d2d36110ba26f130f381da96ddf163e","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Visualizations","v9.0.0","Feature:ES|QL","Team:ESQL","backport:version","v8.18.0"],"title":"[ES|QL]
Supports custom formatters in
charts","number":201540,"url":"https://github.com/elastic/kibana/pull/201540","mergeCommit":{"message":"[ES|QL]
Supports custom formatters in charts
(#201540)","sha":"168e67d50d2d36110ba26f130f381da96ddf163e"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/201540","number":201540,"mergeCommit":{"message":"[ES|QL]
Supports custom formatters in charts
(#201540)","sha":"168e67d50d2d36110ba26f130f381da96ddf163e"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Peter Pisljar <[email protected]>
  • Loading branch information
kibanamachine and ppisljar authored Dec 11, 2024
1 parent d2c551a commit ab026f8
Show file tree
Hide file tree
Showing 18 changed files with 275 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { lastValueFrom } from 'rxjs';
import { Query, AggregateQuery, TimeRange } from '@kbn/es-query';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { Datatable } from '@kbn/expressions-plugin/public';
import { type DataView, textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common';
import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common';

interface TextBasedLanguagesErrorResponse {
error: {
Expand All @@ -26,16 +26,20 @@ export function fetchFieldsFromESQL(
expressions: ExpressionsStart,
time?: TimeRange,
abortController?: AbortController,
dataView?: DataView
timeFieldName?: string
) {
return textBasedQueryStateToAstWithValidation({
query,
time,
dataView,
timeFieldName,
})
.then((ast) => {
if (ast) {
const executionContract = expressions.execute(ast, null);
const executionContract = expressions.execute(ast, null, {
searchContext: {
timeRange: time,
},
});

if (abortController) {
abortController.signal.onabort = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@ export class DatatableUtilitiesService {
timeZone: string;
}> = {}
): DateHistogramMeta | undefined {
if (column.meta.source !== 'esaggs') {
return;
}
if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) {
if (!column.meta.sourceParams || !column.meta.sourceParams.params) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { createStubDataView } from '@kbn/data-views-plugin/common/mocks';
import { textBasedQueryStateToAstWithValidation } from './text_based_query_state_to_ast_with_validation';

describe('textBasedQueryStateToAstWithValidation', () => {
Expand All @@ -25,21 +24,14 @@ describe('textBasedQueryStateToAstWithValidation', () => {
});

it('returns an object with the correct structure for an SQL query with existing dataview', async () => {
const dataView = createStubDataView({
spec: {
id: 'foo',
title: 'foo',
timeFieldName: '@timestamp',
},
});
const actual = await textBasedQueryStateToAstWithValidation({
filters: [],
query: { esql: 'FROM foo' },
time: {
from: 'now',
to: 'now+7d',
},
dataView,
timeFieldName: '@timestamp',
});

expect(actual).toHaveProperty(
Expand Down Expand Up @@ -76,21 +68,14 @@ describe('textBasedQueryStateToAstWithValidation', () => {
});

it('returns an object with the correct structure for ES|QL', async () => {
const dataView = createStubDataView({
spec: {
id: 'foo',
title: 'foo',
timeFieldName: '@timestamp',
},
});
const actual = await textBasedQueryStateToAstWithValidation({
filters: [],
query: { esql: 'from logs*' },
time: {
from: 'now',
to: 'now+7d',
},
dataView,
timeFieldName: '@timestamp',
titleForInspector: 'Custom title',
descriptionForInspector: 'Custom desc',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
*/

import { isOfAggregateQueryType, Query } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { QueryState } from '..';
import { textBasedQueryStateToExpressionAst } from './text_based_query_state_to_ast';

interface Args extends QueryState {
dataView?: DataView;
inputQuery?: Query;
timeFieldName?: string;
titleForInspector?: string;
Expand All @@ -26,7 +24,7 @@ interface Args extends QueryState {
* @param query kibana query or aggregate query
* @param inputQuery
* @param time kibana time range
* @param dataView
* @param timeFieldName
* @param titleForInspector
* @param descriptionForInspector
*/
Expand All @@ -35,7 +33,7 @@ export async function textBasedQueryStateToAstWithValidation({
query,
inputQuery,
time,
dataView,
timeFieldName,
titleForInspector,
descriptionForInspector,
}: Args) {
Expand All @@ -46,7 +44,7 @@ export async function textBasedQueryStateToAstWithValidation({
query,
inputQuery,
time,
timeFieldName: dataView?.timeFieldName,
timeFieldName,
titleForInspector,
descriptionForInspector,
});
Expand Down
12 changes: 11 additions & 1 deletion src/plugins/data/common/search/expressions/esql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,17 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => {
(body.all_columns ?? body.columns)?.map(({ name, type }) => ({
id: name,
name,
meta: { type: esFieldTypeToKibanaFieldType(type), esType: type },
meta: {
type: esFieldTypeToKibanaFieldType(type),
esType: type,
sourceParams:
type === 'date'
? {
appliedTimeRange: input?.timeRange,
params: {},
}
: {},
},
isNull: hasEmptyColumns ? !lookup.has(name) : false,
})) ?? [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function fetchEsql({
filters,
query,
time: timeRange,
dataView,
timeFieldName: dataView.timeFieldName,
inputQuery,
titleForInspector: i18n.translate('discover.inspectorEsqlRequestTitle', {
defaultMessage: 'Table',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,8 @@ describe('map_to_columns', () => {
);

expect(result.columns).toStrictEqual([
{ id: 'a', name: 'A', meta: { type: 'number' } },
{ id: 'b', name: 'B', meta: { type: 'number' } },
{ id: 'a', name: 'A', meta: { type: 'number', field: undefined, params: undefined } },
{ id: 'b', name: 'B', meta: { type: 'number', field: undefined, params: undefined } },
]);

expect(result.rows).toStrictEqual([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ export const mapToOriginalColumnsTextBased: MapToColumnsExpressionFunction['fn']
if (!(column.id in idMap)) {
return [];
}
return idMap[column.id].map((originalColumn) => ({ ...column, id: originalColumn.id }));
return idMap[column.id].map((originalColumn) => ({
...column,
id: originalColumn.id,
name: originalColumn.label,
meta: {
...column.meta,
field: originalColumn.sourceField,
params: originalColumn.format,
},
}));
}),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*/

import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';

export type OriginalColumn = { id: string; label: string } & (
export type OriginalColumn = { id: string; label: string; format?: SerializedFieldFormat } & (
| { operationType: 'date_histogram'; sourceField: string }
| { operationType: string; sourceField: never }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '@kbn/field-formats-plugin/common';
import { css } from '@emotion/react';
import type { DocLinksStart } from '@kbn/core/public';
import { TextBasedLayerColumn } from '../../text_based/types';
import { LensAppServices } from '../../../app_plugin/types';
import { GenericIndexPatternColumn } from '../form_based';
import { isColumnFormatted } from '../operations/definitions/helpers';
Expand Down Expand Up @@ -127,7 +128,7 @@ type FormatParams = NonNullable<ValueFormatConfig['params']>;
type FormatParamsKeys = keyof FormatParams;

export interface FormatSelectorProps {
selectedColumn: GenericIndexPatternColumn;
selectedColumn: GenericIndexPatternColumn | TextBasedLayerColumn;
onChange: (newFormat?: { id: string; params?: FormatParams }) => void;
docLinks: DocLinksStart;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { isEqual } from 'lodash';
import { Query } from '@kbn/es-query';
import { TextBasedLayerColumn } from '../../../text_based/types';
import type { IndexPattern, IndexPatternField } from '../../../../types';
import {
type FieldBasedOperationErrorMessage,
Expand Down Expand Up @@ -178,8 +179,8 @@ export const isColumn = (
};

export function isColumnFormatted(
column: GenericIndexPatternColumn
): column is FormattedIndexPatternColumn {
column: GenericIndexPatternColumn | TextBasedLayerColumn
): column is FormattedIndexPatternColumn | TextBasedLayerColumn {
return Boolean(
'params' in column &&
(column as FormattedIndexPatternColumn).params &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@
* 2.0.
*/

import React, { useEffect, useState, useMemo } from 'react';
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow } from '@elastic/eui';
import { EuiFormRow, useEuiTheme, EuiText } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { fetchFieldsFromESQL } from '@kbn/esql-editor';
import { NameInput } from '@kbn/visualization-ui-components';
import { css } from '@emotion/react';
import { mergeLayer, updateColumnFormat, updateColumnLabel } from '../utils';
import {
FormatSelector,
FormatSelectorProps,
} from '../../form_based/dimension_panel/format_selector';
import type { DatasourceDimensionEditorProps, DataType } from '../../../types';
import { FieldSelect, type FieldOptionCompatible } from './field_select';
import type { TextBasedPrivateState } from '../types';
import { isNotNumeric, isNumeric } from '../utils';
import { TextBasedLayer } from '../types';

export type TextBasedDimensionEditorProps =
DatasourceDimensionEditorProps<TextBasedPrivateState> & {
Expand All @@ -24,14 +32,30 @@ export type TextBasedDimensionEditorProps =
export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) {
const [allColumns, setAllColumns] = useState<FieldOptionCompatible[]>([]);
const query = props.state.layers[props.layerId]?.query;
const { euiTheme } = useEuiTheme();
const {
isFullscreen,
columnId,
layerId,
state,
setState,
indexPatterns,
dateRange,
expressions,
} = props;

useEffect(() => {
// in case the columns are not in the cache, I refetch them
async function fetchColumns() {
if (query) {
const table = await fetchFieldsFromESQL(
{ esql: `${query.esql} | limit 0` },
props.expressions
expressions,
{ from: dateRange.fromDate, to: dateRange.toDate },
undefined,
Object.values(indexPatterns).length
? Object.values(indexPatterns)[0].timeFieldName
: undefined
);
if (table) {
const hasNumberTypeColumns = table.columns?.some(isNumeric);
Expand All @@ -55,13 +79,40 @@ export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) {
}
}
fetchColumns();
}, [props, props.expressions, query]);
}, [
dateRange.fromDate,
dateRange.toDate,
expressions,
indexPatterns,
props,
props.expressions,
query,
]);

const selectedField = useMemo(() => {
const layerColumns = props.state.layers[props.layerId].columns;
return layerColumns?.find((column) => column.columnId === props.columnId);
}, [props.columnId, props.layerId, props.state.layers]);

const updateLayer = useCallback(
(newLayer: Partial<TextBasedLayer>) =>
setState((prevState) => mergeLayer({ state: prevState, layerId, newLayer })),
[layerId, setState]
);

const onFormatChange = useCallback<FormatSelectorProps['onChange']>(
(newFormat) => {
updateLayer(
updateColumnFormat({
layer: state.layers[layerId],
columnId,
value: newFormat,
})
);
},
[columnId, layerId, state.layers, updateLayer]
);

return (
<>
<EuiFormRow
Expand All @@ -80,6 +131,7 @@ export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) {
const newColumn = {
columnId: props.columnId,
fieldName: choice.field,
label: choice.field,
meta,
};
return props.setState(
Expand Down Expand Up @@ -122,6 +174,44 @@ export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) {
{props.dataSectionExtra}
</div>
)}
{!isFullscreen && selectedField && (
<div className="lnsIndexPatternDimensionEditor--padded lnsIndexPatternDimensionEditor--collapseNext">
<EuiText
size="s"
css={css`
margin-bottom: ${euiTheme.size.base};
`}
>
<h4>
{i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingAppearance', {
defaultMessage: 'Appearance',
})}
</h4>
</EuiText>

<NameInput
value={selectedField.label || ''}
defaultValue={''}
onChange={(value) => {
updateLayer(
updateColumnLabel({
layer: state.layers[layerId],
columnId,
value,
})
);
}}
/>

{selectedField.meta?.type === 'number' ? (
<FormatSelector
selectedColumn={selectedField}
onChange={onFormatChange}
docLinks={props.core.docLinks}
/>
) : null}
</div>
)}
</>
);
}
Loading

0 comments on commit ab026f8

Please sign in to comment.