-
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
[Embeddable Rebuild] [Controls] Refactor control editing + creation #187606
Merged
Heenawter
merged 29 commits into
elastic:main
from
Heenawter:embeddableRebuild_controls_add-creation_2024-07-03
Jul 17, 2024
Merged
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
bf391c3
Remove use of state manager for editor + add create button
Heenawter 609a2f2
Small style fix + add tooltip
Heenawter d00bc16
Fix some small bugs with editor
Heenawter 2337d23
Clean up + add data editor tests
Heenawter 0d5a661
Add range slider custom options test
Heenawter d699bd5
Fix tests
Heenawter 8c63479
Fix linting
Heenawter 6ab5cb7
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine 2600815
Small cleanup
Heenawter e153a68
Merge branch 'main' of github.com:elastic/kibana into embeddableRebui…
Heenawter 416a80d
Remove unnecessary default
Heenawter 1bc3de1
Merge branch 'embeddableRebuild_controls_add-creation_2024-07-03' of …
Heenawter 0eb12c1
Cleanup
Heenawter 0faa00f
Fix missed rename
Heenawter dbd3578
Fix typo in comment
Heenawter 2ab0845
Make `openDataControlEditor` synchronous
Heenawter 1ef8e39
Mock registry functions
Heenawter be2636d
Clean up name
Heenawter 3bf7641
Only apply updated changes + fix types
Heenawter bd01531
Merge branch 'main' into embeddableRebuild_controls_add-creation_2024…
Heenawter 6401c66
Merge branch 'main' into embeddableRebuild_controls_add-creation_2024…
Heenawter eb54f16
Merge branch 'main' into embeddableRebuild_controls_add-creation_2024…
Heenawter b5147af
Switch to `isEqual`
Heenawter d81e30c
Small range slider style fix
Heenawter 57223a7
Merge branch 'main' of github.com:elastic/kibana into embeddableRebui…
Heenawter 4fcd85a
More quick style fixes
Heenawter f33c1ca
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine 7b8c87b
Fix types
Heenawter 1d5c7e8
Merge branch 'main' of github.com:elastic/kibana into embeddableRebui…
Heenawter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,15 @@ | |
import classNames from 'classnames'; | ||
import React, { useState } from 'react'; | ||
|
||
import { EuiFlexItem, EuiFormControlLayout, EuiFormLabel, EuiFormRow, EuiIcon } from '@elastic/eui'; | ||
import { | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
EuiFormControlLayout, | ||
EuiFormLabel, | ||
EuiFormRow, | ||
EuiIcon, | ||
EuiToolTip, | ||
} from '@elastic/eui'; | ||
import { ViewMode } from '@kbn/embeddable-plugin/public'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { | ||
|
@@ -135,12 +143,18 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA | |
controlTitle={panelTitle || defaultPanelTitle} | ||
hideEmptyDragHandle={usingTwoLineLayout || Boolean(api?.CustomPrependComponent)} | ||
/> | ||
|
||
{api?.CustomPrependComponent ? ( | ||
<api.CustomPrependComponent /> | ||
) : usingTwoLineLayout ? null : ( | ||
<EuiFormLabel className="controlPanel--label"> | ||
{panelTitle || defaultPanelTitle} | ||
</EuiFormLabel> | ||
<EuiToolTip | ||
anchorClassName="controlPanel--labelWrapper" | ||
content={panelTitle || defaultPanelTitle} | ||
> | ||
<EuiFormLabel className="controlPanel--label"> | ||
{panelTitle || defaultPanelTitle} | ||
</EuiFormLabel> | ||
</EuiToolTip> | ||
Comment on lines
+149
to
+156
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 accidentally forgot to include the tooltip when initially building this, but we need it for when things get truncated. |
||
)} | ||
</> | ||
} | ||
|
236 changes: 236 additions & 0 deletions
236
examples/controls_example/public/react_controls/data_controls/data_control_editor.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import React from 'react'; | ||
import { BehaviorSubject } from 'rxjs'; | ||
|
||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; | ||
import { stubFieldSpecMap } from '@kbn/data-views-plugin/common/field.stub'; | ||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; | ||
import { TimeRange } from '@kbn/es-query'; | ||
import { I18nProvider } from '@kbn/i18n-react'; | ||
import { act, fireEvent, render, RenderResult, waitFor } from '@testing-library/react'; | ||
|
||
import { registerControlFactory } from '../control_factory_registry'; | ||
import { ControlGroupApi } from '../control_group/types'; | ||
import { DataControlEditor } from './data_control_editor'; | ||
import { | ||
getMockedOptionsListControlFactory, | ||
getMockedRangeSliderControlFactory, | ||
getMockedSearchControlFactory, | ||
} from './mocks/data_control_mocks'; | ||
import { DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '@kbn/controls-plugin/common'; | ||
import { DataControlEditorState } from './open_data_control_editor'; | ||
|
||
const mockDataViews = dataViewPluginMocks.createStartContract(); | ||
const mockDataView = createStubDataView({ | ||
spec: { | ||
id: 'logstash-*', | ||
fields: { | ||
...stubFieldSpecMap, | ||
'machine.os.raw': { | ||
name: 'machine.os.raw', | ||
customLabel: 'OS', | ||
type: 'string', | ||
esTypes: ['keyword'], | ||
aggregatable: true, | ||
searchable: true, | ||
}, | ||
}, | ||
title: 'logstash-*', | ||
timeFieldName: '@timestamp', | ||
}, | ||
}); | ||
mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); | ||
|
||
const dashboardApi = { | ||
timeRange$: new BehaviorSubject<TimeRange | undefined>(undefined), | ||
lastUsedDataViewId$: new BehaviorSubject<string>(mockDataView.id!), | ||
}; | ||
const controlGroupApi = { | ||
parentApi: dashboardApi, | ||
grow: new BehaviorSubject(DEFAULT_CONTROL_GROW), | ||
width: new BehaviorSubject(DEFAULT_CONTROL_WIDTH), | ||
} as unknown as ControlGroupApi; | ||
|
||
describe('Data control editor', () => { | ||
const mountComponent = async ({ | ||
initialState, | ||
}: { | ||
initialState?: Partial<DataControlEditorState>; | ||
}) => { | ||
mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); | ||
|
||
const controlEditor = render( | ||
<I18nProvider> | ||
<DataControlEditor | ||
onCancel={() => {}} | ||
onSave={() => {}} | ||
parentApi={controlGroupApi} | ||
initialState={{ | ||
dataViewId: dashboardApi.lastUsedDataViewId$.getValue(), | ||
...initialState, | ||
}} | ||
services={{ dataViews: mockDataViews }} | ||
/> | ||
</I18nProvider> | ||
); | ||
|
||
await waitFor(() => { | ||
expect(mockDataViews.get).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
return controlEditor; | ||
}; | ||
|
||
const selectField = async (controlEditor: RenderResult, fieldName: string) => { | ||
expect(controlEditor.queryByTestId(`field-picker-select-${fieldName}`)).toBeInTheDocument(); | ||
await act(async () => { | ||
fireEvent.click(controlEditor.getByTestId(`field-picker-select-${fieldName}`)); | ||
}); | ||
}; | ||
|
||
const getPressedAttribute = (controlEditor: RenderResult, testId: string) => { | ||
return controlEditor.getByTestId(testId).getAttribute('aria-pressed'); | ||
}; | ||
|
||
beforeAll(() => { | ||
/** Register all of my mocked data controls */ | ||
registerControlFactory('search', async () => { | ||
Heenawter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return getMockedSearchControlFactory({ parentApi: controlGroupApi }); | ||
}); | ||
registerControlFactory('optionsList', async () => { | ||
return getMockedOptionsListControlFactory({ parentApi: controlGroupApi }); | ||
}); | ||
registerControlFactory('rangeSlider', async () => { | ||
return getMockedRangeSliderControlFactory({ parentApi: controlGroupApi }); | ||
}); | ||
}); | ||
|
||
describe('creating a new control', () => { | ||
test('field list does not include fields that are not compatible with any control types', async () => { | ||
const controlEditor = await mountComponent({}); | ||
const nonAggOption = controlEditor.queryByTestId('field-picker-select-machine.os'); | ||
expect(nonAggOption).not.toBeInTheDocument(); | ||
}); | ||
|
||
test('cannot save before selecting a field', async () => { | ||
const controlEditor = await mountComponent({}); | ||
|
||
const saveButton = controlEditor.getByTestId('control-editor-save'); | ||
expect(saveButton).toBeDisabled(); | ||
await selectField(controlEditor, 'machine.os.raw'); | ||
expect(saveButton).toBeEnabled(); | ||
}); | ||
|
||
test('selecting a keyword field - can only create an options list control', async () => { | ||
const controlEditor = await mountComponent({}); | ||
await selectField(controlEditor, 'machine.os.raw'); | ||
|
||
expect(controlEditor.getByTestId('create__optionsList')).toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__rangeSlider')).not.toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__search')).not.toBeEnabled(); | ||
}); | ||
|
||
test('selecting an IP field - can only create an options list control', async () => { | ||
const controlEditor = await mountComponent({}); | ||
await selectField(controlEditor, 'clientip'); | ||
|
||
expect(controlEditor.getByTestId('create__optionsList')).toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__rangeSlider')).not.toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__search')).not.toBeEnabled(); | ||
}); | ||
|
||
describe('selecting a number field', () => { | ||
let controlEditor: RenderResult; | ||
|
||
beforeEach(async () => { | ||
controlEditor = await mountComponent({}); | ||
await selectField(controlEditor, 'bytes'); | ||
}); | ||
|
||
test('can create an options list or range slider control', () => { | ||
expect(controlEditor.getByTestId('create__optionsList')).toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__rangeSlider')).toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__search')).not.toBeEnabled(); | ||
}); | ||
|
||
test('defaults to options list creation', () => { | ||
expect(getPressedAttribute(controlEditor, 'create__optionsList')).toBe('true'); | ||
expect(getPressedAttribute(controlEditor, 'create__rangeSlider')).toBe('false'); | ||
}); | ||
}); | ||
|
||
test('renders custom settings when provided', async () => { | ||
const controlEditor = await mountComponent({}); | ||
await selectField(controlEditor, 'machine.os.raw'); | ||
expect(controlEditor.queryByTestId('optionsListCustomSettings')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
test('selects the default width and grow', async () => { | ||
const controlEditor = await mountComponent({}); | ||
|
||
expect(getPressedAttribute(controlEditor, 'control-editor-width-small')).toBe('false'); | ||
expect(getPressedAttribute(controlEditor, 'control-editor-width-medium')).toBe('true'); | ||
expect(getPressedAttribute(controlEditor, 'control-editor-width-large')).toBe('false'); | ||
expect( | ||
controlEditor.getByTestId('control-editor-grow-switch').getAttribute('aria-checked') | ||
).toBe(`${DEFAULT_CONTROL_GROW}`); | ||
}); | ||
|
||
describe('editing existing control', () => { | ||
describe('control title', () => { | ||
test('auto-fills input with the default title', async () => { | ||
const controlEditor = await mountComponent({ | ||
initialState: { | ||
controlType: 'optionsList', | ||
controlId: 'testId', | ||
fieldName: 'machine.os.raw', | ||
defaultPanelTitle: 'OS', | ||
}, | ||
}); | ||
const titleInput = await controlEditor.findByTestId('control-editor-title-input'); | ||
expect(titleInput.getAttribute('value')).toBe('OS'); | ||
expect(titleInput.getAttribute('placeholder')).toBe('OS'); | ||
}); | ||
|
||
test('auto-fills input with the custom title', async () => { | ||
const controlEditor = await mountComponent({ | ||
initialState: { | ||
controlType: 'optionsList', | ||
controlId: 'testId', | ||
fieldName: 'machine.os.raw', | ||
title: 'Custom title', | ||
}, | ||
}); | ||
const titleInput = await controlEditor.findByTestId('control-editor-title-input'); | ||
expect(titleInput.getAttribute('value')).toBe('Custom title'); | ||
expect(titleInput.getAttribute('placeholder')).toBe('machine.os.raw'); | ||
}); | ||
}); | ||
|
||
test('selects the provided control type', async () => { | ||
const controlEditor = await mountComponent({ | ||
initialState: { | ||
controlType: 'rangeSlider', | ||
controlId: 'testId', | ||
fieldName: 'bytes', | ||
}, | ||
}); | ||
|
||
expect(controlEditor.getByTestId('create__optionsList')).toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__rangeSlider')).toBeEnabled(); | ||
expect(controlEditor.getByTestId('create__search')).not.toBeEnabled(); | ||
|
||
expect(getPressedAttribute(controlEditor, 'create__optionsList')).toBe('false'); | ||
expect(getPressedAttribute(controlEditor, 'create__rangeSlider')).toBe('true'); | ||
expect(getPressedAttribute(controlEditor, 'create__search')).toBe('false'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This fixes a style error where the truncation was happening before the
max-width
applied, so labels were getting cut off too early on the "small" control size. I also verified that the truncation now happens at the same point that it does for the old controls, so there should be no visual changes from the user's perspective once the React controls are officially swapped in Dashboard.