Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Add toolbar api #69263

Merged
merged 20 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,6 +57,7 @@ export interface WorkspacePanelProps {
ExpressionRenderer: ReactExpressionRendererType;
core: CoreStart | CoreSetup;
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
title?: string;
}

export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel);
Expand All @@ -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
Expand Down Expand Up @@ -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>
);
}
Original file line number Diff line number Diff line change
@@ -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(),
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add activeVisualization to dependency array of useCallback?

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>
cchaos marked this conversation as resolved.
Show resolved Hide resolved
<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>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic doesn't look right to me: it hides the title for a saved visualization in the following case:

  1. Go to a previously saved Lens visualization
  2. Clear the layer
  3. Title disappears?

<EuiPageContentBody className="lnsWorkspacePanelWrapper__pageContentBody">
{children}
</EuiPageContentBody>
</EuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
);
}
11 changes: 11 additions & 0 deletions x-pack/plugins/lens/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down