Skip to content

Commit de5ecc0

Browse files
authored
[Lens] Add toolbar api (#69263) (#70044)
1 parent 5dde009 commit de5ecc0

File tree

6 files changed

+188
-38
lines changed

6 files changed

+188
-38
lines changed

x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
.lnsWorkspacePanelWrapper__pageContentHeader {
1111
@include euiTitle('xs');
1212
padding: $euiSizeM;
13-
border-bottom: $euiBorderThin;
1413
// override EuiPage
1514
margin-bottom: 0 !important; // sass-lint:disable-line no-important
1615
}
1716

17+
.lnsWorkspacePanelWrapper__pageContentHeader--unsaved {
18+
color: $euiTextSubduedColor;
19+
}
20+
1821
.lnsWorkspacePanelWrapper__pageContentBody {
1922
@include euiScrollBar;
2023
flex-grow: 1;

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { WorkspacePanel } from './workspace_panel';
2323
import { Document } from '../../persistence/saved_object_store';
2424
import { RootDragDropProvider } from '../../drag_drop';
2525
import { getSavedObjectFormat } from './save';
26-
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
2726
import { generateId } from '../../id_generator';
2827
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
2928
import { EditorFrameStartPlugins } from '../service';
@@ -275,21 +274,20 @@ export function EditorFrame(props: EditorFrameProps) {
275274
}
276275
workspacePanel={
277276
allLoaded && (
278-
<WorkspacePanelWrapper title={state.title}>
279-
<WorkspacePanel
280-
activeDatasourceId={state.activeDatasourceId}
281-
activeVisualizationId={state.visualization.activeId}
282-
datasourceMap={props.datasourceMap}
283-
datasourceStates={state.datasourceStates}
284-
framePublicAPI={framePublicAPI}
285-
visualizationState={state.visualization.state}
286-
visualizationMap={props.visualizationMap}
287-
dispatch={dispatch}
288-
ExpressionRenderer={props.ExpressionRenderer}
289-
core={props.core}
290-
plugins={props.plugins}
291-
/>
292-
</WorkspacePanelWrapper>
277+
<WorkspacePanel
278+
title={state.title}
279+
activeDatasourceId={state.activeDatasourceId}
280+
activeVisualizationId={state.visualization.activeId}
281+
datasourceMap={props.datasourceMap}
282+
datasourceStates={state.datasourceStates}
283+
framePublicAPI={framePublicAPI}
284+
visualizationState={state.visualization.state}
285+
visualizationMap={props.visualizationMap}
286+
dispatch={dispatch}
287+
ExpressionRenderer={props.ExpressionRenderer}
288+
core={props.core}
289+
plugins={props.plugins}
290+
/>
293291
)
294292
}
295293
suggestionsPanel={

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { trackUiEvent } from '../../lens_ui_telemetry';
3737
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
3838
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public';
3939
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
40+
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
4041

4142
export interface WorkspacePanelProps {
4243
activeVisualizationId: string | null;
@@ -56,6 +57,7 @@ export interface WorkspacePanelProps {
5657
ExpressionRenderer: ReactExpressionRendererType;
5758
core: CoreStart | CoreSetup;
5859
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
60+
title?: string;
5961
}
6062

6163
export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel);
@@ -73,6 +75,7 @@ export function InnerWorkspacePanel({
7375
core,
7476
plugins,
7577
ExpressionRenderer: ExpressionRendererComponent,
78+
title,
7679
}: WorkspacePanelProps) {
7780
const IS_DARK_THEME = core.uiSettings.get('theme:darkMode');
7881
const emptyStateGraphicURL = IS_DARK_THEME
@@ -291,13 +294,22 @@ export function InnerWorkspacePanel({
291294
}
292295

293296
return (
294-
<DragDrop
295-
data-test-subj="lnsWorkspace"
296-
draggable={false}
297-
droppable={Boolean(suggestionForDraggedField)}
298-
onDrop={onDrop}
297+
<WorkspacePanelWrapper
298+
title={title}
299+
framePublicAPI={framePublicAPI}
300+
dispatch={dispatch}
301+
emptyExpression={expression === null}
302+
visualizationState={visualizationState}
303+
activeVisualization={activeVisualization}
299304
>
300-
{renderVisualization()}
301-
</DragDrop>
305+
<DragDrop
306+
data-test-subj="lnsWorkspace"
307+
draggable={false}
308+
droppable={Boolean(suggestionForDraggedField)}
309+
onDrop={onDrop}
310+
>
311+
{renderVisualization()}
312+
</DragDrop>
313+
</WorkspacePanelWrapper>
302314
);
303315
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { Visualization } from '../../types';
9+
import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../mocks';
10+
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
11+
import { ReactWrapper } from 'enzyme';
12+
import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper';
13+
14+
describe('workspace_panel_wrapper', () => {
15+
let mockVisualization: jest.Mocked<Visualization>;
16+
let mockFrameAPI: FrameMock;
17+
let instance: ReactWrapper<WorkspacePanelWrapperProps>;
18+
19+
beforeEach(() => {
20+
mockVisualization = createMockVisualization();
21+
mockFrameAPI = createMockFramePublicAPI();
22+
});
23+
24+
afterEach(() => {
25+
instance.unmount();
26+
});
27+
28+
it('should render its children', () => {
29+
const MyChild = () => <span>The child elements</span>;
30+
instance = mount(
31+
<WorkspacePanelWrapper
32+
dispatch={jest.fn()}
33+
framePublicAPI={mockFrameAPI}
34+
visualizationState={{}}
35+
activeVisualization={mockVisualization}
36+
emptyExpression={false}
37+
>
38+
<MyChild />
39+
</WorkspacePanelWrapper>
40+
);
41+
42+
expect(instance.find(MyChild)).toHaveLength(1);
43+
});
44+
45+
it('should call the toolbar renderer if provided', () => {
46+
const renderToolbarMock = jest.fn();
47+
const visState = { internalState: 123 };
48+
instance = mount(
49+
<WorkspacePanelWrapper
50+
dispatch={jest.fn()}
51+
framePublicAPI={mockFrameAPI}
52+
visualizationState={visState}
53+
children={<span />}
54+
activeVisualization={{ ...mockVisualization, renderToolbar: renderToolbarMock }}
55+
emptyExpression={false}
56+
/>
57+
);
58+
59+
expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), {
60+
state: visState,
61+
frame: mockFrameAPI,
62+
setState: expect.anything(),
63+
});
64+
});
65+
});

x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,86 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React from 'react';
8-
import { EuiPageContent, EuiPageContentHeader, EuiPageContentBody } from '@elastic/eui';
7+
import React, { useCallback } from 'react';
8+
import { i18n } from '@kbn/i18n';
9+
import classNames from 'classnames';
10+
import {
11+
EuiPageContent,
12+
EuiPageContentBody,
13+
EuiPageContentHeader,
14+
EuiFlexGroup,
15+
EuiFlexItem,
16+
} from '@elastic/eui';
17+
import { FramePublicAPI, Visualization } from '../../types';
18+
import { NativeRenderer } from '../../native_renderer';
19+
import { Action } from './state_management';
920

10-
interface Props {
11-
title: string;
21+
export interface WorkspacePanelWrapperProps {
1222
children: React.ReactNode | React.ReactNode[];
23+
framePublicAPI: FramePublicAPI;
24+
visualizationState: unknown;
25+
activeVisualization: Visualization | null;
26+
dispatch: (action: Action) => void;
27+
emptyExpression: boolean;
28+
title?: string;
1329
}
1430

15-
export function WorkspacePanelWrapper({ children, title }: Props) {
31+
export function WorkspacePanelWrapper({
32+
children,
33+
framePublicAPI,
34+
visualizationState,
35+
activeVisualization,
36+
dispatch,
37+
title,
38+
emptyExpression,
39+
}: WorkspacePanelWrapperProps) {
40+
const setVisualizationState = useCallback(
41+
(newState: unknown) => {
42+
if (!activeVisualization) {
43+
return;
44+
}
45+
dispatch({
46+
type: 'UPDATE_VISUALIZATION_STATE',
47+
visualizationId: activeVisualization.id,
48+
newState,
49+
clearStagedPreview: false,
50+
});
51+
},
52+
[dispatch]
53+
);
1654
return (
17-
<EuiPageContent className="lnsWorkspacePanelWrapper">
18-
{title && (
19-
<EuiPageContentHeader className="lnsWorkspacePanelWrapper__pageContentHeader">
20-
<span data-test-subj="lns_ChartTitle">{title}</span>
21-
</EuiPageContentHeader>
55+
<EuiFlexGroup gutterSize="s" direction="column" alignItems="stretch">
56+
{activeVisualization && activeVisualization.renderToolbar && (
57+
<EuiFlexItem grow={false}>
58+
<NativeRenderer
59+
render={activeVisualization.renderToolbar}
60+
nativeProps={{
61+
frame: framePublicAPI,
62+
state: visualizationState,
63+
setState: setVisualizationState,
64+
}}
65+
/>
66+
</EuiFlexItem>
2267
)}
23-
<EuiPageContentBody className="lnsWorkspacePanelWrapper__pageContentBody">
24-
{children}
25-
</EuiPageContentBody>
26-
</EuiPageContent>
68+
<EuiFlexItem>
69+
<EuiPageContent className="lnsWorkspacePanelWrapper">
70+
{(!emptyExpression || title) && (
71+
<EuiPageContentHeader
72+
className={classNames('lnsWorkspacePanelWrapper__pageContentHeader', {
73+
'lnsWorkspacePanelWrapper__pageContentHeader--unsaved': !title,
74+
})}
75+
>
76+
<span data-test-subj="lns_ChartTitle">
77+
{title ||
78+
i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })}
79+
</span>
80+
</EuiPageContentHeader>
81+
)}
82+
<EuiPageContentBody className="lnsWorkspacePanelWrapper__pageContentBody">
83+
{children}
84+
</EuiPageContentBody>
85+
</EuiPageContent>
86+
</EuiFlexItem>
87+
</EuiFlexGroup>
2788
);
2889
}

x-pack/plugins/lens/public/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ export type VisualizationLayerWidgetProps<T = unknown> = VisualizationConfigProp
290290
setState: (newState: T) => void;
291291
};
292292

293+
export interface VisualizationToolbarProps<T = unknown> {
294+
setState: (newState: T) => void;
295+
frame: FramePublicAPI;
296+
state: T;
297+
}
298+
293299
export type VisualizationDimensionEditorProps<T = unknown> = VisualizationConfigProps<T> & {
294300
groupId: string;
295301
accessor: string;
@@ -454,6 +460,11 @@ export interface Visualization<T = unknown, P = unknown> {
454460
* for extra configurability, such as for styling the legend or axis
455461
*/
456462
renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void;
463+
/**
464+
* Toolbar rendered above the visualization. This is meant to be used to provide chart-level
465+
* settings for the visualization.
466+
*/
467+
renderToolbar?: (domElement: Element, props: VisualizationToolbarProps<T>) => void;
457468
/**
458469
* Visualizations can provide a custom icon which will open a layer-specific popover
459470
* If no icon is provided, gear icon is default

0 commit comments

Comments
 (0)