[Dashboard][Controls] Add Control Group Search Settings (#128090)

* Added ability to toggle hierarchical chaining, control validation, and query bar sync to the Control Group
This commit is contained in:
Devon Thomson 2022-03-23 18:28:39 -04:00 committed by GitHub
parent 0b4282e1f5
commit 82d4cd56dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1397 additions and 834 deletions

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ControlGroupInput } from '..';
import { ControlStyle, ControlWidth } from '../types';
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine';
export const getDefaultControlGroupInput = (): Omit<ControlGroupInput, 'id'> => ({
panels: {},
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
controlStyle: DEFAULT_CONTROL_STYLE,
chainingSystem: 'HIERARCHICAL',
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
ignoreValidations: false,
},
});

View file

@ -17,11 +17,14 @@ export interface ControlPanelState<TEmbeddableInput extends ControlInput = Contr
width: ControlWidth;
}
export type ControlGroupChainingSystem = 'HIERARCHICAL' | 'NONE';
export interface ControlsPanels {
[panelId: string]: ControlPanelState;
}
export interface ControlGroupInput extends EmbeddableInput, ControlInput {
chainingSystem: ControlGroupChainingSystem;
defaultControlWidth?: ControlWidth;
controlStyle: ControlStyle;
panels: ControlsPanels;

View file

@ -12,3 +12,4 @@ export type { ControlWidth } from './types';
export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types';
export { CONTROL_GROUP_TYPE } from './control_group/types';
export { getDefaultControlGroupInput } from './control_group/control_group_constants';

View file

@ -89,6 +89,7 @@ const ControlGroupStoryComponent: FC<{
);
const controlGroupContainerEmbeddable = await factory.create({
controlStyle: 'oneLine',
chainingSystem: 'NONE', // a chaining system doesn't make sense in storybook since the controls aren't backed by elasticsearch
panels: panels ?? {},
id: uuid.v4(),
viewMode,

View file

@ -46,7 +46,7 @@ export const ControlGroupStrings = {
}),
getTitleInputTitle: () =>
i18n.translate('controls.controlGroup.manageControl.titleInputTitle', {
defaultMessage: 'Title',
defaultMessage: 'Label',
}),
getControlTypeTitle: () =>
i18n.translate('controls.controlGroup.manageControl.controlTypesTitle', {
@ -82,10 +82,6 @@ export const ControlGroupStrings = {
i18n.translate('controls.controlGroup.management.defaultWidthTitle', {
defaultMessage: 'Default size',
}),
getLayoutTitle: () =>
i18n.translate('controls.controlGroup.management.layoutTitle', {
defaultMessage: 'Layout',
}),
getDeleteButtonTitle: () =>
i18n.translate('controls.controlGroup.management.delete', {
defaultMessage: 'Delete control',
@ -120,18 +116,22 @@ export const ControlGroupStrings = {
defaultMessage: 'Large',
}),
},
controlStyle: {
getDesignSwitchLegend: () =>
i18n.translate('controls.controlGroup.management.layout.designSwitchLegend', {
defaultMessage: 'Switch control designs',
labelPosition: {
getLabelPositionTitle: () =>
i18n.translate('controls.controlGroup.management.labelPosition.title', {
defaultMessage: 'Label position',
}),
getSingleLineTitle: () =>
i18n.translate('controls.controlGroup.management.layout.singleLine', {
defaultMessage: 'Single line',
getLabelPositionLegend: () =>
i18n.translate('controls.controlGroup.management.labelPosition.designSwitchLegend', {
defaultMessage: 'Switch label position between inline and above',
}),
getTwoLineTitle: () =>
i18n.translate('controls.controlGroup.management.layout.twoLine', {
defaultMessage: 'Double line',
getInlineTitle: () =>
i18n.translate('controls.controlGroup.management.labelPosition.inline', {
defaultMessage: 'Inline',
}),
getAboveTitle: () =>
i18n.translate('controls.controlGroup.management.labelPosition.above', {
defaultMessage: 'Above',
}),
},
deleteControls: {
@ -192,6 +192,55 @@ export const ControlGroupStrings = {
defaultMessage: 'Cancel',
}),
},
validateSelections: {
getValidateSelectionsTitle: () =>
i18n.translate('controls.controlGroup.management.validate.title', {
defaultMessage: 'Validate user selections',
}),
getValidateSelectionsSubTitle: () =>
i18n.translate('controls.controlGroup.management.validate.subtitle', {
defaultMessage:
'Automatically ignore any control selection that would result in no data.',
}),
},
controlChaining: {
getHierarchyTitle: () =>
i18n.translate('controls.controlGroup.management.hierarchy.title', {
defaultMessage: 'Chain controls',
}),
getHierarchySubTitle: () =>
i18n.translate('controls.controlGroup.management.hierarchy.subtitle', {
defaultMessage:
'Selections in one control narrow down available options in the next. Controls are chained from left to right.',
}),
},
querySync: {
getQuerySettingsTitle: () =>
i18n.translate('controls.controlGroup.management.query.searchSettingsTitle', {
defaultMessage: 'Sync with query bar',
}),
getQuerySettingsSubtitle: () =>
i18n.translate('controls.controlGroup.management.query.useAllSearchSettingsTitle', {
defaultMessage:
'Keeps the control group in sync with the query bar by applying time range, filter pills, and queries from the query bar',
}),
getAdvancedSettingsTitle: () =>
i18n.translate('controls.controlGroup.management.query.advancedSettings', {
defaultMessage: 'Advanced',
}),
getIgnoreTimerangeTitle: () =>
i18n.translate('controls.controlGroup.management.query.ignoreTimerange', {
defaultMessage: 'Ignore timerange',
}),
getIgnoreQueryTitle: () =>
i18n.translate('controls.controlGroup.management.query.ignoreQuery', {
defaultMessage: 'Ignore query bar',
}),
getIgnoreFilterPillsTitle: () =>
i18n.translate('controls.controlGroup.management.query.ignoreFilterPills', {
defaultMessage: 'Ignore filter pills',
}),
},
},
floatingActions: {
getEditButtonTitle: () =>

View file

@ -14,7 +14,9 @@
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { omit } from 'lodash';
import fastIsEqual from 'fast-deep-equal';
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiFlyoutHeader,
EuiButtonGroup,
@ -28,38 +30,101 @@ import {
EuiButtonEmpty,
EuiSpacer,
EuiCheckbox,
EuiForm,
EuiAccordion,
useGeneratedHtmlId,
EuiSwitch,
EuiText,
EuiHorizontalRule,
} from '@elastic/eui';
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from './editor_constants';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlStyle, ControlWidth } from '../../types';
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from './editor_constants';
import { ParentIgnoreSettings } from '../..';
import { ControlsPanels } from '../types';
import { ControlGroupInput } from '..';
import {
DEFAULT_CONTROL_WIDTH,
getDefaultControlGroupInput,
} from '../../../common/control_group/control_group_constants';
interface EditControlGroupProps {
width: ControlWidth;
controlStyle: ControlStyle;
setAllWidths: boolean;
initialInput: ControlGroupInput;
controlCount: number;
updateControlStyle: (controlStyle: ControlStyle) => void;
updateWidth: (newWidth: ControlWidth) => void;
updateAllControlWidths: (newWidth: ControlWidth) => void;
onCancel: () => void;
updateInput: (input: Partial<ControlGroupInput>) => void;
onDeleteAll: () => void;
onClose: () => void;
}
type EditorControlGroupInput = ControlGroupInput &
Required<Pick<ControlGroupInput, 'defaultControlWidth'>>;
const editorControlGroupInputIsEqual = (a: ControlGroupInput, b: ControlGroupInput) =>
fastIsEqual(a, b);
export const ControlGroupEditor = ({
width,
controlStyle,
setAllWidths,
controlCount,
updateControlStyle,
updateWidth,
updateAllControlWidths,
onCancel,
initialInput,
updateInput,
onDeleteAll,
onClose,
}: EditControlGroupProps) => {
const [currentControlStyle, setCurrentControlStyle] = useState(controlStyle);
const [currentWidth, setCurrentWidth] = useState(width);
const [applyToAll, setApplyToAll] = useState(setAllWidths);
const [resetAllWidths, setResetAllWidths] = useState(false);
const advancedSettingsAccordionId = useGeneratedHtmlId({ prefix: 'advancedSettingsAccordion' });
const [controlGroupEditorState, setControlGroupEditorState] = useState<EditorControlGroupInput>({
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
...getDefaultControlGroupInput(),
...initialInput,
});
const updateControlGroupEditorSetting = useCallback(
(newSettings: Partial<ControlGroupInput>) => {
setControlGroupEditorState({
...controlGroupEditorState,
...newSettings,
});
},
[controlGroupEditorState]
);
const updateIgnoreSetting = useCallback(
(newSettings: Partial<ParentIgnoreSettings>) => {
setControlGroupEditorState({
...controlGroupEditorState,
ignoreParentSettings: {
...(controlGroupEditorState.ignoreParentSettings ?? {}),
...newSettings,
},
});
},
[controlGroupEditorState]
);
const fullQuerySyncActive = useMemo(
() =>
!Object.values(omit(controlGroupEditorState.ignoreParentSettings, 'ignoreValidations')).some(
Boolean
),
[controlGroupEditorState]
);
const applyChangesToInput = useCallback(() => {
const inputToApply = { ...controlGroupEditorState };
if (resetAllWidths) {
const newPanels = {} as ControlsPanels;
Object.entries(initialInput.panels).forEach(
([id, panel]) =>
(newPanels[id] = {
...panel,
width: inputToApply.defaultControlWidth,
})
);
inputToApply.panels = newPanels;
}
if (!editorControlGroupInputIsEqual(inputToApply, initialInput)) updateInput(inputToApply);
}, [controlGroupEditorState, resetAllWidths, initialInput, updateInput]);
return (
<>
@ -69,57 +134,183 @@ export const ControlGroupEditor = ({
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj="control-group-settings-flyout">
<EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}>
<EuiButtonGroup
color="primary"
idSelected={currentControlStyle}
legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()}
data-test-subj="control-group-layout-options"
options={CONTROL_LAYOUT_OPTIONS}
onChange={(newControlStyle: string) => {
setCurrentControlStyle(newControlStyle as ControlStyle);
}}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
<EuiButtonGroup
color="primary"
idSelected={currentWidth}
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
data-test-subj="control-group-default-size-options"
options={CONTROL_WIDTH_OPTIONS}
onChange={(newWidth: string) => {
setCurrentWidth(newWidth as ControlWidth);
}}
/>
</EuiFormRow>
{controlCount > 0 ? (
<>
<EuiSpacer size="s" />
<EuiCheckbox
id="editControls_setAllSizesCheckbox"
data-test-subj="set-all-control-sizes-checkbox"
label={ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
checked={applyToAll}
onChange={(e) => {
setApplyToAll(e.target.checked);
<EuiForm>
<EuiFormRow label={ControlGroupStrings.management.labelPosition.getLabelPositionTitle()}>
<EuiButtonGroup
color="primary"
options={CONTROL_LAYOUT_OPTIONS}
data-test-subj="control-group-layout-options"
idSelected={controlGroupEditorState.controlStyle}
legend={ControlGroupStrings.management.labelPosition.getLabelPositionLegend()}
onChange={(newControlStyle: string) => {
// The UI copy calls this setting labelPosition, but to avoid an unnecessary migration it will be left as controlStyle in the state.
updateControlGroupEditorSetting({ controlStyle: newControlStyle as ControlStyle });
}}
/>
<EuiSpacer size="l" />
<EuiButtonEmpty
onClick={onCancel}
aria-label={'delete-all'}
data-test-subj="delete-all-controls-button"
iconType="trash"
color="danger"
flush="left"
size="s"
>
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
</EuiButtonEmpty>
</>
) : null}
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
<>
<EuiButtonGroup
color="primary"
data-test-subj="control-group-default-size-options"
idSelected={controlGroupEditorState.defaultControlWidth}
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
options={CONTROL_WIDTH_OPTIONS}
onChange={(newWidth: string) => {
updateControlGroupEditorSetting({
defaultControlWidth: newWidth as ControlWidth,
});
}}
/>
{controlCount > 0 && (
<>
<EuiSpacer size="s" />
<EuiCheckbox
id="editControls_setAllSizesCheckbox"
data-test-subj="set-all-control-sizes-checkbox"
label={ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
checked={resetAllWidths}
onChange={(e) => {
setResetAllWidths(e.target.checked);
}}
/>
</>
)}
</>
</EuiFormRow>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiSwitch
label={ControlGroupStrings.management.querySync.getQuerySettingsTitle()}
data-test-subj="control-group-query-sync"
showLabel={false}
checked={fullQuerySyncActive}
onChange={(e) => {
const newSetting = !e.target.checked;
updateIgnoreSetting({
ignoreFilters: newSetting,
ignoreTimerange: newSetting,
ignoreQuery: newSetting,
});
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h3>{ControlGroupStrings.management.querySync.getQuerySettingsTitle()}</h3>
</EuiTitle>
<EuiText size="s">
<p>{ControlGroupStrings.management.querySync.getQuerySettingsSubtitle()}</p>
</EuiText>
<EuiSpacer size="s" />
<EuiAccordion
data-test-subj="control-group-query-sync-advanced"
id={advancedSettingsAccordionId}
initialIsOpen={!fullQuerySyncActive}
buttonContent={ControlGroupStrings.management.querySync.getAdvancedSettingsTitle()}
>
<EuiSpacer size="s" />
<EuiFormRow hasChildLabel display="columnCompressedSwitch">
<EuiSwitch
data-test-subj="control-group-query-sync-time-range"
label={ControlGroupStrings.management.querySync.getIgnoreTimerangeTitle()}
compressed
checked={Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreTimerange)}
onChange={(e) => updateIgnoreSetting({ ignoreTimerange: e.target.checked })}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel display="columnCompressedSwitch">
<EuiSwitch
data-test-subj="control-group-query-sync-query"
label={ControlGroupStrings.management.querySync.getIgnoreQueryTitle()}
compressed
checked={Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreQuery)}
onChange={(e) => updateIgnoreSetting({ ignoreQuery: e.target.checked })}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel display="columnCompressedSwitch">
<EuiSwitch
data-test-subj="control-group-query-sync-filters"
label={ControlGroupStrings.management.querySync.getIgnoreFilterPillsTitle()}
compressed
checked={Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreFilters)}
onChange={(e) => updateIgnoreSetting({ ignoreFilters: e.target.checked })}
/>
</EuiFormRow>
</EuiAccordion>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiSwitch
data-test-subj="control-group-validate-selections"
label={ControlGroupStrings.management.validateSelections.getValidateSelectionsTitle()}
showLabel={false}
checked={!Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreValidations)}
onChange={(e) => updateIgnoreSetting({ ignoreValidations: !e.target.checked })}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h3>
{ControlGroupStrings.management.validateSelections.getValidateSelectionsTitle()}
</h3>
</EuiTitle>
<EuiText size="s">
<p>
{ControlGroupStrings.management.validateSelections.getValidateSelectionsSubTitle()}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiSwitch
data-test-subj="control-group-chaining"
label={ControlGroupStrings.management.controlChaining.getHierarchyTitle()}
showLabel={false}
checked={controlGroupEditorState.chainingSystem === 'HIERARCHICAL'}
onChange={(e) =>
updateControlGroupEditorSetting({
chainingSystem: e.target.checked ? 'HIERARCHICAL' : 'NONE',
})
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h3>{ControlGroupStrings.management.controlChaining.getHierarchyTitle()}</h3>
</EuiTitle>
<EuiText size="s">
<p>{ControlGroupStrings.management.controlChaining.getHierarchySubTitle()}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
{controlCount > 0 && (
<>
<EuiHorizontalRule margin="m" />
<EuiSpacer size="m" />
<EuiButtonEmpty
onClick={onDeleteAll}
data-test-subj="delete-all-controls-button"
aria-label={'delete-all'}
iconType="trash"
color="danger"
flush="left"
size="s"
>
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
</EuiButtonEmpty>
</>
)}
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup responsive={false} justifyContent="spaceBetween">
@ -141,15 +332,7 @@ export const ControlGroupEditor = ({
color="primary"
data-test-subj="control-group-editor-save"
onClick={() => {
if (currentControlStyle && currentControlStyle !== controlStyle) {
updateControlStyle(currentControlStyle);
}
if (currentWidth && currentWidth !== width) {
updateWidth(currentWidth);
}
if (applyToAll) {
updateAllControlWidths(currentWidth);
}
applyChangesToInput();
onClose();
}}
>

View file

@ -12,10 +12,10 @@ import React from 'react';
import { pluginServices } from '../../services';
import { ControlEditor } from './control_editor';
import { OverlayRef } from '../../../../../core/public';
import { DEFAULT_CONTROL_WIDTH } from './editor_constants';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlWidth, ControlInput, IEditableControlFactory } from '../../types';
import { toMountPoint } from '../../../../kibana_react/public';
import { DEFAULT_CONTROL_WIDTH } from '../../../common/control_group/control_group_constants';
export type CreateControlButtonTypes = 'toolbar' | 'callout';
export interface CreateControlButtonProps {

View file

@ -9,34 +9,20 @@
import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { DEFAULT_CONTROL_WIDTH, DEFAULT_CONTROL_STYLE } from './editor_constants';
import { ControlsPanels } from '../types';
import { pluginServices } from '../../services';
import { ControlStyle, ControlWidth } from '../../types';
import { ControlGroupStrings } from '../control_group_strings';
import { toMountPoint } from '../../../../kibana_react/public';
import { OverlayRef } from '../../../../../core/public';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlGroupEditor } from './control_group_editor';
import { OverlayRef } from '../../../../../core/public';
import { pluginServices } from '../../services';
import { ControlGroupContainer } from '..';
export interface EditControlGroupButtonProps {
controlStyle: ControlStyle;
panels?: ControlsPanels;
defaultControlWidth?: ControlWidth;
setControlStyle: (setControlStyle: ControlStyle) => void;
setDefaultControlWidth: (defaultControlWidth: ControlWidth) => void;
setAllControlWidths: (defaultControlWidth: ControlWidth) => void;
removeEmbeddable?: (panelId: string) => void;
controlGroupContainer: ControlGroupContainer;
closePopover: () => void;
}
export const EditControlGroup = ({
panels,
defaultControlWidth,
controlStyle,
setControlStyle,
setDefaultControlWidth,
setAllControlWidths,
removeEmbeddable,
controlGroupContainer,
closePopover,
}: EditControlGroupButtonProps) => {
const { overlays } = pluginServices.getServices();
@ -45,15 +31,17 @@ export const EditControlGroup = ({
const editControlGroup = () => {
const PresentationUtilProvider = pluginServices.getContextProvider();
const onCancel = (ref: OverlayRef) => {
if (!removeEmbeddable || !panels) return;
const onDeleteAll = (ref: OverlayRef) => {
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
title: ControlGroupStrings.management.deleteControls.getDeleteAllTitle(),
buttonColor: 'danger',
}).then((confirmed) => {
if (confirmed) Object.keys(panels).forEach((panelId) => removeEmbeddable(panelId));
if (confirmed)
Object.keys(controlGroupContainer.getInput().panels).forEach((panelId) =>
controlGroupContainer.removeEmbeddable(panelId)
);
ref.close();
});
};
@ -62,14 +50,10 @@ export const EditControlGroup = ({
toMountPoint(
<PresentationUtilProvider>
<ControlGroupEditor
width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
controlStyle={controlStyle ?? DEFAULT_CONTROL_STYLE}
setAllWidths={false}
controlCount={Object.keys(panels ?? {}).length}
updateControlStyle={setControlStyle}
updateWidth={setDefaultControlWidth}
updateAllControlWidths={setAllControlWidths}
onCancel={() => onCancel(flyoutInstance)}
initialInput={controlGroupContainer.getInput()}
updateInput={(changes) => controlGroupContainer.updateInput(changes)}
controlCount={Object.keys(controlGroupContainer.getInput().panels ?? {}).length}
onDeleteAll={() => onDeleteAll(flyoutInstance)}
onClose={() => flyoutInstance.close()}
/>
</PresentationUtilProvider>

View file

@ -6,12 +6,8 @@
* Side Public License, v 1.
*/
import { ControlStyle, ControlWidth } from '../../types';
import { ControlGroupStrings } from '../control_group_strings';
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine';
export const CONTROL_WIDTH_OPTIONS = [
{
id: `auto`,
@ -39,11 +35,11 @@ export const CONTROL_LAYOUT_OPTIONS = [
{
id: `oneLine`,
'data-test-subj': 'control-editor-layout-oneLine',
label: ControlGroupStrings.management.controlStyle.getSingleLineTitle(),
label: ControlGroupStrings.management.labelPosition.getInlineTitle(),
},
{
id: `twoLine`,
'data-test-subj': 'control-editor-layout-twoLine',
label: ControlGroupStrings.management.controlStyle.getTwoLineTitle(),
label: ControlGroupStrings.management.labelPosition.getAboveTitle(),
},
];

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Filter } from '@kbn/es-query';
import { Subject } from 'rxjs';
import { ControlEmbeddable } from '../../types';
import { ChildEmbeddableOrderCache } from './control_group_container';
import { EmbeddableContainerSettings, isErrorEmbeddable } from '../../../../embeddable/public';
import { ControlGroupChainingSystem, ControlGroupInput } from '../../../common/control_group/types';
interface GetPrecedingFiltersProps {
id: string;
childOrder: ChildEmbeddableOrderCache;
getChild: (id: string) => ControlEmbeddable;
}
interface OnChildChangedProps {
childOutputChangedId: string;
recalculateFilters$: Subject<null>;
childOrder: ChildEmbeddableOrderCache;
getChild: (id: string) => ControlEmbeddable;
}
interface ChainingSystem {
getContainerSettings: (
initialInput: ControlGroupInput
) => EmbeddableContainerSettings | undefined;
getPrecedingFilters: (props: GetPrecedingFiltersProps) => Filter[] | undefined;
onChildChange: (props: OnChildChangedProps) => void;
}
export const ControlGroupChainingSystems: {
[key in ControlGroupChainingSystem]: ChainingSystem;
} = {
HIERARCHICAL: {
getContainerSettings: (initialInput) => ({
childIdInitializeOrder: Object.values(initialInput.panels)
.sort((a, b) => (a.order > b.order ? 1 : -1))
.map((panel) => panel.explicitInput.id),
initializeSequentially: true,
}),
getPrecedingFilters: ({ id, childOrder, getChild }) => {
let filters: Filter[] = [];
const order = childOrder.IdsToOrder?.[id];
if (!order || order === 0) return filters;
for (let i = 0; i < order; i++) {
const embeddable = getChild(childOrder.idsInOrder[i]);
if (!embeddable || isErrorEmbeddable(embeddable)) return filters;
filters = [...filters, ...(embeddable.getOutput().filters ?? [])];
}
return filters;
},
onChildChange: ({ childOutputChangedId, childOrder, recalculateFilters$, getChild }) => {
if (childOutputChangedId === childOrder.lastChildId) {
// the last control's output has updated, recalculate filters
recalculateFilters$.next();
return;
}
// when output changes on a child which isn't the last - make the next embeddable updateInputFromParent
const nextOrder = childOrder.IdsToOrder[childOutputChangedId] + 1;
if (nextOrder >= childOrder.idsInOrder.length) return;
setTimeout(
() => getChild(childOrder.idsInOrder[nextOrder]).refreshInputFromParent(),
1 // run on next tick
);
},
},
NONE: {
getContainerSettings: () => undefined,
getPrecedingFilters: () => undefined,
onChildChange: ({ recalculateFilters$ }) => recalculateFilters$.next(),
},
};

View file

@ -40,18 +40,18 @@ import { pluginServices } from '../../services';
import { DataView } from '../../../../data_views/public';
import { ControlGroupStrings } from '../control_group_strings';
import { EditControlGroup } from '../editor/edit_control_group';
import { DEFAULT_CONTROL_WIDTH } from '../editor/editor_constants';
import { ControlGroup } from '../component/control_group_component';
import { controlGroupReducers } from '../state/control_group_reducers';
import { Container, EmbeddableFactory } from '../../../../embeddable/public';
import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types';
import { ControlGroupChainingSystems } from './control_group_chaining_system';
import { CreateControlButton, CreateControlButtonTypes } from '../editor/create_control';
import { Container, EmbeddableFactory, isErrorEmbeddable } from '../../../../embeddable/public';
const ControlGroupReduxWrapper = withSuspense<
ReduxEmbeddableWrapperPropsWithChildren<ControlGroupInput>
>(LazyReduxEmbeddableWrapper);
interface ChildEmbeddableOrderCache {
export interface ChildEmbeddableOrderCache {
IdsToOrder: { [key: string]: number };
idsInOrder: string[];
lastChildId: string;
@ -104,22 +104,7 @@ export class ControlGroupContainer extends Container<
};
private getEditControlGroupButton = (closePopover: () => void) => {
return (
<EditControlGroup
controlStyle={this.getInput().controlStyle}
panels={this.getInput().panels}
defaultControlWidth={this.getInput().defaultControlWidth}
setControlStyle={(controlStyle) => this.updateInput({ controlStyle })}
setDefaultControlWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
setAllControlWidths={(defaultControlWidth) => {
Object.keys(this.getInput().panels).forEach(
(panelId) => (this.getInput().panels[panelId].width = defaultControlWidth)
);
}}
removeEmbeddable={(id) => this.removeEmbeddable(id)}
closePopover={closePopover}
/>
);
return <EditControlGroup controlGroupContainer={this} closePopover={closePopover} />;
};
/**
@ -154,12 +139,7 @@ export class ControlGroupContainer extends Container<
{ embeddableLoaded: {} },
pluginServices.getServices().controls.getControlFactory,
parent,
{
childIdInitializeOrder: Object.values(initialInput.panels)
.sort((a, b) => (a.order > b.order ? 1 : -1))
.map((panel) => panel.explicitInput.id),
initializeSequentially: true,
}
ControlGroupChainingSystems[initialInput.chainingSystem]?.getContainerSettings(initialInput)
);
this.recalculateFilters$ = new Subject();
@ -226,20 +206,12 @@ export class ControlGroupContainer extends Container<
.pipe(anyChildChangePipe)
.subscribe((childOutputChangedId) => {
this.recalculateDataViews();
if (childOutputChangedId === this.childOrderCache.lastChildId) {
// the last control's output has updated, recalculate filters
this.recalculateFilters$.next();
return;
}
// when output changes on a child which isn't the last - make the next embeddable updateInputFromParent
const nextOrder = this.childOrderCache.IdsToOrder[childOutputChangedId] + 1;
if (nextOrder >= Object.keys(this.children).length) return;
setTimeout(
() =>
this.getChild(this.childOrderCache.idsInOrder[nextOrder]).refreshInputFromParent(),
1 // run on next tick
);
ControlGroupChainingSystems[this.getInput().chainingSystem].onChildChange({
childOutputChangedId,
childOrder: this.childOrderCache,
getChild: (id) => this.getChild(id),
recalculateFilters$: this.recalculateFilters$,
});
})
);
@ -251,18 +223,6 @@ export class ControlGroupContainer extends Container<
);
};
private getPrecedingFilters = (id: string) => {
let filters: Filter[] = [];
const order = this.childOrderCache.IdsToOrder?.[id];
if (!order || order === 0) return filters;
for (let i = 0; i < order; i++) {
const embeddable = this.getChild<ControlEmbeddable>(this.childOrderCache.idsInOrder[i]);
if (!embeddable || isErrorEmbeddable(embeddable)) return filters;
filters = [...filters, ...(embeddable.getOutput().filters ?? [])];
}
return filters;
};
private getEmbeddableOrderCache = (): ChildEmbeddableOrderCache => {
const panels = this.getInput().panels;
const IdsToOrder: { [key: string]: number } = {};
@ -314,20 +274,25 @@ export class ControlGroupContainer extends Container<
}
return {
order: nextOrder,
width: this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH,
width: this.getInput().defaultControlWidth,
...panelState,
} as ControlPanelState<TEmbeddableInput>;
}
protected getInheritedInput(id: string): ControlInput {
const { filters, query, ignoreParentSettings, timeRange } = this.getInput();
const { filters, query, ignoreParentSettings, timeRange, chainingSystem } = this.getInput();
const precedingFilters = this.getPrecedingFilters(id);
const precedingFilters = ControlGroupChainingSystems[chainingSystem].getPrecedingFilters({
id,
childOrder: this.childOrderCache,
getChild: (getChildId: string) => this.getChild<ControlEmbeddable>(getChildId),
});
const allFilters = [
...(ignoreParentSettings?.ignoreFilters ? [] : filters ?? []),
...precedingFilters,
...(precedingFilters ?? []),
];
return {
ignoreParentSettings,
filters: allFilters,
query: ignoreParentSettings?.ignoreQuery ? undefined : query,
timeRange: ignoreParentSettings?.ignoreTimerange ? undefined : timeRange,

View file

@ -23,6 +23,7 @@ import {
createControlGroupExtract,
createControlGroupInject,
} from '../../../common/control_group/control_group_persistable_state';
import { getDefaultControlGroupInput } from '../../../common/control_group/control_group_constants';
export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition {
public readonly isContainerType = true;
@ -42,14 +43,7 @@ export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition
};
public getDefaultInput(): Partial<ControlGroupInput> {
return {
panels: {},
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
},
};
return getDefaultControlGroupInput();
}
public create = async (initialInput: ControlGroupInput, parent?: Container) => {

View file

@ -56,6 +56,7 @@ interface OptionsListDataFetchProps {
search?: string;
fieldName: string;
dataViewId: string;
validate?: boolean;
query?: ControlInput['query'];
filters?: ControlInput['filters'];
}
@ -115,6 +116,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
private setupSubscriptions = () => {
const dataFetchPipe = this.getInput$().pipe(
map((newInput) => ({
validate: !Boolean(newInput.ignoreParentSettings?.ignoreValidations),
lastReloadRequestTime: newInput.lastReloadRequestTime,
dataViewId: newInput.dataViewId,
fieldName: newInput.fieldName,
@ -218,12 +220,12 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
await this.optionsListService.runOptionsListRequest(
{
field,
query,
filters,
dataView,
timeRange,
selectedOptions,
searchString: this.searchString,
...(ignoreParentSettings?.ignoreQuery ? {} : { query }),
...(ignoreParentSettings?.ignoreFilters ? {} : { filters }),
...(ignoreParentSettings?.ignoreTimerange ? {} : { timeRange }),
},
this.abortController.signal
);

View file

@ -22,6 +22,7 @@ import { CONTROL_GROUP_TYPE } from '../../../controls/common';
const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`;
const controlGroupReferencePrefix = 'controlGroup_';
const controlGroupId = 'dashboard_control_group';
export const createInject = (
persistableStateService: EmbeddablePersistableStateService
@ -89,11 +90,12 @@ export const createInject = (
{
...workingState.controlGroupInput,
type: CONTROL_GROUP_TYPE,
id: controlGroupId,
},
controlGroupReferences
);
workingState.controlGroupInput =
injectedControlGroupState as DashboardContainerControlGroupInput;
injectedControlGroupState as unknown as DashboardContainerControlGroupInput;
}
return workingState as EmbeddableStateWithType;
@ -155,9 +157,10 @@ export const createExtract = (
persistableStateService.extract({
...workingState.controlGroupInput,
type: CONTROL_GROUP_TYPE,
id: controlGroupId,
});
workingState.controlGroupInput =
extractedControlGroupState as DashboardContainerControlGroupInput;
extractedControlGroupState as unknown as DashboardContainerControlGroupInput;
const prefixedControlGroupReferences = controlGroupReferences.map((reference) => ({
...reference,
name: `${controlGroupReferencePrefix}${reference.name}`,

View file

@ -7,57 +7,68 @@
*/
import { SerializableRecord } from '@kbn/utility-types';
import { ControlGroupInput } from '../../../controls/common';
import { ControlStyle } from '../../../controls/common/types';
import { ControlGroupInput, getDefaultControlGroupInput } from '../../../controls/common';
import { RawControlGroupAttributes } from '../types';
export const getDefaultDashboardControlGroupInput = getDefaultControlGroupInput;
export const controlGroupInputToRawAttributes = (
controlGroupInput: Omit<ControlGroupInput, 'id'>
): Omit<RawControlGroupAttributes, 'id'> => {
): RawControlGroupAttributes => {
return {
controlStyle: controlGroupInput.controlStyle,
chainingSystem: controlGroupInput.chainingSystem,
panelsJSON: JSON.stringify(controlGroupInput.panels),
ignoreParentSettingsJSON: JSON.stringify(controlGroupInput.ignoreParentSettings),
};
};
export const getDefaultDashboardControlGroupInput = () => ({
controlStyle: 'oneLine' as ControlGroupInput['controlStyle'],
panels: {},
});
const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
if (!jsonString && typeof jsonString !== 'string') return;
try {
return JSON.parse(jsonString) as OutType;
} catch {
return;
}
};
export const rawAttributesToControlGroupInput = (
rawControlGroupAttributes: Omit<RawControlGroupAttributes, 'id'>
rawControlGroupAttributes: RawControlGroupAttributes
): Omit<ControlGroupInput, 'id'> | undefined => {
const defaultControlGroupInput = getDefaultDashboardControlGroupInput();
const defaultControlGroupInput = getDefaultControlGroupInput();
const { chainingSystem, controlStyle, ignoreParentSettingsJSON, panelsJSON } =
rawControlGroupAttributes;
const panels = safeJSONParse<ControlGroupInput['panels']>(panelsJSON);
const ignoreParentSettings =
safeJSONParse<ControlGroupInput['ignoreParentSettings']>(ignoreParentSettingsJSON);
return {
controlStyle: rawControlGroupAttributes?.controlStyle ?? defaultControlGroupInput.controlStyle,
panels:
rawControlGroupAttributes?.panelsJSON &&
typeof rawControlGroupAttributes?.panelsJSON === 'string'
? JSON.parse(rawControlGroupAttributes?.panelsJSON)
: defaultControlGroupInput.panels,
...defaultControlGroupInput,
...(chainingSystem ? { chainingSystem } : {}),
...(controlStyle ? { controlStyle } : {}),
...(ignoreParentSettings ? { ignoreParentSettings } : {}),
...(panels ? { panels } : {}),
};
};
export const rawAttributesToSerializable = (
rawControlGroupAttributes: Omit<RawControlGroupAttributes, 'id'>
): SerializableRecord => {
const defaultControlGroupInput = getDefaultDashboardControlGroupInput();
const defaultControlGroupInput = getDefaultControlGroupInput();
return {
chainingSystem: rawControlGroupAttributes?.chainingSystem,
controlStyle: rawControlGroupAttributes?.controlStyle ?? defaultControlGroupInput.controlStyle,
panels:
rawControlGroupAttributes?.panelsJSON &&
typeof rawControlGroupAttributes?.panelsJSON === 'string'
? (JSON.parse(rawControlGroupAttributes?.panelsJSON) as SerializableRecord)
: defaultControlGroupInput.panels,
ignoreParentSettings: safeJSONParse(rawControlGroupAttributes?.ignoreParentSettingsJSON) ?? {},
panels: safeJSONParse(rawControlGroupAttributes?.panelsJSON) ?? {},
};
};
export const serializableToRawAttributes = (
controlGroupInput: SerializableRecord
): Omit<RawControlGroupAttributes, 'id'> => {
serializable: SerializableRecord
): Omit<RawControlGroupAttributes, 'id' | 'type'> => {
return {
controlStyle: controlGroupInput.controlStyle as ControlStyle,
panelsJSON: JSON.stringify(controlGroupInput.panels),
controlStyle: serializable.controlStyle as RawControlGroupAttributes['controlStyle'],
chainingSystem: serializable.chainingSystem as RawControlGroupAttributes['chainingSystem'],
ignoreParentSettingsJSON: JSON.stringify(serializable.ignoreParentSettings),
panelsJSON: JSON.stringify(serializable.panels),
};
};

View file

@ -19,7 +19,6 @@ import {
convertSavedDashboardPanelToPanelState,
} from './embeddable/embeddable_saved_object_converters';
import { SavedDashboardPanel } from './types';
import { CONTROL_GROUP_TYPE } from '../../controls/common';
export interface ExtractDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
@ -51,7 +50,6 @@ function dashboardAttributesToState(attributes: SavedObjectAttributes): {
if (controlGroupPanels && typeof controlGroupPanels === 'object') {
controlGroupInput = {
...rawControlGroupInput,
type: CONTROL_GROUP_TYPE,
panels: controlGroupPanels,
};
}

View file

@ -98,17 +98,19 @@ export type SavedDashboardPanel730ToLatest = Pick<
// Making this interface because so much of the Container type from embeddable is tied up in public
// Once that is all available from common, we should be able to move the dashboard_container type to our common as well
export interface DashboardContainerControlGroupInput extends EmbeddableStateWithType {
panels: ControlGroupInput['panels'];
controlStyle: ControlGroupInput['controlStyle'];
id: string;
}
// dashboard only persists part of the Control Group Input
export type DashboardContainerControlGroupInput = Pick<
ControlGroupInput,
'panels' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettings'
>;
export interface RawControlGroupAttributes {
controlStyle: ControlGroupInput['controlStyle'];
export type RawControlGroupAttributes = Omit<
DashboardContainerControlGroupInput,
'panels' | 'ignoreParentSettings'
> & {
ignoreParentSettingsJSON: string;
panelsJSON: string;
id: string;
}
};
export interface DashboardContainerStateWithType extends EmbeddableStateWithType {
panels: {

View file

@ -9,6 +9,7 @@
import { i18n } from '@kbn/i18n';
import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common';
import { identity, pickBy } from 'lodash';
import { DashboardContainerInput } from '../..';
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants';
import type { DashboardContainer, DashboardContainerServices } from './dashboard_container';
@ -90,7 +91,7 @@ export class DashboardContainerFactoryDefinition
const controlGroup = await controlsGroupFactory?.create({
id: `control_group_${id ?? 'new_dashboard'}`,
...getDefaultDashboardControlGroupInput(),
...(controlGroupInput ?? {}),
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
timeRange,
viewMode,
filters,

View file

@ -11,7 +11,8 @@ import deepEqual from 'fast-deep-equal';
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators';
import { DashboardContainer } from '..';
import { pick } from 'lodash';
import { DashboardContainer, DashboardContainerControlGroupInput } from '..';
import { DashboardState } from '../../types';
import { DashboardContainerInput, DashboardSavedObject } from '../..';
import { ControlGroupContainer, ControlGroupInput } from '../../../../controls/public';
@ -20,13 +21,6 @@ import {
getDefaultDashboardControlGroupInput,
rawAttributesToControlGroupInput,
} from '../../../common';
// only part of the control group input should be stored in dashboard state. The rest is passed down from the dashboard.
export interface DashboardControlGroupInput {
panels: ControlGroupInput['panels'];
controlStyle: ControlGroupInput['controlStyle'];
}
interface DiffChecks {
[key: string]: (a?: unknown, b?: unknown) => boolean;
}
@ -60,6 +54,8 @@ export const syncDashboardControlGroup = async ({
const controlGroupDiff: DiffChecks = {
panels: deepEqual,
controlStyle: deepEqual,
chainingSystem: deepEqual,
ignoreParentSettings: deepEqual,
};
subscriptions.add(
@ -71,9 +67,12 @@ export const syncDashboardControlGroup = async ({
)
)
.subscribe(() => {
const { panels, controlStyle } = controlGroup.getInput();
const { panels, controlStyle, chainingSystem, ignoreParentSettings } =
controlGroup.getInput();
if (!isControlGroupInputEqual()) {
dashboardContainer.updateInput({ controlGroupInput: { panels, controlStyle } });
dashboardContainer.updateInput({
controlGroupInput: { panels, controlStyle, chainingSystem, ignoreParentSettings },
});
}
})
);
@ -154,17 +153,17 @@ export const syncDashboardControlGroup = async ({
};
export const controlGroupInputIsEqual = (
a: DashboardControlGroupInput | undefined,
b: DashboardControlGroupInput | undefined
a: DashboardContainerControlGroupInput | undefined,
b: DashboardContainerControlGroupInput | undefined
) => {
const defaultInput = getDefaultDashboardControlGroupInput();
const inputA = {
panels: a?.panels ?? defaultInput.panels,
controlStyle: a?.controlStyle ?? defaultInput.controlStyle,
...defaultInput,
...pick(a, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
};
const inputB = {
panels: b?.panels ?? defaultInput.panels,
controlStyle: b?.controlStyle ?? defaultInput.controlStyle,
...defaultInput,
...pick(b, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
};
if (deepEqual(inputA, inputB)) return true;
return false;
@ -175,7 +174,12 @@ export const serializeControlGroupToDashboardSavedObject = (
dashboardState: DashboardState
) => {
// only save to saved object if control group is not default
if (controlGroupInputIsEqual(dashboardState.controlGroupInput, {} as ControlGroupInput)) {
if (
controlGroupInputIsEqual(
dashboardState.controlGroupInput,
getDefaultDashboardControlGroupInput()
)
) {
dashboardSavedObject.controlGroupInput = undefined;
return;
}

View file

@ -10,8 +10,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Filter, Query, TimeRange } from '../../services/data';
import { ViewMode } from '../../services/embeddable';
import type { DashboardControlGroupInput } from '../lib/dashboard_control_group';
import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types';
import { DashboardContainerControlGroupInput } from '../embeddable';
export const dashboardStateSlice = createSlice({
name: 'dashboardState',
@ -44,7 +44,7 @@ export const dashboardStateSlice = createSlice({
},
setControlGroupState: (
state,
action: PayloadAction<DashboardControlGroupInput | undefined>
action: PayloadAction<DashboardContainerControlGroupInput | undefined>
) => {
state.controlGroupInput = action.payload;
},

View file

@ -29,7 +29,11 @@ import { UrlForwardingStart } from '../../url_forwarding/public';
import { UsageCollectionSetup } from './services/usage_collection';
import { NavigationPublicPluginStart } from './services/navigation';
import { Query, RefreshInterval, TimeRange } from './services/data';
import { DashboardPanelState, SavedDashboardPanel } from '../common/types';
import {
DashboardContainerControlGroupInput,
DashboardPanelState,
SavedDashboardPanel,
} from '../common/types';
import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss';
import { DataPublicPluginStart, DataViewsContract } from './services/data';
import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable';
@ -40,7 +44,6 @@ import type { DashboardContainer, DashboardSavedObject } from '.';
import { VisualizationsStart } from '../../visualizations/public';
import { DashboardAppLocatorParams } from './locator';
import { SpacesPluginStart } from './services/spaces';
import type { DashboardControlGroupInput } from './application/lib/dashboard_control_group';
export type { SavedDashboardPanel };
@ -71,7 +74,7 @@ export interface DashboardState {
panels: DashboardPanelMap;
timeRange?: TimeRange;
controlGroupInput?: DashboardControlGroupInput;
controlGroupInput?: DashboardContainerControlGroupInput;
}
/**
@ -81,7 +84,7 @@ export type RawDashboardState = Omit<DashboardState, 'panels'> & { panels: Saved
export interface DashboardContainerInput extends ContainerInput {
dashboardCapabilities?: DashboardAppCapabilities;
controlGroupInput?: DashboardControlGroupInput;
controlGroupInput?: DashboardContainerControlGroupInput;
refreshConfig?: RefreshInterval;
isEmbeddedExternally?: boolean;
isFullScreenMode: boolean;

View file

@ -55,7 +55,9 @@ export const createDashboardSavedObjectType = ({
controlGroupInput: {
properties: {
controlStyle: { type: 'keyword', index: false, doc_values: false },
chainingSystem: { type: 'keyword', index: false, doc_values: false },
panelsJSON: { type: 'text', index: false },
ignoreParentSettingsJSON: { type: 'text', index: false },
},
},
timeFrom: { type: 'keyword', index: false, doc_values: false },

View file

@ -36,6 +36,7 @@ export type {
EmbeddablePackageState,
EmbeddableRendererProps,
EmbeddableContainerContext,
EmbeddableContainerSettings,
} from './lib';
export {
ACTION_ADD_PANEL,

View file

@ -6,6 +6,12 @@
* Side Public License, v 1.
*/
export type { IContainer, PanelState, ContainerInput, ContainerOutput } from './i_container';
export type {
IContainer,
PanelState,
ContainerInput,
ContainerOutput,
EmbeddableContainerSettings,
} from './i_container';
export { Container } from './container';
export * from './embeddable_child_panel';

View file

@ -1,566 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const security = getService('security');
const queryBar = getService('queryBar');
const pieChart = getService('pieChart');
const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const dashboardAddPanel = getService('dashboardAddPanel');
const find = getService('find');
const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
'header',
]);
describe('Dashboard controls integration', () => {
const clearAllControls = async () => {
const controlIds = await dashboardControls.getAllControlIds();
for (const controlId of controlIds) {
await dashboardControls.removeExistingControl(controlId);
}
};
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/dashboard/current/kibana'
);
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']);
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
await common.navigateToApp('dashboard');
await dashboardControls.enableControlsLab();
await common.navigateToApp('dashboard');
await dashboard.preserveCrossAppState();
});
after(async () => {
await security.testUser.restoreDefaults();
await kibanaServer.savedObjects.cleanStandardList();
});
describe('Controls callout visibility', async () => {
before(async () => {
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboard.saveDashboard('Test Controls Callout');
});
describe('does not show the empty control callout on an empty dashboard', async () => {
it('in view mode', async () => {
await dashboard.clickCancelOutOfEditMode();
await testSubjects.missingOrFail('controls-empty');
});
it('in edit mode', async () => {
await dashboard.switchToEditMode();
await testSubjects.missingOrFail('controls-empty');
});
});
it('show the empty control callout on a dashboard with panels', async () => {
await dashboard.switchToEditMode();
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await testSubjects.existOrFail('controls-empty');
});
it('adding control hides the empty control callout', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await testSubjects.missingOrFail('controls-empty');
});
after(async () => {
await dashboard.clickCancelOutOfEditMode();
await dashboard.gotoDashboardLandingPage();
});
});
describe('Control group settings', async () => {
before(async () => {
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await dashboard.saveDashboard('Test Control Group Settings');
});
it('adjust layout of controls', async () => {
await dashboard.switchToEditMode();
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.adjustControlsLayout('twoLine');
const controlGroupWrapper = await testSubjects.find('controls-group-wrapper');
expect(await controlGroupWrapper.elementHasClass('controlsWrapper--twoLine')).to.be(true);
});
describe('apply new default size', async () => {
it('to new controls only', async () => {
await dashboardControls.updateControlsSize('medium');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
});
const controlIds = await dashboardControls.getAllControlIds();
const firstControl = await find.byXPath(`//div[@data-control-id="${controlIds[0]}"]`);
expect(await firstControl.elementHasClass('controlFrameWrapper--medium')).to.be(false);
const secondControl = await find.byXPath(`//div[@data-control-id="${controlIds[1]}"]`);
expect(await secondControl.elementHasClass('controlFrameWrapper--medium')).to.be(true);
});
it('to all existing controls', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
width: 'large',
});
await dashboardControls.updateControlsSize('small', true);
const controlIds = await dashboardControls.getAllControlIds();
for (const id of controlIds) {
const control = await find.byXPath(`//div[@data-control-id="${id}"]`);
expect(await control.elementHasClass('controlFrameWrapper--small')).to.be(true);
}
});
});
describe('flyout only show settings that are relevant', async () => {
before(async () => {
await dashboard.switchToEditMode();
});
it('when no controls', async () => {
await dashboardControls.deleteAllControls();
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.missingOrFail('delete-all-controls-button');
await testSubjects.missingOrFail('set-all-control-sizes-checkbox');
});
it('when at least one control', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.existOrFail('delete-all-controls-button');
await testSubjects.existOrFail('set-all-control-sizes-checkbox', { allowHidden: true });
});
afterEach(async () => {
await testSubjects.click('euiFlyoutCloseButton');
});
after(async () => {
await dashboardControls.deleteAllControls();
});
});
after(async () => {
await dashboard.clickCancelOutOfEditMode();
await dashboard.gotoDashboardLandingPage();
});
});
describe('Options List Control creation and editing experience', async () => {
it('can add a new options list control from a blank state', async () => {
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboardControls.createOptionsListControl({ fieldName: 'machine.os.raw' });
expect(await dashboardControls.getControlsCount()).to.be(1);
});
it('can add a second options list control with a non-default data view', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
expect(await dashboardControls.getControlsCount()).to.be(2);
// data views should be properly propagated from the control group to the dashboard
expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*');
});
it('renames an existing control', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
const newTitle = 'wow! Animal sounds?';
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
});
it('can change the data view and field of an existing options list', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(firstId);
await dashboardControls.optionsListEditorSetDataView('animals-*');
await dashboardControls.optionsListEditorSetfield('animal.keyword');
await dashboardControls.controlEditorSave();
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
await retry.try(async () => {
await testSubjects.click('addFilter');
const indexPatternSelectExists = await testSubjects.exists('filterIndexPatternsSelect');
await filterBar.ensureFieldEditorModalIsClosed();
expect(indexPatternSelectExists).to.be(false);
});
});
it('deletes an existing control', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.removeExistingControl(firstId);
expect(await dashboardControls.getControlsCount()).to.be(1);
});
after(async () => {
await clearAllControls();
});
});
describe('Interactions between options list and dashboard', async () => {
let controlId: string;
before(async () => {
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sounds',
});
controlId = (await dashboardControls.getAllControlIds())[0];
});
describe('Apply dashboard query and filters to controls', async () => {
it('Applies dashboard query to options list control', async () => {
await queryBar.setQuery('isDog : true ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(5);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
]);
});
await queryBar.setQuery('');
await queryBar.submitQuery();
});
it('Applies dashboard filters to options list control', async () => {
await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(3);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'ruff',
'bark',
'bow ow ow',
]);
});
});
it('Does not apply disabled dashboard filters to options list control', async () => {
await filterBar.toggleFilterEnabled('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
});
await filterBar.toggleFilterEnabled('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
});
it('Negated filters apply to options control', async () => {
await filterBar.toggleFilterNegated('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(5);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'grrr',
'meow',
'growl',
'grr',
]);
});
});
after(async () => {
await filterBar.removeAllFilters();
});
});
describe('Selections made in control apply to dashboard', async () => {
it('Shows available options in options list', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can search options list for available options', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSearchForOption('meo');
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'meow',
]);
});
await dashboardControls.optionsListPopoverClearSearch();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can select multiple available options', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('hiss');
await dashboardControls.optionsListPopoverSelectOption('grr');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Selected options appear in control', async () => {
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
expect(selectionString).to.be('hiss, grr');
});
it('Applies options list control options to dashboard', async () => {
await retry.try(async () => {
expect(await pieChart.getPieSliceCount()).to.be(2);
});
});
it('Applies options list control options to dashboard by default on open', async () => {
await dashboard.gotoDashboardLandingPage();
await header.waitUntilLoadingHasFinished();
await dashboard.clickUnsavedChangesContinueEditing('New Dashboard');
await header.waitUntilLoadingHasFinished();
expect(await pieChart.getPieSliceCount()).to.be(2);
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
expect(selectionString).to.be('hiss, grr');
});
after(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
});
describe('Options List dashboard validation', async () => {
before(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListPopoverSelectOption('bark');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can mark selections invalid with Query', async () => {
await queryBar.setQuery('isDog : false ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(4);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'meow',
'growl',
'grr',
'Ignored selection',
'bark',
]);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
// only valid selections are applied as filters.
expect(await pieChart.getPieSliceCount()).to.be(1);
});
it('can make invalid selections valid again if the parent filter changes', async () => {
await queryBar.setQuery('');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'ruff',
'bark',
'grrr',
'meow',
'growl',
'grr',
'bow ow ow',
]);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
expect(await pieChart.getPieSliceCount()).to.be(2);
});
it('Can mark multiple selections invalid with Filter', async () => {
await filterBar.addFilter('sound.keyword', 'is', ['hiss']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'Ignored selections',
'meow',
'bark',
]);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
// only valid selections are applied as filters.
expect(await pieChart.getPieSliceCount()).to.be(1);
});
});
after(async () => {
await filterBar.removeAllFilters();
await clearAllControls();
});
});
describe('Control group hierarchical chaining', async () => {
let controlIds: string[];
const ensureAvailableOptionsEql = async (controlId: string, expectation: string[]) => {
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(
expectation
);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
};
before(async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
title: 'Animal',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
title: 'Animal Name',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sound',
});
controlIds = await dashboardControls.getAllControlIds();
});
it('Shows all available options in first Options List control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(2);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Selecting an option in the first Options List will filter the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester']);
await ensureAvailableOptionsEql(controlIds[2], ['hiss', 'meow', 'growl', 'grr']);
});
it('Selecting an option in the second Options List will filter the third control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[1]);
await dashboardControls.optionsListPopoverSelectOption('sylvester');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]);
await ensureAvailableOptionsEql(controlIds[2], ['meow', 'hiss']);
});
it('Can select an option in the third Options List', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[2]);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
});
it('Selecting a conflicting option in the first control will validate the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListPopoverSelectOption('dog');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], [
'Fluffy',
'Fee Fee',
'Rover',
'Ignored selection',
'sylvester',
]);
await ensureAvailableOptionsEql(controlIds[2], [
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
'Ignored selection',
'meow',
]);
});
});
});
}

