Skip to content

Commit

Permalink
[ES|QL][Lens] Keeps the chart configuration when possible (elastic#21…
Browse files Browse the repository at this point in the history
…0780)

## Summary

Closes elastic#186366


![meow](https://github.com/user-attachments/assets/6e5bd59a-2dcb-4b0e-a0a3-1ed15a64306f)

It keeps the chart configuration when the user is doing actions
compatible with the current query such as:

- Adding a where filter
- Changing to a compatible chart type (from example from bar to line or
pie to treemap)
- In general changing the query that doesnt affect the generated columns
mapped to a chart.

### Release notes

Keeps the chart configuration changes done by the user when changing the
query whenever it is possible.

### Checklist

- [ ] [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
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
  • Loading branch information
stratoula authored Feb 21, 2025
1 parent 27e1ade commit cb77b97
Show file tree
Hide file tree
Showing 7 changed files with 452 additions and 166 deletions.
105 changes: 54 additions & 51 deletions src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export const ESQLEditor = memo(function ESQLEditor({
esqlVariables,
}: ESQLEditorProps) {
const popoverRef = useRef<HTMLDivElement>(null);
const editorModel = useRef<monaco.editor.ITextModel>();
const editor1 = useRef<monaco.editor.IStandaloneCodeEditor>();
const containerRef = useRef<HTMLElement>(null);

const datePickerOpenStatusRef = useRef<boolean>(false);
const theme = useEuiTheme();
const kibana = useKibana<ESQLEditorDeps>();
Expand Down Expand Up @@ -330,6 +334,12 @@ export const ESQLEditor = memo(function ESQLEditor({
);
});

editor1.current?.addCommand(
// eslint-disable-next-line no-bitwise
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
onQuerySubmit
);

const styles = esqlEditorStyles(
theme.euiTheme,
editorHeight,
Expand All @@ -339,9 +349,6 @@ export const ESQLEditor = memo(function ESQLEditor({
Boolean(editorIsInline),
Boolean(hasOutline)
);
const editorModel = useRef<monaco.editor.ITextModel>();
const editor1 = useRef<monaco.editor.IStandaloneCodeEditor>();
const containerRef = useRef<HTMLElement>(null);

const onMouseDownResize = useCallback<typeof onMouseDownResizeHandler>(
(
Expand Down Expand Up @@ -656,47 +663,50 @@ export const ESQLEditor = memo(function ESQLEditor({

onLayoutChangeRef.current = onLayoutChange;

const codeEditorOptions: CodeEditorProps['options'] = {
hover: {
above: false,
},
accessibilitySupport: 'off',
autoIndent: 'none',
automaticLayout: true,
fixedOverflowWidgets: true,
folding: false,
fontSize: 14,
hideCursorInOverviewRuler: true,
// this becomes confusing with multiple markers, so quick fixes
// will be proposed only within the tooltip
lightbulb: {
enabled: false,
},
lineDecorationsWidth: 20,
lineNumbers: 'on',
lineNumbersMinChars: 3,
minimap: { enabled: false },
overviewRulerLanes: 0,
overviewRulerBorder: false,
padding: {
top: 8,
bottom: 8,
},
quickSuggestions: true,
readOnly: isDisabled,
renderLineHighlight: 'line',
renderLineHighlightOnlyWhenFocus: true,
scrollbar: {
horizontal: 'hidden',
horizontalScrollbarSize: 6,
vertical: 'auto',
verticalScrollbarSize: 6,
},
scrollBeyondLastLine: false,
theme: ESQL_LANG_ID,
wordWrap: 'on',
wrappingIndent: 'none',
};
const codeEditorOptions: CodeEditorProps['options'] = useMemo(
() => ({
hover: {
above: false,
},
accessibilitySupport: 'off',
autoIndent: 'none',
automaticLayout: true,
fixedOverflowWidgets: true,
folding: false,
fontSize: 14,
hideCursorInOverviewRuler: true,
// this becomes confusing with multiple markers, so quick fixes
// will be proposed only within the tooltip
lightbulb: {
enabled: false,
},
lineDecorationsWidth: 20,
lineNumbers: 'on',
lineNumbersMinChars: 3,
minimap: { enabled: false },
overviewRulerLanes: 0,
overviewRulerBorder: false,
padding: {
top: 8,
bottom: 8,
},
quickSuggestions: true,
readOnly: isDisabled,
renderLineHighlight: 'line',
renderLineHighlightOnlyWhenFocus: true,
scrollbar: {
horizontal: 'hidden',
horizontalScrollbarSize: 6,
vertical: 'auto',
verticalScrollbarSize: 6,
},
scrollBeyondLastLine: false,
theme: ESQL_LANG_ID,
wordWrap: 'on',
wrappingIndent: 'none',
}),
[isDisabled]
);

const editorPanel = (
<>
Expand Down Expand Up @@ -793,13 +803,6 @@ export const ESQLEditor = memo(function ESQLEditor({
onEditorFocus();
});

// on CMD/CTRL + Enter submit the query
editor.addCommand(
// eslint-disable-next-line no-bitwise
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
onQuerySubmit
);

// on CMD/CTRL + / comment out the entire line
editor.addCommand(
// eslint-disable-next-line no-bitwise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { getESQLResults } from '@kbn/esql-utils';
import type { LensPluginStartDependencies } from '../../../plugin';
import type { TypedLensSerializedState } from '../../../react_embeddable/types';
import { createMockStartDependencies } from '../../../editor_frame_service/mocks';
import {
mockVisualizationMap,
Expand All @@ -15,7 +16,7 @@ import {
mockAllSuggestions,
} from '../../../mocks';
import { suggestionsApi } from '../../../lens_suggestions_api';
import { getSuggestions } from './helpers';
import { getSuggestions, injectESQLQueryIntoLensLayers } from './helpers';

const mockSuggestionApi = suggestionsApi as jest.Mock;
const mockFetchData = getESQLResults as jest.Mock;
Expand Down Expand Up @@ -82,74 +83,146 @@ jest.mock('@kbn/esql-utils', () => {
};
});

describe('getSuggestions', () => {
const query = {
esql: 'from index1 | limit 10 | stats average = avg(bytes)',
};
const mockStartDependencies =
createMockStartDependencies() as unknown as LensPluginStartDependencies;
const dataViews = dataViewPluginMocks.createStartContract();
dataViews.create.mockResolvedValue(mockDataViewWithTimefield);
const dataviewSpecArr = [
{
id: 'd2588ae7-9ea0-4439-9f5b-f808754a3b97',
title: 'index1',
timeFieldName: '@timestamp',
sourceFilters: [],
fieldFormats: {},
runtimeFieldMap: {},
fieldAttrs: {},
allowNoIndex: false,
name: 'index1',
},
];
const startDependencies = {
...mockStartDependencies,
dataViews,
};
describe('Lens inline editing helpers', () => {
describe('getSuggestions', () => {
const query = {
esql: 'from index1 | limit 10 | stats average = avg(bytes)',
};
const mockStartDependencies =
createMockStartDependencies() as unknown as LensPluginStartDependencies;
const dataViews = dataViewPluginMocks.createStartContract();
dataViews.create.mockResolvedValue(mockDataViewWithTimefield);
const dataviewSpecArr = [
{
id: 'd2588ae7-9ea0-4439-9f5b-f808754a3b97',
title: 'index1',
timeFieldName: '@timestamp',
sourceFilters: [],
fieldFormats: {},
runtimeFieldMap: {},
fieldAttrs: {},
allowNoIndex: false,
name: 'index1',
},
];
const startDependencies = {
...mockStartDependencies,
dataViews,
};

it('returns the suggestions attributes correctly', async () => {
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
jest.fn()
);
expect(suggestionsAttributes?.visualizationType).toBe(mockAllSuggestions[0].visualizationId);
expect(suggestionsAttributes?.state.visualization).toStrictEqual(
mockAllSuggestions[0].visualizationState
);
});
it('returns the suggestions attributes correctly', async () => {
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
jest.fn()
);
expect(suggestionsAttributes?.visualizationType).toBe(mockAllSuggestions[0].visualizationId);
expect(suggestionsAttributes?.state.visualization).toStrictEqual(
mockAllSuggestions[0].visualizationState
);
});

it('returns undefined if no suggestions are computed', async () => {
mockSuggestionApi.mockResolvedValueOnce([]);
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
jest.fn()
);
expect(suggestionsAttributes).toBeUndefined();
it('returns undefined if no suggestions are computed', async () => {
mockSuggestionApi.mockResolvedValueOnce([]);
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
jest.fn()
);
expect(suggestionsAttributes).toBeUndefined();
});

it('returns an error if fetching the data fails', async () => {
mockFetchData.mockImplementation(() => {
throw new Error('sorry!');
});
const setErrorsSpy = jest.fn();
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
setErrorsSpy
);
expect(suggestionsAttributes).toBeUndefined();
expect(setErrorsSpy).toHaveBeenCalled();
});
});

it('returns an error if fetching the data fails', async () => {
mockFetchData.mockImplementation(() => {
throw new Error('sorry!');
describe('injectESQLQueryIntoLensLayers', () => {
const query = {
esql: 'from index1 | limit 10 | stats average = avg(bytes)',
};

it('should inject the query correctly for ES|QL charts', async () => {
const lensAttributes = {
title: 'test',
visualizationType: 'testVis',
state: {
datasourceStates: {
textBased: { layers: { layer1: { query: { esql: 'from index1 | limit 10' } } } },
},
visualization: { preferredSeriesType: 'line' },
},
filters: [],
query: {
esql: 'from index1 | limit 10',
},
references: [],
} as unknown as TypedLensSerializedState['attributes'];

const expectedLensAttributes = {
...lensAttributes,
state: {
...lensAttributes.state,
datasourceStates: {
...lensAttributes.state.datasourceStates,
textBased: {
...lensAttributes.state.datasourceStates.textBased,
layers: {
layer1: {
query: { esql: 'from index1 | limit 10 | stats average = avg(bytes)' },
},
},
},
},
},
};
const newAttributes = injectESQLQueryIntoLensLayers(lensAttributes, query);
expect(newAttributes).toStrictEqual(expectedLensAttributes);
});

it('should return the Lens attributes as they are for unknown datasourceId', async () => {
const attributes = {
visualizationType: 'lnsXY',
state: {
visualization: { preferredSeriesType: 'line' },
datasourceStates: { unknownId: { layers: {} } },
},
} as unknown as TypedLensSerializedState['attributes'];
expect(injectESQLQueryIntoLensLayers(attributes, { esql: 'from foo' })).toStrictEqual(
attributes
);
});

it('should return the Lens attributes as they are for form based charts', async () => {
const attributes = {
visualizationType: 'lnsXY',
state: {
visualization: { preferredSeriesType: 'line' },
datasourceStates: { formBased: { layers: {} } },
},
} as TypedLensSerializedState['attributes'];
expect(injectESQLQueryIntoLensLayers(attributes, { esql: 'from foo' })).toStrictEqual(
attributes
);
});
const setErrorsSpy = jest.fn();
const suggestionsAttributes = await getSuggestions(
query,
startDependencies,
mockDatasourceMap(),
mockVisualizationMap(),
dataviewSpecArr,
setErrorsSpy
);
expect(suggestionsAttributes).toBeUndefined();
expect(setErrorsSpy).toHaveBeenCalled();
});
});
Loading

0 comments on commit cb77b97

Please sign in to comment.