From e8b2303875a8f7c1ed7ac9210fb8397dbed52073 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 3 Jul 2023 15:18:05 +0300 Subject: [PATCH] [Text based] Configure Lens suggestion on the fly from Discover (#159559) ## Summary Part of https://github.com/elastic/kibana/issues/158802 This PR removes the navigation from Discover to Lens and renders a push flyout instead. ![textbased](https://github.com/elastic/kibana/assets/17003240/92c6f290-6cf9-4daa-920e-f1409595d765) Next tasks (follow-up PRs): - [ ] Remove the text based support from Lens dataview picker. The FTs should be removed from there and possibly moved to discover FTs - [ ] Apply the same flyout in dashboard for text based panels - [ ] Allow drag and drop between dimensions - [ ] Investigate why the Field select doesnt close when you click outside the dropdown ### 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 - [ ] 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> Co-authored-by: Andrea Del Rio --- .../public/__mocks__/lens_table_adapter.ts | 40 ++ .../__mocks__/{services.ts => services.tsx} | 3 +- .../public/chart/chart.test.tsx | 11 + .../unified_histogram/public/chart/chart.tsx | 76 ++- .../public/chart/histogram.tsx | 1 + .../hooks/use_chart_config_panel.test.tsx | 66 +++ .../chart/hooks/use_chart_config_panel.tsx | 101 ++++ .../public/chart/hooks/use_chart_styles.tsx | 6 + .../hooks/use_edit_visualization.test.ts | 15 + .../chart/hooks/use_edit_visualization.ts | 4 +- .../public/chart/suggestion_selector.tsx | 8 +- .../container/hooks/use_state_props.test.ts | 126 +++++ .../public/container/hooks/use_state_props.ts | 5 +- .../container/services/state_service.test.ts | 4 + .../container/services/state_service.ts | 13 + .../public/container/utils/state_selectors.ts | 1 + .../public/layout/layout.tsx | 4 + .../get_edit_lens_configuration.tsx | 129 ++++++ .../lens_configuration_flyout.test.tsx | 433 ++++++++++++++++++ .../lens_configuration_flyout.tsx | 174 +++++++ x-pack/plugins/lens/public/async_services.ts | 1 + .../text_based/text_based_languages.test.ts | 16 + .../text_based/text_based_languages.tsx | 8 + .../public/datasources/text_based/types.ts | 2 +- .../config_panel/config_panel.test.tsx | 9 +- .../config_panel/config_panel.tsx | 67 ++- .../config_panel/layer_panel.test.tsx | 9 + .../editor_frame/config_panel/layer_panel.tsx | 35 +- .../editor_frame/config_panel/types.ts | 6 +- .../editor_frame/workspace_panel/index.ts | 1 + .../workspace_panel_wrapper.test.tsx | 8 +- .../workspace_panel_wrapper.tsx | 79 ++-- .../public/editor_frame_service/mocks.tsx | 4 + .../embeddable/embeddable_component.tsx | 2 + .../lens/public/mocks/lens_plugin_mock.tsx | 1 + x-pack/plugins/lens/public/plugin.ts | 22 + .../lens/public/state_management/index.ts | 1 + .../state_management/lens_slice.test.ts | 23 + .../public/state_management/lens_slice.ts | 44 ++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apps/discover/visualize_field.ts | 8 +- 43 files changed, 1477 insertions(+), 92 deletions(-) create mode 100644 src/plugins/unified_histogram/public/__mocks__/lens_table_adapter.ts rename src/plugins/unified_histogram/public/__mocks__/{services.ts => services.tsx} (92%) create mode 100644 src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.test.tsx create mode 100644 src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.tsx create mode 100644 x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx create mode 100644 x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx create mode 100644 x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx diff --git a/src/plugins/unified_histogram/public/__mocks__/lens_table_adapter.ts b/src/plugins/unified_histogram/public/__mocks__/lens_table_adapter.ts new file mode 100644 index 0000000000000..60e38fecbbfae --- /dev/null +++ b/src/plugins/unified_histogram/public/__mocks__/lens_table_adapter.ts @@ -0,0 +1,40 @@ +/* + * 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 { Datatable } from '@kbn/expressions-plugin/common'; + +export const lensTablesAdapterMock: Record = { + default: { + columns: [ + { + id: 'col-0-1', + meta: { + dimensionName: 'Slice size', + type: 'number', + }, + name: 'Field 1', + }, + { + id: 'col-0-2', + meta: { + dimensionName: 'Slice', + type: 'number', + }, + name: 'Field 2', + }, + ], + rows: [ + { + 'col-0-1': 0, + 'col-0-2': 0, + 'col-0-3': 0, + 'col-0-4': 0, + }, + ], + type: 'datatable', + }, +}; diff --git a/src/plugins/unified_histogram/public/__mocks__/services.ts b/src/plugins/unified_histogram/public/__mocks__/services.tsx similarity index 92% rename from src/plugins/unified_histogram/public/__mocks__/services.ts rename to src/plugins/unified_histogram/public/__mocks__/services.tsx index 71058be4f634f..6b6e5a7f46864 100644 --- a/src/plugins/unified_histogram/public/__mocks__/services.ts +++ b/src/plugins/unified_histogram/public/__mocks__/services.tsx @@ -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 from 'react'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; @@ -33,6 +33,7 @@ export const unifiedHistogramServicesMock = { suggestions: jest.fn(() => allSuggestionsMock), }; }), + EditLensConfigPanelApi: jest.fn().mockResolvedValue(Lens Config Panel Component), }, storage: { get: jest.fn(), diff --git a/src/plugins/unified_histogram/public/chart/chart.test.tsx b/src/plugins/unified_histogram/public/chart/chart.test.tsx index a2c5f3b195a21..b32979cc004e5 100644 --- a/src/plugins/unified_histogram/public/chart/chart.test.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.test.tsx @@ -233,6 +233,17 @@ describe('Chart', () => { expect(component.find(SuggestionSelector).exists()).toBeTruthy(); }); + it('should render the edit on the fly button when chart is visible and suggestions exist', async () => { + const component = await mountComponent({ + currentSuggestion: currentSuggestionMock, + allSuggestions: allSuggestionsMock, + isPlainRecord: true, + }); + expect( + component.find('[data-test-subj="unifiedHistogramEditFlyoutVisualization"]').exists() + ).toBeTruthy(); + }); + it('should render the save button when chart is visible and suggestions exist', async () => { const component = await mountComponent({ currentSuggestion: currentSuggestionMock, diff --git a/src/plugins/unified_histogram/public/chart/chart.tsx b/src/plugins/unified_histogram/public/chart/chart.tsx index f99d9b3de1cea..6b112f3578df9 100644 --- a/src/plugins/unified_histogram/public/chart/chart.tsx +++ b/src/plugins/unified_histogram/public/chart/chart.tsx @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { ReactElement, useMemo, useState } from 'react'; -import React, { memo } from 'react'; +import React, { ReactElement, useMemo, useState, useEffect, useCallback, memo } from 'react'; import { EuiButtonIcon, EuiContextMenu, @@ -18,6 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { Suggestion } from '@kbn/lens-plugin/public'; +import type { Datatable } from '@kbn/expressions-plugin/common'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; import type { LensEmbeddableInput } from '@kbn/lens-plugin/public'; import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; @@ -42,6 +42,7 @@ import { useTotalHits } from './hooks/use_total_hits'; import { useRequestParams } from './hooks/use_request_params'; import { useChartStyles } from './hooks/use_chart_styles'; import { useChartActions } from './hooks/use_chart_actions'; +import { useChartConfigPanel } from './hooks/use_chart_config_panel'; import { getLensAttributes } from './utils/get_lens_attributes'; import { useRefetch } from './hooks/use_refetch'; import { useEditVisualization } from './hooks/use_edit_visualization'; @@ -67,6 +68,7 @@ export interface ChartProps { disableTriggers?: LensEmbeddableInput['disableTriggers']; disabledActions?: LensEmbeddableInput['disabledActions']; input$?: UnifiedHistogramInput$; + lensTablesAdapter?: Record; onResetChartHeight?: () => void; onChartHiddenChange?: (chartHidden: boolean) => void; onTimeIntervalChange?: (timeInterval: string) => void; @@ -101,6 +103,7 @@ export function Chart({ disableTriggers, disabledActions, input$: originalInput$, + lensTablesAdapter, onResetChartHeight, onChartHiddenChange, onTimeIntervalChange, @@ -112,6 +115,7 @@ export function Chart({ onBrushEnd, }: ChartProps) { const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const { showChartOptionsPopover, chartRef, @@ -190,6 +194,7 @@ export function Chart({ histogramCss, breakdownFieldSelectorGroupCss, breakdownFieldSelectorItemCss, + suggestionsSelectorItemCss, chartToolButtonCss, } = useChartStyles(chartVisible); @@ -215,6 +220,34 @@ export function Chart({ ] ); + const ChartConfigPanel = useChartConfigPanel({ + services, + lensAttributesContext, + dataView, + lensTablesAdapter, + currentSuggestion, + isFlyoutVisible, + setIsFlyoutVisible, + isPlainRecord, + query: originalQuery, + onSuggestionChange, + }); + + const onSuggestionSelectorChange = useCallback( + (s: Suggestion | undefined) => { + onSuggestionChange?.(s); + }, + [onSuggestionChange] + ); + + useEffect(() => { + // close the flyout for dataview mode + // or if no chart is visible + if (!chartVisible && isFlyoutVisible) { + setIsFlyoutVisible(false); + } + }, [chartVisible, isFlyoutVisible]); + const onEditVisualization = useEditVisualization({ services, dataView, @@ -226,6 +259,24 @@ export function Chart({ const canSaveVisualization = chartVisible && currentSuggestion && services.capabilities.dashboard?.showWriteControls; + const renderEditButton = useMemo( + () => ( + setIsFlyoutVisible(true)} + data-test-subj="unifiedHistogramEditFlyoutVisualization" + aria-label={i18n.translate('unifiedHistogram.editVisualizationButton', { + defaultMessage: 'Edit visualization', + })} + disabled={isFlyoutVisible} + /> + ), + [isFlyoutVisible] + ); + + const canEditVisualizationOnTheFly = isPlainRecord && chartVisible; + return ( )} {chartVisible && currentSuggestion && allSuggestions && allSuggestions?.length > 1 && ( - + )} @@ -296,6 +348,21 @@ export function Chart({ )} + {canEditVisualizationOnTheFly && ( + + {!isFlyoutVisible ? ( + + {renderEditButton} + + ) : ( + renderEditButton + )} + + )} {onEditVisualization && ( )} + {isFlyoutVisible && ChartConfigPanel} ); } diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 61320c627bb13..63a0c9897b8c7 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -146,6 +146,7 @@ export function Histogram({ const chartCss = css` position: relative; flex-grow: 1; + margin-block: ${euiTheme.size.xs}; & > div { height: 100%; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.test.tsx b/src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.test.tsx new file mode 100644 index 0000000000000..5f0d4b17e80db --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-test-renderer'; +import { setTimeout } from 'timers/promises'; +import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; +import { unifiedHistogramServicesMock } from '../../__mocks__/services'; +import { lensTablesAdapterMock } from '../../__mocks__/lens_table_adapter'; +import { useChartConfigPanel } from './use_chart_config_panel'; +import type { LensAttributesContext } from '../utils/get_lens_attributes'; + +describe('useChartConfigPanel', () => { + it('should return a jsx element to edit the visualization', async () => { + const lensAttributes = { + visualizationType: 'lnsXY', + title: 'test', + } as TypedLensByValueInput['attributes']; + const hook = renderHook(() => + useChartConfigPanel({ + services: unifiedHistogramServicesMock, + dataView: dataViewWithTimefieldMock, + lensAttributesContext: { + attributes: lensAttributes, + } as unknown as LensAttributesContext, + isFlyoutVisible: true, + setIsFlyoutVisible: jest.fn(), + isPlainRecord: true, + lensTablesAdapter: lensTablesAdapterMock, + query: { + sql: 'Select * from test', + }, + }) + ); + await act(() => setTimeout(0)); + expect(hook.result.current).toBeDefined(); + expect(hook.result.current).not.toBeNull(); + }); + + it('should return null if not in text based mode', async () => { + const lensAttributes = { + visualizationType: 'lnsXY', + title: 'test', + } as TypedLensByValueInput['attributes']; + const hook = renderHook(() => + useChartConfigPanel({ + services: unifiedHistogramServicesMock, + dataView: dataViewWithTimefieldMock, + lensAttributesContext: { + attributes: lensAttributes, + } as unknown as LensAttributesContext, + isFlyoutVisible: true, + setIsFlyoutVisible: jest.fn(), + isPlainRecord: false, + }) + ); + await act(() => setTimeout(0)); + expect(hook.result.current).toBeNull(); + }); +}); diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.tsx b/src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.tsx new file mode 100644 index 0000000000000..99fb8fbb8fe31 --- /dev/null +++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_config_panel.tsx @@ -0,0 +1,101 @@ +/* + * 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, { useCallback, useEffect, useRef, useState } from 'react'; +import type { AggregateQuery, Query } from '@kbn/es-query'; +import { isEqual } from 'lodash'; +import type { Suggestion } from '@kbn/lens-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { Datatable } from '@kbn/expressions-plugin/common'; + +import type { UnifiedHistogramServices } from '../../types'; +import type { LensAttributesContext } from '../utils/get_lens_attributes'; + +export function useChartConfigPanel({ + services, + lensAttributesContext, + dataView, + lensTablesAdapter, + currentSuggestion, + isFlyoutVisible, + setIsFlyoutVisible, + isPlainRecord, + query, + onSuggestionChange, +}: { + services: UnifiedHistogramServices; + lensAttributesContext: LensAttributesContext; + dataView: DataView; + isFlyoutVisible: boolean; + setIsFlyoutVisible: (flag: boolean) => void; + lensTablesAdapter?: Record; + currentSuggestion?: Suggestion; + isPlainRecord?: boolean; + query?: Query | AggregateQuery; + onSuggestionChange?: (suggestion: Suggestion | undefined) => void; +}) { + const [editLensConfigPanel, setEditLensConfigPanel] = useState(null); + const previousSuggestion = useRef(undefined); + const previousAdapters = useRef | undefined>(undefined); + const previousQuery = useRef(undefined); + const updateSuggestion = useCallback( + (datasourceState, visualizationState) => { + const updatedSuggestion = { + ...currentSuggestion, + ...(datasourceState && { datasourceState }), + ...(visualizationState && { visualizationState }), + } as Suggestion; + onSuggestionChange?.(updatedSuggestion); + }, + [currentSuggestion, onSuggestionChange] + ); + + useEffect(() => { + const dataHasChanged = + Boolean(lensTablesAdapter) && + !isEqual(previousAdapters.current, lensTablesAdapter) && + query !== previousQuery?.current; + async function fetchLensConfigComponent() { + const Component = await services.lens.EditLensConfigPanelApi(); + const panel = ( + + ); + setEditLensConfigPanel(panel); + previousSuggestion.current = currentSuggestion; + previousAdapters.current = lensTablesAdapter; + if (dataHasChanged) { + previousQuery.current = query; + } + } + const suggestionHasChanged = currentSuggestion?.title !== previousSuggestion?.current?.title; + // rerender the component if the data has changed or the suggestion + // as I can have different suggestions for the same data + if (isPlainRecord && (dataHasChanged || suggestionHasChanged || !isFlyoutVisible)) { + fetchLensConfigComponent(); + } + }, [ + lensAttributesContext.attributes, + services.lens, + dataView, + updateSuggestion, + isPlainRecord, + currentSuggestion, + query, + isFlyoutVisible, + lensTablesAdapter, + setIsFlyoutVisible, + ]); + + return isPlainRecord ? editLensConfigPanel : null; +} diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx b/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx index c019c7cef981e..13b527be702c1 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx +++ b/src/plugins/unified_histogram/public/chart/hooks/use_chart_styles.tsx @@ -56,6 +56,11 @@ export const useChartStyles = (chartVisible: boolean) => { align-items: flex-end; padding-left: ${euiTheme.size.s}; `; + const suggestionsSelectorItemCss = css` + min-width: 0; + align-items: flex-start; + padding-left: ${euiTheme.size.s}; + `; const chartToolButtonCss = css` display: flex; justify-content: center; @@ -70,6 +75,7 @@ export const useChartStyles = (chartVisible: boolean) => { histogramCss, breakdownFieldSelectorGroupCss, breakdownFieldSelectorItemCss, + suggestionsSelectorItemCss, chartToolButtonCss, }; }; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.test.ts b/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.test.ts index 8bd9e9161c837..ec213dd53e141 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.test.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.test.ts @@ -81,6 +81,21 @@ describe('useEditVisualization', () => { expect(hook.result.current).toBeUndefined(); }); + it('should return undefined if is on text based mode', async () => { + getTriggerCompatibleActions.mockReturnValue(Promise.resolve([{ id: 'test' }])); + const hook = renderHook(() => + useEditVisualization({ + services: unifiedHistogramServicesMock, + dataView: dataViewWithTimefieldMock, + relativeTimeRange: { from: 'now-15m', to: 'now' }, + lensAttributes: {} as unknown as TypedLensByValueInput['attributes'], + isPlainRecord: true, + }) + ); + await act(() => setTimeout(0)); + expect(hook.result.current).toBeUndefined(); + }); + it('should return undefined if the time field is not visualizable', async () => { getTriggerCompatibleActions.mockReturnValue(Promise.resolve([{ id: 'test' }])); const dataView = { diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts b/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts index e681fd34cd91e..b02732bfcbfc9 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_edit_visualization.ts @@ -32,10 +32,10 @@ export const useEditVisualization = ({ const [canVisualize, setCanVisualize] = useState(false); const checkCanVisualize = useCallback(async () => { - if (!dataView.id) { + if (!dataView.id || isPlainRecord) { return false; } - if (!isPlainRecord && (!dataView.isTimeBased() || !dataView.getTimeField().visualizable)) { + if (!dataView.isTimeBased() || !dataView.getTimeField().visualizable) { return false; } diff --git a/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx b/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx index 85860cf2c9bc8..0196387633396 100644 --- a/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/suggestion_selector.tsx @@ -67,7 +67,7 @@ export const SuggestionSelector = ({ const { euiTheme } = useEuiTheme(); const suggestionComboCss = css` width: 100%; - max-width: ${euiTheme.base * 22}px; + max-width: ${euiTheme.base * 15}px; `; return ( @@ -78,9 +78,7 @@ export const SuggestionSelector = ({ > } placeholder={i18n.translate('unifiedHistogram.suggestionSelectorPlaceholder', { defaultMessage: 'Select visualization', })} @@ -88,9 +86,9 @@ export const SuggestionSelector = ({ options={suggestionOptions} selectedOptions={selectedSuggestion} onChange={onSelectionChange} - compressed fullWidth={true} isClearable={false} + compressed onFocus={disableFieldPopover} onBlur={enableFieldPopover} renderOption={(option) => { diff --git a/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts b/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts index fcdd194410db0..c0eeb9448eee7 100644 --- a/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts +++ b/src/plugins/unified_histogram/public/container/hooks/use_state_props.test.ts @@ -15,6 +15,7 @@ import { UnifiedHistogramFetchStatus } from '../../types'; import { dataViewMock } from '../../__mocks__/data_view'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { currentSuggestionMock } from '../../__mocks__/suggestions'; +import { lensTablesAdapterMock } from '../../__mocks__/lens_table_adapter'; import { unifiedHistogramServicesMock } from '../../__mocks__/services'; import { createStateService, @@ -28,6 +29,7 @@ describe('useStateProps', () => { breakdownField: 'bytes', chartHidden: false, lensRequestAdapter: new RequestAdapter(), + lensTablesAdapter: lensTablesAdapterMock, timeInterval: 'auto', topPanelHeight: 100, totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized, @@ -82,6 +84,37 @@ describe('useStateProps', () => { "total": undefined, }, "isPlainRecord": false, + "lensTablesAdapter": Object { + "default": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + }, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], @@ -126,6 +159,37 @@ describe('useStateProps', () => { "total": undefined, }, "isPlainRecord": true, + "lensTablesAdapter": Object { + "default": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + }, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], @@ -191,6 +255,37 @@ describe('useStateProps', () => { "total": undefined, }, "isPlainRecord": false, + "lensTablesAdapter": Object { + "default": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + }, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], @@ -232,6 +327,37 @@ describe('useStateProps', () => { "total": undefined, }, "isPlainRecord": false, + "lensTablesAdapter": Object { + "default": Object { + "columns": Array [ + Object { + "id": "col-0-1", + "meta": Object { + "dimensionName": "Slice size", + "type": "number", + }, + "name": "Field 1", + }, + Object { + "id": "col-0-2", + "meta": Object { + "dimensionName": "Slice", + "type": "number", + }, + "name": "Field 2", + }, + ], + "rows": Array [ + Object { + "col-0-1": 0, + "col-0-2": 0, + "col-0-3": 0, + "col-0-4": 0, + }, + ], + "type": "datatable", + }, + }, "onBreakdownFieldChange": [Function], "onChartHiddenChange": [Function], "onChartLoad": [Function], diff --git a/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts b/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts index b9c4570cdecb9..a5845731cf12e 100644 --- a/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts +++ b/src/plugins/unified_histogram/public/container/hooks/use_state_props.ts @@ -23,6 +23,7 @@ import { timeIntervalSelector, totalHitsResultSelector, totalHitsStatusSelector, + lensTablesAdapterSelector, } from '../utils/state_selectors'; import { useStateSelector } from '../utils/use_state_selector'; @@ -44,7 +45,7 @@ export const useStateProps = ({ const timeInterval = useStateSelector(stateService?.state$, timeIntervalSelector); const totalHitsResult = useStateSelector(stateService?.state$, totalHitsResultSelector); const totalHitsStatus = useStateSelector(stateService?.state$, totalHitsStatusSelector); - + const lensTablesAdapter = useStateSelector(stateService?.state$, lensTablesAdapterSelector); /** * Contexts */ @@ -139,6 +140,7 @@ export const useStateProps = ({ (event: UnifiedHistogramChartLoadEvent) => { // We need to store the Lens request adapter in order to inspect its requests stateService?.setLensRequestAdapter(event.adapters.requests); + stateService?.setLensTablesAdapter(event.adapters.tables?.tables); }, [stateService] ); @@ -174,6 +176,7 @@ export const useStateProps = ({ breakdown, request, isPlainRecord, + lensTablesAdapter, onTopPanelHeightChange, onTimeIntervalChange, onTotalHitsChange, diff --git a/src/plugins/unified_histogram/public/container/services/state_service.test.ts b/src/plugins/unified_histogram/public/container/services/state_service.test.ts index eb839e6eaba0f..eb7232e889037 100644 --- a/src/plugins/unified_histogram/public/container/services/state_service.test.ts +++ b/src/plugins/unified_histogram/public/container/services/state_service.test.ts @@ -9,6 +9,7 @@ import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { UnifiedHistogramFetchStatus } from '../..'; import { unifiedHistogramServicesMock } from '../../__mocks__/services'; +import { lensTablesAdapterMock } from '../../__mocks__/lens_table_adapter'; import { getChartHidden, getTopPanelHeight, @@ -46,6 +47,7 @@ describe('UnifiedHistogramStateService', () => { breakdownField: 'bytes', chartHidden: false, lensRequestAdapter: new RequestAdapter(), + lensTablesAdapter: lensTablesAdapterMock, timeInterval: 'auto', topPanelHeight: 100, totalHitsStatus: UnifiedHistogramFetchStatus.uninitialized, @@ -134,6 +136,8 @@ describe('UnifiedHistogramStateService', () => { expect(state).toEqual(newState); stateService.setLensRequestAdapter(undefined); newState = { ...newState, lensRequestAdapter: undefined }; + stateService.setLensTablesAdapter(undefined); + newState = { ...newState, lensTablesAdapter: undefined }; expect(state).toEqual(newState); stateService.setTotalHits({ totalHitsStatus: UnifiedHistogramFetchStatus.complete, diff --git a/src/plugins/unified_histogram/public/container/services/state_service.ts b/src/plugins/unified_histogram/public/container/services/state_service.ts index 01d1a7f0c3f60..1fd0905d86c7a 100644 --- a/src/plugins/unified_histogram/public/container/services/state_service.ts +++ b/src/plugins/unified_histogram/public/container/services/state_service.ts @@ -8,6 +8,7 @@ import type { RequestAdapter } from '@kbn/inspector-plugin/common'; import type { Suggestion } from '@kbn/lens-plugin/public'; +import type { Datatable } from '@kbn/expressions-plugin/common'; import { BehaviorSubject, Observable } from 'rxjs'; import { UnifiedHistogramFetchStatus } from '../..'; import type { UnifiedHistogramServices } from '../../types'; @@ -40,6 +41,10 @@ export interface UnifiedHistogramState { * The current Lens request adapter */ lensRequestAdapter: RequestAdapter | undefined; + /** + * The current Lens request table + */ + lensTablesAdapter?: Record; /** * The current time interval of the chart */ @@ -108,6 +113,10 @@ export interface UnifiedHistogramStateService { * Sets the current Lens request adapter */ setLensRequestAdapter: (lensRequestAdapter: RequestAdapter | undefined) => void; + /** + * Sets the current Lens tables + */ + setLensTablesAdapter: (lensTablesAdapter: Record | undefined) => void; /** * Sets the current total hits status and result */ @@ -190,6 +199,10 @@ export const createStateService = ( updateState({ lensRequestAdapter }); }, + setLensTablesAdapter: (lensTablesAdapter: Record | undefined) => { + updateState({ lensTablesAdapter }); + }, + setTotalHits: (totalHits: { totalHitsStatus: UnifiedHistogramFetchStatus; totalHitsResult: number | Error | undefined; diff --git a/src/plugins/unified_histogram/public/container/utils/state_selectors.ts b/src/plugins/unified_histogram/public/container/utils/state_selectors.ts index d11fb1182cc45..80e809f4fc38f 100644 --- a/src/plugins/unified_histogram/public/container/utils/state_selectors.ts +++ b/src/plugins/unified_histogram/public/container/utils/state_selectors.ts @@ -15,3 +15,4 @@ export const topPanelHeightSelector = (state: UnifiedHistogramState) => state.to export const totalHitsResultSelector = (state: UnifiedHistogramState) => state.totalHitsResult; export const totalHitsStatusSelector = (state: UnifiedHistogramState) => state.totalHitsStatus; export const currentSuggestionSelector = (state: UnifiedHistogramState) => state.currentSuggestion; +export const lensTablesAdapterSelector = (state: UnifiedHistogramState) => state.lensTablesAdapter; diff --git a/src/plugins/unified_histogram/public/layout/layout.tsx b/src/plugins/unified_histogram/public/layout/layout.tsx index 3c5b021e0b08d..cd6d2ffb5ec07 100644 --- a/src/plugins/unified_histogram/public/layout/layout.tsx +++ b/src/plugins/unified_histogram/public/layout/layout.tsx @@ -11,6 +11,7 @@ import { PropsWithChildren, ReactElement, RefObject } from 'react'; import React, { useMemo } from 'react'; import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { css } from '@emotion/css'; +import type { Datatable } from '@kbn/expressions-plugin/common'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import type { LensEmbeddableInput, LensSuggestionsApi, Suggestion } from '@kbn/lens-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; @@ -77,6 +78,7 @@ export interface UnifiedHistogramLayoutProps extends PropsWithChildren * Context object for the hits count -- leave undefined to hide the hits count */ hits?: UnifiedHistogramHitsContext; + lensTablesAdapter?: Record; /** * Context object for the chart -- leave undefined to hide the chart */ @@ -169,6 +171,7 @@ export const UnifiedHistogramLayout = ({ columns, request, hits, + lensTablesAdapter, chart: originalChart, breakdown, resizeRef, @@ -273,6 +276,7 @@ export const UnifiedHistogramLayout = ({ onChartLoad={onChartLoad} onFilter={onFilter} onBrushEnd={onBrushEnd} + lensTablesAdapter={lensTablesAdapter} /> {children} diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx new file mode 100644 index 0000000000000..0eb71d6f6e1d7 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx @@ -0,0 +1,129 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { EuiFlyout, EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Provider } from 'react-redux'; +import { PreloadedState } from '@reduxjs/toolkit'; +import { css } from '@emotion/react'; +import type { CoreStart } from '@kbn/core/public'; +import type { LensPluginStartDependencies } from '../../../plugin'; +import { + makeConfigureStore, + LensRootStore, + LensAppState, + LensState, +} from '../../../state_management'; +import { getPreloadedState } from '../../../state_management/lens_slice'; + +import type { DatasourceMap, VisualizationMap } from '../../../types'; +import { + LensEditConfigurationFlyout, + type EditConfigPanelProps, +} from './lens_configuration_flyout'; +import type { LensAppServices } from '../../types'; + +export type EditLensConfigurationProps = Omit< + EditConfigPanelProps, + 'startDependencies' | 'coreStart' | 'visualizationMap' | 'datasourceMap' +>; + +function LoadingSpinnerWithOverlay() { + return ( + + + + ); +} + +export function getEditLensConfiguration( + coreStart: CoreStart, + startDependencies: LensPluginStartDependencies, + visualizationMap?: VisualizationMap, + datasourceMap?: DatasourceMap +) { + return ({ + attributes, + dataView, + updateAll, + setIsFlyoutVisible, + datasourceId, + adaptersTables, + }: EditLensConfigurationProps) => { + const [lensServices, setLensServices] = useState(); + useEffect(() => { + async function loadLensService() { + const { getLensServices, getLensAttributeService } = await import( + '../../../async_services' + ); + const lensServicesT = await getLensServices( + coreStart, + startDependencies, + getLensAttributeService(coreStart, startDependencies) + ); + + setLensServices(lensServicesT); + } + loadLensService(); + }, []); + + if (!lensServices || !datasourceMap || !visualizationMap || !dataView.id) { + return ; + } + const datasourceState = attributes.state.datasourceStates[datasourceId]; + const storeDeps = { + lensServices, + datasourceMap, + visualizationMap, + initialContext: + datasourceState && 'initialContext' in datasourceState + ? datasourceState.initialContext + : undefined, + }; + const lensStore: LensRootStore = makeConfigureStore(storeDeps, { + lens: getPreloadedState(storeDeps) as LensAppState, + } as unknown as PreloadedState); + const closeFlyout = () => { + setIsFlyoutVisible?.(false); + }; + + const configPanelProps = { + attributes, + dataView, + updateAll, + setIsFlyoutVisible, + datasourceId, + adaptersTables, + coreStart, + startDependencies, + visualizationMap, + datasourceMap, + }; + + return ( + + + + + + ); + }; +} diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx new file mode 100644 index 0000000000000..dc05ff1577382 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.test.tsx @@ -0,0 +1,433 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlyoutBody } from '@elastic/eui'; +import { mountWithProvider } from '../../../mocks'; +import type { Query, AggregateQuery } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { + mockVisualizationMap, + mockDatasourceMap, + mockStoreDeps, + mockDataPlugin, +} from '../../../mocks'; +import type { LensPluginStartDependencies } from '../../../plugin'; +import { createMockStartDependencies } from '../../../editor_frame_service/mocks'; +import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; +import { VisualizationToolbar } from '../../../editor_frame_service/editor_frame/workspace_panel'; +import { ConfigPanelWrapper } from '../../../editor_frame_service/editor_frame/config_panel/config_panel'; +import { + LensEditConfigurationFlyout, + type EditConfigPanelProps, +} from './lens_configuration_flyout'; + +let container: HTMLDivElement | undefined; + +beforeEach(() => { + container = document.createElement('div'); + container.id = 'lensContainer'; + document.body.appendChild(container); +}); + +afterEach(() => { + if (container && container.parentNode) { + container.parentNode.removeChild(container); + } + + container = undefined; +}); + +describe('LensEditConfigurationFlyout', () => { + const mockStartDependencies = + createMockStartDependencies() as unknown as LensPluginStartDependencies; + const data = mockDataPlugin(); + (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + const startDependencies = { + ...mockStartDependencies, + data, + }; + + function prepareAndMountComponent( + props: ReturnType, + query?: Query | AggregateQuery + ) { + return mountWithProvider( + , + { + preloadedState: { + datasourceStates: { + testDatasource: { + isLoading: false, + state: 'state', + }, + }, + activeDatasourceId: 'testDatasource', + query: query as Query, + }, + storeDeps: mockStoreDeps({ + datasourceMap: props.datasourceMap, + visualizationMap: props.visualizationMap, + }), + }, + { + attachTo: container, + } + ); + } + + function getDefaultProps( + { datasourceMap = mockDatasourceMap(), visualizationMap = mockVisualizationMap() } = { + datasourceMap: mockDatasourceMap(), + visualizationMap: mockVisualizationMap(), + } + ) { + const lensAttributes = { + title: 'test', + visualizationType: 'testVis', + state: { + datasourceStates: { + testDatasource: {}, + }, + visualization: {}, + filters: [], + query: { + language: 'lucene', + query: '', + }, + }, + filters: [], + query: { + language: 'lucene', + query: '', + }, + references: [], + } as unknown as TypedLensByValueInput['attributes']; + + const dataView = { id: 'index1', isPersisted: () => true } as unknown as DataView; + return { + attributes: lensAttributes, + dataView, + updateAll: jest.fn(), + coreStart: coreMock.createStart(), + startDependencies, + visualizationMap, + datasourceMap, + setIsFlyoutVisible: jest.fn(), + datasourceId: 'testDatasource', + } as unknown as EditConfigPanelProps; + } + + it('should call the setIsFlyout callback if collapse button is clicked', async () => { + const setIsFlyoutVisibleSpy = jest.fn(); + const props = getDefaultProps(); + const newProps = { + ...props, + setIsFlyoutVisible: setIsFlyoutVisibleSpy, + }; + const { instance } = await prepareAndMountComponent(newProps); + expect(instance.find(EuiFlyoutBody).exists()).toBe(true); + instance.find('[data-test-subj="collapseFlyoutButton"]').at(1).simulate('click'); + expect(setIsFlyoutVisibleSpy).toHaveBeenCalled(); + }); + + it('should compute the frame public api correctly', async () => { + const props = getDefaultProps(); + const { instance } = await prepareAndMountComponent(props); + expect(instance.find(ConfigPanelWrapper).exists()).toBe(true); + expect(instance.find(VisualizationToolbar).exists()).toBe(true); + expect(instance.find(VisualizationToolbar).prop('framePublicAPI')).toMatchInlineSnapshot(` + Object { + "activeData": Object {}, + "dataViews": Object { + "indexPatternRefs": Array [], + "indexPatterns": Object { + "index1": Object { + "id": "index1", + "isPersisted": [Function], + }, + }, + }, + "datasourceLayers": Object { + "a": Object { + "datasourceId": "testDatasource", + "getFilters": [MockFunction], + "getMaxPossibleNumValues": [MockFunction], + "getOperationForColumnId": [MockFunction], + "getSourceId": [MockFunction], + "getTableSpec": [MockFunction], + "getVisualDefaults": [MockFunction], + "hasDefaultTimeField": [MockFunction], + "isTextBasedLanguage": [MockFunction] { + "calls": Array [ + Array [], + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + ], + }, + }, + }, + "dateRange": Object { + "fromDate": "2021-01-10T04:00:00.000Z", + "toDate": "2021-01-10T08:00:00.000Z", + }, + } + `); + }); + + it('should compute the activeVisualization correctly', async () => { + const props = getDefaultProps(); + const { instance } = await prepareAndMountComponent(props); + expect(instance.find(VisualizationToolbar).prop('activeVisualization')).toMatchInlineSnapshot(` + Object { + "appendLayer": [MockFunction], + "clearLayer": [MockFunction], + "getConfiguration": [MockFunction] { + "calls": Array [ + Array [ + Object { + "frame": Object { + "activeData": Object {}, + "dataViews": Object { + "indexPatternRefs": Array [], + "indexPatterns": Object { + "index1": Object { + "id": "index1", + "isPersisted": [Function], + }, + }, + }, + "datasourceLayers": Object { + "a": Object { + "datasourceId": "testDatasource", + "getFilters": [MockFunction], + "getMaxPossibleNumValues": [MockFunction], + "getOperationForColumnId": [MockFunction], + "getSourceId": [MockFunction], + "getTableSpec": [MockFunction], + "getVisualDefaults": [MockFunction], + "hasDefaultTimeField": [MockFunction], + "isTextBasedLanguage": [MockFunction] { + "calls": Array [ + Array [], + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + ], + }, + }, + }, + "dateRange": Object { + "fromDate": "2021-01-10T04:00:00.000Z", + "toDate": "2021-01-10T08:00:00.000Z", + }, + }, + "layerId": "layer1", + "state": Object {}, + }, + ], + Array [ + Object { + "frame": Object { + "activeData": Object {}, + "dataViews": Object { + "indexPatternRefs": Array [], + "indexPatterns": Object { + "index1": Object { + "id": "index1", + "isPersisted": [Function], + }, + }, + }, + "datasourceLayers": Object { + "a": Object { + "datasourceId": "testDatasource", + "getFilters": [MockFunction], + "getMaxPossibleNumValues": [MockFunction], + "getOperationForColumnId": [MockFunction], + "getSourceId": [MockFunction], + "getTableSpec": [MockFunction], + "getVisualDefaults": [MockFunction], + "hasDefaultTimeField": [MockFunction], + "isTextBasedLanguage": [MockFunction] { + "calls": Array [ + Array [], + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": false, + }, + Object { + "type": "return", + "value": false, + }, + ], + }, + }, + }, + "dateRange": Object { + "fromDate": "2021-01-10T04:00:00.000Z", + "toDate": "2021-01-10T08:00:00.000Z", + }, + }, + "layerId": "layer1", + "state": Object {}, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Object { + "groups": Array [ + Object { + "accessors": Array [], + "dataTestSubj": "mockVisA", + "filterOperations": [MockFunction], + "groupId": "a", + "groupLabel": "a", + "layerId": "layer1", + "supportsMoreColumns": true, + }, + ], + }, + }, + Object { + "type": "return", + "value": Object { + "groups": Array [ + Object { + "accessors": Array [], + "dataTestSubj": "mockVisA", + "filterOperations": [MockFunction], + "groupId": "a", + "groupLabel": "a", + "layerId": "layer1", + "supportsMoreColumns": true, + }, + ], + }, + }, + ], + }, + "getDescription": [MockFunction] { + "calls": Array [ + Array [ + Object {}, + ], + Array [ + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Object { + "label": "", + }, + }, + Object { + "type": "return", + "value": Object { + "label": "", + }, + }, + ], + }, + "getLayerIds": [MockFunction] { + "calls": Array [ + Array [ + Object {}, + ], + Array [ + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Array [ + "layer1", + ], + }, + Object { + "type": "return", + "value": Array [ + "layer1", + ], + }, + ], + }, + "getLayerType": [MockFunction] { + "calls": Array [ + Array [ + "layer1", + Object {}, + ], + Array [ + "layer1", + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": "data", + }, + Object { + "type": "return", + "value": "data", + }, + ], + }, + "getRenderEventCounters": [MockFunction], + "getSuggestions": [MockFunction], + "getSupportedLayers": [MockFunction], + "getVisualizationTypeId": [MockFunction], + "id": "testVis", + "initialize": [MockFunction], + "removeDimension": [MockFunction], + "removeLayer": [MockFunction], + "renderDimensionEditor": [MockFunction], + "setDimension": [MockFunction], + "switchVisualizationType": [MockFunction], + "toExpression": [MockFunction], + "toPreviewExpression": [MockFunction], + "visualizationTypes": Array [ + Object { + "groupLabel": "testVisGroup", + "icon": "empty", + "id": "testVis", + "label": "TEST", + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx new file mode 100644 index 0000000000000..9fe486c58048a --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -0,0 +1,174 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { + EuiButtonEmpty, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + useEuiTheme, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; +import type { CoreStart } from '@kbn/core/public'; +import type { Datatable } from '@kbn/expressions-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { getResolvedDateRange } from '../../../utils'; +import type { LensPluginStartDependencies } from '../../../plugin'; +import { + DataViewsState, + useLensDispatch, + updateStateFromSuggestion, +} from '../../../state_management'; +import { VisualizationToolbar } from '../../../editor_frame_service/editor_frame/workspace_panel'; + +import type { DatasourceMap, VisualizationMap, DatasourceLayers } from '../../../types'; +import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; +import { ConfigPanelWrapper } from '../../../editor_frame_service/editor_frame/config_panel/config_panel'; + +export interface EditConfigPanelProps { + attributes: TypedLensByValueInput['attributes']; + dataView: DataView; + updateAll: (datasourceState: unknown, visualizationState: unknown) => void; + coreStart: CoreStart; + startDependencies: LensPluginStartDependencies; + visualizationMap: VisualizationMap; + datasourceMap: DatasourceMap; + setIsFlyoutVisible?: (flag: boolean) => void; + datasourceId: 'formBased' | 'textBased'; + adaptersTables?: Record; +} + +export function LensEditConfigurationFlyout({ + attributes, + dataView, + coreStart, + startDependencies, + visualizationMap, + datasourceMap, + datasourceId, + updateAll, + setIsFlyoutVisible, + adaptersTables, +}: EditConfigPanelProps) { + const currentDataViewId = dataView.id ?? ''; + const datasourceState = attributes.state.datasourceStates[datasourceId]; + const activeVisualization = visualizationMap[attributes.visualizationType]; + const activeDatasource = datasourceMap[datasourceId]; + const dispatchLens = useLensDispatch(); + const { euiTheme } = useEuiTheme(); + const dataViews = useMemo(() => { + return { + indexPatterns: { + [currentDataViewId]: dataView, + }, + indexPatternRefs: [], + } as unknown as DataViewsState; + }, [currentDataViewId, dataView]); + dispatchLens( + updateStateFromSuggestion({ + newDatasourceId: datasourceId, + visualizationId: activeVisualization.id, + visualizationState: attributes.state.visualization, + datasourceState, + dataViews, + }) + ); + + const datasourceLayers: DatasourceLayers = useMemo(() => { + return {}; + }, []); + const activeData: Record = useMemo(() => { + return {}; + }, []); + const layers = activeDatasource.getLayers(datasourceState); + layers.forEach((layer) => { + datasourceLayers[layer] = datasourceMap[datasourceId].getPublicAPI({ + state: datasourceState, + layerId: layer, + indexPatterns: dataViews.indexPatterns, + }); + if (adaptersTables) { + activeData[layer] = Object.values(adaptersTables)[0]; + } + }); + + const dateRange = getResolvedDateRange(startDependencies.data.query.timefilter.timefilter); + const framePublicAPI = useMemo(() => { + return { + activeData, + dataViews, + datasourceLayers, + dateRange, + }; + }, [activeData, dataViews, datasourceLayers, dateRange]); + + const closeFlyout = () => { + setIsFlyoutVisible?.(false); + }; + + const layerPanelsProps = { + framePublicAPI, + datasourceMap, + visualizationMap, + core: coreStart, + dataViews: startDependencies.dataViews, + uiActions: startDependencies.uiActions, + hideLayerHeader: true, + onUpdateStateCb: updateAll, + }; + return ( + <> + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/lens/public/async_services.ts b/x-pack/plugins/lens/public/async_services.ts index 38a904c5617c9..d4c6fe5be8dcd 100644 --- a/x-pack/plugins/lens/public/async_services.ts +++ b/x-pack/plugins/lens/public/async_services.ts @@ -30,6 +30,7 @@ export * from './visualizations/gauge/gauge_visualization'; export * from './visualizations/gauge'; export * from './visualizations/tagcloud/tagcloud_visualization'; export * from './visualizations/tagcloud'; +export { getEditLensConfiguration } from './app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration'; export * from './datasources/form_based/form_based'; export { getTextBasedDatasource } from './datasources/text_based/text_based_languages'; diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts index 42b405c939d3c..d12505e93f07a 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.test.ts @@ -410,6 +410,22 @@ describe('Textbased Data Source', () => { ); expect(suggestions[0].state).toEqual({ ...state, + fieldList: [ + { + id: 'newid', + meta: { + type: 'number', + }, + name: 'bytes', + }, + { + id: 'newid', + meta: { + type: 'string', + }, + name: 'dest', + }, + ], layers: { newid: { allColumns: [ diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index 573137da1ebc2..653a3f30e1b44 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -122,6 +122,14 @@ export function getTextBasedDatasource({ const query = context.query; const updatedState = { ...state, + fieldList: + newColumns?.map((c) => { + return { + id: c.columnId, + name: c.fieldName, + meta: c.meta, + }; + }) ?? [], layers: { ...state.layers, [newLayerId]: { diff --git a/x-pack/plugins/lens/public/datasources/text_based/types.ts b/x-pack/plugins/lens/public/datasources/text_based/types.ts index 0594fdcf2fbc2..544996c904b77 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/types.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/types.ts @@ -31,12 +31,12 @@ export interface TextBasedLayer { export interface TextBasedPersistedState { layers: Record; + initialContext?: VisualizeFieldContext | VisualizeEditorContext; } export type TextBasedPrivateState = TextBasedPersistedState & { indexPatternRefs: IndexPatternRef[]; fieldList: DatatableColumn[]; - initialContext?: VisualizeFieldContext | VisualizeEditorContext; }; export interface IndexPatternRef { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 78f7246c52e6d..f9e09143d3e43 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -170,13 +170,19 @@ describe('ConfigPanel', () => { it('allow datasources and visualizations to use setters', async () => { const props = getDefaultProps(); - const { instance, lensStore } = await prepareAndMountComponent(props); + const onUpdateCbSpy = jest.fn(); + const newProps = { + ...props, + onUpdateStateCb: onUpdateCbSpy, + }; + const { instance, lensStore } = await prepareAndMountComponent(newProps); const { updateDatasource, updateAll } = instance.find(LayerPanel).props(); const updater = () => 'updated'; updateDatasource('testDatasource', updater); await waitMs(0); expect(lensStore.dispatch).toHaveBeenCalledTimes(1); + expect(onUpdateCbSpy).toHaveBeenCalled(); expect( (lensStore.dispatch as jest.Mock).mock.calls[0][0].payload.updater( props.datasourceStates.testDatasource.state @@ -184,6 +190,7 @@ describe('ConfigPanel', () => { ).toEqual('updated'); updateAll('testDatasource', updater, props.visualizationState); + expect(onUpdateCbSpy).toHaveBeenCalled(); // wait for one tick so async updater has a chance to trigger await waitMs(0); expect(lensStore.dispatch).toHaveBeenCalledTimes(2); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index af1549e00cc30..571fc5194d5e3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -6,6 +6,7 @@ */ import React, { useMemo, memo, useCallback } from 'react'; +import { useStore } from 'react-redux'; import { EuiForm } from '@elastic/eui'; import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import { isOfAggregateQueryType } from '@kbn/es-query'; @@ -52,7 +53,8 @@ export function LayerPanels( activeVisualization: Visualization; } ) { - const { activeVisualization, datasourceMap, indexPatternService } = props; + const lensStore = useStore(); + const { activeVisualization, datasourceMap, indexPatternService, onUpdateStateCb } = props; const { activeDatasourceId, visualization, datasourceStates, query } = useLensSelector( (state) => state.lens ); @@ -74,8 +76,12 @@ export function LayerPanels( newState, }) ); + if (onUpdateStateCb && activeDatasourceId) { + const dsState = datasourceStates[activeDatasourceId].state; + onUpdateStateCb?.(dsState, newState); + } }, - [activeVisualization, dispatchLens] + [activeDatasourceId, activeVisualization.id, datasourceStates, dispatchLens, onUpdateStateCb] ); const updateDatasource = useMemo( () => @@ -90,9 +96,10 @@ export function LayerPanels( dontSyncLinkedDimensions, }) ); + onUpdateStateCb?.(newState, visualization.state); } }, - [dispatchLens] + [dispatchLens, onUpdateStateCb, visualization.state] ); const updateDatasourceAsync = useMemo( () => (datasourceId: string | undefined, newState: unknown) => { @@ -147,9 +154,10 @@ export function LayerPanels( }, }) ); + onUpdateStateCb?.(newDatasourceState, newVisualizationState); }, 0); }, - [dispatchLens] + [dispatchLens, onUpdateStateCb] ); const toggleFullscreen = useMemo( @@ -213,20 +221,21 @@ export function LayerPanels( visualizationId?: string; layerId?: string; }) => { - const indexPatterns = await props.indexPatternService.ensureIndexPattern({ + const indexPatterns = await props.indexPatternService?.ensureIndexPattern({ id: indexPatternId, cache: props.framePublicAPI.dataViews.indexPatterns, }); - - dispatchLens( - changeIndexPattern({ - indexPatternId, - datasourceIds: datasourceId ? [datasourceId] : [], - visualizationIds: visualizationId ? [visualizationId] : [], - layerId, - dataViews: { indexPatterns }, - }) - ); + if (indexPatterns) { + dispatchLens( + changeIndexPattern({ + indexPatternId, + datasourceIds: datasourceId ? [datasourceId] : [], + visualizationIds: visualizationId ? [visualizationId] : [], + layerId, + dataViews: { indexPatterns }, + }) + ); + } }, [dispatchLens, props.framePublicAPI.dataViews.indexPatterns, props.indexPatternService] ); @@ -262,6 +271,7 @@ export function LayerPanels( updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateDatasourceAsync={updateDatasourceAsync} + displayLayerSettings={!props.hideLayerHeader} onChangeIndexPattern={(args) => { onChangeIndexPattern(args); const layersToRemove = @@ -307,6 +317,13 @@ export function LayerPanels( const datasourcePublicAPI = props.framePublicAPI.datasourceLayers?.[layerId]; const datasourceId = datasourcePublicAPI?.datasourceId; dispatchLens(removeDimension({ ...dimensionProps, datasourceId })); + if (datasourceId && onUpdateStateCb) { + const newState = lensStore.getState().lens; + onUpdateStateCb( + newState.datasourceStates[datasourceId].state, + newState.visualization.state + ); + } }} toggleFullscreen={toggleFullscreen} indexPatternService={indexPatternService} @@ -336,19 +353,21 @@ export function LayerPanels( indexPatternId = dataView.id; } - const newIndexPatterns = await indexPatternService.ensureIndexPattern({ + const newIndexPatterns = await indexPatternService?.ensureIndexPattern({ id: indexPatternId, cache: props.framePublicAPI.dataViews.indexPatterns, }); - dispatchLens( - changeIndexPattern({ - dataViews: { indexPatterns: newIndexPatterns }, - datasourceIds: Object.keys(datasourceStates), - visualizationIds: visualization.activeId ? [visualization.activeId] : [], - indexPatternId, - }) - ); + if (newIndexPatterns) { + dispatchLens( + changeIndexPattern({ + dataViews: { indexPatterns: newIndexPatterns }, + datasourceIds: Object.keys(datasourceStates), + visualizationIds: visualization.activeId ? [visualization.activeId] : [], + indexPatternId, + }) + ); + } }, registerLibraryAnnotationGroup: (groupInfo) => dispatchLens(registerLibraryAnnotationGroup(groupInfo)), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 248681717b082..2b8568bd12908 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -11,6 +11,7 @@ import { EuiFormRow } from '@elastic/eui'; import { ChildDragDropProvider, DragDrop } from '@kbn/dom-drag-drop'; import { FramePublicAPI, Visualization, VisualizationConfigProps } from '../../../types'; import { LayerPanel } from './layer_panel'; +import { LayerActions } from './layer_actions'; import { coreMock } from '@kbn/core/public/mocks'; import { generateId } from '../../../id_generator'; import { @@ -116,6 +117,7 @@ describe('LayerPanel', () => { onChangeIndexPattern: jest.fn(), indexPatternService: createIndexPatternServiceMock(), getUserMessages: () => [], + displayLayerSettings: true, }; } @@ -203,6 +205,13 @@ describe('LayerPanel', () => { expect(optionalLabel.text()).toEqual('Optional'); }); + it('should hide the layer actions if displayLayerSettings is set to false', async () => { + const { instance } = await mountWithProvider( + + ); + expect(instance.find(LayerActions).exists()).toBe(false); + }); + it('should render the group with a way to add a new column', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index bb90c82b235e6..84c0b18d30c5a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -87,8 +87,9 @@ export function LayerPanel( datasourceId?: string; visualizationId?: string; }) => void; - indexPatternService: IndexPatternServiceAPI; - getUserMessages: UserMessagesGetter; + indexPatternService?: IndexPatternServiceAPI; + getUserMessages?: UserMessagesGetter; + displayLayerSettings: boolean; } ) { const [activeDimension, setActiveDimension] = useState( @@ -418,17 +419,20 @@ export function LayerPanel( activeVisualization={activeVisualization} /> - - -
- + {props.displayLayerSettings && ( + + +
+ + )} - {(layerDatasource || activeVisualization.renderLayerPanel) && } - {layerDatasource && ( + {props.indexPatternService && + (layerDatasource || activeVisualization.renderLayerPanel) && } + {layerDatasource && props.indexPatternService && ( { const { columnId } = accessorConfig; - const messages = props.getUserMessages('dimensionButton', { - dimensionId: columnId, - }); + const messages = + props?.getUserMessages?.('dimensionButton', { + dimensionId: columnId, + }) ?? []; return ( void; } export interface LayerPanelProps { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts index c3ba019ca68ad..9f51ea611e9b8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts @@ -6,3 +6,4 @@ */ export { WorkspacePanel } from './workspace_panel'; +export { VisualizationToolbar } from './workspace_panel_wrapper'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx index 42735bde405c4..700fe7f96bf84 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx @@ -65,7 +65,13 @@ describe('workspace_panel_wrapper', () => { isFullscreen={false} lensInspector={{} as unknown as LensInspector} getUserMessages={() => []} - /> + />, + { + preloadedState: { + visualization: { activeId: 'myVis', state: visState }, + datasourceStates: {}, + }, + } ); expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 6b61e4dd374c5..064b268209aea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -16,6 +16,7 @@ import { FramePublicAPI, UserMessagesGetter, VisualizationMap, + Visualization, } from '../../../types'; import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../utils'; import { NativeRenderer } from '../../../native_renderer'; @@ -49,6 +50,52 @@ export interface WorkspacePanelWrapperProps { getUserMessages: UserMessagesGetter; } +export function VisualizationToolbar(props: { + activeVisualization: Visualization | null; + framePublicAPI: FramePublicAPI; + onUpdateStateCb?: (datasourceState: unknown, visualizationState: unknown) => void; +}) { + const dispatchLens = useLensDispatch(); + const { activeDatasourceId, visualization, datasourceStates } = useLensSelector( + (state) => state.lens + ); + const setVisualizationState = useCallback( + (newState: unknown) => { + if (!props.activeVisualization) { + return; + } + dispatchLens( + updateVisualizationState({ + visualizationId: props.activeVisualization.id, + newState, + }) + ); + if (activeDatasourceId && props.onUpdateStateCb) { + const dsState = datasourceStates[activeDatasourceId].state; + props.onUpdateStateCb?.(dsState, newState); + } + }, + [activeDatasourceId, datasourceStates, dispatchLens, props] + ); + + return ( + <> + {props.activeVisualization && props.activeVisualization.renderToolbar && ( + + + + )} + + ); +} + export function WorkspacePanelWrapper({ children, framePublicAPI, @@ -65,21 +112,6 @@ export function WorkspacePanelWrapper({ const autoApplyEnabled = useLensSelector(selectAutoApplyEnabled); const activeVisualization = visualizationId ? visualizationMap[visualizationId] : null; - const setVisualizationState = useCallback( - (newState: unknown) => { - if (!activeVisualization) { - return; - } - dispatchLens( - updateVisualizationState({ - visualizationId: activeVisualization.id, - newState, - }) - ); - }, - [dispatchLens, activeVisualization] - ); - const userMessages = getUserMessages('toolbar'); return ( @@ -116,19 +148,10 @@ export function WorkspacePanelWrapper({ framePublicAPI={framePublicAPI} /> - - {activeVisualization && activeVisualization.renderToolbar && ( - - - - )} + )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index edccc071a57d1..4513e0f4bffd4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -10,6 +10,8 @@ import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/publ import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; @@ -57,5 +59,7 @@ export function createMockStartDependencies() { embeddable: embeddablePluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), } as unknown as MockedStartDependencies; } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index 943e87c9c00c2..67076fb9c9200 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -23,6 +23,7 @@ import { import type { LensByReferenceInput, LensByValueInput } from './embeddable'; import type { Document } from '../persistence'; import type { FormBasedPersistedState } from '../datasources/form_based/types'; +import type { TextBasedPersistedState } from '../datasources/text_based/types'; import type { XYState } from '../visualizations/xy/types'; import type { PieVisualizationState, @@ -45,6 +46,7 @@ type LensAttributes = Omit< state: Omit & { datasourceStates: { formBased: FormBasedPersistedState; + textBased?: TextBasedPersistedState; }; visualization: TVisState; }; diff --git a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx index cbf310cb2f50a..f526e46d8f5ec 100644 --- a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx @@ -21,6 +21,7 @@ export const lensPluginMock = { SaveModalComponent: jest.fn(() => { return Lens Save Modal Component; }), + EditLensConfigPanelApi: jest.fn().mockResolvedValue(Lens Config Panel Component), canUseEditor: jest.fn(() => true), navigateToPrefilledEditor: jest.fn(), getXyVisTypes: jest diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b207f07266ae8..1a195183142c3 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -126,6 +126,7 @@ import { type LensAppLocator, LensAppLocatorDefinition } from '../common/locator import { downloadCsvShareProvider } from './app_plugin/csv_download_provider/csv_download_provider'; import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; +import type { EditLensConfigurationProps } from './app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -215,6 +216,14 @@ export interface LensPublicStart { * @experimental */ SaveModalComponent: React.ComponentType>; + /** + * React component which can be used to embed a Lens Visualization Config Panel Component. + * + * This API might undergo breaking changes even in minor versions. + * + * @experimental + */ + EditLensConfigPanelApi: () => Promise; /** * Method which navigates to the Lens editor, loading the state specified by the `input` parameter. * See `x-pack/examples/embedded_lens_example` for exemplary usage. @@ -252,6 +261,8 @@ export interface LensPublicStart { }>; } +export type EditLensConfigPanelComponent = React.ComponentType; + export type LensSuggestionsApi = ( context: VisualizeFieldContext | VisualizeEditorContext, dataViews: DataView, @@ -649,6 +660,17 @@ export class LensPlugin { }, }; }, + EditLensConfigPanelApi: async () => { + const { getEditLensConfiguration } = await import('./async_services'); + if (!this.editorFrameService) { + this.initDependenciesForApi(); + } + const [visualizationMap, datasourceMap] = await Promise.all([ + this.editorFrameService!.loadVisualizations(), + this.editorFrameService!.loadDatasources(), + ]); + return getEditLensConfiguration(core, startDependencies, visualizationMap, datasourceMap); + }, }; } diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index 9a9a4005714aa..f4b333e25c815 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -35,6 +35,7 @@ export const { submitSuggestion, switchDatasource, switchAndCleanDatasource, + updateStateFromSuggestion, updateIndexPatterns, setToggleFullscreen, initEmpty, diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts index c69931837b3aa..0371d5564d503 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.test.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.test.ts @@ -10,6 +10,7 @@ import type { Query } from '@kbn/es-query'; import { switchDatasource, switchAndCleanDatasource, + updateStateFromSuggestion, switchVisualization, setState, updateState, @@ -271,6 +272,28 @@ describe('lensSlice', () => { }); }); + describe('update the state from the suggestion', () => { + it('should switch active datasource and initialize new state', () => { + store.dispatch( + updateStateFromSuggestion({ + newDatasourceId: 'testDatasource2', + visualizationId: 'testVis', + visualizationState: ['col1', 'col2'], + datasourceState: {}, + dataViews: { indexPatterns: {} } as DataViewsState, + }) + ); + expect(store.getState().lens.activeDatasourceId).toEqual('testDatasource2'); + expect(store.getState().lens.datasourceStates.testDatasource2.isLoading).toEqual(false); + expect(store.getState().lens.datasourceStates.testDatasource2.state).toStrictEqual({}); + expect(store.getState().lens.visualization).toStrictEqual({ + activeId: 'testVis', + state: ['col1', 'col2'], + }); + expect(store.getState().lens.dataViews).toEqual({ indexPatterns: {} }); + }); + }); + describe('adding or removing layer', () => { const testDatasource = (datasourceId: string) => { return { diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index e7829361ca5f9..cb35f16f0e8c5 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -173,6 +173,13 @@ export const switchAndCleanDatasource = createAction<{ visualizationId: string | null; currentIndexPatternId?: string; }>('lens/switchAndCleanDatasource'); +export const updateStateFromSuggestion = createAction<{ + newDatasourceId: string; + visualizationId: string | null; + visualizationState: unknown; + datasourceState: unknown; + dataViews: DataViewsState; +}>('lens/updateStateFromSuggestion'); export const navigateAway = createAction('lens/navigateAway'); export const loadInitial = createAction<{ initialInput?: LensEmbeddableInput; @@ -267,6 +274,7 @@ export const lensActions = { submitSuggestion, switchDatasource, switchAndCleanDatasource, + updateStateFromSuggestion, navigateAway, loadInitial, initEmpty, @@ -848,6 +856,42 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { }, }; }, + [updateStateFromSuggestion.type]: ( + state, + { + payload, + }: { + payload: { + newDatasourceId: string; + visualizationId: string; + visualizationState: unknown; + datasourceState: unknown; + dataViews: DataViewsState; + }; + } + ) => { + const visualization = { + activeId: payload.visualizationId, + state: payload.visualizationState, + }; + + const datasourceState = payload.datasourceState; + + return { + ...state, + datasourceStates: { + [payload.newDatasourceId]: { + state: datasourceState, + isLoading: false, + }, + }, + activeDatasourceId: payload.newDatasourceId, + visualization: { + ...visualization, + }, + dataViews: payload.dataViews, + }; + }, [navigateAway.type]: (state) => state, [loadInitial.type]: ( state, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d28554d63bbc3..121e0dce922c9 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5673,7 +5673,6 @@ "unifiedHistogram.lensTitle": "Modifier la visualisation", "unifiedHistogram.resetChartHeight": "Réinitialiser à la hauteur par défaut", "unifiedHistogram.showChart": "Afficher le graphique", - "unifiedHistogram.suggestionSelectorLabel": "Visualisation", "unifiedHistogram.suggestionSelectorPlaceholder": "Sélectionner la visualisation", "unifiedHistogram.timeIntervals": "Intervalles de temps", "unifiedHistogram.timeIntervalWithValueWarning": "Avertissement", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d8763e4030c78..2789358b1e0f2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5674,7 +5674,6 @@ "unifiedHistogram.lensTitle": "ビジュアライゼーションを編集", "unifiedHistogram.resetChartHeight": "デフォルトの高さにリセット", "unifiedHistogram.showChart": "グラフを表示", - "unifiedHistogram.suggestionSelectorLabel": "ビジュアライゼーション", "unifiedHistogram.suggestionSelectorPlaceholder": "ビジュアライゼーションを選択", "unifiedHistogram.timeIntervals": "時間間隔", "unifiedHistogram.timeIntervalWithValueWarning": "警告", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 59d89ca314551..fd0126b228758 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5673,7 +5673,6 @@ "unifiedHistogram.lensTitle": "编辑可视化", "unifiedHistogram.resetChartHeight": "重置为默认高度", "unifiedHistogram.showChart": "显示图表", - "unifiedHistogram.suggestionSelectorLabel": "可视化", "unifiedHistogram.suggestionSelectorPlaceholder": "选择可视化", "unifiedHistogram.timeIntervals": "时间间隔", "unifiedHistogram.timeIntervalWithValueWarning": "警告", diff --git a/x-pack/test/functional/apps/discover/visualize_field.ts b/x-pack/test/functional/apps/discover/visualize_field.ts index bb914c45fa2b7..22049dc351caa 100644 --- a/x-pack/test/functional/apps/discover/visualize_field.ts +++ b/x-pack/test/functional/apps/discover/visualize_field.ts @@ -156,11 +156,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('TextBasedLangEditor-expand'); - await testSubjects.click('unifiedHistogramEditVisualization'); + await testSubjects.click('unifiedHistogramEditFlyoutVisualization'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('lens visualization', async () => { + await retry.waitFor('lens flyout', async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased'); return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'average'; }); @@ -175,11 +175,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.click('querySubmitButton'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('TextBasedLangEditor-expand'); - await testSubjects.click('unifiedHistogramEditVisualization'); + await testSubjects.click('unifiedHistogramEditFlyoutVisualization'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.waitFor('lens visualization', async () => { + await retry.waitFor('lens flyout', async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger-textBased'); return dimensions.length === 2 && (await dimensions[1].getVisibleText()) === 'average'; });