View file

@ -72,7 +72,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./full_screen_mode'));
loadTestFile(require.resolve('./dashboard_filter_bar'));
loadTestFile(require.resolve('./dashboard_filtering'));
loadTestFile(require.resolve('./dashboard_controls_integration'));
loadTestFile(require.resolve('./panel_expand_toggle'));
loadTestFile(require.resolve('./dashboard_grid'));
loadTestFile(require.resolve('./view_edit'));

View file

@ -0,0 +1,146 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const { dashboardControls, common, dashboard, timePicker } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
]);
describe('Dashboard control group hierarchical chaining', () => {
let controlIds: string[];
const ensureAvailableOptionsEql = async (controlId: string, expectation: string[]) => {
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(expectation);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
};
before(async () => {
await common.navigateToApp('dashboard');
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
// populate an initial set of controls and get their ids.
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
title: 'Animal',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
title: 'Animal Name',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sound',
});
controlIds = await dashboardControls.getAllControlIds();
});
it('Shows all available options in first Options List control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(2);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Selecting an option in the first Options List will filter the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester']);
await ensureAvailableOptionsEql(controlIds[2], ['hiss', 'meow', 'growl', 'grr']);
});
it('Selecting an option in the second Options List will filter the third control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[1]);
await dashboardControls.optionsListPopoverSelectOption('sylvester');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]);
await ensureAvailableOptionsEql(controlIds[2], ['meow', 'hiss']);
});
it('Can select an option in the third Options List', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[2]);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
});
it('Selecting a conflicting option in the first control will validate the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListPopoverSelectOption('dog');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], [
'Fluffy',
'Fee Fee',
'Rover',
'Ignored selection',
'sylvester',
]);
await ensureAvailableOptionsEql(controlIds[2], [
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
'Ignored selection',
'meow',
]);
});
describe('Hierarchical chaining off', async () => {
before(async () => {
await dashboardControls.updateChainingSystem('NONE');
});
it('Selecting an option in the first Options List will not filter the second or third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], [
'Fluffy',
'Tiger',
'sylvester',
'Fee Fee',
'Rover',
]);
await ensureAvailableOptionsEql(controlIds[2], [
'hiss',
'ruff',
'bark',
'grrr',
'meow',
'growl',
'grr',
'bow ow ow',
]);
});
});
});
}

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const { dashboardControls, common, dashboard } = getPageObjects([
'dashboardControls',
'dashboard',
'common',
]);
describe('Dashboard control group settings', () => {
before(async () => {
await common.navigateToApp('dashboard');
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await dashboard.saveDashboard('Test Control Group Settings');
});
it('adjust layout of controls', async () => {
await dashboard.switchToEditMode();
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.adjustControlsLayout('twoLine');
const controlGroupWrapper = await testSubjects.find('controls-group-wrapper');
expect(await controlGroupWrapper.elementHasClass('controlsWrapper--twoLine')).to.be(true);
});
describe('apply new default size', async () => {
it('to new controls only', async () => {
await dashboardControls.updateControlsSize('medium');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
});
const controlIds = await dashboardControls.getAllControlIds();
const firstControl = await find.byXPath(`//div[@data-control-id="${controlIds[0]}"]`);
expect(await firstControl.elementHasClass('controlFrameWrapper--medium')).to.be(false);
const secondControl = await find.byXPath(`//div[@data-control-id="${controlIds[1]}"]`);
expect(await secondControl.elementHasClass('controlFrameWrapper--medium')).to.be(true);
});
it('to all existing controls', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
width: 'large',
});
await dashboardControls.updateControlsSize('small', true);
const controlIds = await dashboardControls.getAllControlIds();
for (const id of controlIds) {
const control = await find.byXPath(`//div[@data-control-id="${id}"]`);
expect(await control.elementHasClass('controlFrameWrapper--small')).to.be(true);
}
});
});
describe('flyout only show settings that are relevant', async () => {
before(async () => {
await dashboard.switchToEditMode();
});
it('when no controls', async () => {
await dashboardControls.deleteAllControls();
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.missingOrFail('delete-all-controls-button');
await testSubjects.missingOrFail('set-all-control-sizes-checkbox');
});
it('when at least one control', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.existOrFail('delete-all-controls-button');
await testSubjects.existOrFail('set-all-control-sizes-checkbox', { allowHidden: true });
});
afterEach(async () => {
await testSubjects.click('euiFlyoutCloseButton');
});
after(async () => {
await dashboardControls.deleteAllControls();
});
});
});
}

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const { dashboardControls, timePicker, dashboard } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
'header',
]);
describe('Controls callout', () => {
describe('callout visibility', async () => {
before(async () => {
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboard.saveDashboard('Test Controls Callout');
});
describe('does not show the empty control callout on an empty dashboard', async () => {
it('in view mode', async () => {
await dashboard.clickCancelOutOfEditMode();
await testSubjects.missingOrFail('controls-empty');
});
it('in edit mode', async () => {
await dashboard.switchToEditMode();
await testSubjects.missingOrFail('controls-empty');
});
});
it('show the empty control callout on a dashboard with panels', async () => {
await dashboard.switchToEditMode();
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await testSubjects.existOrFail('controls-empty');
});
it('adding control hides the empty control callout', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await testSubjects.missingOrFail('controls-empty');
});
after(async () => {
await dashboard.clickCancelOutOfEditMode();
await dashboard.gotoDashboardLandingPage();
});
});
});
}

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile, getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const security = getService('security');
const { dashboardControls, common, dashboard } = getPageObjects([
'dashboardControls',
'dashboard',
'common',
]);
async function setup() {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data');
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/dashboard/current/kibana'
);
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']);
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
// enable the controls lab and navigate to the dashboard listing page to start
await common.navigateToApp('dashboard');
await dashboardControls.enableControlsLab();
await common.navigateToApp('dashboard');
await dashboard.preserveCrossAppState();
}
async function teardown() {
await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data');
await security.testUser.restoreDefaults();
await kibanaServer.savedObjects.cleanStandardList();
}
describe('Controls', function () {
before(setup);
after(teardown);
loadTestFile(require.resolve('./controls_callout'));
loadTestFile(require.resolve('./control_group_settings'));
loadTestFile(require.resolve('./options_list'));
loadTestFile(require.resolve('./control_group_chaining'));
});
}

