-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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] Data panel styling and optimizations #40787
Changes from 86 commits
f46855e
dcde41d
4a3ea3f
30f69e3
82e2865
fe4238a
4d09416
6f81ded
3bf54d4
241187b
8c50598
063715c
8359b28
6a3e6f8
9cb80a8
4718369
5510db0
404944f
8e0dc86
520f0e7
7d6f3c9
cdcc822
3e7c3b4
b72998d
41eb4d4
0674c2c
6d420e6
2f54558
d3e356c
a1f55cf
60c0239
4511c81
5183115
6d91f2e
2cdc322
aa3c8fd
1ae848f
09328ad
82b7ce4
df18560
78f45c5
f82a200
2798552
486bae9
c2b00cf
754b29f
b87f18e
3be4f8e
ed3cf75
6c6d037
46f594a
2690790
bbf573b
2634b9d
0dcc1f4
baba0ef
e10488d
68d50ca
6137613
1536038
c4947bc
084cd26
d89743b
de52e8b
34fe345
08afe00
094fa73
6c7ab30
0c54840
fcd2ff4
6449718
4561870
38c1ce5
aced115
38cd6b2
2140f2e
0b82a6c
2425799
da9ea15
26e5a0e
d0824f6
35a644b
6aa0f03
c10378e
b1792a4
2008051
cd105d9
57b0ff2
f936629
eec45aa
a4b5325
dddf92f
6aa2e0e
6366cf4
e9ad95f
80afe61
fd9a0a5
df1cab5
770e2a7
b130c56
bdbff66
5fd04d0
41a6ebc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.html()).toMatchInlineSnapshot(`"<h1>there</h1>"`); | ||
await new Promise(r => setTimeout(r, 1)); | ||
expect(component.html()).toMatchInlineSnapshot(`"<h1>yall</h1>"`); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These snapshot tests are really weak- please assert something specific about the content, such as:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find the snapshot to be more effective than the examples you gave. Maybe we can discuss. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason I say they're weak is that snapshots aren't testing the behavior of the system. If I change the behavior here, I want tests to fail. Instead of tests failing with a specific message, it says that the snapshots have changed and I see a diff that I have to interpret myself. There's no assertion of specific behavior. For example, the issue with the SeriesType in the XY chart was made worse by snapshot testing. We were snapshotting the rendered chart, but without any assertions that the right series was rendered. So until I fixed it, we were rendering the My preferred solution is combination, like I did for the SeriesType issue: test('it renders stacked horizontal bar', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart data={data} args={{ ...args, seriesType: 'horizontal_bar_stacked' }} />
);
expect(component).toMatchSnapshot();
expect(component.find(BarSeries)).toHaveLength(1);
expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1);
expect(component.find(Settings).prop('rotation')).toEqual(90);
}); That kind of test will catch both unexpected additions and fail fast if the required behavior changes. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* 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'; | ||
|
||
export function debouncedComponent<TProps>(component: FunctionComponent<TProps>, delay = 256) { | ||
const MemoizedComponent = (memo(component) as unknown) as FunctionComponent<TProps>; | ||
|
||
return memo((props: TProps) => { | ||
const [rendered, setRendered] = useState(props); | ||
const delayRender = useMemo(() => debounce(setRendered, delay), []); | ||
|
||
delayRender(props); | ||
|
||
return React.createElement(MemoizedComponent, rendered); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't reading this correctly the first time around, it seems like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. Good point. Rendered is a terrible name. |
||
}); | ||
} |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be great to have a test for the equality of the dragging property during a continuous drag so we don't accidentally break this optimization There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what you mean. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something like: expect(component1.prop('setDragging')).toEqual(component2.prop('setDragging')) |
||
|
||
return ( | ||
<ChildDragDropProvider dragging={state.dragging} setDragging={setDragging}> | ||
|
@@ -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 |
---|---|---|
|
@@ -4,8 +4,8 @@ | |
* 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 { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty hard to test this out since we only have one data source for now! It's maybe not worth optimizing for switching data sources? |
||
import { DatasourceDataPanelProps, Datasource } from '../../../public'; | ||
import { NativeRenderer } from '../../native_renderer'; | ||
import { Action } from './state_management'; | ||
|
@@ -36,21 +36,52 @@ 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="Switch to datasource" | ||
title="Switch to datasource" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i18n please |
||
data-test-subj="datasource-switch" | ||
onClick={() => setDatasourceSwitcher(true)} | ||
iconType="gear" | ||
/> | ||
} | ||
isOpen={showDatasourceSwitcher} | ||
closePopover={() => setDatasourceSwitcher(false)} | ||
panelPaddingSize="none" | ||
anchorPosition="rightUp" | ||
> | ||
<EuiContextMenuPanel | ||
title="Switch to datasource" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i18n please |
||
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} | ||
/> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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({ | ||
|
@@ -502,6 +504,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(); | ||
|
@@ -512,9 +518,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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't need to wrap |
||
}); | ||
act(() => { | ||
(document.querySelector( | ||
'[data-test-subj="datasource-switch-testDatasource2"]' | ||
) as HTMLButtonElement).click(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason you're switching to |
||
}); | ||
expect(mockDatasource2.initialize).toHaveBeenCalled(); | ||
}); | ||
|
@@ -523,9 +532,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(); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm skeptical of any tests using real timers, even if this is effectively immediate. I would recommend using
jest.useFakeTimers()
: https://jestjs.io/docs/en/timer-mocksThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When possible, I prefer to use the real thing, but I'm not strongly opinionated on this.
Edit: Doesn't seem to work, due to the internals of debounce.