From 3e460608549ea0138d8bf9a6177f61a226ca512b Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Fri, 17 Jan 2020 14:01:44 +0300 Subject: [PATCH] [Vis: Default editor] EUIficate and Reactify the sidebar (#49864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * EUIficate the sidebar * Create a state reducer and a state context * Create an editor context and actions * Improve types * Apply aggs reordering * Fix functionality * Improve types * Fix sub_agg changes * Remove legacy dependencies * Watch dirty state * Fix dirty state changes * Update actions and reducers * Handle keyboard submit * Apply editor form validation * Remove fancy forms * Update validation * Use embeddable instead of visualize loader * Add auto apply behavior * Remove legacy styles * Remove the sidebar * Restrict responsive to the bottom_bar * Upgrade @elastic/eui to v14.10.0 * Replace EuiBottomBar with EuiControlBar * Get rid of mutations in control vis * Revert "Upgrade @elastic/eui to v14.10.0" This reverts commit 2cd86c51d2f93270c0d93e6c4122235452538406. * Replace bottom bar with a control panel for sidebar * Replace selectors * Use editor resizer * Apply selectors * Change selectors * Fix sub agg change values * Add collapse button * Fix tests * Get rid of editor editor_state_context, simplify the code * Fix jest tests, update snapshots * Fix types * Moving collapse button to right of index pattern * Tweaks bottom buttons * Moved Vega buttons so they don’t scroll away * Fix responsiveness * Resolve UI comments * Fix console resizer * Update dev docs * Bail out of additional render in metrics and axes * Apply performance optimizations for metrics and axis panel * Remove unused translations * Use debounce when autoapply enabled Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../development-create-visualization.asciidoc | 11 +- .../__snapshots__/split_panel.test.tsx.snap | 36 ++- .../split_panel/components/resizer.tsx | 22 +- .../split_panel/containers/panel.tsx | 16 +- .../containers/panel_container.tsx | 61 ++++- .../components/split_panel/registry.ts | 4 +- .../split_panel/split_panel.test.tsx | 4 +- .../application/containers/editor/editor.tsx | 6 +- .../__snapshots__/controls_tab.test.tsx.snap | 6 +- .../components/editor/control_editor.tsx | 23 +- .../public/components/editor/controls_tab.tsx | 66 ++--- .../editor/list_control_editor.test.tsx | 64 ++--- .../components/editor/list_control_editor.tsx | 37 +-- .../editor/range_control_editor.test.tsx | 44 +--- .../editor/range_control_editor.tsx | 20 +- .../public/input_control_vis_type.ts | 1 - .../np_ready/dashboard_state.test.ts | 6 + .../kibana/public/visualize/index.ts | 3 - .../kibana/public/visualize/legacy_imports.ts | 2 - .../visualize/np_ready/editor/_editor.scss | 5 - .../visualize/np_ready/editor/editor.js | 5 + .../public/visualize/np_ready/types.d.ts | 11 +- .../kibana/public/visualize/plugin.ts | 3 - .../timelion/public/vis/index.tsx | 4 +- .../public/_markdown_vis.scss | 9 +- .../public/components/table_vis_options.tsx | 3 +- .../vis_type_table/public/vis_controller.ts | 6 + .../vis_type_vega/public/_vega_editor.scss | 13 +- .../vis_type_vega/public/vega_type.ts | 4 +- .../__snapshots__/index.test.tsx.snap | 12 +- .../options/metrics_axes/index.test.tsx | 60 +---- .../components/options/metrics_axes/index.tsx | 34 +-- .../visualizations/public/legacy_imports.ts | 1 + .../np_ready/public/types/base_vis_type.js | 4 +- .../public/np_ready/public/vis.js | 16 +- src/legacy/ui/public/agg_types/agg_config.ts | 1 + src/legacy/ui/public/agg_types/agg_configs.ts | 6 +- src/legacy/ui/public/styles/_mixins.scss | 18 +- .../agg_params.d.ts => editor_size.ts} | 18 +- .../config/editor_config_providers.test.ts | 15 +- .../editors/config/editor_config_providers.ts | 17 +- .../public/vis/editors/default/_default.scss | 66 +++-- .../ui/public/vis/editors/default/_index.scss | 2 - .../public/vis/editors/default/_sidebar.scss | 108 +++------ .../public/vis/editors/default/agg_group.js | 98 -------- .../__snapshots__/agg.test.tsx.snap | 4 +- .../__snapshots__/agg_group.test.tsx.snap | 1 + .../__snapshots__/agg_params.test.tsx.snap | 31 +-- .../editors/default/components/agg.test.tsx | 61 +++-- .../vis/editors/default/components/agg.tsx | 70 ++++-- .../default/components/agg_common_props.ts | 39 +-- .../default/components/agg_group.test.tsx | 51 +--- .../editors/default/components/agg_group.tsx | 68 +++--- .../default/components/agg_group_state.tsx | 2 +- .../editors/default/components/agg_param.tsx | 52 +++- .../default/components/agg_param_props.ts | 13 +- .../default/components/agg_params.test.tsx | 20 +- .../editors/default/components/agg_params.tsx | 157 ++++++------ .../components/agg_params_helper.test.ts | 15 -- .../default/components/agg_params_helper.ts | 19 +- .../editors/default/components/agg_select.tsx | 35 ++- .../default/components/sidebar/controls.tsx | 153 ++++++++++++ .../default/components/sidebar/data_tab.tsx | 135 +++++++++++ .../default/components/sidebar/index.ts} | 8 +- .../default/components/sidebar/navbar.tsx | 59 +++++ .../default/components/sidebar/sidebar.tsx | 226 ++++++++++++++++++ .../components/sidebar/state/actions.ts | 171 +++++++++++++ .../sidebar/state/constants.ts} | 21 +- .../sidebar/state/editor_form_state.ts | 68 ++++++ .../default/components/sidebar/state/index.ts | 59 +++++ .../components/sidebar/state/reducers.ts | 182 ++++++++++++++ .../default/controls/agg_control_props.tsx | 10 +- .../vis/editors/default/controls/agg_utils.ts | 2 +- .../editors/default/controls/field.test.tsx | 13 +- .../vis/editors/default/controls/field.tsx | 9 +- .../editors/default/controls/order_agg.tsx | 50 ++-- .../default/controls/radius_ratio_option.tsx | 16 +- .../default/controls/rows_or_columns.tsx | 12 +- .../vis/editors/default/controls/string.tsx | 6 +- .../vis/editors/default/controls/sub_agg.tsx | 37 +-- .../editors/default/controls/sub_metric.tsx | 32 +-- .../editors/default/controls/test_utils.ts | 3 +- .../vis/editors/default/controls/utils.ts | 65 +++++ .../public/vis/editors/default/default.html | 17 -- .../ui/public/vis/editors/default/default.js | 213 ----------------- .../vis/editors/default/default_editor.tsx | 127 ++++++++++ .../default/default_editor_controller.tsx | 89 +++++++ .../fancy_forms/__tests__/fancy_forms.js | 62 ----- .../__tests__/nested_fancy_forms.js | 202 ---------------- .../default/fancy_forms/fancy_forms.js | 29 --- .../vis/editors/default/fancy_forms/index.js | 20 -- .../fancy_forms/kbn_form_controller.js | 87 ------- .../fancy_forms/kbn_model_controller.js | 54 ----- .../ui/public/vis/editors/default/index.ts | 2 +- .../ui/public/vis/editors/default/schemas.ts | 5 + .../public/vis/editors/default/sidebar.html | 191 --------------- .../ui/public/vis/editors/default/sidebar.js | 105 -------- .../vis/editors/default/vis_editor_resizer.js | 62 ----- .../public/vis/editors/default/vis_options.js | 121 ---------- .../vis/editors/default/vis_options_props.tsx | 3 +- .../public/vis/vis_types/angular_vis_type.js | 6 + test/functional/apps/visualize/_inspector.js | 2 + .../apps/visualize/_markdown_vis.js | 2 +- .../apps/visualize/_point_series_options.js | 2 +- test/functional/apps/visualize/_region_map.js | 3 + test/functional/apps/visualize/_tag_cloud.js | 6 +- .../page_objects/point_series_page.js | 4 - .../page_objects/visualize_editor_page.ts | 26 +- .../functional/page_objects/visualize_page.ts | 2 +- test/functional/services/browser.ts | 4 +- .../common/layouts/preserve_layout.css | 7 +- .../export_types/common/layouts/print.css | 7 +- .../rollup/public/visualize/editor_config.js | 2 +- .../translations/translations/ja-JP.json | 8 - .../translations/translations/zh-CN.json | 8 - 115 files changed, 2218 insertions(+), 2206 deletions(-) rename src/legacy/ui/public/vis/{editors/default/agg_params.d.ts => editor_size.ts} (69%) delete mode 100644 src/legacy/ui/public/vis/editors/default/agg_group.js create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/controls.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/data_tab.tsx rename src/legacy/ui/public/vis/{editor_size.js => editors/default/components/sidebar/index.ts} (85%) create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/navbar.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/sidebar.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/state/actions.ts rename src/legacy/ui/public/vis/editors/default/{vis_options_react_wrapper.tsx => components/sidebar/state/constants.ts} (66%) create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/state/editor_form_state.ts create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/state/index.ts create mode 100644 src/legacy/ui/public/vis/editors/default/components/sidebar/state/reducers.ts create mode 100644 src/legacy/ui/public/vis/editors/default/controls/utils.ts delete mode 100644 src/legacy/ui/public/vis/editors/default/default.html delete mode 100644 src/legacy/ui/public/vis/editors/default/default.js create mode 100644 src/legacy/ui/public/vis/editors/default/default_editor.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/default_editor_controller.tsx delete mode 100644 src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js delete mode 100644 src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js delete mode 100644 src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js delete mode 100644 src/legacy/ui/public/vis/editors/default/fancy_forms/index.js delete mode 100644 src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js delete mode 100644 src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js delete mode 100644 src/legacy/ui/public/vis/editors/default/sidebar.html delete mode 100644 src/legacy/ui/public/vis/editors/default/sidebar.js delete mode 100644 src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js delete mode 100644 src/legacy/ui/public/vis/editors/default/vis_options.js diff --git a/docs/developer/visualize/development-create-visualization.asciidoc b/docs/developer/visualize/development-create-visualization.asciidoc index b782428b83135..faaa9b36a7a00 100644 --- a/docs/developer/visualize/development-create-visualization.asciidoc +++ b/docs/developer/visualize/development-create-visualization.asciidoc @@ -208,8 +208,8 @@ This is the sidebar editor you see in many of the Kibana visualizations. You can [[development-default-editor]] ==== `default` editor controller -The default editor controller receives an `optionsTemplate` or `optionsTabs` parameter. -These can be either an AngularJS template or React component. +The default editor controller receives an `optionsTemplate` or `optionTabs` parameter. +These tabs should be React components. ["source","js"] ----------- @@ -220,12 +220,9 @@ These can be either an AngularJS template or React component. description: 'Cool new chart', editor: 'default', editorConfig: { - optionsTemplate: '' // or optionsTemplate: MyReactComponent // or if multiple tabs are required: - optionsTabs: [ - { title: 'tab 1', template: '
....
}, - { title: 'tab 2', template: '' }, - { title: 'tab 3', template: MyReactComponent } + optionTabs: [ + { title: 'tab 3', editor: MyReactComponent } ] } } diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap index 0a40e3e84211d..36f4dec1a1f54 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/__snapshots__/split_panel.test.tsx.snap @@ -8,13 +8,13 @@ exports[`Split panel should render correctly 1`] = ` "panels": Array [ Object { "getWidth": [Function], - "initialWidth": "100%", "setWidth": [Function], + "width": 100, }, Object { "getWidth": [Function], - "initialWidth": "100%", "setWidth": [Function], + "width": 100, }, ], } @@ -55,15 +55,39 @@ exports[`Split panel should render correctly 1`] = ` -
- ︙ -
+ + + + + +
; +export type ResizerMouseEvent = React.MouseEvent; +export type ResizerKeyDownEvent = React.KeyboardEvent; export interface Props { + onKeyDown: (eve: ResizerKeyDownEvent) => void; onMouseDown: (eve: ResizerMouseEvent) => void; + className?: string; } -/** - * TODO: This component uses styling constants from public UI - should be removed, next iteration should incl. horizontal and vertical resizers. - */ export function Resizer(props: Props) { return ( -
- ︙ -
+ ); } diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx index 80960a7772ba1..2eb39f0808ad0 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel.tsx @@ -22,20 +22,26 @@ import { usePanelContext } from '../context'; export interface Props { children: ReactNode[] | ReactNode; - initialWidth?: string; + className?: string; + + /** + * initial width of the panel in percents + */ + initialWidth?: number; style?: CSSProperties; } -export function Panel({ children, initialWidth = '100%', style = {} }: Props) { - const [width, setWidth] = useState(initialWidth); +export function Panel({ children, className, initialWidth = 100, style = {} }: Props) { + const [width, setWidth] = useState(`${initialWidth}%`); const { registry } = usePanelContext(); const divRef = useRef(null); useEffect(() => { registry.registerPanel({ - initialWidth, + width: initialWidth, setWidth(value) { setWidth(value + '%'); + this.width = value; }, getWidth() { return divRef.current!.getBoundingClientRect().width; @@ -44,7 +50,7 @@ export function Panel({ children, initialWidth = '100%', style = {} }: Props) { }, [initialWidth, registry]); return ( -
+
{children}
); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx index fef65a954bd60..c9d7b01f87967 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/containers/panel_container.tsx @@ -17,14 +17,17 @@ * under the License. */ -import React, { Children, ReactNode, useRef, useState } from 'react'; +import React, { Children, ReactNode, useRef, useState, useCallback } from 'react'; +import { keyCodes } from '@elastic/eui'; import { PanelContextProvider } from '../context'; -import { Resizer } from '../components/resizer'; +import { Resizer, ResizerMouseEvent, ResizerKeyDownEvent } from '../components/resizer'; import { PanelRegistry } from '../registry'; export interface Props { children: ReactNode; + className?: string; + resizerClassName?: string; onPanelWidthChange?: (arrayOfPanelWidths: number[]) => any; } @@ -37,7 +40,12 @@ const initialState: State = { isDragging: false, currentResizerPos: -1 }; const pxToPercent = (proportion: number, whole: number) => (proportion / whole) * 100; -export function PanelsContainer({ children, onPanelWidthChange }: Props) { +export function PanelsContainer({ + children, + className, + onPanelWidthChange, + resizerClassName, +}: Props) { const [firstChild, secondChild] = Children.toArray(children); const registryRef = useRef(new PanelRegistry()); @@ -48,18 +56,48 @@ export function PanelsContainer({ children, onPanelWidthChange }: Props) { return containerRef.current!.getBoundingClientRect().width; }; + const handleMouseDown = useCallback( + (event: ResizerMouseEvent) => { + setState({ + ...state, + isDragging: true, + currentResizerPos: event.clientX, + }); + }, + [state] + ); + + const handleKeyDown = useCallback( + (ev: ResizerKeyDownEvent) => { + const { keyCode } = ev; + + if (keyCode === keyCodes.LEFT || keyCode === keyCodes.RIGHT) { + ev.preventDefault(); + + const { current: registry } = registryRef; + const [left, right] = registry.getPanels(); + + const leftPercent = left.width - (keyCode === keyCodes.LEFT ? 1 : -1); + const rightPercent = right.width - (keyCode === keyCodes.RIGHT ? 1 : -1); + + left.setWidth(leftPercent); + right.setWidth(rightPercent); + + if (onPanelWidthChange) { + onPanelWidthChange([leftPercent, rightPercent]); + } + } + }, + [onPanelWidthChange] + ); + const childrenWithResizer = [ firstChild, { - event.preventDefault(); - setState({ - ...state, - isDragging: true, - currentResizerPos: event.clientX, - }); - }} + className={resizerClassName} + onKeyDown={handleKeyDown} + onMouseDown={handleMouseDown} />, secondChild, ]; @@ -67,6 +105,7 @@ export function PanelsContainer({ children, onPanelWidthChange }: Props) { return (
{ diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts index 5f06ab8915270..e275da9e2ac74 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/registry.ts @@ -20,7 +20,7 @@ export interface PanelController { setWidth: (percent: number) => void; getWidth: () => number; - initialWidth: string; + width: number; } export class PanelRegistry { @@ -35,6 +35,6 @@ export class PanelRegistry { } getPanels() { - return this.panels.map(panel => ({ ...panel })); + return this.panels; } } diff --git a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx index 304535421a78a..02153d1a1d3cd 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/components/split_panel/split_panel.test.tsx @@ -65,8 +65,8 @@ describe('Split panel', () => { const panelContainer = mount( - {testComponentA} - {testComponentB} + {testComponentA} + {testComponentB} ); diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx index 56449bfb45417..7be1382760eb9 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/editor.tsx @@ -55,10 +55,10 @@ export const Editor = ({ loading }: Props) => { if (!currentTextObject) return null; return ( - + {loading ? ( @@ -68,7 +68,7 @@ export const Editor = ({ loading }: Props) => { {loading ? : } diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap index 278811ca85df9..249f42a6ebf3f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap @@ -44,11 +44,10 @@ exports[`renders ControlsTab 1`] = ` } } getIndexPattern={[Function]} - handleCheckboxOptionChange={[Function]} handleFieldNameChange={[Function]} handleIndexPatternChange={[Function]} handleLabelChange={[Function]} - handleNumberOptionChange={[Function]} + handleOptionsChange={[Function]} handleParentChange={[Function]} handleRemoveControl={[Function]} key="1" @@ -101,11 +100,10 @@ exports[`renders ControlsTab 1`] = ` } } getIndexPattern={[Function]} - handleCheckboxOptionChange={[Function]} handleFieldNameChange={[Function]} handleIndexPatternChange={[Function]} handleLabelChange={[Function]} - handleNumberOptionChange={[Function]} + handleOptionsChange={[Function]} handleParentChange={[Function]} handleRemoveControl={[Function]} key="2" diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx index dbac5d9d94371..2bd0baea6eff8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -29,7 +29,6 @@ import { EuiFormRow, EuiPanel, EuiSpacer, - EuiSwitchEvent, } from '@elastic/eui'; import { RangeControlEditor } from './range_control_editor'; @@ -41,33 +40,28 @@ import { InputControlVisDependencies } from '../../plugin'; interface ControlEditorUiProps { controlIndex: number; controlParams: ControlParams; - handleLabelChange: (controlIndex: number, event: ChangeEvent) => void; + handleLabelChange: (controlIndex: number, value: string) => void; moveControl: (controlIndex: number, direction: number) => void; handleRemoveControl: (controlIndex: number) => void; handleIndexPatternChange: (controlIndex: number, indexPatternId: string) => void; handleFieldNameChange: (controlIndex: number, fieldName: string) => void; getIndexPattern: (indexPatternId: string) => Promise; - handleCheckboxOptionChange: ( + handleOptionsChange: ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: EuiSwitchEvent - ) => void; - handleNumberOptionChange: ( - controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent + optionName: T, + value: ControlParamsOptions[T] ) => void; parentCandidates: Array<{ value: string; text: string; }>; - handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + handleParentChange: (controlIndex: number, parent: string) => void; deps: InputControlVisDependencies; } class ControlEditorUi extends PureComponent { changeLabel = (event: ChangeEvent) => { - this.props.handleLabelChange(this.props.controlIndex, event); + this.props.handleLabelChange(this.props.controlIndex, event.target.value); }; removeControl = () => { @@ -101,8 +95,7 @@ class ControlEditorUi extends PureComponent ); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx index 56381ef7d1570..214cff4ddf9d5 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { PureComponent, ChangeEvent } from 'react'; +import React, { PureComponent } from 'react'; import { InjectedIntlProps } from 'react-intl'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; @@ -28,7 +28,6 @@ import { EuiFormRow, EuiPanel, EuiSelect, - EuiSwitchEvent, } from '@elastic/eui'; import { ControlEditor } from './control_editor'; @@ -73,44 +72,44 @@ class ControlsTabUi extends PureComponent this.props.setValue('controls', value); - handleLabelChange = (controlIndex: number, event: ChangeEvent) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.label = event.target.value; + handleLabelChange = (controlIndex: number, label: string) => { + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + label, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleIndexPatternChange = (controlIndex: number, indexPatternId: string) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.indexPattern = indexPatternId; - updatedControl.fieldName = ''; + handleIndexPatternChange = (controlIndex: number, indexPattern: string) => { + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + indexPattern, + fieldName: '', + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; handleFieldNameChange = (controlIndex: number, fieldName: string) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.fieldName = fieldName; + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + fieldName, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleCheckboxOptionChange = ( + handleOptionsChange = ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: EuiSwitchEvent + optionName: T, + value: ControlParamsOptions[T] ) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - // @ts-ignore - updatedControl.options[optionName] = event.target.checked; - this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); - }; - - handleNumberOptionChange = ( - controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent - ) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - // @ts-ignore - updatedControl.options[optionName] = parseFloat(event.target.value); + const control = this.props.stateParams.controls[controlIndex]; + const updatedControl = { + ...control, + options: { + ...control.options, + [optionName]: value, + }, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; @@ -126,9 +125,11 @@ class ControlsTabUi extends PureComponent) => { - const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.parent = event.target.value; + handleParentChange = (controlIndex: number, parent: string) => { + const updatedControl = { + ...this.props.stateParams.controls[controlIndex], + parent, + }; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; @@ -151,8 +152,7 @@ class ControlsTabUi extends PureComponent { handleFieldNameChange = sinon.spy(); handleIndexPatternChange = sinon.spy(); - handleCheckboxOptionChange = sinon.spy(); - handleNumberOptionChange = sinon.spy(); + handleOptionsChange = sinon.spy(); }); describe('renders', () => { @@ -82,8 +80,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -107,8 +104,7 @@ describe('renders', () => { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={parentCandidates} /> @@ -143,8 +139,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -178,8 +173,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -213,8 +207,7 @@ describe('renders', () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -227,7 +220,7 @@ describe('renders', () => { }); }); -test('handleCheckboxOptionChange - multiselect', async () => { +test('handleOptionsChange - multiselect', async () => { const component = mountWithIntl( { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -249,25 +241,12 @@ test('handleCheckboxOptionChange - multiselect', async () => { checkbox.simulate('click'); sinon.assert.notCalled(handleFieldNameChange); sinon.assert.notCalled(handleIndexPatternChange); - sinon.assert.notCalled(handleNumberOptionChange); const expectedControlIndex = 0; const expectedOptionName = 'multiselect'; - sinon.assert.calledWith( - handleCheckboxOptionChange, - expectedControlIndex, - expectedOptionName, - sinon.match(event => { - // Synthetic `event.target.checked` does not get altered by EuiSwitch, - // but its aria attribute is correctly updated - if (event.target.getAttribute('aria-checked') === 'true') { - return true; - } - return false; - }, 'unexpected checkbox input event') - ); + sinon.assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName); }); -test('handleNumberOptionChange - size', async () => { +test('handleOptionsChange - size', async () => { const component = mountWithIntl( { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> @@ -286,23 +264,12 @@ test('handleNumberOptionChange - size', async () => { await updateComponent(component); const input = findTestSubject(component, 'listControlSizeInput'); - input.simulate('change', { target: { value: 7 } }); - sinon.assert.notCalled(handleCheckboxOptionChange); + input.simulate('change', { target: { valueAsNumber: 7 } }); sinon.assert.notCalled(handleFieldNameChange); sinon.assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'size'; - sinon.assert.calledWith( - handleNumberOptionChange, - expectedControlIndex, - expectedOptionName, - sinon.match(event => { - if (event.target.value === 7) { - return true; - } - return false; - }, 'unexpected input event') - ); + sinon.assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName, 7); }); test('field name change', async () => { @@ -314,8 +281,7 @@ test('field name change', async () => { controlParams={controlParamsBase} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleCheckboxOptionChange={handleCheckboxOptionChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} handleParentChange={() => {}} parentCandidates={[]} /> diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx index ed68894d39ae4..9772cb5fc2548 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -17,10 +17,10 @@ * under the License. */ -import React, { PureComponent, ChangeEvent, ComponentType } from 'react'; +import React, { PureComponent, ComponentType } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect, EuiSwitchEvent } from '@elastic/eui'; +import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; @@ -45,17 +45,12 @@ interface ListControlEditorProps { controlParams: ControlParams; handleFieldNameChange: (fieldName: string) => void; handleIndexPatternChange: (indexPatternId: string) => void; - handleCheckboxOptionChange: ( + handleOptionsChange: ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: EuiSwitchEvent + optionName: T, + value: ControlParamsOptions[T] ) => void; - handleNumberOptionChange: ( - controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent - ) => void; - handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + handleParentChange: (controlIndex: number, parent: string) => void; parentCandidates: React.ComponentProps['options']; deps: InputControlVisDependencies; } @@ -177,7 +172,7 @@ export class ListControlEditor extends PureComponent< options={parentCandidatesOptions} value={this.props.controlParams.parent} onChange={event => { - this.props.handleParentChange(this.props.controlIndex, event); + this.props.handleParentChange(this.props.controlIndex, event.target.value); }} /> @@ -204,7 +199,11 @@ export class ListControlEditor extends PureComponent< } checked={this.props.controlParams.options.multiselect ?? true} onChange={event => { - this.props.handleCheckboxOptionChange(this.props.controlIndex, 'multiselect', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'multiselect', + event.target.checked + ); }} data-test-subj="listControlMultiselectInput" /> @@ -237,7 +236,11 @@ export class ListControlEditor extends PureComponent< } checked={this.props.controlParams.options.dynamicOptions ?? false} onChange={event => { - this.props.handleCheckboxOptionChange(this.props.controlIndex, 'dynamicOptions', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'dynamicOptions', + event.target.checked + ); }} disabled={this.state.isStringField ? false : true} data-test-subj="listControlDynamicOptionsSwitch" @@ -268,7 +271,11 @@ export class ListControlEditor extends PureComponent< min={1} value={this.props.controlParams.options.size} onChange={event => { - this.props.handleNumberOptionChange(this.props.controlIndex, 'size', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'size', + event.target.valueAsNumber + ); }} data-test-subj="listControlSizeInput" /> diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx index e7f9b6083890c..55c4c71ce430b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { SinonSpy, spy, assert, match } from 'sinon'; +import { SinonSpy, spy, assert } from 'sinon'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; // @ts-ignore @@ -46,12 +46,12 @@ const controlParams: ControlParams = { const deps = getDepsMock(); let handleFieldNameChange: SinonSpy; let handleIndexPatternChange: SinonSpy; -let handleNumberOptionChange: SinonSpy; +let handleOptionsChange: SinonSpy; beforeEach(() => { handleFieldNameChange = spy(); handleIndexPatternChange = spy(); - handleNumberOptionChange = spy(); + handleOptionsChange = spy(); }); test('renders RangeControlEditor', async () => { @@ -63,7 +63,7 @@ test('renders RangeControlEditor', async () => { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} /> ); @@ -72,7 +72,7 @@ test('renders RangeControlEditor', async () => { expect(component).toMatchSnapshot(); // eslint-disable-line }); -test('handleNumberOptionChange - step', async () => { +test('handleOptionsChange - step', async () => { const component = mountWithIntl( { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} /> ); await updateComponent(component); findTestSubject(component, 'rangeControlSizeInput0').simulate('change', { - target: { value: 0.5 }, + target: { valueAsNumber: 0.5 }, }); assert.notCalled(handleFieldNameChange); assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'step'; - assert.calledWith( - handleNumberOptionChange, - expectedControlIndex, - expectedOptionName, - match(event => { - if (event.target.value === 0.5) { - return true; - } - return false; - }, 'unexpected input event') - ); + assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName, 0.5); }); -test('handleNumberOptionChange - decimalPlaces', async () => { +test('handleOptionsChange - decimalPlaces', async () => { const component = mountWithIntl( { controlParams={controlParams} handleFieldNameChange={handleFieldNameChange} handleIndexPatternChange={handleIndexPatternChange} - handleNumberOptionChange={handleNumberOptionChange} + handleOptionsChange={handleOptionsChange} /> ); await updateComponent(component); findTestSubject(component, 'rangeControlDecimalPlacesInput0').simulate('change', { - target: { value: 2 }, + target: { valueAsNumber: 2 }, }); assert.notCalled(handleFieldNameChange); assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'decimalPlaces'; - assert.calledWith( - handleNumberOptionChange, - expectedControlIndex, - expectedOptionName, - match(event => { - if (event.target.value === 2) { - return true; - } - return false; - }, 'unexpected input event') - ); + assert.calledWith(handleOptionsChange, expectedControlIndex, expectedOptionName, 2); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 44477eafda6b1..97850879a2d38 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { Component, Fragment, ChangeEvent, ComponentType } from 'react'; +import React, { Component, Fragment, ComponentType } from 'react'; import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,10 +37,10 @@ interface RangeControlEditorProps { getIndexPattern: (indexPatternId: string) => Promise; handleFieldNameChange: (fieldName: string) => void; handleIndexPatternChange: (indexPatternId: string) => void; - handleNumberOptionChange: ( + handleOptionsChange: ( controlIndex: number, - optionName: keyof ControlParamsOptions, - event: ChangeEvent + optionName: T, + value: ControlParamsOptions[T] ) => void; deps: InputControlVisDependencies; } @@ -109,7 +109,11 @@ export class RangeControlEditor extends Component< { - this.props.handleNumberOptionChange(this.props.controlIndex, 'step', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'step', + event.target.valueAsNumber + ); }} data-test-subj={`rangeControlSizeInput${this.props.controlIndex}`} /> @@ -128,7 +132,11 @@ export class RangeControlEditor extends Component< min={0} value={this.props.controlParams.options.decimalPlaces} onChange={event => { - this.props.handleNumberOptionChange(this.props.controlIndex, 'decimalPlaces', event); + this.props.handleOptionsChange( + this.props.controlIndex, + 'decimalPlaces', + event.target.valueAsNumber + ); }} data-test-subj={`rangeControlDecimalPlacesInput${this.props.controlIndex}`} /> diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts index b6774aa87b43c..9473ea5a20b35 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -50,7 +50,6 @@ export function createInputControlVisTypeDefinition(deps: InputControlVisDepende pinFilters: false, }, }, - editor: 'default', editorConfig: { optionTabs: [ { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index 4d5101e1f9e5f..d9a93b2ceedc3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -28,6 +28,12 @@ import { ViewMode } from 'src/plugins/embeddable/public'; jest.mock('ui/state_management/state', () => ({ State: {}, })); +jest.mock('ui/agg_types', () => ({ + aggTypes: { + metrics: [], + buckets: [], + }, +})); describe('DashboardState', function() { let dashboardState: DashboardStateManager; diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index a389a44197baf..f113c81256f8e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/index.ts @@ -17,9 +17,6 @@ * under the License. */ -import 'ui/collapsible_sidebar'; // used in default editor -import 'ui/vis/editors/default/sidebar'; - import { IPrivate, legacyChrome, diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 189624fdbb2b3..7d0a07323378a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -66,8 +66,6 @@ export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; -// @ts-ignore -export { defaultEditor } from 'ui/vis/editors/default/default'; export { VisType } from 'ui/vis'; export { wrapInI18nContext } from 'ui/i18n'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss index f738820677beb..2f48ecc322fea 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/_editor.scss @@ -30,9 +30,4 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ @include flex-parent(); width: 100%; z-index: 0; - - // overrides for tablet and desktop - @include euiBreakpoint('l', 'xl') { - flex-direction: row; - } } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index a80aed5302d1f..0e085b8553bf0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -116,6 +116,11 @@ function VisualizeAppController( dirty: !savedVis.id, }); + vis.on('dirtyStateChange', ({ isDirty }) => { + vis.dirty = isDirty; + $scope.$digest(); + }); + $scope.topNavMenu = [ ...(visualizeCapabilities.save ? [ diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index f47a54baac9a1..2e0eaeb484c0a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -17,7 +17,16 @@ * under the License. */ -import { VisSavedObject } from '../legacy_imports'; +import { TimeRange, Query, esFilters } from 'src/plugins/data/public'; +import { VisSavedObject, AppState, PersistedState } from '../legacy_imports'; + +export interface EditorRenderProps { + appState: AppState; + filters: esFilters.Filter[]; + uiState: PersistedState; + timeRange: TimeRange; + query?: Query; +} export interface SavedVisualizations { urlFor: (id: string) => string; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 660e8169664c4..80e17b1631f3e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -43,7 +43,6 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../../plugins/home/public'; -import { defaultEditor, VisEditorTypesRegistryProvider } from './legacy_imports'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { createSavedVisLoader } from './saved_visualizations/saved_visualizations'; // @ts-ignore @@ -155,8 +154,6 @@ export class VisualizePlugin implements Plugin { showOnHomePage: true, category: FeatureCatalogueCategory.DATA, }); - - VisEditorTypesRegistryProvider.register(defaultEditor); } public start( diff --git a/src/legacy/core_plugins/timelion/public/vis/index.tsx b/src/legacy/core_plugins/timelion/public/vis/index.tsx index 1edcb0a5ce71c..11d1a7385c408 100644 --- a/src/legacy/core_plugins/timelion/public/vis/index.tsx +++ b/src/legacy/core_plugins/timelion/public/vis/index.tsx @@ -19,9 +19,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { DefaultEditorSize } from 'ui/vis/editor_size'; + import { VisOptionsProps } from 'ui/vis/editors/default'; +import { DefaultEditorSize } from '../../../visualizations/public'; import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; import { getTimelionRequestHandler } from './timelion_request_handler'; import visConfigTemplate from './timelion_vis.html'; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss b/src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss index da38d6d2ed211..fb0a3d05e5e85 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss +++ b/src/legacy/core_plugins/vis_type_markdown/public/_markdown_vis.scss @@ -4,13 +4,8 @@ } .visEditor--markdown { - vis-editor-vis-options, vis-options-react-wrapper { - flex-grow: 1; - display: flex; - flex-direction: column; - } - - .visEditor--markdown__textarea { + .visEditorSidebar__config > *, + .visEditor--markdown__textarea { flex-grow: 1; } diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index be82b52dee0fc..33d7480de5a8e 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -34,7 +34,6 @@ import { totalAggregations, isAggConfigNumeric } from './utils'; function TableOptions({ aggs, - aggsLabels, stateParams, setValidity, setValue, @@ -51,7 +50,7 @@ function TableOptions({ .filter(col => isAggConfigNumeric(get(col, 'aggConfig.type.name'), stateParams.dimensions)) .map(({ name }) => ({ value: name, text: name })), ], - [aggs, aggsLabels, stateParams.percentageCol, stateParams.dimensions] + [aggs, stateParams.percentageCol, stateParams.dimensions] ); const isPerPageValid = stateParams.perPage === '' || stateParams.perPage > 0; diff --git a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts index 7adaa21cac593..a792fc98842f1 100644 --- a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts +++ b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts @@ -19,6 +19,7 @@ import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; import $ from 'jquery'; +import { isEqual } from 'lodash'; import { Vis, VisParams } from '../../visualizations/public'; import { npStart } from './legacy_imports'; @@ -75,6 +76,11 @@ export class TableVisualizationController { this.$scope.vis = this.vis; this.$scope.visState = this.vis.getState(); this.$scope.esResponse = esResponse; + + if (!isEqual(this.$scope.visParams, visParams)) { + this.vis.emit('updateEditorStateParams', visParams); + } + this.$scope.visParams = visParams; this.$scope.renderComplete = resolve; this.$scope.renderFailed = reject; diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss b/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss index f4276541d5d9e..709aaa2030f68 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss +++ b/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss @@ -1,13 +1,6 @@ .visEditor--vega { .visEditorSidebar__config { padding: 0; - position: relative; - } - - .visEditorSidebar__options { - @include euiScrollBar; - flex-shrink: 1; - overflow-y: auto; } } @@ -22,8 +15,8 @@ .vgaEditor__aceEditorActions { position: absolute; z-index: $euiZLevel1; - top: 0; - // Adjust for possible scrollbars - right: $euiSize; + top: $euiSizeS; + // Adjust for sidebar collapse button + right: $euiSizeXXL; line-height: 1; } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index 9ab5f820cec31..81c98c6ddb96b 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -19,10 +19,8 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { DefaultEditorSize } from 'ui/vis/editor_size'; -// @ts-ignore import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; -import { Status } from '../../visualizations/public'; +import { Status, DefaultEditorSize } from '../../visualizations/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 256df603a7f33..d34cf930f7e61 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -17,11 +17,10 @@ exports[`MetricsAxisOptions component should init with the default set of props "bySchemaName": [Function], } } - aggsLabels="" changeValueAxis={[Function]} + isTabSelected={true} setParamByIndex={[Function]} setValue={[MockFunction]} - setVisType={[MockFunction]} stateParams={ Object { "categoryAxes": Array [ @@ -95,6 +94,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "setVisType": [MockFunction], "type": Object { "schemas": Object { "metrics": Array [ @@ -127,13 +127,12 @@ exports[`MetricsAxisOptions component should init with the default set of props "bySchemaName": [Function], } } - aggsLabels="" isCategoryAxisHorizontal={true} + isTabSelected={true} onValueAxisPositionChanged={[Function]} removeValueAxis={[Function]} setParamByIndex={[Function]} setValue={[MockFunction]} - setVisType={[MockFunction]} stateParams={ Object { "categoryAxes": Array [ @@ -207,6 +206,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "setVisType": [MockFunction], "type": Object { "schemas": Object { "metrics": Array [ @@ -238,7 +238,6 @@ exports[`MetricsAxisOptions component should init with the default set of props "bySchemaName": [Function], } } - aggsLabels="" axis={ Object { "id": "CategoryAxis-1", @@ -260,10 +259,10 @@ exports[`MetricsAxisOptions component should init with the default set of props "type": "category", } } + isTabSelected={true} onPositionChanged={[Function]} setCategoryAxis={[Function]} setValue={[MockFunction]} - setVisType={[MockFunction]} stateParams={ Object { "categoryAxes": Array [ @@ -337,6 +336,7 @@ exports[`MetricsAxisOptions component should init with the default set of props } vis={ Object { + "setVisType": [MockFunction], "type": Object { "schemas": Object { "metrics": Array [ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index df1920bd4013c..514b957765a99 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -63,7 +63,6 @@ const createAggs = (aggs: any[]) => ({ describe('MetricsAxisOptions component', () => { let setValue: jest.Mock; - let setVisType: jest.Mock; let defaultProps: ValidationVisOptionsProps; let axis: ValueAxis; let axisRight: ValueAxis; @@ -71,7 +70,6 @@ describe('MetricsAxisOptions component', () => { beforeEach(() => { setValue = jest.fn(); - setVisType = jest.fn(); axis = { ...valueAxis, @@ -91,12 +89,13 @@ describe('MetricsAxisOptions component', () => { defaultProps = { aggs: createAggs([aggCount]), - aggsLabels: '', + isTabSelected: true, vis: { type: { type: ChartTypes.AREA, schemas: { metrics: [{ name: 'metric' }] }, }, + setVisType: jest.fn(), }, stateParams: { valueAxes: [axis], @@ -105,7 +104,6 @@ describe('MetricsAxisOptions component', () => { grid: { valueAxis: defaultValueAxisId }, }, setValue, - setVisType, } as any; }); @@ -120,7 +118,6 @@ describe('MetricsAxisOptions component', () => { const comp = mount(); comp.setProps({ aggs: createAggs([aggCount, aggAverage]), - aggsLabels: `${aggCount.makeLabel()}, ${aggAverage.makeLabel()}`, }); const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; @@ -135,7 +132,7 @@ describe('MetricsAxisOptions component', () => { }); const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; - expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, updatedSeries); }); it('should update visType when one seriesParam', () => { @@ -149,7 +146,7 @@ describe('MetricsAxisOptions component', () => { }, }); - expect(setVisType).toHaveBeenLastCalledWith(ChartTypes.LINE); + expect(defaultProps.vis.setVisType).toHaveBeenLastCalledWith(ChartTypes.LINE); }); it('should set histogram visType when multiple seriesParam', () => { @@ -163,7 +160,7 @@ describe('MetricsAxisOptions component', () => { }, }); - expect(setVisType).toHaveBeenLastCalledWith(ChartTypes.HISTOGRAM); + expect(defaultProps.vis.setVisType).toHaveBeenLastCalledWith(ChartTypes.HISTOGRAM); }); }); @@ -177,7 +174,6 @@ describe('MetricsAxisOptions component', () => { }; comp.setProps({ aggs: createAggs([newAgg]), - aggsLabels: `${newAgg.makeLabel()}`, }); const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); @@ -192,11 +188,14 @@ describe('MetricsAxisOptions component', () => { }; comp.setProps({ aggs: createAggs([agg]), - aggsLabels: agg.makeLabel(), }); + const updatedSeriesParams = [{ ...chart, data: { ...chart.data, label: agg.makeLabel() } }]; const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + + expect(setValue).toHaveBeenCalledTimes(3); + expect(setValue).toHaveBeenNthCalledWith(2, SERIES_PARAMS, updatedSeriesParams); + expect(setValue).toHaveBeenNthCalledWith(3, VALUE_AXES, updatedValues); }); it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { @@ -204,7 +203,6 @@ describe('MetricsAxisOptions component', () => { const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; comp.setProps({ aggs: createAggs([agg, aggAverage]), - aggsLabels: `${agg.makeLabel()}, ${aggAverage.makeLabel()}`, stateParams: { ...defaultProps.stateParams, seriesParams: [chart, chart], @@ -224,48 +222,10 @@ describe('MetricsAxisOptions component', () => { }; comp.setProps({ aggs: createAggs([agg]), - aggsLabels: agg.makeLabel(), }); expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); }); - - it('should overwrite the custom title when the agg type changes', () => { - defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; - const comp = mount(); - const agg = { - id: aggCount.id, - type: { name: 'max' }, - makeLabel: () => 'Max', - }; - comp.setProps({ - aggs: createAggs([agg]), - aggsLabels: agg.makeLabel(), - }); - - const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); - }); - - it('should overwrite the custom title when the agg field changes', () => { - defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; - const agg = { - id: aggCount.id, - type: { name: 'max' }, - makeLabel: () => 'Max', - } as AggConfig; - defaultProps.aggs = createAggs([agg]) as any; - const comp = mount(); - agg.params = { field: { name: 'Field' } }; - agg.makeLabel = () => 'Max, Field'; - comp.setProps({ - aggs: createAggs([agg]), - aggsLabels: agg.makeLabel(), - }); - - const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; - expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); - }); }); it('should add value axis', () => { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index 85077ed492331..c4dcbfaa47265 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -52,7 +52,7 @@ export type ChangeValueAxis = ( const VALUE_AXIS_PREFIX = 'ValueAxis-'; function MetricsAxisOptions(props: ValidationVisOptionsProps) { - const { stateParams, setValue, aggs, aggsLabels, setVisType, vis } = props; + const { stateParams, setValue, aggs, vis, isTabSelected } = props; const [isCategoryAxisHorizontal, setIsCategoryAxisHorizontal] = useState(true); @@ -89,9 +89,11 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) } ); - const updateAxisTitle = () => { + const updateAxisTitle = (seriesParams?: SeriesParam[]) => { + const series = seriesParams || stateParams.seriesParams; const axes = cloneDeep(stateParams.valueAxes); let isAxesChanged = false; + let lastValuesChanged = false; const lastLabels = { ...lastCustomLabels }; const lastMatchingSeriesAgg = { ...lastSeriesAgg }; @@ -99,8 +101,8 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) let newCustomLabel = ''; const matchingSeries: AggConfig[] = []; - stateParams.seriesParams.forEach((series, seriesIndex) => { - if ((axisNumber === 0 && !series.valueAxis) || series.valueAxis === axis.id) { + series.forEach((serie, seriesIndex) => { + if ((axisNumber === 0 && !serie.valueAxis) || serie.valueAxis === axis.id) { const aggByIndex = aggs.bySchemaName('metric')[seriesIndex]; matchingSeries.push(aggByIndex); } @@ -125,6 +127,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) field: matchingSeriesAggField, }; lastLabels[axis.id] = newCustomLabel; + lastValuesChanged = true; if ( Object.keys(lastCustomLabels).length !== 0 && @@ -147,8 +150,10 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) setValue('valueAxes', axes); } - setLastSeriesAgg(lastMatchingSeriesAgg); - setLastCustomLabels(lastLabels); + if (lastValuesChanged) { + setLastSeriesAgg(lastMatchingSeriesAgg); + setLastCustomLabels(lastLabels); + } }; const onValueAxisPositionChanged = useCallback( @@ -242,7 +247,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) const metrics = useMemo(() => { const schemaName = vis.type.schemas.metrics[0].name; return aggs.bySchemaName(schemaName); - }, [vis.type.schemas.metrics[0].name, aggs, aggsLabels]); + }, [vis.type.schemas.metrics[0].name, aggs]); const firstValueAxesId = stateParams.valueAxes[0].id; @@ -272,7 +277,8 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) }); setValue('seriesParams', updatedSeries); - }, [aggsLabels, metrics, firstValueAxesId]); + updateAxisTitle(updatedSeries); + }, [metrics, firstValueAxesId]); const visType = useMemo(() => { const types = uniq(stateParams.seriesParams.map(({ type }) => type)); @@ -280,14 +286,10 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) }, [stateParams.seriesParams]); useEffect(() => { - setVisType(visType); - }, [visType]); - - useEffect(() => { - updateAxisTitle(); - }, [aggsLabels]); + vis.setVisType(visType); + }, [vis, visType]); - return ( + return isTabSelected ? ( <> @@ -307,7 +309,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) setCategoryAxis={setCategoryAxis} /> - ); + ) : null; } export { MetricsAxisOptions }; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index e10cbe9aefea6..b750557c24b94 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -27,6 +27,7 @@ export { } from '../../../ui/public/agg_types/buckets/date_histogram'; export { createFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; export { I18nContext } from '../../../ui/public/i18n'; +export { DefaultEditorController } from '../../../ui/public/vis/editors/default/default_editor_controller'; import chrome from '../../../ui/public/chrome'; export { chrome as legacyChrome }; import '../../../ui/public/directives/bind'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index 2f60de3b80614..f849cbfb290ca 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -18,7 +18,9 @@ */ import _ from 'lodash'; + import { createFiltersFromEvent, onBrushEvent } from '../filters'; +import { DefaultEditorController } from '../../../legacy_imports'; export class BaseVisType { constructor(opts = {}) { @@ -46,7 +48,7 @@ export class BaseVisType { }, requestHandler: 'courier', // select one from registry or pass a function responseHandler: 'none', - editor: 'default', + editor: DefaultEditorController, editorConfig: { collections: {}, // collections used for configuration (list of positions, ...) }, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js index 115bbd2731840..4f1526c20cb6f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.js @@ -97,6 +97,10 @@ class Vis extends EventEmitter { } } + setVisType(type) { + this.type.type = type; + } + updateState() { this.setState(this.getCurrentState(true)); this.emit('update'); @@ -118,18 +122,6 @@ class Vis extends EventEmitter { }; } - getSerializableState(state) { - return { - title: state.title, - type: state.type, - params: _.cloneDeep(state.params), - aggs: state.aggs.aggs - .map(agg => agg.toJSON()) - .filter(agg => agg.enabled) - .filter(Boolean), - }; - } - copyCurrentState(includeDisabled = false) { const state = this.getCurrentState(includeDisabled); state.aggs = new AggConfigs( diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index 07e0d46e4eb70..0edf782318862 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -123,6 +123,7 @@ export class AggConfig { public enabled: boolean; public params: any; public parent?: AggConfigs; + public brandNew?: boolean; private __schema: Schema; private __type: AggType; diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 6e811afb1849d..bd2f261c0bf1d 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -126,10 +126,10 @@ export class AggConfigs { return aggConfigs; } - createAggConfig( + createAggConfig = ( params: AggConfig | AggConfigOptions, { addToAggConfigs = true } = {} - ) { + ) => { let aggConfig; if (params instanceof AggConfig) { aggConfig = params; @@ -141,7 +141,7 @@ export class AggConfigs { this.aggs.push(aggConfig); } return aggConfig as T; - } + }; /** * Data-by-data comparison of this Aggregation diff --git a/src/legacy/ui/public/styles/_mixins.scss b/src/legacy/ui/public/styles/_mixins.scss index ae529a4678d5d..2d78768684841 100644 --- a/src/legacy/ui/public/styles/_mixins.scss +++ b/src/legacy/ui/public/styles/_mixins.scss @@ -25,6 +25,7 @@ // Standardizes the look and layout of resizable area handles @mixin kbnResizer($size: ($euiSizeM + 2px), $direction: horizontal) { + position: relative; display: flex; flex: 0 0 $size; background-color: $euiPageBackgroundColor; @@ -34,10 +35,8 @@ user-select: none; @if ($direction == horizontal) { - cursor: ew-resize; width: $size; } @else if ($direction == vertical) { - cursor: ns-resize; height: $size; width: 100%; } @else { @@ -53,6 +52,21 @@ background-color: $euiColorPrimary; color: $euiColorEmptyShade; } + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + @if ($direction == horizontal) { + cursor: ew-resize; + } @else if ($direction == vertical) { + cursor: ns-resize; + } + } } @mixin kibanaFullScreenGraphics($euiZLevel: $euiZLevel9) { diff --git a/src/legacy/ui/public/vis/editors/default/agg_params.d.ts b/src/legacy/ui/public/vis/editor_size.ts similarity index 69% rename from src/legacy/ui/public/vis/editors/default/agg_params.d.ts rename to src/legacy/ui/public/vis/editor_size.ts index 89896c0e1be3e..5fdda4c2dad41 100644 --- a/src/legacy/ui/public/vis/editors/default/agg_params.d.ts +++ b/src/legacy/ui/public/vis/editor_size.ts @@ -17,6 +17,20 @@ * under the License. */ -export interface AggParams { - [key: string]: unknown; +export enum DefaultEditorSize { + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', } + +export const getInitialWidth = (size: DefaultEditorSize) => { + switch (size) { + case DefaultEditorSize.SMALL: + return 15; + case DefaultEditorSize.LARGE: + return 50; + case DefaultEditorSize.MEDIUM: + default: + return 30; + } +}; diff --git a/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts b/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts index fa15f8642c4dc..0a5d0ea748b5e 100644 --- a/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts +++ b/src/legacy/ui/public/vis/editors/config/editor_config_providers.test.ts @@ -19,7 +19,6 @@ import { EditorConfigProviderRegistry } from './editor_config_providers'; import { EditorParamConfig, FixedParam, NumericIntervalParam, TimeIntervalParam } from './types'; -import { AggType } from '../../../agg_types'; import { AggConfig } from '../..'; jest.mock('ui/new_platform'); @@ -49,10 +48,9 @@ describe('EditorConfigProvider', () => { const provider = jest.fn(() => ({})); registry.register(provider); expect(provider).not.toHaveBeenCalled(); - const aggType = {} as AggType; const aggConfig = {} as AggConfig; - registry.getConfigForAgg(aggType, indexPattern, aggConfig); - expect(provider).toHaveBeenCalledWith(aggType, indexPattern, aggConfig); + registry.getConfigForAgg(indexPattern, aggConfig); + expect(provider).toHaveBeenCalledWith(indexPattern, aggConfig); }); it('should call all registered providers with given parameters', () => { @@ -62,11 +60,10 @@ describe('EditorConfigProvider', () => { registry.register(provider2); expect(provider).not.toHaveBeenCalled(); expect(provider2).not.toHaveBeenCalled(); - const aggType = {} as AggType; const aggConfig = {} as AggConfig; - registry.getConfigForAgg(aggType, indexPattern, aggConfig); - expect(provider).toHaveBeenCalledWith(aggType, indexPattern, aggConfig); - expect(provider2).toHaveBeenCalledWith(aggType, indexPattern, aggConfig); + registry.getConfigForAgg(indexPattern, aggConfig); + expect(provider).toHaveBeenCalledWith(indexPattern, aggConfig); + expect(provider2).toHaveBeenCalledWith(indexPattern, aggConfig); }); describe('merging configs', () => { @@ -75,7 +72,7 @@ describe('EditorConfigProvider', () => { } function getOutputConfig(reg: EditorConfigProviderRegistry) { - return reg.getConfigForAgg({} as AggType, indexPattern, {} as AggConfig).singleParam; + return reg.getConfigForAgg(indexPattern, {} as AggConfig).singleParam; } it('should have hidden true if at least one config was hidden true', () => { diff --git a/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts b/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts index c7fb937b97424..80dc2bcd68f08 100644 --- a/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts +++ b/src/legacy/ui/public/vis/editors/config/editor_config_providers.ts @@ -19,18 +19,13 @@ import { TimeIntervalParam } from 'ui/vis/editors/config/types'; import { AggConfig } from '../..'; -import { AggType } from '../../../agg_types'; import { IndexPattern } from '../../../../../../plugins/data/public'; import { leastCommonMultiple } from '../../lib/least_common_multiple'; import { parseEsInterval } from '../../../../../core_plugins/data/public'; import { leastCommonInterval } from '../../lib/least_common_interval'; import { EditorConfig, EditorParamConfig, FixedParam, NumericIntervalParam } from './types'; -type EditorConfigProvider = ( - aggType: AggType, - indexPattern: IndexPattern, - aggConfig: AggConfig -) => EditorConfig; +type EditorConfigProvider = (indexPattern: IndexPattern, aggConfig: AggConfig) => EditorConfig; class EditorConfigProviderRegistry { private providers: Set = new Set(); @@ -39,14 +34,8 @@ class EditorConfigProviderRegistry { this.providers.add(configProvider); } - public getConfigForAgg( - aggType: AggType, - indexPattern: IndexPattern, - aggConfig: AggConfig - ): EditorConfig { - const configs = Array.from(this.providers).map(provider => - provider(aggType, indexPattern, aggConfig) - ); + public getConfigForAgg(indexPattern: IndexPattern, aggConfig: AggConfig): EditorConfig { + const configs = Array.from(this.providers).map(provider => provider(indexPattern, aggConfig)); return this.mergeConfigs(configs); } diff --git a/src/legacy/ui/public/vis/editors/default/_default.scss b/src/legacy/ui/public/vis/editors/default/_default.scss index 2a32eadd55b1e..7562583b037df 100644 --- a/src/legacy/ui/public/vis/editors/default/_default.scss +++ b/src/legacy/ui/public/vis/editors/default/_default.scss @@ -1,8 +1,4 @@ .visEditor--default { - // Prevent the default editor from overflowing. Without that you can cause - // a weird issue where the complete page can be scrolled out of view if - // the editor within the sidebar is too height. - overflow-y: hidden; flex: 1 1 auto; display: flex; @@ -16,12 +12,11 @@ */ .visEditor__collapsibleSidebar { - @include flex-parent(0, 0, auto); - margin-right: $euiSizeL; - flex-direction: row; + background: $euiColorLightestShade; min-width: $vis-editor-sidebar-min-width; - width: $vis-editor-sidebar-min-width; max-width: 100%; + position: relative; + flex-shrink: 0; @include euiBreakpoint('xs', 's', 'm') { // If we are on a small screen we force the editor to take 100% width. @@ -33,35 +28,25 @@ } } -.visEditor__collapsibleSidebar.closed { +// !importants on width are required to override resizable panel inline widths +.visEditor__collapsibleSidebar-isClosed { min-width: 0; -} - -.visEditor__collapsibleSidebar--small { - width: 15%; -} + width: $euiSizeXL !important; // Just enough room for the collapse button -.visEditor__collapsibleSidebar--medium { - width: 30%; -} + .visEditorSidebar { + display: none; + } -.visEditor__collapsibleSidebar--large { - width: 50%; + @include euiBreakpoint('xs', 's', 'm') { + height: $euiSizeXXL; // Just enough room for the collapse button + width: 100% !important; + } } - -/** - * Actual sidebar - */ - -.visEditor__sidebar { - @include flex-parent(1, 0, auto); - - // overridden for tablet and desktop - @include euiBreakpoint('l', 'xl') { - flex-basis: $vis-editor-sidebar-basis; - max-width: calc(100% - #{$vis-editor-resizer-width}); - } +.visEditor__collapsibleSidebarButton { + position: absolute; + right: $euiSizeXS; + top: $euiSizeS; } /** @@ -69,17 +54,30 @@ */ .visEditor__resizer { - @include kbnResizer($vis-editor-resizer-width); - + @include kbnResizer($euiSizeM); @include euiBreakpoint('xs', 's', 'm') { display: none; } } +.visEditor__resizer-isHidden { + display: none; +} + /** * Canvas area */ +.visEditor__visualization { + display: flex; + flex-basis: 100%; + + @include euiBreakpoint('xs', 's', 'm') { + // If we are on a small screen we force the visualization to take 100% width. + width: 100% !important; + } +} + .visEditor__canvas { background-color: $euiColorEmptyShade; display: flex; diff --git a/src/legacy/ui/public/vis/editors/default/_index.scss b/src/legacy/ui/public/vis/editors/default/_index.scss index d5938a0298d36..6abb45dc546a3 100644 --- a/src/legacy/ui/public/vis/editors/default/_index.scss +++ b/src/legacy/ui/public/vis/editors/default/_index.scss @@ -1,6 +1,4 @@ -$vis-editor-sidebar-basis: (100/12) * 2%; // two of twelve columns $vis-editor-sidebar-min-width: 350px; -$vis-editor-resizer-width: $euiSizeM; // Main layout @import './default'; diff --git a/src/legacy/ui/public/vis/editors/default/_sidebar.scss b/src/legacy/ui/public/vis/editors/default/_sidebar.scss index e6b75b1a1f783..cbe7172d62341 100644 --- a/src/legacy/ui/public/vis/editors/default/_sidebar.scss +++ b/src/legacy/ui/public/vis/editors/default/_sidebar.scss @@ -2,18 +2,22 @@ // LAYOUT // -.visEditorSidebar__container { - @include flex-parent(1, 1, auto); - background-color: $euiColorLightestShade; +.visEditorSidebar { + min-width: $vis-editor-sidebar-min-width; } .visEditorSidebar__form { @include flex-parent(1, 1, auto); + max-width: 100%; } .visEditorSidebar__config { padding: $euiSizeS; + > * { + flex-grow: 0; + } + @include euiBreakpoint('l', 'xl') { @include flex-parent(1, 1, 1px); @include euiScrollBar; @@ -21,64 +25,26 @@ } } +.visEditorSidebar__config-isHidden { + display: none; +} + // // NAVIGATION // .visEditorSidebar__indexPattern { - font-weight: $euiFontWeightBold; - padding: $euiSizeXS $euiSizeS; - background-color: shadeOrTint($euiColorPrimary, 60%, 60%); - color: $euiColorEmptyShade; - line-height: $euiSizeL; + @include euiTextTruncate; + padding: $euiSizeS $euiSizeXL $euiSizeS $euiSizeS; // Extra padding on the right for the collapse button } -.visEditorSidebar__nav { - min-height: 0; - - .navbar-right { - // Match correct bootstrap container spacing to pull buttons fully right - margin-right: -15px; - } +.visEditorSidebar__indexPatternPlaceholder { + min-height: $euiSizeXXL; + border-bottom: $euiBorderThin; } -/** - * 1. TODO: Override bootstrap styles. Remove !important once we're rid of bootstrap. - */ -.visEditorSidebar__navLink { - padding: 2px $euiSizeS !important; /* 1 */ - color: $euiColorDarkShade !important; /* 1 */ - - &.visEditorSidebar__navLink-isSelected { - border-bottom: 2px solid $euiColorPrimary; - border-color: $euiColorPrimary !important; - color: $euiColorPrimary !important; - - &:before { - display: none; - } - - &:hover { - background-color: transparent; - } - } -} - -/** - * 1. TODO: Override bootstrap styles. Remove !important once we're rid of bootstrap. - */ -.visEditorSidebar__navLink--danger { - color: $euiColorEmptyShade !important; /* 1 */ - background-color: $euiColorDanger; - - &:hover { - background-color: shadeOrTint($euiColorDanger, 12%, 0%) !important; /* 1 */ - } -} - -.visEditorSidebar__navButtonLink { - // Make the line-height the same size as the icon for better alignment - line-height: $euiSize; +.visEditorSidebar__nav { + flex-grow: 0; } // @@ -104,15 +70,6 @@ } } -.visEditorSidebar__sectionTitle { - @include euiFontSizeL; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: $euiSizeM; - text-transform: capitalize; -} - // Collapsible section .visEditorSidebar__collapsible { @@ -127,15 +84,6 @@ // FORMS // -.visEditorSidebar__input, -.visEditorSidebar__select { - @include __legacyInputStyles__bad; -} - -.visEditorSidebar__select { - @include __legacySelectStyles__bad; -} - .visEditorSidebar__formRow { display: flex; align-items: center; @@ -155,14 +103,6 @@ flex: 1 1 60%; } -.visEditorSidebar__formRow--expand { - .visEditorSidebar__formLabel, - .visEditorSidebar__formControl { - flex-basis: auto; - flex-grow: 0; - } -} - .visEditorSidebar__aggGroupAccordionButtonContent { font-size: $euiFontSizeS; @@ -170,3 +110,15 @@ color: $euiColorDarkShade; } } + +.visEditorSidebar__controls { + border-top: $euiBorderThin; + padding: $euiSizeS; + display: flex; + justify-content: flex-end; + align-items: center; + + .visEditorSidebar__autoApplyButton { + margin-left: $euiSizeM; + } +} diff --git a/src/legacy/ui/public/vis/editors/default/agg_group.js b/src/legacy/ui/public/vis/editors/default/agg_group.js deleted file mode 100644 index 8fc4934022cf6..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/agg_group.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import 'ngreact'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from '../../../modules'; -import { DefaultEditorAggGroup } from './components/agg_group'; - -uiModules - .get('app/visualize') - .directive('visEditorAggGroupWrapper', reactDirective => - reactDirective(wrapInI18nContext(DefaultEditorAggGroup), [ - ['metricAggs', { watchDepth: 'reference' }], // we watch reference to identify each aggs change in useEffects - ['schemas', { watchDepth: 'collection' }], - ['state', { watchDepth: 'reference' }], - ['addSchema', { watchDepth: 'reference' }], - ['onAggParamsChange', { watchDepth: 'reference' }], - ['onAggTypeChange', { watchDepth: 'reference' }], - ['onToggleEnableAgg', { watchDepth: 'reference' }], - ['removeAgg', { watchDepth: 'reference' }], - ['reorderAggs', { watchDepth: 'reference' }], - ['setTouched', { watchDepth: 'reference' }], - ['setValidity', { watchDepth: 'reference' }], - 'groupName', - 'formIsTouched', - 'lastParentPipelineAggTitle', - 'currentTab', - ]) - ) - .directive('visEditorAggGroup', function() { - return { - restrict: 'E', - scope: true, - require: '?^ngModel', - template: function() { - return ``; - }, - link: function($scope, $el, attr, ngModelCtrl) { - $scope.groupName = attr.groupName; - $scope.$bind('schemas', attr.schemas); - // The model can become touched either onBlur event or when the form is submitted. - // We also watch $touched to identify when the form is submitted. - $scope.$watch( - () => { - return ngModelCtrl.$touched; - }, - value => { - $scope.formIsTouched = value; - } - ); - - $scope.setValidity = isValid => { - ngModelCtrl.$setValidity(`aggGroup${$scope.groupName}`, isValid); - }; - - $scope.setTouched = isTouched => { - if (isTouched) { - ngModelCtrl.$setTouched(); - } else { - ngModelCtrl.$setUntouched(); - } - }; - }, - }; - }); diff --git a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap index b78503b298d04..aed0285fd3405 100644 --- a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap +++ b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg.test.tsx.snap @@ -57,9 +57,9 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` groupName="metrics" indexPattern={Object {}} metricAggs={Array []} - onAggParamsChange={[MockFunction]} onAggTypeChange={[Function]} - setTouched={[MockFunction]} + setAggParamValue={[MockFunction]} + setTouched={[Function]} setValidity={[Function]} state={Object {}} /> diff --git a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap index 29af0887db2b8..373ff6b4c3ee4 100644 --- a/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap +++ b/src/legacy/ui/public/vis/editors/default/components/__snapshots__/agg_group.test.tsx.snap @@ -5,6 +5,7 @@ exports[`DefaultEditorAgg component should init with the default set of props 1` onDragEnd={[Function]} > diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx index c19f101fa2c32..6849d00158b06 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx @@ -26,6 +26,7 @@ import { act } from 'react-dom/test-utils'; import { DefaultEditorAggParams } from './agg_params'; import { AggType } from 'ui/agg_types'; import { IndexPattern } from '../../../../../../../plugins/data/public'; +import { AGGS_ACTION_KEYS } from './agg_group_state'; jest.mock('./agg_params', () => ({ DefaultEditorAggParams: () => null, @@ -33,18 +34,18 @@ jest.mock('./agg_params', () => ({ describe('DefaultEditorAgg component', () => { let defaultProps: DefaultEditorAggProps; - let onAggParamsChange: jest.Mock; - let setTouched: jest.Mock; + let setAggParamValue: jest.Mock; + let setStateParamValue: jest.Mock; let onToggleEnableAgg: jest.Mock; let removeAgg: jest.Mock; - let setValidity: jest.Mock; + let setAggsState: jest.Mock; beforeEach(() => { - onAggParamsChange = jest.fn(); - setTouched = jest.fn(); + setAggParamValue = jest.fn(); + setStateParamValue = jest.fn(); onToggleEnableAgg = jest.fn(); removeAgg = jest.fn(); - setValidity = jest.fn(); + setAggsState = jest.fn(); defaultProps = { agg: { @@ -66,10 +67,10 @@ describe('DefaultEditorAgg component', () => { isRemovable: false, metricAggs: [], state: {} as VisState, - onAggParamsChange, + setAggParamValue, + setStateParamValue, onAggTypeChange: () => {}, - setValidity, - setTouched, + setAggsState, onToggleEnableAgg, removeAgg, }; @@ -98,7 +99,11 @@ describe('DefaultEditorAgg component', () => { .setValidity(false); }); comp.update(); - expect(setValidity).toBeCalledWith(false); + expect(setAggsState).toBeCalledWith({ + type: AGGS_ACTION_KEYS.VALID, + payload: false, + aggId: defaultProps.agg.id, + }); expect( comp.find('.visEditorSidebar__aggGroupAccordionButtonContent span').exists() @@ -119,7 +124,11 @@ describe('DefaultEditorAgg component', () => { .setValidity(true); }); comp.update(); - expect(setValidity).toBeCalledWith(true); + expect(setAggsState).toBeCalledWith({ + type: AGGS_ACTION_KEYS.VALID, + payload: true, + aggId: defaultProps.agg.id, + }); expect(comp.find('.visEditorSidebar__aggGroupAccordionButtonContent span').text()).toBe( 'Agg description' @@ -128,16 +137,20 @@ describe('DefaultEditorAgg component', () => { it('should call setTouched when accordion is collapsed', () => { const comp = mount(); - expect(defaultProps.setTouched).toBeCalledTimes(0); + expect(defaultProps.setAggsState).toBeCalledTimes(0); comp.find('.euiAccordion__button').simulate('click'); // make sure that the accordion is collapsed expect(comp.find('.euiAccordion-isOpen').exists()).toBeFalsy(); - expect(defaultProps.setTouched).toBeCalledWith(true); + expect(defaultProps.setAggsState).toBeCalledWith({ + type: AGGS_ACTION_KEYS.TOUCHED, + payload: true, + aggId: defaultProps.agg.id, + }); }); - it('should call setValidity inside onSetValidity', () => { + it('should call setAggsState inside setValidity', () => { const comp = mount(); act(() => { @@ -147,7 +160,11 @@ describe('DefaultEditorAgg component', () => { .setValidity(false); }); - expect(setValidity).toBeCalledWith(false); + expect(setAggsState).toBeCalledWith({ + type: AGGS_ACTION_KEYS.VALID, + payload: false, + aggId: defaultProps.agg.id, + }); expect( comp.find('.visEditorSidebar__aggGroupAccordionButtonContent span').exists() @@ -197,7 +214,7 @@ describe('DefaultEditorAgg component', () => { const comp = mount(); comp.find('[data-test-subj="toggleDisableAggregationBtn disable"] button').simulate('click'); - expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg, false); + expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg.id, false); }); it('should disable the disableAggregation button', () => { @@ -217,7 +234,7 @@ describe('DefaultEditorAgg component', () => { const comp = mount(); comp.find('[data-test-subj="toggleDisableAggregationBtn enable"] button').simulate('click'); - expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg, true); + expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg.id, true); }); it('should call removeAgg', () => { @@ -225,7 +242,7 @@ describe('DefaultEditorAgg component', () => { const comp = mount(); comp.find('[data-test-subj="removeDimensionBtn"] button').simulate('click'); - expect(defaultProps.removeAgg).toBeCalledWith(defaultProps.agg); + expect(defaultProps.removeAgg).toBeCalledWith(defaultProps.agg.id); }); }); @@ -269,8 +286,8 @@ describe('DefaultEditorAgg component', () => { const comp = mount(); comp.setProps({ agg: { ...defaultProps.agg, type: { name: 'histogram' } } }); - expect(defaultProps.onAggParamsChange).toHaveBeenCalledWith( - defaultProps.agg.params, + expect(defaultProps.setAggParamValue).toHaveBeenCalledWith( + defaultProps.agg.id, 'min_doc_count', true ); @@ -283,8 +300,8 @@ describe('DefaultEditorAgg component', () => { const comp = mount(); comp.setProps({ agg: { ...defaultProps.agg, type: { name: 'date_histogram' } } }); - expect(defaultProps.onAggParamsChange).toHaveBeenCalledWith( - defaultProps.agg.params, + expect(defaultProps.setAggParamValue).toHaveBeenCalledWith( + defaultProps.agg.id, 'min_doc_count', 0 ); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.tsx index 345c9254ff6c1..5c5905abdb9f0 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiAccordion, EuiToolTip, @@ -31,6 +31,7 @@ import { i18n } from '@kbn/i18n'; import { AggConfig } from '../../..'; import { DefaultEditorAggParams } from './agg_params'; import { DefaultEditorAggCommonProps } from './agg_common_props'; +import { AGGS_ACTION_KEYS, AggsAction } from './agg_group_state'; export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { agg: AggConfig; @@ -41,6 +42,7 @@ export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { isDraggable: boolean; isLastBucket: boolean; isRemovable: boolean; + setAggsState: React.Dispatch; } function DefaultEditorAgg({ @@ -57,12 +59,12 @@ function DefaultEditorAgg({ metricAggs, lastParentPipelineAggTitle, state, - onAggParamsChange, + setAggParamValue, + setStateParamValue, onAggTypeChange, onToggleEnableAgg, removeAgg, - setTouched, - setValidity, + setAggsState, }: DefaultEditorAggProps) { const [isEditorOpen, setIsEditorOpen] = useState((agg as any).brandNew); const [validState, setValidState] = useState(true); @@ -103,8 +105,8 @@ function DefaultEditorAgg({ useEffect(() => { if (isLastBucketAgg && ['date_histogram', 'histogram'].includes(agg.type.name)) { - onAggParamsChange( - agg.params, + setAggParamValue( + agg.id, 'min_doc_count', // "histogram" agg has an editor for "min_doc_count" param, which accepts boolean // "date_histogram" agg doesn't have an editor for "min_doc_count" param, it should be set as a numeric value @@ -113,17 +115,38 @@ function DefaultEditorAgg({ } }, [lastParentPipelineAggTitle, isLastBucket, agg.type]); - const onToggle = (isOpen: boolean) => { - setIsEditorOpen(isOpen); - if (!isOpen) { - setTouched(true); - } - }; + const setTouched = useCallback( + (touched: boolean) => { + setAggsState({ + type: AGGS_ACTION_KEYS.TOUCHED, + payload: touched, + aggId: agg.id, + }); + }, + [setAggsState] + ); - const onSetValidity = (isValid: boolean) => { - setValidity(isValid); - setValidState(isValid); - }; + const setValidity = useCallback( + (isValid: boolean) => { + setAggsState({ + type: AGGS_ACTION_KEYS.VALID, + payload: isValid, + aggId: agg.id, + }); + setValidState(isValid); + }, + [setAggsState] + ); + + const onToggle = useCallback( + (isOpen: boolean) => { + setIsEditorOpen(isOpen); + if (!isOpen) { + setTouched(true); + } + }, + [setTouched] + ); const renderAggButtons = () => { const actionIcons = []; @@ -146,7 +169,7 @@ function DefaultEditorAgg({ color: 'text', disabled: isDisabled, type: 'eye', - onClick: () => onToggleEnableAgg(agg, false), + onClick: () => onToggleEnableAgg(agg.id, false), tooltip: i18n.translate('common.ui.vis.editors.agg.disableAggButtonTooltip', { defaultMessage: 'Disable aggregation', }), @@ -158,7 +181,7 @@ function DefaultEditorAgg({ id: 'enableAggregation', color: 'text', type: 'eyeClosed', - onClick: () => onToggleEnableAgg(agg, true), + onClick: () => onToggleEnableAgg(agg.id, true), tooltip: i18n.translate('common.ui.vis.editors.agg.enableAggButtonTooltip', { defaultMessage: 'Enable aggregation', }), @@ -180,7 +203,7 @@ function DefaultEditorAgg({ id: 'removeDimension', color: 'danger', type: 'cross', - onClick: () => removeAgg(agg), + onClick: () => removeAgg(agg.id), tooltip: i18n.translate('common.ui.vis.editors.agg.removeDimensionButtonTooltip', { defaultMessage: 'Remove dimension', }), @@ -248,9 +271,10 @@ function DefaultEditorAgg({ {SchemaComponent && ( )} diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts b/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts index 232eaba76f3a1..a0ddc9a757cc7 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts @@ -18,29 +18,32 @@ */ import { AggType } from 'ui/agg_types'; -import { AggConfig, VisState, VisParams } from '../../..'; -import { AggParams } from '../agg_params'; +import { AggConfig, VisState, VisParams } from 'ui/vis'; import { AggGroupNames } from '../agg_groups'; +import { Schema } from '../schemas'; -export type OnAggParamsChange = < - Params extends AggParams | VisParams, - ParamName extends keyof Params ->( - params: Params, - paramName: ParamName, - value: Params[ParamName] -) => void; +type AggId = AggConfig['id']; +type AggParams = AggConfig['params']; -export interface DefaultEditorAggCommonProps { +export type AddSchema = (schemas: Schema) => void; +export type ReorderAggs = (sourceAgg: AggConfig, destinationAgg: AggConfig) => void; + +export interface DefaultEditorCommonProps { formIsTouched: boolean; groupName: AggGroupNames; - lastParentPipelineAggTitle?: string; metricAggs: AggConfig[]; state: VisState; - onAggParamsChange: OnAggParamsChange; - onAggTypeChange: (agg: AggConfig, aggType: AggType) => void; - onToggleEnableAgg: (agg: AggConfig, isEnable: boolean) => void; - removeAgg: (agg: AggConfig) => void; - setTouched: (isTouched: boolean) => void; - setValidity: (isValid: boolean) => void; + setAggParamValue: ( + aggId: AggId, + paramName: T, + value: AggParams[T] + ) => void; + onAggTypeChange: (aggId: AggId, aggType: AggType) => void; +} + +export interface DefaultEditorAggCommonProps extends DefaultEditorCommonProps { + lastParentPipelineAggTitle?: string; + setStateParamValue: (paramName: T, value: VisParams[T]) => void; + onToggleEnableAgg: (aggId: AggId, isEnable: boolean) => void; + removeAgg: (aggId: AggId) => void; } diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx index 05e8c44b9c720..ae36503c16133 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group.test.tsx @@ -109,8 +109,9 @@ describe('DefaultEditorAgg component', () => { reorderAggs, addSchema: () => {}, removeAgg: () => {}, - onAggParamsChange: () => {}, - onAggTypeChange: () => {}, + setAggParamValue: jest.fn(), + setStateParamValue: jest.fn(), + onAggTypeChange: jest.fn(), onToggleEnableAgg: () => {}, }; }); @@ -127,46 +128,6 @@ describe('DefaultEditorAgg component', () => { expect(setTouched).toBeCalledWith(false); }); - it('should mark group as touched when all invalid aggs are touched', () => { - defaultProps.groupName = AggGroupNames.Buckets; - const comp = mount(); - act(() => { - const aggProps = comp.find(DefaultEditorAgg).props(); - aggProps.setValidity(false); - aggProps.setTouched(true); - }); - - expect(setTouched).toBeCalledWith(true); - }); - - it('should mark group as touched when the form applied', () => { - const comp = mount(); - act(() => { - comp - .find(DefaultEditorAgg) - .first() - .props() - .setValidity(false); - }); - expect(setTouched).toBeCalledWith(false); - comp.setProps({ formIsTouched: true }); - - expect(setTouched).toBeCalledWith(true); - }); - - it('should mark group as invalid when at least one agg is invalid', () => { - const comp = mount(); - act(() => { - comp - .find(DefaultEditorAgg) - .first() - .props() - .setValidity(false); - }); - - expect(setValidity).toBeCalledWith(false); - }); - it('should last bucket has truthy isLastBucket prop', () => { defaultProps.groupName = AggGroupNames.Buckets; const comp = mount(); @@ -182,10 +143,10 @@ describe('DefaultEditorAgg component', () => { comp.props().onDragEnd({ source: { index: 0 }, destination: { index: 1 } }); }); - expect(reorderAggs).toHaveBeenCalledWith([ - defaultProps.state.aggs.aggs[1], + expect(reorderAggs).toHaveBeenCalledWith( defaultProps.state.aggs.aggs[0], - ]); + defaultProps.state.aggs.aggs[1] + ); }); it('should show add button when schemas count is less than max', () => { diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx index 1c8690f6deb79..7416c36bd5cf1 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useReducer, useMemo } from 'react'; +import React, { useEffect, useReducer, useMemo, useCallback } from 'react'; import { EuiTitle, EuiDragDropContext, @@ -34,7 +34,7 @@ import { AggConfig } from '../../../../agg_types/agg_config'; import { aggGroupNamesMap, AggGroupNames } from '../agg_groups'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; -import { DefaultEditorAggCommonProps } from './agg_common_props'; +import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; import { isInvalidAggsTouched, isAggRemovable, @@ -46,8 +46,10 @@ import { Schema } from '../schemas'; export interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps { schemas: Schema[]; - addSchema: (schemas: Schema) => void; - reorderAggs: (group: AggConfig[]) => void; + addSchema: AddSchema; + reorderAggs: ReorderAggs; + setValidity(modelName: string, value: boolean): void; + setTouched(isTouched: boolean): void; } function DefaultEditorAggGroup({ @@ -58,7 +60,8 @@ function DefaultEditorAggGroup({ state, schemas = [], addSchema, - onAggParamsChange, + setAggParamValue, + setStateParamValue, onAggTypeChange, onToggleEnableAgg, removeAgg, @@ -68,8 +71,10 @@ function DefaultEditorAggGroup({ }: DefaultEditorAggGroupProps) { const groupNameLabel = (aggGroupNamesMap() as any)[groupName]; // e.g. buckets can have no aggs - const group: AggConfig[] = - state.aggs.aggs.filter((agg: AggConfig) => agg.schema.group === groupName) || []; + const group: AggConfig[] = useMemo( + () => state.aggs.aggs.filter((agg: AggConfig) => agg.schema.group === groupName) || [], + [state.aggs.aggs] + ); const stats = { max: 0, @@ -101,7 +106,7 @@ function DefaultEditorAggGroup({ // when isAllAggsTouched is true, it means that all invalid aggs are touched and we will set ngModel's touched to true // which indicates that Apply button can be changed to Error button (when all invalid ngModels are touched) setTouched(isAllAggsTouched); - }, [isAllAggsTouched]); + }, [isAllAggsTouched, setTouched]); useEffect(() => { // when not all invalid aggs are touched and formIsTouched becomes true, it means that Apply button was clicked. @@ -118,38 +123,21 @@ function DefaultEditorAggGroup({ }, [formIsTouched]); useEffect(() => { - setValidity(isGroupValid); - }, [isGroupValid]); - - const onDragEnd: DragDropContextProps['onDragEnd'] = ({ source, destination }) => { - if (source && destination) { - const orderedGroup = Array.from(group); - const [removed] = orderedGroup.splice(source.index, 1); - orderedGroup.splice(destination.index, 0, removed); - - reorderAggs(orderedGroup); - } - }; - - const setTouchedHandler = (aggId: string, touched: boolean) => { - setAggsState({ - type: AGGS_ACTION_KEYS.TOUCHED, - payload: touched, - aggId, - }); - }; - - const setValidityHandler = (aggId: string, valid: boolean) => { - setAggsState({ - type: AGGS_ACTION_KEYS.VALID, - payload: valid, - aggId, - }); - }; + setValidity(`aggGroup__${groupName}`, isGroupValid); + }, [groupName, isGroupValid, setValidity]); + + const onDragEnd: DragDropContextProps['onDragEnd'] = useCallback( + ({ source, destination }) => { + if (source && destination) { + reorderAggs(group[source.index], group[destination.index]); + } + }, + [reorderAggs, group] + ); return ( - +

{groupNameLabel}

@@ -184,12 +172,12 @@ function DefaultEditorAggGroup({ lastParentPipelineAggTitle={lastParentPipelineAggTitle} metricAggs={metricAggs} state={state} - onAggParamsChange={onAggParamsChange} + setAggParamValue={setAggParamValue} + setStateParamValue={setStateParamValue} onAggTypeChange={onAggTypeChange} onToggleEnableAgg={onToggleEnableAgg} removeAgg={removeAgg} - setTouched={isTouched => setTouchedHandler(agg.id, isTouched)} - setValidity={isValid => setValidityHandler(agg.id, isValid)} + setAggsState={setAggsState} /> )} diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx index 980889743c20d..0b787e45a5008 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx @@ -33,7 +33,7 @@ export interface AggsState { [aggId: string]: AggsItem; } -interface AggsAction { +export interface AggsAction { type: AGGS_ACTION_KEYS; payload: boolean; aggId: string; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx index 72c802d7d237e..d3bbf3cc9903a 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_param.tsx @@ -17,18 +17,59 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { AggParamEditorProps, AggParamCommonProps } from './agg_param_props'; -import { OnAggParamsChange } from './agg_common_props'; +import { DefaultEditorAggCommonProps } from './agg_common_props'; +import { AGG_PARAMS_ACTION_KEYS, AggParamsAction } from './agg_params_state'; interface DefaultEditorAggParamProps extends AggParamCommonProps { paramEditor: React.ComponentType>; - onChange: OnAggParamsChange; + setAggParamValue: DefaultEditorAggCommonProps['setAggParamValue']; + onChangeParamsState: React.Dispatch; } function DefaultEditorAggParam(props: DefaultEditorAggParamProps) { - const { agg, aggParam, paramEditor: ParamEditor, onChange, setValidity, ...rest } = props; + const { + agg, + aggParam, + paramEditor: ParamEditor, + setAggParamValue, + onChangeParamsState, + ...rest + } = props; + + const setValidity = useCallback( + (valid: boolean) => { + onChangeParamsState({ + type: AGG_PARAMS_ACTION_KEYS.VALID, + paramName: aggParam.name, + payload: valid, + }); + }, + [onChangeParamsState, aggParam.name] + ); + + // setTouched can be called from sub-agg which passes a parameter + const setTouched = useCallback( + (isTouched: boolean = true) => { + onChangeParamsState({ + type: AGG_PARAMS_ACTION_KEYS.TOUCHED, + paramName: aggParam.name, + payload: isTouched, + }); + }, + [onChangeParamsState, aggParam.name] + ); + + const setValue = useCallback( + (value: T) => { + if (props.value !== value) { + setAggParamValue(agg.id, aggParam.name, value); + } + }, + [setAggParamValue, agg.id, aggParam.name, props.value] + ); useEffect(() => { if (aggParam.shouldShow && !aggParam.shouldShow(agg)) { @@ -45,7 +86,8 @@ function DefaultEditorAggParam(props: DefaultEditorAggParamProps) { agg={agg} aggParam={aggParam} setValidity={setValidity} - setValue={(value: T) => onChange(agg.params, aggParam.name, value)} + setTouched={setTouched} + setValue={setValue} {...rest} /> ); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_param_props.ts b/src/legacy/ui/public/vis/editors/default/components/agg_param_props.ts index df77ea9e43eab..953f49d84f5c5 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_param_props.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_param_props.ts @@ -22,28 +22,27 @@ import { AggConfig } from '../../../../agg_types/agg_config'; import { ComboBoxGroupedOptions } from '../utils'; import { EditorConfig } from '../../config/types'; import { VisState } from '../../..'; -import { SubAggParamsProp } from './agg_params'; import { Field } from '../../../../../../../plugins/data/public'; // NOTE: we cannot export the interface with export { InterfaceName } // as there is currently a bug on babel typescript transform plugin for it // https://github.com/babel/babel/issues/7641 // -export interface AggParamCommonProps { +export interface AggParamCommonProps { agg: AggConfig; - aggParam: AggParam; + aggParam: P; disabled?: boolean; editorConfig: EditorConfig; + formIsTouched: boolean; indexedFields?: ComboBoxGroupedOptions; showValidation: boolean; state: VisState; value?: T; metricAggs: AggConfig[]; - subAggParams: SubAggParamsProp; - setValidity(isValid: boolean): void; - setTouched(): void; } -export interface AggParamEditorProps extends AggParamCommonProps { +export interface AggParamEditorProps extends AggParamCommonProps { setValue(value?: T): void; + setValidity(isValid: boolean): void; + setTouched(): void; } diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx index 9f3f5bff3f56e..8c59d69da9478 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params.test.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { AggConfig, VisState } from '../../..'; import { DefaultEditorAggParams, DefaultEditorAggParamsProps } from './agg_params'; +import { AggGroupNames } from '../agg_groups'; import { IndexPattern } from '../../../../../../../plugins/data/public'; const mockEditorConfig = { @@ -79,7 +80,7 @@ jest.mock('./agg_param', () => ({ })); describe('DefaultEditorAggParams component', () => { - let onAggParamsChange: jest.Mock; + let setAggParamValue: jest.Mock; let onAggTypeChange: jest.Mock; let setTouched: jest.Mock; let setValidity: jest.Mock; @@ -87,7 +88,7 @@ describe('DefaultEditorAggParams component', () => { let defaultProps: DefaultEditorAggParamsProps; beforeEach(() => { - onAggParamsChange = jest.fn(); + setAggParamValue = jest.fn(); onAggTypeChange = jest.fn(); setTouched = jest.fn(); setValidity = jest.fn(); @@ -99,13 +100,16 @@ describe('DefaultEditorAggParams component', () => { params: [{ name: 'interval', deserialize: intervalDeserialize }], }, params: {}, + schema: { + title: '', + }, } as any) as AggConfig, - groupName: 'metrics', + groupName: AggGroupNames.Metrics, formIsTouched: false, indexPattern: {} as IndexPattern, metricAggs: [], state: {} as VisState, - onAggParamsChange, + setAggParamValue, onAggTypeChange, setTouched, setValidity, @@ -131,16 +135,16 @@ describe('DefaultEditorAggParams component', () => { it('should set fixed and default values when editorConfig is defined (works in rollup index)', () => { mount(); - expect(onAggParamsChange).toHaveBeenNthCalledWith( + expect(setAggParamValue).toHaveBeenNthCalledWith( 1, - defaultProps.agg.params, + defaultProps.agg.id, 'useNormalizedEsInterval', false ); expect(intervalDeserialize).toHaveBeenCalledWith('1m'); - expect(onAggParamsChange).toHaveBeenNthCalledWith( + expect(setAggParamValue).toHaveBeenNthCalledWith( 2, - defaultProps.agg.params, + defaultProps.agg.id, 'interval', 'deserialized' ); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx index c62e2837908c7..0f47d9a555d21 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params.tsx @@ -17,60 +17,48 @@ * under the License. */ -import React, { useReducer, useEffect, useMemo } from 'react'; +import React, { useCallback, useReducer, useEffect, useMemo } from 'react'; import { EuiForm, EuiAccordion, EuiSpacer, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import useUnmount from 'react-use/lib/useUnmount'; -import { VisState } from 'ui/vis'; -import { aggTypes, AggType, AggParam, AggConfig } from 'ui/agg_types/'; +import { AggConfig } from 'ui/agg_types/'; import { IndexPattern } from '../../../../../../../plugins/data/public'; import { DefaultEditorAggSelect } from './agg_select'; import { DefaultEditorAggParam } from './agg_param'; import { getAggParamsToRender, - getError, getAggTypeOptions, - ParamInstance, isInvalidParamsTouched, } from './agg_params_helper'; import { aggTypeReducer, - AGG_TYPE_ACTION_KEYS, aggParamsReducer, AGG_PARAMS_ACTION_KEYS, initAggParamsState, - AggParamsItem, } from './agg_params_state'; import { editorConfigProviders } from '../../config/editor_config_providers'; import { FixedParam, TimeIntervalParam, EditorParamConfig } from '../../config/types'; import { AggGroupNames } from '../agg_groups'; -import { OnAggParamsChange } from './agg_common_props'; +import { DefaultEditorCommonProps } from './agg_common_props'; const FIXED_VALUE_PROP = 'fixedValue'; const DEFAULT_PROP = 'default'; type EditorParamConfigType = EditorParamConfig & { [key: string]: unknown; }; -export interface SubAggParamsProp { - formIsTouched: boolean; - onAggParamsChange: OnAggParamsChange; - onAggTypeChange: (agg: AggConfig, aggType: AggType) => void; -} -export interface DefaultEditorAggParamsProps extends SubAggParamsProp { + +export interface DefaultEditorAggParamsProps extends DefaultEditorCommonProps { agg: AggConfig; aggError?: string; aggIndex?: number; aggIsTooLow?: boolean; className?: string; disabledParams?: string[]; - groupName: string; indexPattern: IndexPattern; - metricAggs: AggConfig[]; - state: VisState; - setTouched: (isTouched: boolean) => void; setValidity: (isValid: boolean) => void; + setTouched: (isTouched: boolean) => void; } function DefaultEditorAggParams({ @@ -84,20 +72,34 @@ function DefaultEditorAggParams({ formIsTouched, indexPattern, metricAggs, - state = {} as VisState, - onAggParamsChange, + state, + setAggParamValue, onAggTypeChange, setTouched, setValidity, }: DefaultEditorAggParamsProps) { - const groupedAggTypeOptions = getAggTypeOptions(agg, indexPattern, groupName); - const errors = getError(agg, aggIsTooLow); - - const editorConfig = useMemo( - () => editorConfigProviders.getConfigForAgg((aggTypes as any)[groupName], indexPattern, agg), - [groupName, agg.type] - ); - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state }); + const groupedAggTypeOptions = useMemo(() => getAggTypeOptions(agg, indexPattern, groupName), [ + agg, + indexPattern, + groupName, + ]); + const error = aggIsTooLow + ? i18n.translate('common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage', { + defaultMessage: '"{schema}" aggs must run before all other buckets!', + values: { schema: agg.schema.title }, + }) + : ''; + + const editorConfig = useMemo(() => editorConfigProviders.getConfigForAgg(indexPattern, agg), [ + indexPattern, + agg, + ]); + const params = useMemo(() => getAggParamsToRender({ agg, editorConfig, metricAggs, state }), [ + agg, + editorConfig, + metricAggs, + state, + ]); const allParams = [...params.basic, ...params.advanced]; const [paramsState, onChangeParamsState] = useReducer( aggParamsReducer, @@ -107,21 +109,30 @@ function DefaultEditorAggParams({ const [aggType, onChangeAggType] = useReducer(aggTypeReducer, { touched: false, valid: true }); const isFormValid = - !errors.length && + !error && aggType.valid && Object.entries(paramsState).every(([, paramState]) => paramState.valid); const isAllInvalidParamsTouched = - !!errors.length || isInvalidParamsTouched(agg.type, aggType, paramsState); + !!error || isInvalidParamsTouched(agg.type, aggType, paramsState); + + const onAggSelect = useCallback( + value => { + if (agg.type !== value) { + onAggTypeChange(agg.id, value); + // reset touched and valid of params + onChangeParamsState({ type: AGG_PARAMS_ACTION_KEYS.RESET }); + } + }, + [onAggTypeChange, agg] + ); // reset validity before component destroyed useUnmount(() => setValidity(true)); useEffect(() => { Object.entries(editorConfig).forEach(([param, paramConfig]) => { - const paramOptions = agg.type.params.find( - (paramOption: AggParam) => paramOption.name === param - ); + const paramOptions = agg.type.params.find(paramOption => paramOption.name === param); const hasFixedValue = paramConfig.hasOwnProperty(FIXED_VALUE_PROP); const hasDefault = paramConfig.hasOwnProperty(DEFAULT_PROP); @@ -142,7 +153,11 @@ function DefaultEditorAggParams({ } else { newValue = typedParamConfig[property]; } - onAggParamsChange(agg.params, param, newValue); + + // this check is obligatory to avoid infinite render, because setAggParamValue creates a brand new agg object + if (agg.params[param] !== newValue) { + setAggParamValue(agg.id, param, newValue); + } } }); }, [editorConfig]); @@ -160,43 +175,11 @@ function DefaultEditorAggParams({ setTouched(isAllInvalidParamsTouched); }, [isAllInvalidParamsTouched]); - const renderParam = (paramInstance: ParamInstance, model: AggParamsItem) => { - return ( - { - onChangeParamsState({ - type: AGG_PARAMS_ACTION_KEYS.VALID, - paramName: paramInstance.aggParam.name, - payload: valid, - }); - }} - // setTouched can be called from sub-agg which passes a parameter - setTouched={(isTouched: boolean = true) => { - onChangeParamsState({ - type: AGG_PARAMS_ACTION_KEYS.TOUCHED, - paramName: paramInstance.aggParam.name, - payload: isTouched, - }); - }} - subAggParams={{ - onAggParamsChange, - onAggTypeChange, - formIsTouched, - }} - {...paramInstance} - /> - ); - }; - return ( = 1 && groupName === AggGroupNames.Buckets} showValidation={formIsTouched || aggType.touched} - setValue={value => { - onAggTypeChange(agg, value); - // reset touched and valid of params - onChangeParamsState({ type: AGG_PARAMS_ACTION_KEYS.RESET }); - }} - setTouched={() => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.TOUCHED, payload: true })} - setValidity={valid => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.VALID, payload: valid })} + setValue={onAggSelect} + onChangeAggType={onChangeAggType} /> - {params.basic.map((param: ParamInstance) => { + {params.basic.map(param => { const model = paramsState[param.aggParam.name] || { touched: false, valid: true, }; - return renderParam(param, model); + return ( + + ); })} {params.advanced.length ? ( @@ -238,12 +226,23 @@ function DefaultEditorAggParams({ )} > - {params.advanced.map((param: ParamInstance) => { + {params.advanced.map(param => { const model = paramsState[param.aggParam.name] || { touched: false, valid: true, }; - return renderParam(param, model); + + return ( + + ); })} diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts index fb37dd5d94618..c983bb7813de7 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts @@ -22,7 +22,6 @@ import { AggType } from 'ui/agg_types'; import { IndexedArray } from 'ui/indexed_array'; import { getAggParamsToRender, - getError, getAggTypeOptions, isInvalidParamsTouched, } from './agg_params_helper'; @@ -162,20 +161,6 @@ describe('DefaultEditorAggParams helpers', () => { }); }); - describe('getError', () => { - it('should not have any errors', () => { - const errors = getError({ schema: { title: 'Split series' } } as AggConfig, false); - - expect(errors).toEqual([]); - }); - - it('should push an error if an agg is too low', () => { - const errors = getError({ schema: { title: 'Split series' } } as AggConfig, true); - - expect(errors).toEqual(['"Split series" aggs must run before all other buckets!']); - }); - }); - describe('getAggTypeOptions', () => { it('should return agg type options grouped by subtype', () => { const indexPattern = {} as IndexPattern; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts index e0e014f69ef3f..3970238a68435 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.ts @@ -18,7 +18,6 @@ */ import { get, isEmpty } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { aggTypeFilters } from 'ui/agg_types/filter'; import { aggTypes, AggParam, FieldParamType, AggType } from 'ui/agg_types'; import { aggTypeFieldFilters } from 'ui/agg_types/param_types/filter'; @@ -91,27 +90,13 @@ function getAggParamsToRender({ agg, editorConfig, metricAggs, state }: ParamIns metricAggs, state, value: agg.params[param.name], - } as ParamInstance); + }); } }); return params; } -function getError(agg: AggConfig, aggIsTooLow: boolean) { - const errors = []; - if (aggIsTooLow) { - errors.push( - i18n.translate('common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage', { - defaultMessage: '"{schema}" aggs must run before all other buckets!', - values: { schema: agg.schema.title }, - }) - ); - } - - return errors; -} - function getAggTypeOptions( agg: AggConfig, indexPattern: IndexPattern, @@ -148,4 +133,4 @@ function isInvalidParamsTouched( return invalidParams.every(param => param.touched); } -export { getAggParamsToRender, getError, getAggTypeOptions, isInvalidParamsTouched }; +export { getAggParamsToRender, getAggTypeOptions, isInvalidParamsTouched }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx index 443c655912a55..6b6bb93b29b3e 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_select.tsx @@ -17,7 +17,7 @@ * under the License. */ import { get, has } from 'lodash'; -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -26,6 +26,7 @@ import { AggType } from 'ui/agg_types'; import { documentationLinks } from '../../../../documentation_links/documentation_links'; import { ComboBoxGroupedOptions } from '../utils'; import { IndexPattern } from '../../../../../../../plugins/data/public'; +import { AGG_TYPE_ACTION_KEYS, AggTypeAction } from './agg_params_state'; interface DefaultEditorAggSelectProps { aggError?: string; @@ -35,9 +36,8 @@ interface DefaultEditorAggSelectProps { showValidation: boolean; isSubAggregation: boolean; value: AggType; - setValidity: (isValid: boolean) => void; + onChangeAggType: React.Dispatch; setValue: (aggType: AggType) => void; - setTouched: () => void; } function DefaultEditorAggSelect({ @@ -49,8 +49,7 @@ function DefaultEditorAggSelect({ aggTypeOptions, showValidation, isSubAggregation, - setTouched, - setValidity, + onChangeAggType, }: DefaultEditorAggSelectProps) { const selectedOptions: ComboBoxGroupedOptions = value ? [{ label: value.title, target: value }] @@ -101,6 +100,25 @@ function DefaultEditorAggSelect({ const isValid = !!value && !errors.length; + const onChange = useCallback( + (options: EuiComboBoxOptionProps[]) => { + const selectedOption = get(options, '0.target'); + if (selectedOption) { + setValue(selectedOption as AggType); + } + }, + [setValue] + ); + + const setTouched = useCallback( + () => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.TOUCHED, payload: true }), + [onChangeAggType] + ); + const setValidity = useCallback( + valid => onChangeAggType({ type: AGG_TYPE_ACTION_KEYS.VALID, payload: valid }), + [onChangeAggType] + ); + useEffect(() => { setValidity(isValid); }, [isValid]); @@ -111,13 +129,6 @@ function DefaultEditorAggSelect({ } }, [errors.length]); - const onChange = (options: EuiComboBoxOptionProps[]) => { - const selectedOption = get(options, '0.target'); - if (selectedOption) { - setValue(selectedOption as AggType); - } - }; - return ( ; + vis: Vis; +} + +function DefaultEditorControls({ + applyChanges, + isDirty, + isInvalid, + isTouched, + dispatch, + vis, +}: DefaultEditorControlsProps) { + const { enableAutoApply } = vis.type.editorConfig; + const [autoApplyEnabled, setAutoApplyEnabled] = useState(false); + const toggleAutoApply = useCallback(e => setAutoApplyEnabled(e.target.checked), []); + const onClickDiscard = useCallback(() => dispatch(discardChanges(vis)), [dispatch, vis]); + + useDebounce( + () => { + if (autoApplyEnabled && isDirty) { + applyChanges(); + } + }, + 300, + [isDirty, autoApplyEnabled, applyChanges] + ); + + return ( +
+ {!autoApplyEnabled && ( + + + + + + + + + {isInvalid && isTouched ? ( + + + + + + ) : ( + + + + )} + + + )} + {enableAutoApply && ( + + + + )} +
+ ); +} + +export { DefaultEditorControls }; diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/data_tab.tsx b/src/legacy/ui/public/vis/editors/default/components/sidebar/data_tab.tsx new file mode 100644 index 0000000000000..4481ec5cb9b83 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/data_tab.tsx @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useMemo, useCallback } from 'react'; +import { findLast } from 'lodash'; +import { EuiSpacer } from '@elastic/eui'; + +import { parentPipelineAggHelper } from 'ui/agg_types/metrics/lib/parent_pipeline_agg_helper'; +import { AggConfig } from 'ui/agg_types'; +import { MetricAggType } from 'ui/agg_types/metrics/metric_agg_type'; +import { VisState } from 'ui/vis'; +import { DefaultEditorAggGroup } from '../agg_group'; +import { AggGroupNames } from '../../agg_groups'; +import { + EditorAction, + addNewAgg, + removeAgg, + reorderAggs, + setAggParamValue, + changeAggType, + toggleEnabledAgg, +} from './state'; +import { ISchemas } from '../../schemas'; +import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from '../agg_common_props'; + +export interface DefaultEditorDataTabProps { + dispatch: React.Dispatch; + formIsTouched: boolean; + isTabSelected: boolean; + metricAggs: AggConfig[]; + schemas: ISchemas; + state: VisState; + setTouched(isTouched: boolean): void; + setValidity(modelName: string, value: boolean): void; + setStateValue: DefaultEditorAggCommonProps['setStateParamValue']; +} + +function DefaultEditorDataTab({ + dispatch, + formIsTouched, + metricAggs, + schemas, + state, + setTouched, + setValidity, + setStateValue, +}: DefaultEditorDataTabProps) { + const lastParentPipelineAgg = useMemo( + () => + findLast( + metricAggs, + ({ type }: { type: MetricAggType }) => type.subtype === parentPipelineAggHelper.subtype + ), + [metricAggs] + ); + const lastParentPipelineAggTitle = lastParentPipelineAgg && lastParentPipelineAgg.type.title; + + const addSchema: AddSchema = useCallback(schema => dispatch(addNewAgg(schema)), [dispatch]); + + const onAggRemove: DefaultEditorAggCommonProps['removeAgg'] = useCallback( + aggId => dispatch(removeAgg(aggId)), + [dispatch] + ); + + const onReorderAggs: ReorderAggs = useCallback((...props) => dispatch(reorderAggs(...props)), [ + dispatch, + ]); + + const onAggParamValueChange: DefaultEditorAggCommonProps['setAggParamValue'] = useCallback( + (...props) => dispatch(setAggParamValue(...props)), + [dispatch] + ); + + const onAggTypeChange: DefaultEditorAggCommonProps['onAggTypeChange'] = useCallback( + (...props) => dispatch(changeAggType(...props)), + [dispatch] + ); + + const onToggleEnableAgg: DefaultEditorAggCommonProps['onToggleEnableAgg'] = useCallback( + (...props) => dispatch(toggleEnabledAgg(...props)), + [dispatch] + ); + + const commonProps = { + addSchema, + formIsTouched, + lastParentPipelineAggTitle, + metricAggs, + state, + reorderAggs: onReorderAggs, + setAggParamValue: onAggParamValueChange, + setStateParamValue: setStateValue, + onAggTypeChange, + onToggleEnableAgg, + setValidity, + setTouched, + removeAgg: onAggRemove, + }; + + return ( + <> + + + + + + + ); +} + +export { DefaultEditorDataTab }; diff --git a/src/legacy/ui/public/vis/editor_size.js b/src/legacy/ui/public/vis/editors/default/components/sidebar/index.ts similarity index 85% rename from src/legacy/ui/public/vis/editor_size.js rename to src/legacy/ui/public/vis/editors/default/components/sidebar/index.ts index 5383772b3ad00..31228aad85d1e 100644 --- a/src/legacy/ui/public/vis/editor_size.js +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/index.ts @@ -17,8 +17,6 @@ * under the License. */ -export const DefaultEditorSize = { - SMALL: 'small', - MEDIUM: 'medium', - LARGE: 'large', -}; +export { DefaultEditorSideBar } from './sidebar'; +export { DefaultEditorDataTab } from './data_tab'; +export { OptionTab } from './navbar'; diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/navbar.tsx b/src/legacy/ui/public/vis/editors/default/components/sidebar/navbar.tsx new file mode 100644 index 0000000000000..a1b5003a092f7 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/navbar.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiTabs, EuiTab } from '@elastic/eui'; + +import { VisOptionsProps } from '../../vis_options_props'; +import { DefaultEditorDataTabProps } from './data_tab'; + +export interface OptionTab { + editor: React.ComponentType; + name: string; + title: string; +} + +interface DefaultEditorNavBarProps { + optionTabs: OptionTab[]; + selectedTab: string; + setSelectedTab(name: string): void; +} + +function DefaultEditorNavBar({ + selectedTab, + setSelectedTab, + optionTabs, +}: DefaultEditorNavBarProps) { + return ( + + {optionTabs.map(({ name, title }) => ( + setSelectedTab(name)} + > + {title} + + ))} + + ); +} + +export { DefaultEditorNavBar }; diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/sidebar.tsx b/src/legacy/ui/public/vis/editors/default/components/sidebar/sidebar.tsx new file mode 100644 index 0000000000000..bf35c46dbb7b5 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/sidebar.tsx @@ -0,0 +1,226 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect } from 'react'; +import { get, isEqual } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; + +import { Vis } from 'ui/vis'; +import { PersistedState } from 'ui/persisted_state'; +import { DefaultEditorNavBar, OptionTab } from './navbar'; +import { DefaultEditorControls } from './controls'; +import { setStateParamValue, useEditorReducer, useEditorFormState } from './state'; +import { AggGroupNames } from '../../agg_groups'; +import { DefaultEditorAggCommonProps } from '../agg_common_props'; + +interface DefaultEditorSideBarProps { + isCollapsed: boolean; + onClickCollapse: () => void; + optionTabs: OptionTab[]; + uiState: PersistedState; + vis: Vis; +} + +function DefaultEditorSideBar({ + isCollapsed, + onClickCollapse, + optionTabs, + uiState, + vis, +}: DefaultEditorSideBarProps) { + const [selectedTab, setSelectedTab] = useState(optionTabs[0].name); + const [isDirty, setDirty] = useState(false); + const [state, dispatch] = useEditorReducer(vis); + const { formState, setTouched, setValidity, resetValidity } = useEditorFormState(); + + const responseAggs = useMemo(() => state.aggs.getResponseAggs(), [state.aggs]); + const metricAggs = useMemo( + () => responseAggs.filter(agg => get(agg, 'schema.group') === AggGroupNames.Metrics), + [responseAggs] + ); + const hasHistogramAgg = useMemo(() => responseAggs.some(agg => agg.type.name === 'histogram'), [ + responseAggs, + ]); + + const setStateValidity = useCallback( + (value: boolean) => { + setValidity('visOptions', value); + }, + [setValidity] + ); + + const setStateValue: DefaultEditorAggCommonProps['setStateParamValue'] = useCallback( + (paramName, value) => { + const shouldUpdate = !isEqual(state.params[paramName], value); + + if (shouldUpdate) { + dispatch(setStateParamValue(paramName, value)); + } + }, + [dispatch, state.params] + ); + + const applyChanges = useCallback(() => { + if (formState.invalid || !isDirty) { + setTouched(true); + + return; + } + + vis.setCurrentState(state); + vis.updateState(); + vis.emit('dirtyStateChange', { + isDirty: false, + }); + setTouched(false); + }, [vis, state, formState.invalid, setDirty, setTouched, isDirty]); + + const onSubmit: KeyboardEventHandler = useCallback( + event => { + if (event.ctrlKey && event.keyCode === keyCodes.ENTER) { + event.preventDefault(); + event.stopPropagation(); + + applyChanges(); + } + }, + [applyChanges] + ); + + useEffect(() => { + vis.on('dirtyStateChange', ({ isDirty: dirty }: { isDirty: boolean }) => { + setDirty(dirty); + + if (!dirty) { + resetValidity(); + } + }); + }, [resetValidity, vis]); + + const dataTabProps = { + dispatch, + formIsTouched: formState.touched, + metricAggs, + state, + schemas: vis.type.schemas, + setValidity, + setTouched, + setStateValue, + }; + + const optionTabProps = { + aggs: state.aggs, + hasHistogramAgg, + stateParams: state.params, + vis, + uiState, + setValue: setStateValue, + setValidity: setStateValidity, + setTouched, + }; + + return ( + <> + + +
+ {vis.type.requiresSearch && vis.type.options.showIndexSelection ? ( + +

+ {vis.indexPattern.title} +

+
+ ) : ( +
+ )} + + {optionTabs.length > 1 && ( + + )} + + {optionTabs.map(({ editor: Editor, name }) => { + const isTabSelected = selectedTab === name; + + return ( +
+ +
+ ); + })} + + + + + + + + + + + ); +} + +export { DefaultEditorSideBar }; diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/state/actions.ts b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/actions.ts new file mode 100644 index 0000000000000..ab1d65c626ae3 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/actions.ts @@ -0,0 +1,171 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AggConfig, Vis, VisParams } from 'ui/vis'; +import { EditorStateActionTypes } from './constants'; +import { Schema } from '../../../schemas'; + +export interface ActionType { + type: T; + payload: P; +} + +type AggId = AggConfig['id']; +type AggParams = AggConfig['params']; + +type AddNewAgg = ActionType; +type DiscardChanges = ActionType; +type ChangeAggType = ActionType< + EditorStateActionTypes.CHANGE_AGG_TYPE, + { aggId: AggId; value: AggConfig['type'] } +>; +type SetAggParamValue = ActionType< + EditorStateActionTypes.SET_AGG_PARAM_VALUE, + { + aggId: AggId; + paramName: T; + value: AggParams[T]; + } +>; +type SetStateParamValue = ActionType< + EditorStateActionTypes.SET_STATE_PARAM_VALUE, + { paramName: T; value: AggParams[T] } +>; +type RemoveAgg = ActionType; +type ReorderAggs = ActionType< + EditorStateActionTypes.REORDER_AGGS, + { sourceAgg: AggConfig; destinationAgg: AggConfig } +>; +type ToggleEnabledAgg = ActionType< + EditorStateActionTypes.TOGGLE_ENABLED_AGG, + { aggId: AggId; enabled: AggConfig['enabled'] } +>; +type UpdateStateParams = ActionType< + EditorStateActionTypes.UPDATE_STATE_PARAMS, + { params: VisParams } +>; + +export type EditorAction = + | AddNewAgg + | DiscardChanges + | ChangeAggType + | SetAggParamValue + | SetStateParamValue + | RemoveAgg + | ReorderAggs + | ToggleEnabledAgg + | UpdateStateParams; + +export interface EditorActions { + addNewAgg(schema: Schema): AddNewAgg; + discardChanges(vis: Vis): DiscardChanges; + changeAggType(aggId: AggId, value: AggConfig['type']): ChangeAggType; + setAggParamValue( + aggId: AggId, + paramName: T, + value: AggParams[T] + ): SetAggParamValue; + setStateParamValue( + paramName: T, + value: AggParams[T] + ): SetStateParamValue; + removeAgg(aggId: AggId): RemoveAgg; + reorderAggs(sourceAgg: AggConfig, destinationAgg: AggConfig): ReorderAggs; + toggleEnabledAgg(aggId: AggId, enabled: AggConfig['enabled']): ToggleEnabledAgg; + updateStateParams(params: VisParams): UpdateStateParams; +} + +const addNewAgg: EditorActions['addNewAgg'] = schema => ({ + type: EditorStateActionTypes.ADD_NEW_AGG, + payload: { + schema, + }, +}); + +const discardChanges: EditorActions['discardChanges'] = vis => ({ + type: EditorStateActionTypes.DISCARD_CHANGES, + payload: vis, +}); + +const changeAggType: EditorActions['changeAggType'] = (aggId, value) => ({ + type: EditorStateActionTypes.CHANGE_AGG_TYPE, + payload: { + aggId, + value, + }, +}); + +const setAggParamValue: EditorActions['setAggParamValue'] = (aggId, paramName, value) => ({ + type: EditorStateActionTypes.SET_AGG_PARAM_VALUE, + payload: { + aggId, + paramName, + value, + }, +}); + +const setStateParamValue: EditorActions['setStateParamValue'] = (paramName, value) => ({ + type: EditorStateActionTypes.SET_STATE_PARAM_VALUE, + payload: { + paramName, + value, + }, +}); + +const removeAgg: EditorActions['removeAgg'] = aggId => ({ + type: EditorStateActionTypes.REMOVE_AGG, + payload: { + aggId, + }, +}); + +const reorderAggs: EditorActions['reorderAggs'] = (sourceAgg, destinationAgg) => ({ + type: EditorStateActionTypes.REORDER_AGGS, + payload: { + sourceAgg, + destinationAgg, + }, +}); + +const toggleEnabledAgg: EditorActions['toggleEnabledAgg'] = (aggId, enabled) => ({ + type: EditorStateActionTypes.TOGGLE_ENABLED_AGG, + payload: { + aggId, + enabled, + }, +}); + +const updateStateParams: EditorActions['updateStateParams'] = params => ({ + type: EditorStateActionTypes.UPDATE_STATE_PARAMS, + payload: { + params, + }, +}); + +export { + addNewAgg, + discardChanges, + changeAggType, + setAggParamValue, + setStateParamValue, + removeAgg, + reorderAggs, + toggleEnabledAgg, + updateStateParams, +}; diff --git a/src/legacy/ui/public/vis/editors/default/vis_options_react_wrapper.tsx b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/constants.ts similarity index 66% rename from src/legacy/ui/public/vis/editors/default/vis_options_react_wrapper.tsx rename to src/legacy/ui/public/vis/editors/default/components/sidebar/state/constants.ts index d214abecb9c0c..2c5f5f1384858 100644 --- a/src/legacy/ui/public/vis/editors/default/vis_options_react_wrapper.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/constants.ts @@ -17,15 +17,14 @@ * under the License. */ -import React from 'react'; -import { VisOptionsProps } from './vis_options_props'; - -interface VisOptionsReactWrapperProps extends VisOptionsProps { - component: React.ComponentType; -} - -function VisOptionsReactWrapper({ component: Component, ...rest }: VisOptionsReactWrapperProps) { - return ; +export enum EditorStateActionTypes { + ADD_NEW_AGG = 'ADD_NEW_AGG', + DISCARD_CHANGES = 'DISCARD_CHANGES', + CHANGE_AGG_TYPE = 'CHANGE_AGG_TYPE', + SET_AGG_PARAM_VALUE = 'SET_AGG_PARAM_VALUE', + SET_STATE_PARAM_VALUE = 'SET_STATE_PARAM_VALUE', + TOGGLE_ENABLED_AGG = 'TOGGLE_ENABLED_AGG', + REMOVE_AGG = 'REMOVE_AGG', + REORDER_AGGS = 'REORDER_AGGS', + UPDATE_STATE_PARAMS = 'UPDATE_STATE_PARAMS', } - -export { VisOptionsReactWrapper }; diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/state/editor_form_state.ts b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/editor_form_state.ts new file mode 100644 index 0000000000000..1f98a5f7fa7df --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/editor_form_state.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useState, useCallback } from 'react'; + +export type SetValidity = (modelName: string, value: boolean) => void; +export type SetTouched = (value: boolean) => void; + +const initialFormState = { + validity: {}, + touched: false, + invalid: false, +}; + +function useEditorFormState() { + const [formState, setFormState] = useState(initialFormState); + + const setValidity: SetValidity = useCallback((modelName, value) => { + setFormState(model => { + const validity = { + ...model.validity, + [modelName]: value, + }; + + return { + ...model, + validity, + invalid: Object.values(validity).some(valid => !valid), + }; + }); + }, []); + + const resetValidity = useCallback(() => { + setFormState(initialFormState); + }, []); + + const setTouched = useCallback((touched: boolean) => { + setFormState(model => ({ + ...model, + touched, + })); + }, []); + + return { + formState, + setValidity, + setTouched, + resetValidity, + }; +} + +export { useEditorFormState }; diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/state/index.ts b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/index.ts new file mode 100644 index 0000000000000..6dbd9a69d82c0 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/index.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEffect, useReducer, useCallback } from 'react'; +import { isEqual } from 'lodash'; + +import { Vis, VisState, VisParams } from 'ui/vis'; +import { editorStateReducer, initEditorState } from './reducers'; +import { EditorStateActionTypes } from './constants'; +import { EditorAction, updateStateParams } from './actions'; + +export * from './editor_form_state'; +export * from './actions'; + +export function useEditorReducer(vis: Vis): [VisState, React.Dispatch] { + const [state, dispatch] = useReducer(editorStateReducer, vis, initEditorState); + + useEffect(() => { + const handleVisUpdate = (params: VisParams) => { + if (!isEqual(params, state.params)) { + dispatch(updateStateParams(params)); + } + }; + + // fires when visualization state changes, and we need to copy changes to editorState + vis.on('updateEditorStateParams', handleVisUpdate); + + return () => vis.off('updateEditorStateParams', handleVisUpdate); + }, [vis, state.params]); + + const wrappedDispatch = useCallback( + (action: EditorAction) => { + dispatch(action); + + vis.emit('dirtyStateChange', { + isDirty: action.type !== EditorStateActionTypes.DISCARD_CHANGES, + }); + }, + [vis] + ); + + return [state, wrappedDispatch]; +} diff --git a/src/legacy/ui/public/vis/editors/default/components/sidebar/state/reducers.ts b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/reducers.ts new file mode 100644 index 0000000000000..db52291c823e7 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/components/sidebar/state/reducers.ts @@ -0,0 +1,182 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cloneDeep } from 'lodash'; + +import { AggConfigs, AggConfig } from 'ui/agg_types'; +import { Vis, VisState } from 'ui/vis'; +import { move } from 'ui/utils/collection'; +import { EditorStateActionTypes } from './constants'; +import { AggGroupNames } from '../../../agg_groups'; +import { getEnabledMetricAggsCount } from '../../agg_group_helper'; +import { EditorAction } from './actions'; + +function initEditorState(vis: Vis) { + return vis.copyCurrentState(true); +} + +function editorStateReducer(state: VisState, action: EditorAction): VisState { + switch (action.type) { + case EditorStateActionTypes.ADD_NEW_AGG: { + const aggConfig = state.aggs.createAggConfig(action.payload as AggConfig, { + addToAggConfigs: false, + }); + aggConfig.brandNew = true; + const newAggs = [...state.aggs.aggs, aggConfig]; + + return { + ...state, + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + }; + } + + case EditorStateActionTypes.DISCARD_CHANGES: { + return initEditorState(action.payload); + } + + case EditorStateActionTypes.CHANGE_AGG_TYPE: { + const { aggId, value } = action.payload; + + const newAggs = state.aggs.aggs.map(agg => { + if (agg.id === aggId) { + agg.type = value; + + return agg.toJSON(); + } + + return agg; + }); + + return { + ...state, + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + }; + } + + case EditorStateActionTypes.SET_AGG_PARAM_VALUE: { + const { aggId, paramName, value } = action.payload; + + const newAggs = state.aggs.aggs.map(agg => { + if (agg.id === aggId) { + const parsedAgg = agg.toJSON(); + + return { + ...parsedAgg, + params: { + ...parsedAgg.params, + [paramName]: value, + }, + }; + } + + return agg; + }); + + return { + ...state, + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + }; + } + + case EditorStateActionTypes.SET_STATE_PARAM_VALUE: { + const { paramName, value } = action.payload; + + return { + ...state, + params: { + ...state.params, + [paramName]: value, + }, + }; + } + + case EditorStateActionTypes.REMOVE_AGG: { + let isMetric = false; + + const newAggs = state.aggs.aggs.filter(({ id, schema }) => { + if (id === action.payload.aggId) { + if (schema.group === AggGroupNames.Metrics) { + isMetric = true; + } + + return false; + } + + return true; + }); + + if (isMetric && getEnabledMetricAggsCount(newAggs) === 0) { + const aggToEnable = newAggs.find(agg => agg.schema.name === 'metric'); + + if (aggToEnable) { + aggToEnable.enabled = true; + } + } + + return { + ...state, + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + }; + } + + case EditorStateActionTypes.REORDER_AGGS: { + const { sourceAgg, destinationAgg } = action.payload; + const destinationIndex = state.aggs.aggs.indexOf(destinationAgg); + const newAggs = move([...state.aggs.aggs], sourceAgg, destinationIndex); + + return { + ...state, + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + }; + } + + case EditorStateActionTypes.TOGGLE_ENABLED_AGG: { + const { aggId, enabled } = action.payload; + + const newAggs = state.aggs.aggs.map(agg => { + if (agg.id === aggId) { + const parsedAgg = agg.toJSON(); + + return { + ...parsedAgg, + enabled, + }; + } + + return agg; + }); + + return { + ...state, + aggs: new AggConfigs(state.aggs.indexPattern, newAggs, state.aggs.schemas), + }; + } + + case EditorStateActionTypes.UPDATE_STATE_PARAMS: { + const { params } = action.payload; + + return { + ...state, + params: cloneDeep(params), + }; + } + } +} + +export { editorStateReducer, initEditorState }; diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx b/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx index f215cf755886d..55cd237a56689 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx @@ -17,12 +17,12 @@ * under the License. */ -import { VisParams } from '../../..'; -import { AggParams } from '../agg_params'; -import { OnAggParamsChange } from '../components/agg_common_props'; +import { AggConfig, VisParams } from 'ui/vis'; +import { DefaultEditorAggCommonProps } from '../components/agg_common_props'; export interface AggControlProps { - aggParams: AggParams; + agg: AggConfig; editorStateParams: VisParams; - setValue: OnAggParamsChange; + setAggParamValue: DefaultEditorAggCommonProps['setAggParamValue']; + setStateParamValue: DefaultEditorAggCommonProps['setStateParamValue']; } diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts b/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts index 6491ef2e46054..98e4931b23ea3 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts @@ -55,7 +55,7 @@ function useFallbackMetric( .filter(isCompatibleAgg) .find(aggregation => aggregation.id === value); - if (!respAgg) { + if (!respAgg && value !== fallbackValue) { setValue(fallbackValue); } } diff --git a/src/legacy/ui/public/vis/editors/default/controls/field.test.tsx b/src/legacy/ui/public/vis/editors/default/controls/field.test.tsx index 4d15ac8e80e63..67ce3ba6d5072 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/field.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/field.test.tsx @@ -22,7 +22,7 @@ import { act } from 'react-dom/test-utils'; import { mount, shallow, ReactWrapper } from 'enzyme'; import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui'; import { Field } from '../../../../../../../plugins/data/public'; -import { ComboBoxGroupedOptions, SubAggParamsProp } from '..'; +import { ComboBoxGroupedOptions } from '..'; import { FieldParamEditor, FieldParamEditorProps } from './field'; import { AggConfig, VisState } from '../../..'; @@ -69,6 +69,7 @@ describe('FieldParamEditor component', () => { editorComponent: () => null, onChange, } as any, + formIsTouched: false, value: undefined, editorConfig: {}, indexedFields, @@ -78,7 +79,6 @@ describe('FieldParamEditor component', () => { setTouched, state: {} as VisState, metricAggs: [] as AggConfig[], - subAggParams: {} as SubAggParamsProp, }; }); @@ -130,15 +130,6 @@ describe('FieldParamEditor component', () => { expect(setValidity).toHaveBeenCalledWith(false); }); - it('should call setTouched when the control is invalid', () => { - defaultProps.value = field; - const comp = mount(); - expect(setTouched).not.toHaveBeenCalled(); - comp.setProps({ customError: 'customError' }); - - expect(setTouched).toHaveBeenCalled(); - }); - it('should call onChange when a field selected', () => { const comp = mount(); act(() => { diff --git a/src/legacy/ui/public/vis/editors/default/controls/field.tsx b/src/legacy/ui/public/vis/editors/default/controls/field.tsx index 75a9e24cd0dee..b8cd0d630a019 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/field.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/field.tsx @@ -26,6 +26,7 @@ import { AggConfig } from '../../..'; import { Field } from '../../../../../../../plugins/data/public'; import { formatListAsProse, parseCommaSeparatedList } from '../../../../../../utils'; import { AggParam, FieldParamType } from '../../../../agg_types'; +import { useValidation } from './agg_utils'; import { AggParamEditorProps, ComboBoxGroupedOptions } from '..'; const label = i18n.translate('common.ui.aggTypes.field.fieldLabel', { defaultMessage: 'Field' }); @@ -78,13 +79,7 @@ function FieldParamEditor({ const isValid = !!value && !errors.length; - useEffect(() => { - setValidity(isValid); - - if (!!errors.length) { - setTouched(); - } - }, [isValid]); + useValidation(setValidity, isValid); useEffect(() => { // set field if only one available diff --git a/src/legacy/ui/public/vis/editors/default/controls/order_agg.tsx b/src/legacy/ui/public/vis/editors/default/controls/order_agg.tsx index 9d337035f8734..efa8366ec550b 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/order_agg.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/order_agg.tsx @@ -17,41 +17,43 @@ * under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { AggParamType } from '../../../../agg_types/param_types/agg'; import { AggConfig } from '../../..'; +import { useSubAggParamsHandlers } from './utils'; +import { AggGroupNames } from '../agg_groups'; import { AggParamEditorProps, DefaultEditorAggParams } from '..'; function OrderAggParamEditor({ agg, + aggParam, + formIsTouched, value, metricAggs, state, setValue, setValidity, setTouched, - subAggParams, -}: AggParamEditorProps) { +}: AggParamEditorProps) { + const orderBy = agg.params.orderBy; + useEffect(() => { - if (metricAggs) { - const orderBy = agg.params.orderBy; + if (orderBy === 'custom' && !value) { + setValue(aggParam.makeAgg(agg)); + } - // we aren't creating a custom aggConfig - if (!orderBy || orderBy !== 'custom') { - setValue(undefined); - } else if (value) { - setValue(value); - } else { - const paramDef = agg.type.paramByName('orderAgg'); - if (paramDef) { - setValue((paramDef as AggParamType).makeAgg(agg)); - } - } + if (orderBy !== 'custom' && value) { + setValue(undefined); } - }, [agg.params.orderBy, metricAggs]); + }, [orderBy]); - const [innerState, setInnerState] = useState(true); + const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( + agg, + aggParam, + value as AggConfig, + setValue + ); if (!agg.params.orderAgg) { return null; @@ -62,18 +64,14 @@ function OrderAggParamEditor({ { - // to force update when sub-agg params are changed - setInnerState(!innerState); - subAggParams.onAggParamsChange(...rest); - }} - onAggTypeChange={subAggParams.onAggTypeChange} + setAggParamValue={setAggParamValue} + onAggTypeChange={onAggTypeChange} setValidity={setValidity} setTouched={setTouched} /> diff --git a/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx b/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx index d9e0abc0cce59..4d481bd74e8a3 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiFormRow, EuiIconTip, EuiRange, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -26,7 +26,7 @@ import { AggControlProps } from './agg_control_props'; const DEFAULT_VALUE = 50; const PARAM_NAME = 'radiusRatio'; -function RadiusRatioOptionControl({ editorStateParams, setValue }: AggControlProps) { +function RadiusRatioOptionControl({ editorStateParams, setStateParamValue }: AggControlProps) { const label = ( <> { if (!editorStateParams.radiusRatio) { - setValue(editorStateParams, PARAM_NAME, DEFAULT_VALUE); + setStateParamValue(PARAM_NAME, DEFAULT_VALUE); } }, []); + const onChange = useCallback( + (e: React.ChangeEvent | React.MouseEvent) => + setStateParamValue(PARAM_NAME, parseFloat(e.currentTarget.value)), + [setStateParamValue] + ); + return ( <> @@ -58,9 +64,7 @@ function RadiusRatioOptionControl({ editorStateParams, setValue }: AggControlPro min={1} max={100} value={editorStateParams.radiusRatio || DEFAULT_VALUE} - onChange={( - e: React.ChangeEvent | React.MouseEvent - ) => setValue(editorStateParams, PARAM_NAME, parseFloat(e.currentTarget.value))} + onChange={onChange} showRange showValue valueAppend="%" diff --git a/src/legacy/ui/public/vis/editors/default/controls/rows_or_columns.tsx b/src/legacy/ui/public/vis/editors/default/controls/rows_or_columns.tsx index e6e4d34aea836..3ba4279a370dd 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/rows_or_columns.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/rows_or_columns.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiButtonGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AggControlProps } from './agg_control_props'; @@ -28,8 +28,8 @@ const PARAMS = { COLUMNS: 'visEditorSplitBy__false', }; -function RowsOrColumnsControl({ aggParams, setValue }: AggControlProps) { - const idSelected = `visEditorSplitBy__${aggParams.row}`; +function RowsOrColumnsControl({ agg, setAggParamValue }: AggControlProps) { + const idSelected = `visEditorSplitBy__${agg.params.row}`; const options = [ { id: PARAMS.ROWS, @@ -44,6 +44,10 @@ function RowsOrColumnsControl({ aggParams, setValue }: AggControlProps) { }), }, ]; + const onChange = useCallback( + optionId => setAggParamValue(agg.id, PARAMS.NAME, optionId === PARAMS.ROWS), + [setAggParamValue] + ); return ( <> @@ -56,7 +60,7 @@ function RowsOrColumnsControl({ aggParams, setValue }: AggControlProps) { options={options} isFullWidth={true} idSelected={idSelected} - onChange={optionId => setValue(aggParams, PARAMS.NAME, optionId === PARAMS.ROWS)} + onChange={onChange} /> diff --git a/src/legacy/ui/public/vis/editors/default/controls/string.tsx b/src/legacy/ui/public/vis/editors/default/controls/string.tsx index 63827205afdb3..dbfd0a7db33fb 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/string.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/string.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { AggParamEditorProps } from '..'; @@ -37,6 +37,8 @@ function StringParamEditor({ setValidity(isValid); }, [isValid]); + const onChange = useCallback(ev => setValue(ev.target.value), [setValue]); + return ( setValue(ev.target.value)} + onChange={onChange} fullWidth={true} compressed onBlur={setTouched} diff --git a/src/legacy/ui/public/vis/editors/default/controls/sub_agg.tsx b/src/legacy/ui/public/vis/editors/default/controls/sub_agg.tsx index 559a3f47db563..b233480cb35ba 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/sub_agg.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/sub_agg.tsx @@ -17,35 +17,40 @@ * under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType } from '../../../../agg_types/param_types/agg'; + +import { AggParamType } from 'ui/agg_types/param_types/agg'; import { AggConfig } from '../../..'; -import { AggParamEditorProps, DefaultEditorAggParams } from '..'; +import { useSubAggParamsHandlers } from './utils'; +import { AggParamEditorProps, DefaultEditorAggParams, AggGroupNames } from '..'; function SubAggParamEditor({ agg, + aggParam, + formIsTouched, value, metricAggs, state, setValue, setValidity, setTouched, - subAggParams, -}: AggParamEditorProps) { +}: AggParamEditorProps) { useEffect(() => { // we aren't creating a custom aggConfig if (agg.params.metricAgg !== 'custom') { setValue(undefined); } else if (!agg.params.customMetric) { - const customMetric = agg.type.paramByName('customMetric'); - if (customMetric) { - setValue((customMetric as AggParamType).makeAgg(agg)); - } + setValue(aggParam.makeAgg(agg)); } }, [value, metricAggs]); - const [innerState, setInnerState] = useState(true); + const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( + agg, + aggParam, + agg.params.customMetric, + setValue + ); if (agg.params.metricAgg !== 'custom' || !agg.params.customMetric) { return null; @@ -56,18 +61,14 @@ function SubAggParamEditor({ { - // to force update when sub-agg params are changed - setInnerState(!innerState); - subAggParams.onAggParamsChange(...rest); - }} - onAggTypeChange={subAggParams.onAggTypeChange} + setAggParamValue={setAggParamValue} + onAggTypeChange={onAggTypeChange} setValidity={setValidity} setTouched={setTouched} /> diff --git a/src/legacy/ui/public/vis/editors/default/controls/sub_metric.tsx b/src/legacy/ui/public/vis/editors/default/controls/sub_metric.tsx index df1640273135e..d0a44d1d35d1c 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/sub_metric.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/sub_metric.tsx @@ -17,23 +17,25 @@ * under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggParamType } from '../../../../agg_types/param_types/agg'; + +import { AggParamType } from 'ui/agg_types/param_types/agg'; import { AggConfig } from '../../../../agg_types/agg_config'; +import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps, DefaultEditorAggParams, AggGroupNames } from '..'; function SubMetricParamEditor({ agg, aggParam, + formIsTouched, metricAggs, state, setValue, setValidity, setTouched, - subAggParams, -}: AggParamEditorProps) { +}: AggParamEditorProps) { const metricTitle = i18n.translate('common.ui.aggTypes.metrics.metricTitle', { defaultMessage: 'Metric', }); @@ -49,14 +51,16 @@ function SubMetricParamEditor({ if (agg.params[type]) { setValue(agg.params[type]); } else { - const param = agg.type.paramByName(type); - if (param) { - setValue((param as AggParamType).makeAgg(agg)); - } + setValue(aggParam.makeAgg(agg)); } }, []); - const [innerState, setInnerState] = useState(true); + const { onAggTypeChange, setAggParamValue } = useSubAggParamsHandlers( + agg, + aggParam, + agg.params[type], + setValue + ); if (!agg.params[type]) { return null; @@ -71,16 +75,12 @@ function SubMetricParamEditor({ agg={agg.params[type]} groupName={aggGroup} className="visEditorAgg__subAgg" - formIsTouched={subAggParams.formIsTouched} + formIsTouched={formIsTouched} indexPattern={agg.getIndexPattern()} metricAggs={metricAggs} state={state} - onAggParamsChange={(...rest) => { - // to force update when sub-agg params are changed - setInnerState(!innerState); - subAggParams.onAggParamsChange(...rest); - }} - onAggTypeChange={subAggParams.onAggTypeChange} + setAggParamValue={setAggParamValue} + onAggTypeChange={onAggTypeChange} setValidity={setValidity} setTouched={setTouched} /> diff --git a/src/legacy/ui/public/vis/editors/default/controls/test_utils.ts b/src/legacy/ui/public/vis/editors/default/controls/test_utils.ts index 2e00b62d9b7fd..c5abf31a3cd8f 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/test_utils.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/test_utils.ts @@ -20,14 +20,13 @@ import { AggConfig, VisState } from '../../..'; import { EditorConfig } from '../../config/types'; import { AggParam } from '../../../../agg_types'; -import { SubAggParamsProp } from '..'; export const aggParamCommonPropsMock = { agg: {} as AggConfig, aggParam: {} as AggParam, editorConfig: {} as EditorConfig, + formIsTouched: false, metricAggs: [] as AggConfig[], - subAggParams: {} as SubAggParamsProp, state: {} as VisState, showValidation: false, }; diff --git a/src/legacy/ui/public/vis/editors/default/controls/utils.ts b/src/legacy/ui/public/vis/editors/default/controls/utils.ts new file mode 100644 index 0000000000000..5fd7c284fa23d --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/utils.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useCallback } from 'react'; +import { AggConfig } from 'ui/vis'; +import { AggParamType } from 'ui/agg_types/param_types/agg'; + +type SetValue = (value?: AggConfig) => void; + +function useSubAggParamsHandlers( + agg: AggConfig, + aggParam: AggParamType, + subAgg: AggConfig, + setValue: SetValue +) { + const setAggParamValue = useCallback( + (aggId, paramName, val) => { + const parsedParams = subAgg.toJSON(); + const params = { + ...parsedParams, + params: { + ...parsedParams.params, + [paramName]: val, + }, + }; + + setValue(aggParam.makeAgg(agg, params)); + }, + [agg, aggParam, setValue, subAgg] + ); + + const onAggTypeChange = useCallback( + (aggId, aggType) => { + const parsedAgg = subAgg.toJSON(); + + const params = { + ...parsedAgg, + type: aggType, + }; + + setValue(aggParam.makeAgg(agg, params)); + }, + [agg, aggParam, setValue, subAgg] + ); + + return { onAggTypeChange, setAggParamValue }; +} + +export { useSubAggParamsHandlers }; diff --git a/src/legacy/ui/public/vis/editors/default/default.html b/src/legacy/ui/public/vis/editors/default/default.html deleted file mode 100644 index 60fcbafdb88f5..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/default.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
- - -
- -
- -
diff --git a/src/legacy/ui/public/vis/editors/default/default.js b/src/legacy/ui/public/vis/editors/default/default.js deleted file mode 100644 index 66f52ea84398f..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/default.js +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import 'ui/angular-bootstrap'; -import './fancy_forms'; -import './sidebar'; -import { i18n } from '@kbn/i18n'; -import './vis_options'; -import './vis_editor_resizer'; -import './vis_type_agg_filter'; -import $ from 'jquery'; - -import _ from 'lodash'; -import angular from 'angular'; -import defaultEditorTemplate from './default.html'; -import { keyCodes } from '@elastic/eui'; -import { parentPipelineAggHelper } from 'ui/agg_types/metrics/lib/parent_pipeline_agg_helper'; -import { DefaultEditorSize } from '../../editor_size'; - -import { AggGroupNames } from './agg_groups'; - -import { start as embeddables } from '../../../../../core_plugins/embeddable_api/public/np_ready/public/legacy'; - -const defaultEditor = function($rootScope, $compile) { - return class DefaultEditor { - static key = 'default'; - - constructor(el, savedObj) { - this.el = $(el); - this.savedObj = savedObj; - this.vis = savedObj.vis; - - if (!this.vis.type.editorConfig.optionTabs && this.vis.type.editorConfig.optionsTemplate) { - this.vis.type.editorConfig.optionTabs = [ - { - name: 'options', - title: i18n.translate('common.ui.vis.editors.sidebar.tabs.optionsLabel', { - defaultMessage: 'Options', - }), - editor: this.vis.type.editorConfig.optionsTemplate, - }, - ]; - } - } - - render({ uiState, timeRange, filters, query, appState }) { - let $scope; - - const updateScope = () => { - $scope.vis = this.vis; - $scope.uiState = uiState; - //$scope.$apply(); - }; - - return new Promise(async resolve => { - if (!this.$scope) { - this.$scope = $scope = $rootScope.$new(); - - updateScope(); - - $scope.state = $scope.vis.copyCurrentState(true); - $scope.oldState = $scope.vis.getSerializableState($scope.state); - - $scope.toggleSidebar = () => { - $scope.$broadcast('render'); - }; - - this.el.one('renderComplete', resolve); - // track state of editable vis vs. "actual" vis - $scope.stageEditableVis = () => { - $scope.oldState = $scope.vis.getSerializableState($scope.state); - $scope.vis.setCurrentState($scope.state); - $scope.vis.updateState(); - $scope.vis.dirty = false; - }; - $scope.resetEditableVis = () => { - $scope.state = $scope.vis.copyCurrentState(true); - $scope.vis.dirty = false; - }; - - $scope.autoApplyEnabled = false; - if ($scope.vis.type.editorConfig.enableAutoApply) { - $scope.toggleAutoApply = () => { - $scope.autoApplyEnabled = !$scope.autoApplyEnabled; - }; - - $scope.$watch( - 'vis.dirty', - _.debounce(() => { - if (!$scope.autoApplyEnabled || !$scope.vis.dirty) return; - $scope.stageEditableVis(); - }, 800) - ); - } - - $scope.submitEditorWithKeyboard = event => { - if (event.ctrlKey && event.keyCode === keyCodes.ENTER) { - event.preventDefault(); - event.stopPropagation(); - $scope.stageEditableVis(); - } - }; - - $scope.getSidebarClass = () => { - if ($scope.vis.type.editorConfig.defaultSize === DefaultEditorSize.SMALL) { - return 'visEditor__collapsibleSidebar--small'; - } else if ($scope.vis.type.editorConfig.defaultSize === DefaultEditorSize.MEDIUM) { - return 'visEditor__collapsibleSidebar--medium'; - } else if ($scope.vis.type.editorConfig.defaultSize === DefaultEditorSize.LARGE) { - return 'visEditor__collapsibleSidebar--large'; - } - }; - - $scope.$watch( - () => { - return $scope.vis.getSerializableState($scope.state); - }, - function(newState) { - $scope.vis.dirty = !angular.equals(newState, $scope.oldState); - const responseAggs = $scope.state.aggs.getResponseAggs(); - $scope.hasHistogramAgg = responseAggs.some(agg => agg.type.name === 'histogram'); - $scope.metricAggs = responseAggs.filter( - agg => _.get(agg, 'schema.group') === AggGroupNames.Metrics - ); - const lastParentPipelineAgg = _.findLast( - $scope.metricAggs, - ({ type }) => type.subtype === parentPipelineAggHelper.subtype - ); - $scope.lastParentPipelineAggTitle = - lastParentPipelineAgg && lastParentPipelineAgg.type.title; - }, - true - ); - - // fires when visualization state changes, and we need to copy changes to editorState - $scope.$watch( - () => { - return $scope.vis.getCurrentState(false); - }, - newState => { - if (!_.isEqual(newState, $scope.oldState)) { - $scope.state = $scope.vis.copyCurrentState(true); - $scope.oldState = newState; - } - }, - true - ); - - // Load the default editor template, attach it to the DOM and compile it. - // It should be added to the DOM before compiling, to prevent some resize - // listener issues. - const template = $(defaultEditorTemplate); - this.el.html(template); - $compile(template)($scope); - } else { - $scope = this.$scope; - updateScope(); - } - - if (!this._handler) { - const visualizationEl = this.el.find('.visEditor__canvas')[0]; - - this._handler = await embeddables - .getEmbeddableFactory('visualization') - .createFromObject(this.savedObj, { - uiState: uiState, - appState, - timeRange: timeRange, - filters: filters || [], - query: query, - }); - this._handler.render(visualizationEl); - } else { - this._handler.updateInput({ - timeRange: timeRange, - filters: filters || [], - query: query, - }); - } - }); - } - - resize() {} - - destroy() { - if (this.$scope) { - this.$scope.$destroy(); - this.$scope = null; - } - if (this._handler) { - this._handler.destroy(); - } - } - }; -}; - -export { defaultEditor }; diff --git a/src/legacy/ui/public/vis/editors/default/default_editor.tsx b/src/legacy/ui/public/vis/editors/default/default_editor.tsx new file mode 100644 index 0000000000000..3e99bb83d224f --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/default_editor.tsx @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useRef, useState, useCallback } from 'react'; + +import { start as embeddables } from '../../../../../core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { EditorRenderProps } from '../../../../../core_plugins/kibana/public/visualize/np_ready/types'; +import { VisualizeEmbeddable } from '../../../../../core_plugins/visualizations/public/embeddable'; +import { VisualizeEmbeddableFactory } from '../../../../../core_plugins/visualizations/public/embeddable/visualize_embeddable_factory'; +import { + PanelsContainer, + Panel, +} from '../../../../../core_plugins/console/public/np_ready/application/components/split_panel'; + +import './vis_type_agg_filter'; +import { DefaultEditorSideBar } from './components/sidebar'; +import { DefaultEditorControllerState } from './default_editor_controller'; +import { getInitialWidth } from '../../editor_size'; + +function DefaultEditor({ + savedObj, + uiState, + timeRange, + filters, + appState, + optionTabs, + query, +}: DefaultEditorControllerState & EditorRenderProps) { + const visRef = useRef(null); + const visHandler = useRef(null); + const [isCollapsed, setIsCollapsed] = useState(false); + const [factory, setFactory] = useState(null); + const { vis } = savedObj; + + const onClickCollapse = useCallback(() => { + setIsCollapsed(value => !value); + }, []); + + useEffect(() => { + async function visualize() { + if (!visRef.current || (!visHandler.current && factory)) { + return; + } + + if (!visHandler.current) { + const embeddableFactory = embeddables.getEmbeddableFactory( + 'visualization' + ) as VisualizeEmbeddableFactory; + setFactory(embeddableFactory); + + visHandler.current = (await embeddableFactory.createFromObject(savedObj, { + // should be look through createFromObject interface again because of "id" param + id: '', + uiState, + appState, + timeRange, + filters, + query, + })) as VisualizeEmbeddable; + + visHandler.current.render(visRef.current); + } else { + visHandler.current.updateInput({ + timeRange, + filters, + query, + }); + } + } + + visualize(); + }, [uiState, savedObj, timeRange, filters, appState, query, factory]); + + useEffect(() => { + return () => { + if (visHandler.current) { + visHandler.current.destroy(); + } + }; + }, []); + + const editorInitialWidth = getInitialWidth(vis.type.editorConfig.defaultSize); + + return ( + + +
+ + + + + + + ); +} + +export { DefaultEditor }; diff --git a/src/legacy/ui/public/vis/editors/default/default_editor_controller.tsx b/src/legacy/ui/public/vis/editors/default/default_editor_controller.tsx new file mode 100644 index 0000000000000..bf843a98deaa5 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/default_editor_controller.tsx @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; + +import { EditorRenderProps } from '../../../../../core_plugins/kibana/public/visualize/np_ready/types'; +import { VisSavedObject } from '../../../../../core_plugins/visualizations/public/embeddable/visualize_embeddable'; +import { DefaultEditor } from './default_editor'; +import { DefaultEditorDataTab, OptionTab } from './components/sidebar'; + +export interface DefaultEditorControllerState { + savedObj: VisSavedObject; + optionTabs: OptionTab[]; +} + +class DefaultEditorController { + private el: HTMLElement; + private state: DefaultEditorControllerState; + + constructor(el: HTMLElement, savedObj: VisSavedObject) { + this.el = el; + const { type: visType } = savedObj.vis; + + const optionTabs = [ + ...(visType.schemas.buckets || visType.schemas.metrics + ? [ + { + name: 'data', + title: i18n.translate('common.ui.vis.editors.sidebar.tabs.dataLabel', { + defaultMessage: 'Data', + }), + editor: DefaultEditorDataTab, + }, + ] + : []), + + ...(!visType.editorConfig.optionTabs && visType.editorConfig.optionsTemplate + ? [ + { + name: 'options', + title: i18n.translate('common.ui.vis.editors.sidebar.tabs.optionsLabel', { + defaultMessage: 'Options', + }), + editor: visType.editorConfig.optionsTemplate, + }, + ] + : visType.editorConfig.optionTabs), + ]; + + this.state = { + savedObj, + optionTabs, + }; + } + + render(props: EditorRenderProps) { + render( + + + , + this.el + ); + } + + destroy() { + unmountComponentAtNode(this.el); + } +} + +export { DefaultEditorController }; diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js deleted file mode 100644 index e9fda6bb9efab..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/fancy_forms.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -describe('fancy forms', function() { - let $el; - let $scope; - let $compile; - let $rootScope; - let ngForm; - - function generateEl() { - return $('
').html($('')); - } - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - - $scope = $rootScope.$new(); - $el = generateEl(); - - $compile($el)($scope); - $scope.$apply(); - - ngForm = $el.controller('form'); - }) - ); - - describe('ngFormController', function() { - it('counts errors', function() { - expect(ngForm.errorCount()).to.be(1); - }); - - it('clears errors', function() { - $scope.val = 'something'; - $scope.$apply(); - expect(ngForm.errorCount()).to.be(0); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js deleted file mode 100644 index 969fa3bfbd888..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/__tests__/nested_fancy_forms.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import testSubjSelector from '@kbn/test-subj-selector'; -import sinon from 'sinon'; -import $ from 'jquery'; - -const template = ` - - -
    -
  • - - - - -
  • -
- -
-`; - -describe('fancy forms', function() { - let setup; - const trash = []; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject($injector => { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - setup = function(options = {}) { - const { name = 'person1', tasks = [], onSubmit = () => {} } = options; - - const $el = $(template).appendTo('body'); - trash.push(() => $el.remove()); - const $scope = $rootScope.$new(); - - $scope.name = name; - $scope.tasks = tasks; - $scope.onSubmit = onSubmit; - - $compile($el)($scope); - $scope.$apply(); - - return { - $el, - $scope, - }; - }; - }) - ); - - afterEach(() => trash.splice(0).forEach(fn => fn())); - - describe('nested forms', function() { - it('treats new fields as "soft" errors', function() { - const { $scope } = setup({ name: '' }); - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(0); - }); - - it('upgrades fields to regular errors on attempted submit', function() { - const { $scope, $el } = setup({ name: '' }); - - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(0); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(1); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('prevents submit when there are errors', function() { - const onSubmit = sinon.stub(); - const { $scope, $el } = setup({ name: '', onSubmit }); - - expect($scope.person.errorCount()).to.be(1); - sinon.assert.notCalled(onSubmit); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(1); - sinon.assert.notCalled(onSubmit); - - $scope.$apply(() => { - $scope.name = 'foo'; - }); - - expect($scope.person.errorCount()).to.be(0); - sinon.assert.notCalled(onSubmit); - $el.find(testSubjSelector('submit')).click(); - expect($scope.person.errorCount()).to.be(0); - sinon.assert.calledOnce(onSubmit); - }); - - it('new fields are no longer soft after blur', function() { - const { $scope, $el } = setup({ name: '' }); - expect($scope.person.softErrorCount()).to.be(0); - $el.find(testSubjSelector('name')).blur(); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('counts errors/softErrors in sub forms', function() { - const { $scope, $el } = setup(); - - expect($scope.person.errorCount()).to.be(0); - - $scope.$apply(() => { - $scope.tasks = [ - { - name: 'foo', - description: '', - }, - { - name: 'foo', - description: '', - }, - ]; - }); - - expect($scope.person.errorCount()).to.be(2); - expect($scope.person.softErrorCount()).to.be(0); - - $el - .find(testSubjSelector('taskDesc')) - .first() - .blur(); - - expect($scope.person.errorCount()).to.be(2); - expect($scope.person.softErrorCount()).to.be(1); - }); - - it('only counts down', function() { - const { $scope, $el } = setup({ - tasks: [ - { - name: 'foo', - description: '', - }, - { - name: 'bar', - description: '', - }, - { - name: 'baz', - description: '', - }, - ], - }); - - // top level form sees 3 errors - expect($scope.person.errorCount()).to.be(3); - expect($scope.person.softErrorCount()).to.be(0); - - $el - .find('ng-form') - .toArray() - .forEach((el, i) => { - const $task = $(el); - const $taskScope = $task.scope(); - const form = $task.controller('form'); - - // sub forms only see one error - expect(form.errorCount()).to.be(1); - expect(form.softErrorCount()).to.be(0); - - // blurs only count locally - $task.find(testSubjSelector('taskDesc')).blur(); - expect(form.softErrorCount()).to.be(1); - - // but parent form see them - expect($scope.person.softErrorCount()).to.be(1); - - $taskScope.$apply(() => { - $taskScope.task.description = 'valid'; - }); - - expect(form.errorCount()).to.be(0); - expect(form.softErrorCount()).to.be(0); - expect($scope.person.errorCount()).to.be(2 - i); - expect($scope.person.softErrorCount()).to.be(0); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js deleted file mode 100644 index 1f0788cf74d1d..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/fancy_forms.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiModules } from '../../../../modules'; - -import { decorateFormController } from './kbn_form_controller'; -import { decorateModelController } from './kbn_model_controller'; - -uiModules.get('kibana').config(function($provide) { - $provide.decorator('formDirective', decorateFormController); - $provide.decorator('ngFormDirective', decorateFormController); - $provide.decorator('ngModelDirective', decorateModelController); -}); diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js deleted file mode 100644 index 927e6d69e3c8a..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './fancy_forms'; diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js deleted file mode 100644 index 90971140482f7..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_form_controller.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function decorateFormController($delegate, $injector) { - const [directive] = $delegate; - const FormController = directive.controller; - - class KbnFormController extends FormController { - // prevent inheriting FormController's static $inject property - // which is angular's cache of the DI arguments for a function - static $inject = ['$scope', '$element']; - - constructor($scope, $element, ...superArgs) { - super(...superArgs); - - const onSubmit = event => { - this._markInvalidTouched(event); - }; - - $element.on('submit', onSubmit); - $scope.$on('$destroy', () => { - $element.off('submit', onSubmit); - }); - } - - errorCount() { - return this._getInvalidModels().length; - } - - // same as error count, but filters out untouched and pristine models - softErrorCount() { - return this._getInvalidModels().filter(model => model.$touched || model.$dirty).length; - } - - $setTouched() { - this._getInvalidModels().forEach(model => model.$setTouched()); - } - - _markInvalidTouched(event) { - if (this.errorCount()) { - event.preventDefault(); - event.stopImmediatePropagation(); - this.$setTouched(); - } - } - - _getInvalidModels() { - return this.$$controls.reduce((acc, control) => { - // recurse into sub-form - if (typeof control._getInvalidModels === 'function') { - return [...acc, ...control._getInvalidModels()]; - } - - if (control.$invalid) { - return [...acc, control]; - } - - return acc; - }, []); - } - } - - // replace controller with our wrapper - directive.controller = [ - ...$injector.annotate(KbnFormController), - ...$injector.annotate(FormController), - (...args) => new KbnFormController(...args), - ]; - - return $delegate; -} diff --git a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js b/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js deleted file mode 100644 index bb4d026aa1810..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/fancy_forms/kbn_model_controller.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function decorateModelController($delegate, $injector) { - const [directive] = $delegate; - const ModelController = directive.controller; - - class KbnModelController extends ModelController { - // prevent inheriting ModelController's static $inject property - // which is angular's cache of the DI arguments for a function - static $inject = ['$scope', '$element']; - - constructor($scope, $element, ...superArgs) { - super(...superArgs); - - const onInvalid = () => { - this.$setTouched(); - }; - - // the browser emits an "invalid" event when browser supplied - // validation fails, which implies that the user has indirectly - // interacted with the control and it should be treated as "touched" - $element.on('invalid', onInvalid); - $scope.$on('$destroy', () => { - $element.off('invalid', onInvalid); - }); - } - } - - // replace controller with our wrapper - directive.controller = [ - ...$injector.annotate(KbnModelController), - ...$injector.annotate(ModelController), - (...args) => new KbnModelController(...args), - ]; - - return $delegate; -} diff --git a/src/legacy/ui/public/vis/editors/default/index.ts b/src/legacy/ui/public/vis/editors/default/index.ts index 7079ba23afb5c..fada4e5d2266f 100644 --- a/src/legacy/ui/public/vis/editors/default/index.ts +++ b/src/legacy/ui/public/vis/editors/default/index.ts @@ -18,7 +18,7 @@ */ export { AggParamEditorProps } from './components/agg_param_props'; -export { DefaultEditorAggParams, SubAggParamsProp } from './components/agg_params'; +export { DefaultEditorAggParams } from './components/agg_params'; export { ComboBoxGroupedOptions } from './utils'; export * from './vis_options_props'; export * from './utils'; diff --git a/src/legacy/ui/public/vis/editors/default/schemas.ts b/src/legacy/ui/public/vis/editors/default/schemas.ts index e86a73732c3f4..3cacd1cfbe68f 100644 --- a/src/legacy/ui/public/vis/editors/default/schemas.ts +++ b/src/legacy/ui/public/vis/editors/default/schemas.ts @@ -28,6 +28,11 @@ import { RadiusRatioOptionControl } from './controls/radius_ratio_option'; import { AggGroupNames } from './agg_groups'; import { AggControlProps } from './controls/agg_control_props'; +export interface ISchemas { + [AggGroupNames.Buckets]: Schema[]; + [AggGroupNames.Metrics]: Schema[]; +} + export interface Schema { aggFilter: string | string[]; editor: boolean | string; diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.html b/src/legacy/ui/public/vis/editors/default/sidebar.html deleted file mode 100644 index b0a03e461fc1c..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/sidebar.html +++ /dev/null @@ -1,191 +0,0 @@ -
-
- -

- {{ vis.indexPattern.title }} -

- - - -
- - -
- - -
- -
- -
- -
-
diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.js b/src/legacy/ui/public/vis/editors/default/sidebar.js deleted file mode 100644 index 195ae68c0e959..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/sidebar.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import './agg_group'; -import './vis_options'; -import 'ui/directives/css_truncate'; -import { uiModules } from '../../../modules'; -import sidebarTemplate from './sidebar.html'; -import { move } from '../../../utils/collection'; -import { AggGroupNames } from './agg_groups'; -import { getEnabledMetricAggsCount } from './components/agg_group_helper'; - -uiModules.get('app/visualize').directive('visEditorSidebar', function() { - return { - restrict: 'E', - template: sidebarTemplate, - scope: true, - require: '?^ngModel', - controllerAs: 'sidebar', - controller: function($scope) { - $scope.$watch('vis.type', visType => { - if (visType) { - this.showData = visType.schemas.buckets || visType.schemas.metrics; - if (_.has(visType, 'editorConfig.optionTabs')) { - const activeTabs = visType.editorConfig.optionTabs.filter(tab => { - return _.get(tab, 'active', false); - }); - if (activeTabs.length > 0) { - this.section = activeTabs[0].name; - } - } - this.section = - this.section || - (this.showData ? 'data' : _.get(visType, 'editorConfig.optionTabs[0].name')); - } - }); - - $scope.onAggTypeChange = (agg, value) => { - if (agg.type !== value) { - agg.type = value; - } - }; - - $scope.onAggParamsChange = (params, paramName, value) => { - if (params[paramName] !== value) { - params[paramName] = value; - } - }; - - $scope.addSchema = function(schema) { - const aggConfig = $scope.state.aggs.createAggConfig({ schema }); - aggConfig.brandNew = true; - }; - - $scope.removeAgg = function(agg) { - const aggs = $scope.state.aggs.aggs; - const index = aggs.indexOf(agg); - - if (index === -1) { - return; - } - - aggs.splice(index, 1); - - if (agg.schema.group === AggGroupNames.Metrics) { - const metrics = $scope.state.aggs.bySchemaGroup(AggGroupNames.Metrics); - - if (getEnabledMetricAggsCount(metrics) === 0) { - metrics.find(aggregation => aggregation.schema.name === 'metric').enabled = true; - } - } - }; - - $scope.onToggleEnableAgg = (agg, isEnable) => { - agg.enabled = isEnable; - }; - - $scope.reorderAggs = group => { - //the aggs have been reordered in [group] and we need - //to apply that ordering to [vis.aggs] - const indexOffset = $scope.state.aggs.aggs.indexOf(group[0]); - _.forEach(group, (agg, index) => { - move($scope.state.aggs.aggs, agg, indexOffset + index); - }); - }; - }, - }; -}); diff --git a/src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js b/src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js deleted file mode 100644 index 3cbc94a029326..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/vis_editor_resizer.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import { uiModules } from '../../../modules'; -import { keyCodes } from '@elastic/eui'; - -uiModules.get('kibana').directive('visEditorResizer', function() { - return { - restrict: 'E', - link: function($scope, $el) { - const $left = $el.parent(); - - $el.on('mousedown', function(event) { - $el.addClass('active'); - const startWidth = $left.width(); - const startX = event.pageX; - - function onMove(event) { - const newWidth = startWidth + event.pageX - startX; - $left.width(newWidth); - } - - $(document.body) - .on('mousemove', onMove) - .one('mouseup', () => { - $el.removeClass('active'); - $(document.body).off('mousemove', onMove); - $scope.$broadcast('render'); - }); - }); - - $el.on('keydown', event => { - const { keyCode } = event; - - if (keyCode === keyCodes.LEFT || keyCode === keyCodes.RIGHT) { - event.preventDefault(); - const startWidth = $left.width(); - const newWidth = startWidth + (keyCode === keyCodes.LEFT ? -15 : 15); - $left.width(newWidth); - $scope.$broadcast('render'); - } - }); - }, - }; -}); diff --git a/src/legacy/ui/public/vis/editors/default/vis_options.js b/src/legacy/ui/public/vis/editors/default/vis_options.js deleted file mode 100644 index 9c9b0353cee27..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/vis_options.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from '../../../modules'; -import { VisOptionsReactWrapper } from './vis_options_react_wrapper'; -import { safeMakeLabel } from './controls/agg_utils'; - -/** - * This directive sort of "transcludes" in whatever template you pass in via the `editor` attribute. - * This lets you specify a full-screen UI for editing a vis type, instead of using the regular - * sidebar. - */ - -uiModules - .get('app/visualize') - .directive('visOptionsReactWrapper', reactDirective => - reactDirective(wrapInI18nContext(VisOptionsReactWrapper), [ - ['component', { wrapApply: false }], - ['aggs', { watchDepth: 'collection' }], - ['stateParams', { watchDepth: 'collection' }], - ['vis', { watchDepth: 'collection' }], - ['uiState', { watchDepth: 'collection' }], - ['setValue', { watchDepth: 'reference' }], - ['setValidity', { watchDepth: 'reference' }], - ['setVisType', { watchDepth: 'reference' }], - ['setTouched', { watchDepth: 'reference' }], - 'hasHistogramAgg', - 'currentTab', - 'aggsLabels', - ]) - ) - .directive('visEditorVisOptions', function($compile) { - return { - restrict: 'E', - require: '?^ngModel', - scope: { - vis: '=', - visData: '=', - uiState: '=', - editor: '=', - visualizeEditor: '=', - editorState: '=', - onAggParamsChange: '=', - hasHistogramAgg: '=', - currentTab: '=', - }, - link: function($scope, $el, attrs, ngModelCtrl) { - $scope.setValue = (paramName, value) => - $scope.onAggParamsChange($scope.editorState.params, paramName, value); - - $scope.setValidity = isValid => { - ngModelCtrl.$setValidity(`visOptions`, isValid); - }; - - $scope.setTouched = isTouched => { - if (isTouched) { - ngModelCtrl.$setTouched(); - } else { - ngModelCtrl.$setUntouched(); - } - }; - - $scope.setVisType = type => { - $scope.vis.type.type = type; - }; - - // since aggs reference isn't changed when an agg is updated, we need somehow to let React component know about it - $scope.aggsLabels = ''; - - $scope.$watch( - () => { - return $scope.editorState.aggs.aggs - .map(agg => { - return safeMakeLabel(agg); - }) - .join(); - }, - value => { - $scope.aggsLabels = value; - } - ); - - const comp = - typeof $scope.editor === 'string' - ? $scope.editor - : ` - `; - const $editor = $compile(comp)($scope); - $el.append($editor); - }, - }; - }); diff --git a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx index 3f9fa9f5f352f..5b4badc103645 100644 --- a/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx +++ b/src/legacy/ui/public/vis/editors/default/vis_options_props.tsx @@ -23,13 +23,12 @@ import { Vis } from './../..'; export interface VisOptionsProps { aggs: AggConfigs; - aggsLabels: string; hasHistogramAgg: boolean; + isTabSelected: boolean; stateParams: VisParamType; vis: Vis; uiState: PersistedState; setValue(paramName: T, value: VisParamType[T]): void; setValidity(isValid: boolean): void; - setVisType(type: string): void; setTouched(isTouched: boolean): void; } diff --git a/src/legacy/ui/public/vis/vis_types/angular_vis_type.js b/src/legacy/ui/public/vis/vis_types/angular_vis_type.js index 88412c76d0d36..c34294d45548c 100644 --- a/src/legacy/ui/public/vis/vis_types/angular_vis_type.js +++ b/src/legacy/ui/public/vis/vis_types/angular_vis_type.js @@ -18,6 +18,7 @@ */ import $ from 'jquery'; +import { isEqual } from 'lodash'; import chrome from 'ui/chrome'; export class AngularVisController { @@ -37,6 +38,11 @@ export class AngularVisController { this.$scope.vis = this.vis; this.$scope.visState = this.vis.getState(); this.$scope.esResponse = esResponse; + + if (!isEqual(this.$scope.visParams, visParams)) { + this.vis.emit('updateEditorStateParams', visParams); + } + this.$scope.visParams = visParams; this.$scope.renderComplete = resolve; this.$scope.renderFailed = reject; diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index 84f955d9c7879..d989f8e2539a0 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -37,6 +37,7 @@ export default function({ getService, getPageObjects }) { it('should update table header when columns change', async function() { await inspector.open(); await inspector.expectTableHeaders(['Count']); + await inspector.close(); log.debug('Add Average Metric on machine.ram field'); await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); @@ -45,6 +46,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickGo(); await inspector.open(); await inspector.expectTableHeaders(['Count', 'Average machine.ram']); + await inspector.close(); }); describe('filtering on inspector table values', function() { diff --git a/test/functional/apps/visualize/_markdown_vis.js b/test/functional/apps/visualize/_markdown_vis.js index 51c03c90f507b..fee6c074af5d2 100644 --- a/test/functional/apps/visualize/_markdown_vis.js +++ b/test/functional/apps/visualize/_markdown_vis.js @@ -63,7 +63,7 @@ export default function({ getPageObjects, getService }) { }); it('should resize the editor', async function() { - const editorSidebar = await find.byCssSelector('.visEditor__sidebar'); + const editorSidebar = await find.byCssSelector('.visEditor__collapsibleSidebar'); const initialSize = await editorSidebar.getSize(); await PageObjects.visEditor.sizeUpEditor(); const afterSize = await editorSidebar.getSize(); diff --git a/test/functional/apps/visualize/_point_series_options.js b/test/functional/apps/visualize/_point_series_options.js index e7ce5808554b4..d0f7810b6f8bb 100644 --- a/test/functional/apps/visualize/_point_series_options.js +++ b/test/functional/apps/visualize/_point_series_options.js @@ -57,7 +57,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.selectField('machine.ram', 'metrics'); // go to options page log.debug('Going to axis options'); - await pointSeriesVis.clickAxisOptions(); + await PageObjects.visEditor.clickMetricsAndAxes(); // add another value axis log.debug('adding axis'); await pointSeriesVis.clickAddAxis(); diff --git a/test/functional/apps/visualize/_region_map.js b/test/functional/apps/visualize/_region_map.js index 10cbd9913c70c..2467a54061643 100644 --- a/test/functional/apps/visualize/_region_map.js +++ b/test/functional/apps/visualize/_region_map.js @@ -57,6 +57,7 @@ export default function({ getService, getPageObjects }) { ]; await inspector.open(); await inspector.expectTableData(expectedData); + await inspector.close(); }); it('should change results after changing layer to world', async function() { @@ -94,6 +95,8 @@ export default function({ getService, getPageObjects }) { ['BR', '415'], ]; expect(actualData).to.eql(expectedData); + + await inspector.close(); }); it('should contain a dropdown with the default road_map base layer as an option', async () => { diff --git a/test/functional/apps/visualize/_tag_cloud.js b/test/functional/apps/visualize/_tag_cloud.js index 4f921cec1fdf1..a527e9bcad42f 100644 --- a/test/functional/apps/visualize/_tag_cloud.js +++ b/test/functional/apps/visualize/_tag_cloud.js @@ -77,12 +77,12 @@ export default function({ getService, getPageObjects }) { }); it('should collapse the sidebar', async function() { - const editorSidebar = await find.byCssSelector('.collapsible-sidebar'); + const editorSidebar = await find.byCssSelector('.visEditorSidebar'); await PageObjects.visEditor.clickEditorSidebarCollapse(); // Give d3 tag cloud some time to rearrange tags await PageObjects.common.sleep(1000); - const afterSize = await editorSidebar.getSize(); - expect(afterSize.width).to.be(0); + const isDisplayed = await editorSidebar.isDisplayed(); + expect(isDisplayed).to.be(false); await PageObjects.visEditor.clickEditorSidebarCollapse(); }); diff --git a/test/functional/page_objects/point_series_page.js b/test/functional/page_objects/point_series_page.js index 74bf07b59bc38..594facb8b74b5 100644 --- a/test/functional/page_objects/point_series_page.js +++ b/test/functional/page_objects/point_series_page.js @@ -23,10 +23,6 @@ export function PointSeriesPageProvider({ getService }) { const find = getService('find'); class PointSeriesVis { - async clickAxisOptions() { - return await testSubjects.click('visEditorTabadvanced'); - } - async clickAddAxis() { return await testSubjects.click('visualizeAddYAxisButton'); } diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index 30e13d551fa28..1e098e86216e3 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -37,19 +37,19 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP class VisualizeEditorPage { public async clickDataTab() { - await testSubjects.click('visualizeEditDataLink'); + await testSubjects.click('visEditorTab__data'); } public async clickOptionsTab() { - await testSubjects.click('visEditorTaboptions'); + await testSubjects.click('visEditorTab__options'); } public async clickMetricsAndAxes() { - await testSubjects.click('visEditorTabadvanced'); + await testSubjects.click('visEditorTab__advanced'); } public async clickVisEditorTab(tabName: string) { - await testSubjects.click('visEditorTab' + tabName); + await testSubjects.click(`visEditorTab__${tabName}`); await header.waitUntilLoadingHasFinished(); } @@ -134,7 +134,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP public async getBucketErrorMessage() { const error = await find.byCssSelector( - '[group-name="buckets"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText' + '[data-test-subj="bucketsAggGroup"] [data-test-subj="defaultEditorAggSelect"] + .euiFormErrorText' ); const errorMessage = await error.getAttribute('innerText'); log.debug(errorMessage); @@ -152,7 +152,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP ) { log.debug(`selectField ${fieldValue}`); const selector = ` - [group-name="${groupName}"] + [data-test-subj="${groupName}AggGroup"] [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen [data-test-subj="visAggEditorParams"] ${childAggregationType ? '.visEditorAgg__subAgg' : ''} @@ -180,7 +180,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP childAggregationType = false ) { const comboBoxElement = await find.byCssSelector(` - [group-name="${groupName}"] + [data-test-subj="${groupName}AggGroup"] [data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen ${childAggregationType ? '.visEditorAgg__subAgg' : ''} [data-test-subj="defaultEditorAggSelect"] @@ -291,8 +291,9 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async sizeUpEditor() { - await testSubjects.click('visualizeEditorResizer'); - await browser.pressKeys(browser.keys.ARROW_RIGHT); + const resizerPanel = await testSubjects.find('splitPanelResizer'); + // Drag panel 100 px left + await browser.dragAndDrop({ location: resizerPanel }, { location: { x: -100, y: 0 } }); } public async toggleDisabledAgg(agg: string) { @@ -320,7 +321,10 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async toggleAutoMode() { - await testSubjects.click('visualizeEditorAutoButton'); + // this is a temporary solution, should be replaced with initial after fixing the EuiToggleButton + // passing the data-test-subj attribute to a checkbox + await find.clickByCssSelector('.visEditorSidebar__controls input[type="checkbox"]'); + // await testSubjects.click('visualizeEditorAutoButton'); } public async isApplyEnabled() { @@ -428,7 +432,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP } public async clickMetricEditor() { - await find.clickByCssSelector('[group-name="metrics"] .euiAccordion__button'); + await find.clickByCssSelector('[data-test-subj="metricsAggGroup"] .euiAccordion__button'); } public async clickMetricByIndex(index: number) { diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 0071b8d993f70..e54e3d1d01154 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -200,7 +200,7 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide } public async getSideEditorExists() { - return await find.existsByCssSelector('.collapsible-sidebar'); + return await find.existsByCssSelector('.visEditor__collapsibleSidebar'); } public async clickSavedSearch(savedSearchName: string) { diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 6a689e85de214..2d799b7daca73 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -240,8 +240,8 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { * @return {Promise} */ public async dragAndDrop( - from: { offset: { x: any; y: any }; location: any }, - to: { offset: { x: any; y: any }; location: any } + from: { offset?: { x: any; y: any }; location: any }, + to: { offset?: { x: any; y: any }; location: any } ) { if (this.isW3CEnabled) { // The offset should be specified in pixels relative to the center of the element's bounding box diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css index ab88e4780936e..2c203e507260f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/preserve_layout.css @@ -53,9 +53,10 @@ discover-app .discover-table-footer { * Visualize Editor Tweaks */ -/* hide unusable controls */ -visualization-editor .visEditor--default > :not(.visEditor__canvas) { - display: none; +/* hide unusable controls +* !important is required to override resizable panel inline display */ +visualization-editor .visEditor--default > :not(.visEditor__visualization) { + display: none !important; } /** THIS IS FOR TSVB UNTIL REFACTOR **/ diff --git a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css index 8aca042144b3b..b5c9861208b7b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css +++ b/x-pack/legacy/plugins/reporting/export_types/common/layouts/print.css @@ -52,9 +52,10 @@ discover-app .discover-table-footer { * Visualize Editor Tweaks */ -/* hide unusable controls */ -visualization-editor .visEditor--default > :not(.visEditor__canvas) { - display: none; +/* hide unusable controls +* !important is required to override resizable panel inline display */ +visualization-editor .visEditor--default > :not(.visEditor__visualization) { + display: none !important; } /** THIS IS FOR TSVB UNTIL REFACTOR **/ .tvbEditorVisualization { diff --git a/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js b/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js index 590f3dc85740e..897caa07fd873 100644 --- a/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js +++ b/x-pack/legacy/plugins/rollup/public/visualize/editor_config.js @@ -9,7 +9,7 @@ import { editorConfigProviders } from 'ui/vis/editors/config/editor_config_provi export function initEditorConfig() { // Limit agg params based on rollup capabilities - editorConfigProviders.register((aggType, indexPattern, aggConfig) => { + editorConfigProviders.register((indexPattern, aggConfig) => { if (indexPattern.type !== 'rollup') { return {}; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8402092695590..c8a58d90595c4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -503,15 +503,7 @@ "common.ui.vis.editors.aggGroups.bucketsText": "バケット", "common.ui.vis.editors.aggGroups.metricsText": "メトリック", "common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage": "「{schema}」集約は他のバケットの前に実行する必要があります!", - "common.ui.vis.editors.resizeAriaLabels": "左右のキーでエディターのサイズを変更します", - "common.ui.vis.editors.sidebar.applyChangesAriaLabel": "ビジュアライゼーションを変更と共に更新します", - "common.ui.vis.editors.sidebar.applyChangesTooltip": "変更を適用", "common.ui.vis.editors.sidebar.autoApplyChangesAriaLabel": "変更されるごとにビジュアライゼーションを自動的に更新します", - "common.ui.vis.editors.sidebar.autoApplyChangesLabel": "自動適用", - "common.ui.vis.editors.sidebar.autoApplyChangesTooltip": "変更を自動適用", - "common.ui.vis.editors.sidebar.discardChangesAriaLabel": "ビジュアライゼーションをリセット", - "common.ui.vis.editors.sidebar.discardChangesTooltip": "変更を破棄", - "common.ui.vis.editors.sidebar.errorButtonAriaLabel": "ハイライトされたフィールドのエラーを解決する必要があります。", "common.ui.vis.editors.sidebar.errorButtonTooltip": "ハイライトされたフィールドのエラーを解決する必要があります。", "common.ui.vis.editors.sidebar.tabs.dataLabel": "データ", "common.ui.vis.editors.sidebar.tabs.optionsLabel": "オプション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f3af5ec10338c..583495972c4c1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -503,15 +503,7 @@ "common.ui.vis.editors.aggGroups.bucketsText": "存储桶", "common.ui.vis.editors.aggGroups.metricsText": "指标", "common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage": "“{schema}” 聚合必须在所有其他存储桶之前运行!", - "common.ui.vis.editors.resizeAriaLabels": "按向左/向右键以调整编辑器的大小", - "common.ui.vis.editors.sidebar.applyChangesAriaLabel": "使用您的更改更新可视化", - "common.ui.vis.editors.sidebar.applyChangesTooltip": "应用更改", "common.ui.vis.editors.sidebar.autoApplyChangesAriaLabel": "每次更改时自动更新可视化", - "common.ui.vis.editors.sidebar.autoApplyChangesLabel": "自动应用", - "common.ui.vis.editors.sidebar.autoApplyChangesTooltip": "自动应用更改", - "common.ui.vis.editors.sidebar.discardChangesAriaLabel": "重置可视化", - "common.ui.vis.editors.sidebar.discardChangesTooltip": "放弃更改", - "common.ui.vis.editors.sidebar.errorButtonAriaLabel": "需要解决突出显示的字段中的错误。", "common.ui.vis.editors.sidebar.errorButtonTooltip": "需要解决突出显示的字段中的错误。", "common.ui.vis.editors.sidebar.tabs.dataLabel": "数据", "common.ui.vis.editors.sidebar.tabs.optionsLabel": "选项",