View file

@ -0,0 +1,369 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const queryBar = getService('queryBar');
const pieChart = getService('pieChart');
const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
'header',
]);
describe('Dashboard options list integration', () => {
before(async () => {
await common.navigateToApp('dashboard');
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
});
describe('Options List Control creation and editing experience', async () => {
it('can add a new options list control from a blank state', async () => {
await dashboardControls.createOptionsListControl({ fieldName: 'machine.os.raw' });
expect(await dashboardControls.getControlsCount()).to.be(1);
});
it('can add a second options list control with a non-default data view', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
expect(await dashboardControls.getControlsCount()).to.be(2);
// data views should be properly propagated from the control group to the dashboard
expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*');
});
it('renames an existing control', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
const newTitle = 'wow! Animal sounds?';
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
});
it('can change the data view and field of an existing options list', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(firstId);
await dashboardControls.optionsListEditorSetDataView('animals-*');
await dashboardControls.optionsListEditorSetfield('animal.keyword');
await dashboardControls.controlEditorSave();
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
await retry.try(async () => {
await testSubjects.click('addFilter');
const indexPatternSelectExists = await testSubjects.exists('filterIndexPatternsSelect');
await filterBar.ensureFieldEditorModalIsClosed();
expect(indexPatternSelectExists).to.be(false);
});
});
it('deletes an existing control', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.removeExistingControl(firstId);
expect(await dashboardControls.getControlsCount()).to.be(1);
});
after(async () => {
await dashboardControls.clearAllControls();
});
});
describe('Interactions between options list and dashboard', async () => {
let controlId: string;
const allAvailableOptions = [
'hiss',
'ruff',
'bark',
'grrr',
'meow',
'growl',
'grr',
'bow ow ow',
];
const ensureAvailableOptionsEql = async (expectation: string[], skipOpen?: boolean) => {
if (!skipOpen) await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(
expectation
);
});
if (!skipOpen) await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
};
before(async () => {
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sounds',
});
controlId = (await dashboardControls.getAllControlIds())[0];
});
describe('Applies query settings to controls', async () => {
it('Applies dashboard query to options list control', async () => {
await queryBar.setQuery('isDog : true ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(['ruff', 'bark', 'grrr', 'bow ow ow', 'grr']);
await queryBar.setQuery('');
await queryBar.submitQuery();
// using the query hides the time range. Clicking anywhere else shows it again.
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Applies dashboard time range to options list control', async () => {
// set time range to time with no documents
await timePicker.setAbsoluteRange(
'Jan 1, 2017 @ 00:00:00.000',
'Jan 1, 2017 @ 00:00:00.000'
);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0);
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
await timePicker.setDefaultDataRange();
});
describe('dashboard filters', async () => {
before(async () => {
await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
});
it('Applies dashboard filters to options list control', async () => {
await ensureAvailableOptionsEql(['ruff', 'bark', 'bow ow ow']);
});
it('Does not apply disabled dashboard filters to options list control', async () => {
await filterBar.toggleFilterEnabled('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(allAvailableOptions);
await filterBar.toggleFilterEnabled('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
});
it('Negated filters apply to options control', async () => {
await filterBar.toggleFilterNegated('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(['hiss', 'grrr', 'meow', 'growl', 'grr']);
});
after(async () => {
await filterBar.removeAllFilters();
});
});
});
describe('Does not apply query settings to controls', async () => {
before(async () => {
await dashboardControls.updateAllQuerySyncSettings(false);
});
after(async () => {
await dashboardControls.updateAllQuerySyncSettings(true);
});
it('Does not apply query to options list control', async () => {
await queryBar.setQuery('isDog : true ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(allAvailableOptions);
await queryBar.setQuery('');
await queryBar.submitQuery();
});
it('Does not apply filters to options list control', async () => {
await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(allAvailableOptions);
await filterBar.removeAllFilters();
});
it('Does not apply time range to options list control', async () => {
// set time range to time with no documents
await timePicker.setAbsoluteRange(
'Jan 1, 2017 @ 00:00:00.000',
'Jan 1, 2017 @ 00:00:00.000'
);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(allAvailableOptions);
await timePicker.setDefaultDataRange();
});
});
describe('Selections made in control apply to dashboard', async () => {
it('Shows available options in options list', async () => {
await ensureAvailableOptionsEql(allAvailableOptions);
});
it('Can search options list for available options', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSearchForOption('meo');
await ensureAvailableOptionsEql(['meow'], true);
await dashboardControls.optionsListPopoverClearSearch();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can select multiple available options', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('hiss');
await dashboardControls.optionsListPopoverSelectOption('grr');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Selected options appear in control', async () => {
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
expect(selectionString).to.be('hiss, grr');
});
it('Applies options list control options to dashboard', async () => {
await retry.try(async () => {
expect(await pieChart.getPieSliceCount()).to.be(2);
});
});
it('Applies options list control options to dashboard by default on open', async () => {
await dashboard.gotoDashboardLandingPage();
await header.waitUntilLoadingHasFinished();
await dashboard.clickUnsavedChangesContinueEditing('New Dashboard');
await header.waitUntilLoadingHasFinished();
expect(await pieChart.getPieSliceCount()).to.be(2);
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
expect(selectionString).to.be('hiss, grr');
});
after(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
});
describe('Options List dashboard validation', async () => {
before(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListPopoverSelectOption('bark');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
after(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
await filterBar.removeAllFilters();
});
it('Can mark selections invalid with Query', async () => {
await queryBar.setQuery('isDog : false ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql([
'hiss',
'meow',
'growl',
'grr',
'Ignored selection',
'bark',
]);
// only valid selections are applied as filters.
expect(await pieChart.getPieSliceCount()).to.be(1);
});
it('can make invalid selections valid again if the parent filter changes', async () => {
await queryBar.setQuery('');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(allAvailableOptions);
expect(await pieChart.getPieSliceCount()).to.be(2);
});
it('Can mark multiple selections invalid with Filter', async () => {
await filterBar.addFilter('sound.keyword', 'is', ['hiss']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(['hiss', 'Ignored selections', 'meow', 'bark']);
// only valid selections are applied as filters.
expect(await pieChart.getPieSliceCount()).to.be(1);
});
});
describe('Options List dashboard no validation', async () => {
before(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListPopoverSelectOption('bark');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
await dashboardControls.updateValidationSetting(false);
});
it('Does not mark selections invalid with Query', async () => {
await queryBar.setQuery('isDog : false ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(['hiss', 'meow', 'growl', 'grr']);
});
it('Does not mark multiple selections invalid with Filter', async () => {
await filterBar.addFilter('sound.keyword', 'is', ['hiss']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await ensureAvailableOptionsEql(['hiss']);
});
});
after(async () => {
await filterBar.removeAllFilters();
await dashboardControls.clearAllControls();
});
});
});
}

