mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
0b4282e1f5
commit
82d4cd56dc
35 changed files with 1397 additions and 834 deletions
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: () =>
|
||||
|
|
|
@ -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();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -36,6 +36,7 @@ export type {
|
|||
EmbeddablePackageState,
|
||||
EmbeddableRendererProps,
|
||||
EmbeddableContainerContext,
|
||||
EmbeddableContainerSettings,
|
||||
} from './lib';
|
||||
export {
|
||||
ACTION_ADD_PANEL,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
54
test/functional/apps/dashboard_elements/controls/index.ts
Normal file
54
test/functional/apps/dashboard_elements/controls/index.ts
Normal 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'));
|
||||
});
|
||||
}
|
369
test/functional/apps/dashboard_elements/controls/options_list.ts
Normal file
369
test/functional/apps/dashboard_elements/controls/options_list.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
----------------------------------------------------------- */
|
||||
|
|
|
@ -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": "ドロップダウンでの複数選択を許可",
|
||||
|
|
|
@ -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": "下拉列表中允许多选",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue