Skip to content

Commit d8c5140

Browse files
[Lens] Move toolbar from Lens editor workspace to config panel (#239879)
## Summary Moves toolbar from workspace to config panel. #### After <img width="2560" height="1295" alt="Screenshot 2025-10-23 at 14 17 59" src="https://github.com/user-attachments/assets/cdd1a3ed-7420-4e55-8c1b-7aa77ba88175" /> #### With disabled "Auto-apply visualization changes" <img width="2560" height="1294" alt="Screenshot 2025-10-23 at 14 22 24" src="https://github.com/user-attachments/assets/631f46c4-8a83-411f-bc29-27df257eaeef" /> ### 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 - [x] 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) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Issues Closes #239719 ### Release Notes Moves the Lens visualization toolbar from the workspace section to the config panel. --------- Co-authored-by: kibanamachine <[email protected]>
1 parent 718e7a2 commit d8c5140

File tree

17 files changed

+231
-200
lines changed

17 files changed

+231
-200
lines changed

x-pack/platform/plugins/shared/lens/public/app_plugin/shared/edit_on_the_fly/flyout_wrapper.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ export const FlyoutWrapper = ({
113113
</EuiText>
114114
</EuiFlexItem>
115115
)}
116-
{toolbar && <EuiFlexItem grow={false}>{toolbar}</EuiFlexItem>}
116+
{toolbar && (
117+
<EuiFlexItem grow={false} data-test-subj="lnsVisualizationToolbar">
118+
{toolbar}
119+
</EuiFlexItem>
120+
)}
117121
</EuiFlexGroup>
118122
</EuiFlyoutHeader>
119123
)}

x-pack/platform/plugins/shared/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { LayerConfiguration } from './layer_configuration_section';
2929
import type { EditConfigPanelProps } from './types';
3030
import { FlyoutWrapper } from './flyout_wrapper';
3131
import { SuggestionPanel } from '../../../editor_frame_service/editor_frame/suggestion_panel';
32-
import { VisualizationToolbarWrapper } from '../../../editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper';
32+
import { VisualizationToolbarWrapper } from '../../../editor_frame_service/editor_frame/visualization_toolbar';
3333
import { useEditorFrameService } from '../../../editor_frame_service/editor_frame_service_context';
3434
import { useApplicationUserMessages } from '../../get_application_user_messages';
3535
import { trackSaveUiCounterEvents } from '../../../lens_ui_telemetry';