View file

@ -27,6 +27,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
this.tags('ciGroup10');
loadTestFile(require.resolve('./input_control_vis'));
loadTestFile(require.resolve('./controls'));
loadTestFile(require.resolve('./_markdown_vis'));
});
});

View file

@ -9,6 +9,7 @@
import expect from '@kbn/expect';
import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper';
import { OPTIONS_LIST_CONTROL, ControlWidth } from '../../../src/plugins/controls/common';
import { ControlGroupChainingSystem } from '../../../src/plugins/controls/common/control_group/types';
import { FtrService } from '../ftr_provider_context';
@ -63,6 +64,13 @@ export class DashboardPageControls extends FtrService {
return allTitles.length;
}
public async clearAllControls() {
const controlIds = await this.getAllControlIds();
for (const controlId of controlIds) {
await this.removeExistingControl(controlId);
}
}
public async openCreateControlFlyout(type: string) {
this.log.debug(`Opening flyout for ${type} control`);
await this.testSubjects.click('dashboard-controls-menu-button');
@ -119,6 +127,85 @@ export class DashboardPageControls extends FtrService {
await this.testSubjects.click('control-group-editor-save');
}
public async updateChainingSystem(chainingSystem: ControlGroupChainingSystem) {
this.log.debug(`Update control group chaining system to ${chainingSystem}`);
await this.openControlGroupSettingsFlyout();
await this.testSubjects.existOrFail('control-group-chaining');
// currently there are only two chaining systems, so a switch is used.
const switchStateToChainingSystem: { [key: string]: ControlGroupChainingSystem } = {
true: 'HIERARCHICAL',
false: 'NONE',
};
const switchState = await this.testSubjects.getAttribute('control-group-chaining', 'checked');
if (chainingSystem !== switchStateToChainingSystem[switchState]) {
await this.testSubjects.click('control-group-chaining');
}
await this.testSubjects.click('control-group-editor-save');
}
public async setSwitchState(goalState: boolean, subject: string) {
await this.testSubjects.existOrFail(subject);
const currentStateIsChecked =
(await this.testSubjects.getAttribute(subject, 'aria-checked')) === 'true';
if (currentStateIsChecked !== goalState) {
await this.testSubjects.click(subject);
}
await this.retry.try(async () => {
const stateIsChecked = (await this.testSubjects.getAttribute(subject, 'checked')) === 'true';
expect(stateIsChecked).to.be(goalState);
});
}
public async updateValidationSetting(validate: boolean) {
this.log.debug(`Update control group validation setting to ${validate}`);
await this.openControlGroupSettingsFlyout();
await this.setSwitchState(validate, 'control-group-validate-selections');
await this.testSubjects.click('control-group-editor-save');
}
public async updateAllQuerySyncSettings(querySync: boolean) {
this.log.debug(`Update all control group query sync settings to ${querySync}`);
await this.openControlGroupSettingsFlyout();
await this.setSwitchState(querySync, 'control-group-query-sync');
await this.testSubjects.click('control-group-editor-save');
}
public async ensureAdvancedQuerySyncIsOpened() {
const advancedAccordion = await this.testSubjects.find(`control-group-query-sync-advanced`);
const opened = await advancedAccordion.elementHasClass('euiAccordion-isOpen');
if (!opened) {
await this.testSubjects.click(`control-group-query-sync-advanced`);
await this.retry.try(async () => {
expect(await advancedAccordion.elementHasClass('euiAccordion-isOpen')).to.be(true);
});
}
}
public async updateSyncTimeRangeAdvancedSetting(syncTimeRange: boolean) {
this.log.debug(`Update filter sync advanced setting to ${syncTimeRange}`);
await this.openControlGroupSettingsFlyout();
await this.ensureAdvancedQuerySyncIsOpened();
await this.setSwitchState(syncTimeRange, 'control-group-query-sync-time-range');
await this.testSubjects.click('control-group-editor-save');
}
public async updateSyncQueryAdvancedSetting(syncQuery: boolean) {
this.log.debug(`Update filter sync advanced setting to ${syncQuery}`);
await this.openControlGroupSettingsFlyout();
await this.ensureAdvancedQuerySyncIsOpened();
await this.setSwitchState(syncQuery, 'control-group-query-sync-query');
await this.testSubjects.click('control-group-editor-save');
}
public async updateSyncFilterAdvancedSetting(syncFilters: boolean) {
this.log.debug(`Update filter sync advanced setting to ${syncFilters}`);
await this.openControlGroupSettingsFlyout();
await this.ensureAdvancedQuerySyncIsOpened();
await this.setSwitchState(syncFilters, 'control-group-query-sync-filters');
await this.testSubjects.click('control-group-editor-save');
}
/* -----------------------------------------------------------
Individual controls functions
----------------------------------------------------------- */

View file

@ -1221,13 +1221,9 @@
"controls.controlGroup.management.flyoutTitle": "コントロールを構成",
"controls.controlGroup.management.layout.auto": "自動",
"controls.controlGroup.management.layout.controlWidthLegend": "コントロールサイズを変更",
"controls.controlGroup.management.layout.designSwitchLegend": "コントロール設計を切り替え",
"controls.controlGroup.management.layout.large": "大",
"controls.controlGroup.management.layout.medium": "中",
"controls.controlGroup.management.layout.singleLine": "1行",
"controls.controlGroup.management.layout.small": "小",
"controls.controlGroup.management.layout.twoLine": "2行",
"controls.controlGroup.management.layoutTitle": "レイアウト",
"controls.controlGroup.management.setAllWidths": "すべてのサイズをデフォルトに設定",
"controls.controlGroup.title": "コントロールグループ",
"controls.optionsList.editor.allowMultiselectTitle": "ドロップダウンでの複数選択を許可",

View file

@ -1227,13 +1227,9 @@
"controls.controlGroup.management.flyoutTitle": "配置控件",
"controls.controlGroup.management.layout.auto": "自动",
"controls.controlGroup.management.layout.controlWidthLegend": "更改控件大小",
"controls.controlGroup.management.layout.designSwitchLegend": "切换控件设计",
"controls.controlGroup.management.layout.large": "大",
"controls.controlGroup.management.layout.medium": "中",
"controls.controlGroup.management.layout.singleLine": "单行",
"controls.controlGroup.management.layout.small": "小",
"controls.controlGroup.management.layout.twoLine": "双行",
"controls.controlGroup.management.layoutTitle": "布局",
"controls.controlGroup.management.setAllWidths": "将所有大小设为默认值",
"controls.controlGroup.title": "控件组",
"controls.optionsList.editor.allowMultiselectTitle": "下拉列表中允许多选",