Skip to content

Commit

Permalink
[Lens] Data panel styling and optimizations (#40787)
Browse files Browse the repository at this point in the history
Style the data panel (mostly Joe Reuter's doing). Optimize a bunch of the Lens stack.
  • Loading branch information
chrisdavies authored Jul 17, 2019
1 parent dd49f8a commit 9202916
Show file tree
Hide file tree
Showing 26 changed files with 1,039 additions and 250 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { debouncedComponent } from './debounced_component';

describe('debouncedComponent', () => {
test('immediately renders', () => {
const TestComponent = debouncedComponent(({ title }: { title: string }) => {
return <h1>{title}</h1>;
});
expect(mount(<TestComponent title="hoi" />).html()).toMatchInlineSnapshot(`"<h1>hoi</h1>"`);
});

test('debounces changes', async () => {
const TestComponent = debouncedComponent(({ title }: { title: string }) => {
return <h1>{title}</h1>;
}, 1);
const component = mount(<TestComponent title="there" />);
component.setProps({ title: 'yall' });
expect(component.text()).toEqual('there');
await new Promise(r => setTimeout(r, 1));
expect(component.text()).toEqual('yall');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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, { useState, useMemo, memo, FunctionComponent } from 'react';
import { debounce } from 'lodash';

/**
* debouncedComponent wraps the specified React component, returning a component which
* only renders once there is a pause in props changes for at least `delay` milliseconds.
* During the debounce phase, it will return the previously rendered value.
*/
export function debouncedComponent<TProps>(component: FunctionComponent<TProps>, delay = 256) {
const MemoizedComponent = (memo(component) as unknown) as FunctionComponent<TProps>;

return (props: TProps) => {
const [cachedProps, setCachedProps] = useState(props);
const delayRender = useMemo(() => debounce(setCachedProps, delay), []);

delayRender(props);

return React.createElement(MemoizedComponent, cachedProps);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/

export * from './debounced_component';
40 changes: 40 additions & 0 deletions x-pack/legacy/plugins/lens/public/drag_drop/providers.test.tsx
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useContext } from 'react';
import { mount } from 'enzyme';
import { RootDragDropProvider, DragContext } from './providers';

jest.useFakeTimers();

describe('RootDragDropProvider', () => {
test('reuses contexts for each render', () => {
const contexts: any[] = [];
const TestComponent = ({ name }: { name: string }) => {
const context = useContext(DragContext);
contexts.push(context);
return (
<div data-test-subj="test-component">
{name} {!!context.dragging}
</div>
);
};

const RootComponent = ({ name }: { name: string }) => (
<RootDragDropProvider>
<TestComponent name={name} />
</RootDragDropProvider>
);

const component = mount(<RootComponent name="aaaa" />);

component.setProps({ name: 'bbbb' });

expect(component.find('[data-test-subj="test-component"]').text()).toContain('bbb');
expect(contexts.length).toEqual(2);
expect(contexts[0]).toStrictEqual(contexts[1]);
});
});
7 changes: 4 additions & 3 deletions x-pack/legacy/plugins/lens/public/drag_drop/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';

/**
* The shape of the drag / drop context.
Expand Down Expand Up @@ -64,7 +64,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
const [state, setState] = useState<{ dragging: unknown }>({
dragging: undefined,
});
const setDragging = (dragging: unknown) => setState({ dragging });
const setDragging = useMemo(() => (dragging: unknown) => setState({ dragging }), [setState]);

return (
<ChildDragDropProvider dragging={state.dragging} setDragging={setDragging}>
Expand All @@ -81,5 +81,6 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }
* @param props
*/
export function ChildDragDropProvider({ dragging, setDragging, children }: ProviderProps) {
return <DragContext.Provider value={{ dragging, setDragging }}>{children}</DragContext.Provider>;
const value = useMemo(() => ({ dragging, setDragging }), [setDragging, dragging]);
return <DragContext.Provider value={value}>{children}</DragContext.Provider>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo, useContext } from 'react';
import React, { useMemo, useContext, memo } from 'react';
import { EuiSelect } from '@elastic/eui';
import { NativeRenderer } from '../../native_renderer';
import { Action } from './state_management';
Expand Down Expand Up @@ -43,7 +43,7 @@ function getSuggestedVisualizationState(
return visualization.initialize(datasource, suggestions[0].state);
}

export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
const context = useContext(DragContext);
const setVisualizationState = useMemo(
() => (newState: unknown) => {
Expand Down Expand Up @@ -89,4 +89,4 @@ export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) {
)}
</>
);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo, memo, useContext } from 'react';
import { EuiSelect } from '@elastic/eui';
import React, { useMemo, memo, useContext, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
import { DatasourceDataPanelProps, Datasource } from '../../../public';
import { NativeRenderer } from '../../native_renderer';
import { Action } from './state_management';
Expand Down Expand Up @@ -36,21 +37,58 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
setState: setDatasourceState,
};

const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false);

return (
<>
<EuiSelect
data-test-subj="datasource-switch"
options={Object.keys(props.datasourceMap).map(datasourceId => ({
value: datasourceId,
text: datasourceId,
}))}
value={props.activeDatasource || undefined}
onChange={e => {
props.dispatch({ type: 'SWITCH_DATASOURCE', newDatasourceId: e.target.value });
}}
/>
{Object.keys(props.datasourceMap).length > 1 && (
<EuiPopover
id="datasource-switch"
className="lnsDatasourceSwitch"
button={
<EuiButtonIcon
aria-label={i18n.translate('xpack.lens.dataPanelWrapper.switchDatasource', {
defaultMessage: 'Switch to datasource',
})}
title={i18n.translate('xpack.lens.dataPanelWrapper.switchDatasource', {
defaultMessage: 'Switch to datasource',
})}
data-test-subj="datasource-switch"
onClick={() => setDatasourceSwitcher(true)}
iconType="gear"
/>
}
isOpen={showDatasourceSwitcher}
closePopover={() => setDatasourceSwitcher(false)}
panelPaddingSize="none"
anchorPosition="rightUp"
>
<EuiContextMenuPanel
title={i18n.translate('xpack.lens.dataPanelWrapper.switchDatasource', {
defaultMessage: 'Switch to datasource',
})}
items={Object.keys(props.datasourceMap).map(datasourceId => (
<EuiContextMenuItem
key={datasourceId}
data-test-subj={`datasource-switch-${datasourceId}`}
icon={props.activeDatasource === datasourceId ? 'check' : 'empty'}
onClick={() => {
setDatasourceSwitcher(false);
props.dispatch({
type: 'SWITCH_DATASOURCE',
newDatasourceId: datasourceId,
});
}}
>
{datasourceId}
</EuiContextMenuItem>
))}
/>
</EuiPopover>
)}
{props.activeDatasource && !props.datasourceIsLoading && (
<NativeRenderer
className="lnsSidebarContainer"
render={props.datasourceMap[props.activeDatasource].renderDataPanel}
nativeProps={datasourceProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,16 @@ Object {

await waitForPromises();

const updatedState = {};
const updatedState = {
title: 'shazm',
};
const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1]
.setState;
act(() => {
setDatasourceState(updatedState);
});

expect(mockDatasource.renderDataPanel).toHaveBeenCalledTimes(3);
expect(mockDatasource.renderDataPanel).toHaveBeenCalledTimes(2);
expect(mockDatasource.renderDataPanel).toHaveBeenLastCalledWith(
expect.any(Element),
expect.objectContaining({
Expand Down Expand Up @@ -501,6 +503,10 @@ Object {
instance.update();
});

afterEach(() => {
instance.unmount();
});

it('should have initialized only the initial datasource and visualization', () => {
expect(mockDatasource.initialize).toHaveBeenCalled();
expect(mockDatasource2.initialize).not.toHaveBeenCalled();
Expand All @@ -511,9 +517,12 @@ Object {

it('should initialize other datasource on switch', async () => {
act(() => {
instance
.find('select[data-test-subj="datasource-switch"]')
.simulate('change', { target: { value: 'testDatasource2' } });
instance.find('button[data-test-subj="datasource-switch"]').simulate('click');
});
act(() => {
(document.querySelector(
'[data-test-subj="datasource-switch-testDatasource2"]'
) as HTMLButtonElement).click();
});
expect(mockDatasource2.initialize).toHaveBeenCalled();
});
Expand All @@ -522,9 +531,11 @@ Object {
const initialState = {};
mockDatasource2.initialize.mockResolvedValue(initialState);

instance
.find('select[data-test-subj="datasource-switch"]')
.simulate('change', { target: { value: 'testDatasource2' } });
instance.find('button[data-test-subj="datasource-switch"]').simulate('click');

(document.querySelector(
'[data-test-subj="datasource-switch-testDatasource2"]'
) as HTMLButtonElement).click();

await waitForPromises();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,34 @@

.lnsPageMainContent {
display: flex;
overflow: auto;
}

.lnsSidebar {
@include euiScrollBar;
overflow: hidden auto;
padding: $euiSize;
margin: 0;
flex: 1 0 18%;
min-width: ($euiSize * 16);
height: 100%;
min-width: ($euiSize * 22);
display: flex;
flex-direction: column;
position: relative;
}

.lnsSidebar--right {
min-width: ($euiSize * 18);
min-width: ($euiSize * 22);
@include euiScrollBar;
overflow: hidden auto;
padding: $euiSize;
}

.lnsSidebarContainer {
flex: 1 0 100%;
overflow: hidden;
}

.lnsDatasourceSwitch {
position: absolute;
right: $euiSize + $euiSizeXS;
top: $euiSize + $euiSizeXS;
}

.lnsPageBody {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Datasource, Visualization } from '../../types';
import { getSuggestions, toSwitchAction, Suggestion } from './suggestion_helpers';
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public';
import { prependDatasourceExpression } from './expression_helpers';
import { debouncedComponent } from '../../debounced_component';

export interface SuggestionPanelProps {
activeDatasource: Datasource;
Expand Down Expand Up @@ -86,7 +87,9 @@ const SuggestionPreview = ({
);
};

export function SuggestionPanel({
export const SuggestionPanel = debouncedComponent(InnerSuggestionPanel, 2000);

function InnerSuggestionPanel({
activeDatasource,
datasourceState,
activeVisualizationId,
Expand Down
Loading

0 comments on commit 9202916

Please sign in to comment.