x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ describe('editor_frame', () => {
9292
let datasourceMap: DatasourceMap;
9393

9494
beforeEach(() => {
95-
mockVisualization = createMockVisualization();
95+
mockVisualization = {
96+
...createMockVisualization(),
97+
ToolbarComponent: jest.fn(() => <div />),
98+
};
99+
96100
mockVisualization2 = createMockVisualization('testVis2', ['second']);
97101

98102
mockDatasource = createMockDatasource();
@@ -156,12 +160,15 @@ describe('editor_frame', () => {
156160
const queryWorkspacePanel = () => screen.queryByTestId('lnsWorkspace');
157161
const queryDataPanel = () => screen.queryByTestId('lnsDataPanelWrapper');
158162

163+
const queryVisualizationToolbar = () => screen.queryByTestId('lnsVisualizationToolbar');
164+
159165
return {
160166
...rtlRender,
161167
store,
162168
queryLayerPanel,
163169
queryWorkspacePanel,
164170
queryDataPanel,
171+
queryVisualizationToolbar,
165172
simulateLoadingDatasource: () =>
166173
store.dispatch(
167174
setState({
@@ -180,24 +187,31 @@ describe('editor_frame', () => {
180187

181188
describe('initialization', () => {
182189
it('should render workspace panel, data panel and layer panel when all datasources are initialized', async () => {
183-
const { queryWorkspacePanel, queryDataPanel, queryLayerPanel, simulateLoadingDatasource } =
184-
renderEditorFrame(undefined, {
185-
preloadedStateOverrides: {
186-
datasourceStates: {
187-
testDatasource: {
188-
isLoading: true,
189-
state: {
190-
internalState: 'datasourceState',
191-
},
190+
const {
191+
queryWorkspacePanel,
192+
queryDataPanel,
193+
queryLayerPanel,
194+
queryVisualizationToolbar,
195+
simulateLoadingDatasource,
196+
} = renderEditorFrame(undefined, {
197+
preloadedStateOverrides: {
198+
datasourceStates: {
199+
testDatasource: {
200+
isLoading: true,
201+
state: {
202+
internalState: 'datasourceState',
192203
},
193204
},
194205
},
195-
});
206+
},
207+
});
196208

197209
expect(mockVisualization.getConfiguration).not.toHaveBeenCalled();
210+
198211
expect(queryWorkspacePanel()).not.toBeInTheDocument();
199212
expect(queryDataPanel()).not.toBeInTheDocument();
200213
expect(queryLayerPanel()).not.toBeInTheDocument();
214+
expect(queryVisualizationToolbar()).not.toBeInTheDocument();
201215

202216
act(() => {
203217
simulateLoadingDatasource();
@@ -210,7 +224,9 @@ describe('editor_frame', () => {
210224
expect(queryWorkspacePanel()).toBeInTheDocument();
211225
expect(queryDataPanel()).toBeInTheDocument();
212226
expect(queryLayerPanel()).toBeInTheDocument();
227+
expect(queryVisualizationToolbar()).toBeInTheDocument();
213228
});
229+
214230
it('should render the resulting expression using the expression renderer', async () => {
215231
renderEditorFrame();
216232
expect(screen.getByTestId('lnsExpressionRenderer')).toHaveTextContent(

x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/editor_frame.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import type {
1717
AddUserMessages,
1818
LensInspector,
1919
} from '@kbn/lens-common';
20+
import type { UseEuiTheme } from '@elastic/eui';
21+
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
22+
import { css } from '@emotion/react';
23+
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
2024
import { getAbsoluteDateRange } from '../../utils';
2125
import { trackUiCounterEvents } from '../../lens_ui_telemetry';
2226
import { DataPanelWrapper } from './data_panel_wrapper';
@@ -40,6 +44,7 @@ import { ErrorBoundary, showMemoizedErrorNotification } from '../../lens_ui_erro
4044
import type { IndexPatternServiceAPI } from '../../data_views_service/service';
4145
import { getLongMessage } from '../../user_messages_utils';
4246
import { useEditorFrameService } from '../editor_frame_service_context';
47+
import { VisualizationToolbarWrapper } from './visualization_toolbar';
4348

4449
export interface EditorFrameProps {
4550
ExpressionRenderer: ReactExpressionRendererType;
@@ -59,6 +64,9 @@ export function EditorFrame(props: EditorFrameProps) {
5964
const datasourceStates = useLensSelector(selectDatasourceStates);
6065
const visualization = useLensSelector(selectVisualization);
6166
const areDatasourcesLoaded = useLensSelector(selectAreDatasourcesLoaded);
67+
68+
const styles = useMemoCss(componentStyles);
69+
6270
const isVisualizationLoaded = !!visualization.state;
6371
const visualizationTypeIsKnown = Boolean(
6472
visualization.activeId && visualizationMap[visualization.activeId]
@@ -147,15 +155,27 @@ export function EditorFrame(props: EditorFrameProps) {
147155
configPanel={
148156
areDatasourcesLoaded && (
149157
<ErrorBoundary onError={onError}>
150-
<ConfigPanelWrapper
151-
core={props.core}
152-
framePublicAPI={framePublicAPI}
153-
uiActions={props.plugins.uiActions}
154-
dataViews={props.plugins.dataViews}
155-
data={props.plugins.data}
156-
indexPatternService={props.indexPatternService}
157-
getUserMessages={props.getUserMessages}
158-
/>
158+
<>
159+
<EuiFlexGroup
160+
css={styles.visualizationToolbar}
161+
justifyContent="flexEnd"
162+
responsive={false}
163+
wrap={true}
164+
>
165+
<EuiFlexItem grow={false} data-test-subj="lnsVisualizationToolbar">
166+
<VisualizationToolbarWrapper framePublicAPI={framePublicAPI} />
167+
</EuiFlexItem>
168+
</EuiFlexGroup>
169+
<ConfigPanelWrapper
170+
core={props.core}
171+
framePublicAPI={framePublicAPI}
172+
uiActions={props.plugins.uiActions}
173+
dataViews={props.plugins.dataViews}
174+
data={props.plugins.data}
175+
indexPatternService={props.indexPatternService}
176+
getUserMessages={props.getUserMessages}
177+
/>
178+
</>
159179
</ErrorBoundary>
160180
)
161181
}
@@ -195,3 +215,8 @@ export function EditorFrame(props: EditorFrameProps) {
195215
</RootDragDropProvider>
196216
);
197217
}
218+
219+
const componentStyles = {
220+
visualizationToolbar: ({ euiTheme }: UseEuiTheme) =>
221+
css({ margin: `${euiTheme.size.base} ${euiTheme.size.base} 0 ${euiTheme.size.base}` }),
222+
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { memo, useCallback } from 'react';
9+
import type { FramePublicAPI, Visualization } from '@kbn/lens-common';
10+
import {
11+
useLensDispatch,
12+
updateVisualizationState,
13+
useLensSelector,
14+
selectVisualizationState,
15+
selectVisualization,
16+
} from '../../state_management';
17+
import { useEditorFrameService } from '../editor_frame_service_context';
18+
19+
const VisualizationToolbar = memo(function VisualizationToolbar({
20+
activeVisualization,
21+
framePublicAPI,
22+
enableFlyoutToolbar = false,
23+
}: {
24+
activeVisualization: Visualization | null;
25+
framePublicAPI: FramePublicAPI;
26+
enableFlyoutToolbar?: boolean;
27+
}) {
28+
const dispatchLens = useLensDispatch();
29+
const visualization = useLensSelector(selectVisualizationState);
30+
const setVisualizationState = useCallback(
31+
(newState: unknown) => {
32+
if (!activeVisualization) {
33+
return;
34+
}
35+
dispatchLens(
36+
updateVisualizationState({
37+
visualizationId: activeVisualization.id,
38+
newState,
39+
})
40+
);
41+
},
42+
[dispatchLens, activeVisualization]
43+
);
44+
45+
const { FlyoutToolbarComponent, ToolbarComponent: RegularToolbarComponent } =
46+
activeVisualization ?? {};
47+
48+
let ToolbarComponent;
49+
if (enableFlyoutToolbar) {
50+
ToolbarComponent = FlyoutToolbarComponent ?? RegularToolbarComponent;
51+
} else {
52+
ToolbarComponent = RegularToolbarComponent;
53+
}
54+
55+
if (!ToolbarComponent) {
56+
return null;
57+
}
58+
59+
return ToolbarComponent({
60+
frame: framePublicAPI,
61+
state: visualization.state,
62+
setState: setVisualizationState,
63+
});
64+
});
65+
66+
export function VisualizationToolbarWrapper({
67+
framePublicAPI,
68+
}: {
69+
framePublicAPI: FramePublicAPI;
70+
}) {
71+
const { visualizationMap } = useEditorFrameService();
72+
const visualization = useLensSelector(selectVisualization);
73+
74+
const activeVisualization = visualization.activeId
75+
? visualizationMap[visualization.activeId]
76+
: null;
77+
78+
return activeVisualization && visualization.state ? (
79+
<VisualizationToolbar
80+
framePublicAPI={framePublicAPI}
81+
activeVisualization={activeVisualization}
82+
/>
83+
) : null;
84+
}

x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/workspace_panel/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@
66
*/
77

88
export { WorkspacePanel } from './workspace_panel';
9-
export { VisualizationToolbar } from './workspace_panel_wrapper';

x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,11 +678,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
678678

679679
return (
680680
<WorkspacePanelWrapper
681-
framePublicAPI={framePublicAPI}
682-
visualizationId={visualization.activeId}
683-
datasourceStates={datasourceStates}
684681
isFullscreen={isFullscreen}
685-
lensInspector={lensInspector}
686682
getUserMessages={getUserMessages}
687683
displayOptions={chartSizeSpec}
688684
>

x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.test.tsx

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,8 @@
66
*/
77

88
import React from 'react';
9-
import type { Visualization, LensInspector, LensAppState } from '@kbn/lens-common';
10-
import type { FrameMock } from '../../../mocks';
11-
import {
12-
createMockVisualization,
13-
createMockFramePublicAPI,
14-
renderWithReduxStore,
15-
} from '../../../mocks';
9+
import type { Visualization, LensAppState } from '@kbn/lens-common';
10+
import { createMockVisualization, renderWithReduxStore } from '../../../mocks';
1611
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
1712
import { updateVisualizationState } from '../../../state_management';
1813
import { setChangesApplied } from '../../../state_management/lens_slice';
@@ -24,8 +19,6 @@ import { EditorFrameServiceProvider } from '../../editor_frame_service_context';
2419

2520
describe('workspace_panel_wrapper', () => {
2621
let mockVisualization: jest.Mocked<Visualization>;
27-
let mockFrameAPI: FrameMock;
28-
const ToolbarComponentMock = jest.fn(() => null);
2922

3023
const renderWorkspacePanelWrapper = (
3124
propsOverrides = {},
@@ -34,16 +27,12 @@ describe('workspace_panel_wrapper', () => {
3427
const { store, ...rtlRender } = renderWithReduxStore(
3528
<EditorFrameServiceProvider
3629
visualizationMap={{
37-
myVis: { ...mockVisualization, ToolbarComponent: ToolbarComponentMock },
30+
myVis: { ...mockVisualization },
3831
}}
3932
datasourceMap={{}}
4033
>
4134
<WorkspacePanelWrapper
42-
framePublicAPI={mockFrameAPI}
43-
visualizationId="myVis"
44-
datasourceStates={{}}
4535
isFullscreen={false}
46-
lensInspector={{} as unknown as LensInspector}
4736
getUserMessages={() => []}
4837
children={<span />}
4938
displayOptions={undefined}
@@ -95,8 +84,6 @@ describe('workspace_panel_wrapper', () => {
9584

9685
beforeEach(() => {
9786
mockVisualization = createMockVisualization();
98-
mockFrameAPI = createMockFramePublicAPI();
99-
ToolbarComponentMock.mockClear();
10087
});
10188

10289
it('should render its children', async () => {
@@ -105,25 +92,6 @@ describe('workspace_panel_wrapper', () => {
10592
expect(screen.getByText(customElementText)).toBeInTheDocument();
10693
});
10794

108-
it('should call the toolbar renderer if provided', async () => {
109-
const visState = { internalState: 123 };
110-
renderWorkspacePanelWrapper(
111-
{},
112-
{
113-
preloadedState: {
114-
visualization: { activeId: 'myVis', state: visState },
115-
datasourceStates: {},
116-
},
117-
}
118-
);
119-
120-
expect(ToolbarComponentMock).toHaveBeenCalledWith({
121-
state: visState,
122-
frame: mockFrameAPI,
123-
setState: expect.anything(),
124-
});
125-
});
126-
12795
describe('auto-apply controls', () => {
12896
it('shows and hides apply-changes button depending on whether auto-apply is enabled', async () => {
12997
const { toggleAutoApply, getApplyChangesToolbar } = renderWorkspacePanelWrapper();
@@ -146,14 +114,14 @@ describe('workspace_panel_wrapper', () => {
146114
editVisualization();
147115
});
148116

149-
// // simulate workspace panel behavior
117+
// simulate workspace panel behavior
150118
act(() => {
151119
store.dispatch(setChangesApplied(false));
152120
});
153121

154122
expect(getApplyChangesToolbar()).not.toBeDisabled();
155123

156-
// // simulate workspace panel behavior
124+
// simulate workspace panel behavior
157125
act(() => {
158126
store.dispatch(setChangesApplied(true));
159127
});

0 commit comments

Comments
 (0)