[Lens] Add toolbar api (#69263)

This commit is contained in:
Joe Reuter 2020-06-26 09:38:35 +02:00 committed by GitHub
parent eedc86fbe3
commit 67e48527e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 38 deletions

View file

@ -10,11 +10,14 @@
.lnsWorkspacePanelWrapper__pageContentHeader {
@include euiTitle('xs');
padding: $euiSizeM;
border-bottom: $euiBorderThin;
// override EuiPage
margin-bottom: 0 !important; // sass-lint:disable-line no-important
}
.lnsWorkspacePanelWrapper__pageContentHeader--unsaved {
color: $euiTextSubduedColor;
}
.lnsWorkspacePanelWrapper__pageContentBody {
@include euiScrollBar;
flex-grow: 1;

View file

@ -23,7 +23,6 @@ import { WorkspacePanel } from './workspace_panel';
import { Document } from '../../persistence/saved_object_store';
import { RootDragDropProvider } from '../../drag_drop';
import { getSavedObjectFormat } from './save';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import { generateId } from '../../id_generator';
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
import { EditorFrameStartPlugins } from '../service';
@ -275,21 +274,20 @@ export function EditorFrame(props: EditorFrameProps) {
}
workspacePanel={
allLoaded && (
<WorkspacePanelWrapper title={state.title}>
<WorkspacePanel
activeDatasourceId={state.activeDatasourceId}
activeVisualizationId={state.visualization.activeId}
datasourceMap={props.datasourceMap}
datasourceStates={state.datasourceStates}
framePublicAPI={framePublicAPI}
visualizationState={state.visualization.state}
visualizationMap={props.visualizationMap}
dispatch={dispatch}
ExpressionRenderer={props.ExpressionRenderer}
core={props.core}
plugins={props.plugins}
/>
</WorkspacePanelWrapper>
<WorkspacePanel
title={state.title}
activeDatasourceId={state.activeDatasourceId}
activeVisualizationId={state.visualization.activeId}
datasourceMap={props.datasourceMap}
datasourceStates={state.datasourceStates}
framePublicAPI={framePublicAPI}
visualizationState={state.visualization.state}
visualizationMap={props.visualizationMap}
dispatch={dispatch}
ExpressionRenderer={props.ExpressionRenderer}
core={props.core}
plugins={props.plugins}
/>
)
}
suggestionsPanel={

View file

@ -37,6 +37,7 @@ import { trackUiEvent } from '../../lens_ui_telemetry';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
export interface WorkspacePanelProps {
activeVisualizationId: string | null;
@ -56,6 +57,7 @@ export interface WorkspacePanelProps {
ExpressionRenderer: ReactExpressionRendererType;
core: CoreStart | CoreSetup;
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
title?: string;
}
export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel);
@ -73,6 +75,7 @@ export function InnerWorkspacePanel({
core,
plugins,
ExpressionRenderer: ExpressionRendererComponent,
title,
}: WorkspacePanelProps) {
const IS_DARK_THEME = core.uiSettings.get('theme:darkMode');
const emptyStateGraphicURL = IS_DARK_THEME
@ -291,13 +294,22 @@ export function InnerWorkspacePanel({
}
return (
<DragDrop
data-test-subj="lnsWorkspace"
draggable={false}
droppable={Boolean(suggestionForDraggedField)}
onDrop={onDrop}
<WorkspacePanelWrapper
title={title}
framePublicAPI={framePublicAPI}
dispatch={dispatch}
emptyExpression={expression === null}
visualizationState={visualizationState}
activeVisualization={activeVisualization}
>
{renderVisualization()}
</DragDrop>
<DragDrop
data-test-subj="lnsWorkspace"
draggable={false}
droppable={Boolean(suggestionForDraggedField)}
onDrop={onDrop}
>
{renderVisualization()}
</DragDrop>
</WorkspacePanelWrapper>
);
}

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Visualization } from '../../types';
import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../mocks';
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { ReactWrapper } from 'enzyme';
import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper';
describe('workspace_panel_wrapper', () => {
let mockVisualization: jest.Mocked<Visualization>;
let mockFrameAPI: FrameMock;
let instance: ReactWrapper<WorkspacePanelWrapperProps>;
beforeEach(() => {
mockVisualization = createMockVisualization();
mockFrameAPI = createMockFramePublicAPI();
});
afterEach(() => {
instance.unmount();
});
it('should render its children', () => {
const MyChild = () => <span>The child elements</span>;
instance = mount(
<WorkspacePanelWrapper
dispatch={jest.fn()}
framePublicAPI={mockFrameAPI}
visualizationState={{}}
activeVisualization={mockVisualization}
emptyExpression={false}
>
<MyChild />
</WorkspacePanelWrapper>
);
expect(instance.find(MyChild)).toHaveLength(1);
});
it('should call the toolbar renderer if provided', () => {
const renderToolbarMock = jest.fn();
const visState = { internalState: 123 };
instance = mount(
<WorkspacePanelWrapper
dispatch={jest.fn()}
framePublicAPI={mockFrameAPI}
visualizationState={visState}
children={<span />}
activeVisualization={{ ...mockVisualization, renderToolbar: renderToolbarMock }}
emptyExpression={false}
/>
);
expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), {
state: visState,
frame: mockFrameAPI,
setState: expect.anything(),
});
});
});

View file

@ -4,25 +4,86 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiPageContent, EuiPageContentHeader, EuiPageContentBody } from '@elastic/eui';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
import {
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { FramePublicAPI, Visualization } from '../../types';
import { NativeRenderer } from '../../native_renderer';
import { Action } from './state_management';
interface Props {
title: string;
export interface WorkspacePanelWrapperProps {
children: React.ReactNode | React.ReactNode[];
framePublicAPI: FramePublicAPI;
visualizationState: unknown;
activeVisualization: Visualization | null;
dispatch: (action: Action) => void;
emptyExpression: boolean;
title?: string;
}
export function WorkspacePanelWrapper({ children, title }: Props) {
export function WorkspacePanelWrapper({
children,
framePublicAPI,
visualizationState,
activeVisualization,
dispatch,
title,
emptyExpression,
}: WorkspacePanelWrapperProps) {
const setVisualizationState = useCallback(
(newState: unknown) => {
if (!activeVisualization) {
return;
}
dispatch({
type: 'UPDATE_VISUALIZATION_STATE',
visualizationId: activeVisualization.id,
newState,
clearStagedPreview: false,
});
},
[dispatch]
);
return (
<EuiPageContent className="lnsWorkspacePanelWrapper">
{title && (
<EuiPageContentHeader className="lnsWorkspacePanelWrapper__pageContentHeader">
<span data-test-subj="lns_ChartTitle">{title}</span>
</EuiPageContentHeader>
<EuiFlexGroup gutterSize="s" direction="column" alignItems="stretch">
{activeVisualization && activeVisualization.renderToolbar && (
<EuiFlexItem grow={false}>
<NativeRenderer
render={activeVisualization.renderToolbar}
nativeProps={{
frame: framePublicAPI,
state: visualizationState,
setState: setVisualizationState,
}}
/>
</EuiFlexItem>
)}
<EuiPageContentBody className="lnsWorkspacePanelWrapper__pageContentBody">
{children}
</EuiPageContentBody>
</EuiPageContent>
<EuiFlexItem>
<EuiPageContent className="lnsWorkspacePanelWrapper">
{(!emptyExpression || title) && (
<EuiPageContentHeader
className={classNames('lnsWorkspacePanelWrapper__pageContentHeader', {
'lnsWorkspacePanelWrapper__pageContentHeader--unsaved': !title,
})}
>
<span data-test-subj="lns_ChartTitle">
{title ||
i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })}
</span>
</EuiPageContentHeader>
)}
<EuiPageContentBody className="lnsWorkspacePanelWrapper__pageContentBody">
{children}
</EuiPageContentBody>
</EuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -290,6 +290,12 @@ export type VisualizationLayerWidgetProps<T = unknown> = VisualizationConfigProp
setState: (newState: T) => void;
};
export interface VisualizationToolbarProps<T = unknown> {
setState: (newState: T) => void;
frame: FramePublicAPI;
state: T;
}
export type VisualizationDimensionEditorProps<T = unknown> = VisualizationConfigProps<T> & {
groupId: string;
accessor: string;
@ -454,6 +460,11 @@ export interface Visualization<T = unknown, P = unknown> {
* for extra configurability, such as for styling the legend or axis
*/
renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void;
/**
* Toolbar rendered above the visualization. This is meant to be used to provide chart-level
* settings for the visualization.
*/
renderToolbar?: (domElement: Element, props: VisualizationToolbarProps<T>) => void;
/**
* Visualizations can provide a custom icon which will open a layer-specific popover
* If no icon is provided, gear icon is default