mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Integrate react control group embeddable into dashboard container (#190273)
closes https://github.com/elastic/kibana/issues/191137, https://github.com/elastic/kibana/issues/190988, https://github.com/elastic/kibana/issues/191155 PR replaces legacy embeddable control group implementation with react control group implementation in DashboardContainer. ### Test instructions 1. Open dashboard via dashboard application or portable dashboard 2. Mess around with controls. There should be no changes in behavior --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
This commit is contained in:
parent
cd56f4103b
commit
c272d9715e
64 changed files with 1164 additions and 1058 deletions
|
@ -90,12 +90,12 @@ const initialSerializedControlGroupState = {
|
|||
} as object,
|
||||
references: [
|
||||
{
|
||||
name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL}DataView`,
|
||||
name: `controlGroup_${rangeSliderControlId}:rangeSliderDataView`,
|
||||
type: 'index-pattern',
|
||||
id: WEB_LOGS_DATA_VIEW_ID,
|
||||
},
|
||||
{
|
||||
name: `controlGroup_${optionsListId}:${OPTIONS_LIST_CONTROL}DataView`,
|
||||
name: `controlGroup_${optionsListId}:optionsListDataView`,
|
||||
type: 'index-pattern',
|
||||
id: WEB_LOGS_DATA_VIEW_ID,
|
||||
},
|
||||
|
|
|
@ -11,8 +11,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { controlGroupStateBuilder } from '@kbn/controls-plugin/public';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardRenderer,
|
||||
|
@ -63,16 +62,15 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
|||
<EuiPanel hasBorder={true}>
|
||||
<DashboardRenderer
|
||||
getCreationOptions={async (): Promise<DashboardCreationOptions> => {
|
||||
const builder = controlGroupInputBuilder;
|
||||
const controlGroupInput = getDefaultControlGroupInput();
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
const controlGroupState = {};
|
||||
await controlGroupStateBuilder.addDataControlFromField(controlGroupState, {
|
||||
dataViewId: dataView.id ?? '',
|
||||
title: 'Destintion country',
|
||||
fieldName: 'geo.dest',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
await controlGroupStateBuilder.addDataControlFromField(controlGroupState, {
|
||||
dataViewId: dataView.id ?? '',
|
||||
fieldName: 'bytes',
|
||||
width: 'medium',
|
||||
|
@ -85,7 +83,7 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
|||
getInitialInput: () => ({
|
||||
timeRange: { from: 'now-30d', to: 'now' },
|
||||
viewMode: ViewMode.VIEW,
|
||||
controlGroupInput,
|
||||
controlGroupState,
|
||||
}),
|
||||
};
|
||||
}}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import deepEqual from 'fast-deep-equal';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import { pick, omit, xor } from 'lodash';
|
||||
|
||||
import {
|
||||
|
@ -23,7 +22,6 @@ import {
|
|||
} from './control_group_panel_diff_system';
|
||||
import { ControlGroupInput } from '..';
|
||||
import {
|
||||
ControlsPanels,
|
||||
PersistableControlGroupInput,
|
||||
persistableControlGroupInputKeys,
|
||||
RawControlGroupAttributes,
|
||||
|
@ -103,32 +101,6 @@ const getPanelsAreEqual = (
|
|||
return true;
|
||||
};
|
||||
|
||||
export const controlGroupInputToRawControlGroupAttributes = (
|
||||
controlGroupInput: Omit<ControlGroupInput, 'id'>
|
||||
): RawControlGroupAttributes => {
|
||||
return {
|
||||
controlStyle: controlGroupInput.controlStyle,
|
||||
chainingSystem: controlGroupInput.chainingSystem,
|
||||
showApplySelections: controlGroupInput.showApplySelections,
|
||||
panelsJSON: JSON.stringify(controlGroupInput.panels),
|
||||
ignoreParentSettingsJSON: JSON.stringify(controlGroupInput.ignoreParentSettings),
|
||||
};
|
||||
};
|
||||
|
||||
export const generateNewControlIds = (controlGroupInput?: PersistableControlGroupInput) => {
|
||||
if (!controlGroupInput?.panels) return;
|
||||
|
||||
const newPanelsMap: ControlsPanels = {};
|
||||
for (const panel of Object.values(controlGroupInput.panels)) {
|
||||
const newId = v4();
|
||||
newPanelsMap[newId] = {
|
||||
...panel,
|
||||
explicitInput: { ...panel.explicitInput, id: newId },
|
||||
};
|
||||
}
|
||||
return { ...controlGroupInput, panels: newPanelsMap };
|
||||
};
|
||||
|
||||
export const rawControlGroupAttributesToControlGroupInput = (
|
||||
rawControlGroupAttributes: RawControlGroupAttributes
|
||||
): PersistableControlGroupInput | undefined => {
|
||||
|
|
|
@ -22,14 +22,12 @@ export {
|
|||
persistableControlGroupInputKeys,
|
||||
} from './control_group/types';
|
||||
export {
|
||||
controlGroupInputToRawControlGroupAttributes,
|
||||
rawControlGroupAttributesToControlGroupInput,
|
||||
rawControlGroupAttributesToSerializable,
|
||||
serializableToRawControlGroupAttributes,
|
||||
getDefaultControlGroupPersistableInput,
|
||||
persistableControlGroupInputIsEqual,
|
||||
getDefaultControlGroupInput,
|
||||
generateNewControlIds,
|
||||
} from './control_group/control_group_persistence';
|
||||
|
||||
export {
|
||||
|
|
|
@ -121,6 +121,7 @@ export function ControlGroup({
|
|||
paddingSize="none"
|
||||
color={draggingId ? 'success' : 'transparent'}
|
||||
className="controlsWrapper"
|
||||
data-test-subj="controls-group-wrapper"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
|
|
|
@ -24,11 +24,7 @@ import { apiPublishesAsyncFilters } from '../controls/data_controls/publishes_as
|
|||
|
||||
export type ControlGroupComparatorState = Pick<
|
||||
ControlGroupRuntimeState,
|
||||
| 'autoApplySelections'
|
||||
| 'chainingSystem'
|
||||
| 'ignoreParentSettings'
|
||||
| 'initialChildControlState'
|
||||
| 'labelPosition'
|
||||
'autoApplySelections' | 'chainingSystem' | 'ignoreParentSettings' | 'labelPosition'
|
||||
> & {
|
||||
controlsInOrder: ControlsInOrder;
|
||||
};
|
||||
|
@ -38,6 +34,7 @@ export function initializeControlGroupUnsavedChanges(
|
|||
children$: PresentationContainer['children$'],
|
||||
comparators: StateComparators<ControlGroupComparatorState>,
|
||||
snapshotControlsRuntimeState: () => ControlPanelsState,
|
||||
resetControlsUnsavedChanges: () => void,
|
||||
parentApi: unknown,
|
||||
lastSavedRuntimeState: ControlGroupRuntimeState
|
||||
) {
|
||||
|
@ -47,7 +44,6 @@ export function initializeControlGroupUnsavedChanges(
|
|||
chainingSystem: lastSavedRuntimeState.chainingSystem,
|
||||
controlsInOrder: getControlsInOrder(lastSavedRuntimeState.initialChildControlState),
|
||||
ignoreParentSettings: lastSavedRuntimeState.ignoreParentSettings,
|
||||
initialChildControlState: lastSavedRuntimeState.initialChildControlState,
|
||||
labelPosition: lastSavedRuntimeState.labelPosition,
|
||||
},
|
||||
parentApi,
|
||||
|
@ -72,6 +68,7 @@ export function initializeControlGroupUnsavedChanges(
|
|||
),
|
||||
asyncResetUnsavedChanges: async () => {
|
||||
controlGroupUnsavedChanges.api.resetUnsavedChanges();
|
||||
resetControlsUnsavedChanges();
|
||||
|
||||
const filtersReadyPromises: Array<Promise<void>> = [];
|
||||
Object.values(children$.value).forEach((controlApi) => {
|
||||
|
|
|
@ -34,12 +34,19 @@ import { chaining$, controlFetch$, controlGroupFetch$ } from './control_fetch';
|
|||
import { initControlsManager } from './init_controls_manager';
|
||||
import { openEditControlGroupFlyout } from './open_edit_control_group_flyout';
|
||||
import { deserializeControlGroup } from './serialization_utils';
|
||||
import { ControlGroupApi, ControlGroupRuntimeState, ControlGroupSerializedState } from './types';
|
||||
import {
|
||||
ControlGroupApi,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupSerializedState,
|
||||
ControlPanelsState,
|
||||
} from './types';
|
||||
import { ControlGroup } from './components/control_group';
|
||||
import { initSelectionsManager } from './selections_manager';
|
||||
import { initializeControlGroupUnsavedChanges } from './control_group_unsaved_changes_api';
|
||||
import { openDataControlEditor } from '../controls/data_controls/open_data_control_editor';
|
||||
|
||||
const DEFAULT_CHAINING_SYSTEM = 'HIERARCHICAL';
|
||||
|
||||
export const getControlGroupEmbeddableFactory = (services: {
|
||||
core: CoreStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
|
@ -60,7 +67,6 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
lastSavedRuntimeState
|
||||
) => {
|
||||
const {
|
||||
initialChildControlState,
|
||||
labelPosition: initialLabelPosition,
|
||||
chainingSystem,
|
||||
autoApplySelections,
|
||||
|
@ -68,19 +74,22 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
} = initialRuntimeState;
|
||||
|
||||
const autoApplySelections$ = new BehaviorSubject<boolean>(autoApplySelections);
|
||||
const parentDataViewId = apiPublishesDataViews(parentApi)
|
||||
? parentApi.dataViews.value?.[0]?.id
|
||||
: undefined;
|
||||
const defaultDataViewId = await services.dataViews.getDefaultId();
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>(
|
||||
lastSavedRuntimeState.initialChildControlState
|
||||
);
|
||||
const controlsManager = initControlsManager(
|
||||
initialChildControlState,
|
||||
parentDataViewId ?? (await services.dataViews.getDefaultId())
|
||||
initialRuntimeState.initialChildControlState,
|
||||
lastSavedControlsState$
|
||||
);
|
||||
const selectionsManager = initSelectionsManager({
|
||||
...controlsManager.api,
|
||||
autoApplySelections$,
|
||||
});
|
||||
const dataViews = new BehaviorSubject<DataView[] | undefined>(undefined);
|
||||
const chainingSystem$ = new BehaviorSubject<ControlGroupChainingSystem>(chainingSystem);
|
||||
const chainingSystem$ = new BehaviorSubject<ControlGroupChainingSystem>(
|
||||
chainingSystem ?? DEFAULT_CHAINING_SYSTEM
|
||||
);
|
||||
const ignoreParentSettings$ = new BehaviorSubject<ParentIgnoreSettings | undefined>(
|
||||
ignoreParentSettings
|
||||
);
|
||||
|
@ -104,6 +113,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
chainingSystem: [
|
||||
chainingSystem$,
|
||||
(next: ControlGroupChainingSystem) => chainingSystem$.next(next),
|
||||
(a, b) => (a ?? DEFAULT_CHAINING_SYSTEM) === (b ?? DEFAULT_CHAINING_SYSTEM),
|
||||
],
|
||||
ignoreParentSettings: [
|
||||
ignoreParentSettings$,
|
||||
|
@ -113,6 +123,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
labelPosition: [labelPosition$, (next: ControlStyle) => labelPosition$.next(next)],
|
||||
},
|
||||
controlsManager.snapshotControlsRuntimeState,
|
||||
controlsManager.resetControlsUnsavedChanges,
|
||||
parentApi,
|
||||
lastSavedRuntimeState
|
||||
);
|
||||
|
@ -159,20 +170,28 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
i18n.translate('controls.controlGroup.displayName', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
openAddDataControlFlyout: (settings) => {
|
||||
const { controlInputTransform } = settings ?? {
|
||||
controlInputTransform: (state) => state,
|
||||
};
|
||||
openAddDataControlFlyout: (options) => {
|
||||
const parentDataViewId = apiPublishesDataViews(parentApi)
|
||||
? parentApi.dataViews.value?.[0]?.id
|
||||
: undefined;
|
||||
const newControlState = controlsManager.getNewControlState();
|
||||
openDataControlEditor({
|
||||
initialState: controlsManager.getNewControlState(),
|
||||
initialState: {
|
||||
...newControlState,
|
||||
dataViewId:
|
||||
newControlState.dataViewId ?? parentDataViewId ?? defaultDataViewId ?? undefined,
|
||||
},
|
||||
onSave: ({ type: controlType, state: initialState }) => {
|
||||
controlsManager.api.addNewPanel({
|
||||
panelType: controlType,
|
||||
initialState: controlInputTransform!(
|
||||
initialState as Partial<ControlGroupSerializedState>,
|
||||
controlType
|
||||
),
|
||||
initialState: options?.controlInputTransform
|
||||
? options.controlInputTransform(
|
||||
initialState as Partial<ControlGroupSerializedState>,
|
||||
controlType
|
||||
)
|
||||
: initialState,
|
||||
});
|
||||
options?.onSave?.();
|
||||
},
|
||||
controlGroupApi: api,
|
||||
services,
|
||||
|
@ -207,6 +226,20 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
dataViews.next(newDataViews)
|
||||
);
|
||||
|
||||
const saveNotificationSubscription = apiHasSaveNotification(parentApi)
|
||||
? parentApi.saveNotification$.subscribe(() => {
|
||||
lastSavedControlsState$.next(controlsManager.snapshotControlsRuntimeState());
|
||||
|
||||
if (
|
||||
typeof autoApplySelections$.value === 'boolean' &&
|
||||
!autoApplySelections$.value &&
|
||||
selectionsManager.hasUnappliedSelections$.value
|
||||
) {
|
||||
selectionsManager.applySelections();
|
||||
}
|
||||
})
|
||||
: undefined;
|
||||
|
||||
/** Fetch the allowExpensiveQuries setting for the children to use if necessary */
|
||||
try {
|
||||
const { allowExpensiveQueries } = await services.core.http.get<{
|
||||
|
@ -235,6 +268,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
return () => {
|
||||
selectionsManager.cleanup();
|
||||
childrenDataViewsSubscription.unsubscribe();
|
||||
saveNotificationSubscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -6,27 +6,26 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DefaultDataControlState } from '../controls/data_controls/types';
|
||||
import { DefaultControlApi } from '../controls/types';
|
||||
import { initControlsManager, getLastUsedDataViewId } from './init_controls_manager';
|
||||
import { ControlPanelState } from './types';
|
||||
import { ControlPanelState, ControlPanelsState } from './types';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: jest.fn().mockReturnValue('delta'),
|
||||
}));
|
||||
|
||||
const DEFAULT_DATA_VIEW_ID = 'myDataView';
|
||||
|
||||
describe('PresentationContainer api', () => {
|
||||
const intialControlsState = {
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
bravo: { type: 'testControl', order: 1 },
|
||||
charlie: { type: 'testControl', order: 2 },
|
||||
};
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>(intialControlsState);
|
||||
|
||||
test('addNewPanel should add control at end of controls', async () => {
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
bravo: { type: 'testControl', order: 1 },
|
||||
charlie: { type: 'testControl', order: 2 },
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
const addNewPanelPromise = controlsManager.api.addNewPanel({
|
||||
panelType: 'testControl',
|
||||
initialState: {},
|
||||
|
@ -42,14 +41,7 @@ describe('PresentationContainer api', () => {
|
|||
});
|
||||
|
||||
test('removePanel should remove control', () => {
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
bravo: { type: 'testControl', order: 1 },
|
||||
charlie: { type: 'testControl', order: 2 },
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
controlsManager.api.removePanel('bravo');
|
||||
expect(controlsManager.controlsInOrder$.value.map((element) => element.id)).toEqual([
|
||||
'alpha',
|
||||
|
@ -58,14 +50,7 @@ describe('PresentationContainer api', () => {
|
|||
});
|
||||
|
||||
test('replacePanel should replace control', async () => {
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
bravo: { type: 'testControl', order: 1 },
|
||||
charlie: { type: 'testControl', order: 2 },
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
const replacePanelPromise = controlsManager.api.replacePanel('bravo', {
|
||||
panelType: 'testControl',
|
||||
initialState: {},
|
||||
|
@ -81,13 +66,7 @@ describe('PresentationContainer api', () => {
|
|||
|
||||
describe('untilInitialized', () => {
|
||||
test('should not resolve until all controls are initialized', async () => {
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
bravo: { type: 'testControl', order: 1 },
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
let isDone = false;
|
||||
controlsManager.api.untilInitialized().then(() => {
|
||||
isDone = true;
|
||||
|
@ -101,19 +80,18 @@ describe('PresentationContainer api', () => {
|
|||
|
||||
controlsManager.setControlApi('bravo', {} as unknown as DefaultControlApi);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(isDone).toBe(false);
|
||||
|
||||
controlsManager.setControlApi('charlie', {} as unknown as DefaultControlApi);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(isDone).toBe(true);
|
||||
});
|
||||
|
||||
test('should resolve when all control already initialized ', async () => {
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
bravo: { type: 'testControl', order: 1 },
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
controlsManager.setControlApi('alpha', {} as unknown as DefaultControlApi);
|
||||
controlsManager.setControlApi('bravo', {} as unknown as DefaultControlApi);
|
||||
controlsManager.setControlApi('charlie', {} as unknown as DefaultControlApi);
|
||||
|
||||
let isDone = false;
|
||||
controlsManager.api.untilInitialized().then(() => {
|
||||
|
@ -127,14 +105,14 @@ describe('PresentationContainer api', () => {
|
|||
});
|
||||
|
||||
describe('snapshotControlsRuntimeState', () => {
|
||||
const intialControlsState = {
|
||||
alpha: { type: 'testControl', order: 1 },
|
||||
bravo: { type: 'testControl', order: 0 },
|
||||
};
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>(intialControlsState);
|
||||
|
||||
test('should snapshot runtime state for all controls', async () => {
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: { type: 'testControl', order: 1 },
|
||||
bravo: { type: 'testControl', order: 0 },
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
controlsManager.setControlApi('alpha', {
|
||||
snapshotRuntimeState: () => {
|
||||
return { key1: 'alpha value' };
|
||||
|
@ -190,28 +168,120 @@ describe('getLastUsedDataViewId', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('resetControlsUnsavedChanges', () => {
|
||||
test(`should remove previous sessions's unsaved changes on reset`, () => {
|
||||
// last session's unsaved changes added 1 control
|
||||
const intialControlsState = {
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
};
|
||||
// last saved state is empty control group
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>({});
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
controlsManager.setControlApi('alpha', {} as unknown as DefaultControlApi);
|
||||
|
||||
expect(controlsManager.controlsInOrder$.value).toEqual([
|
||||
{
|
||||
id: 'alpha',
|
||||
type: 'testControl',
|
||||
},
|
||||
]);
|
||||
|
||||
controlsManager.resetControlsUnsavedChanges();
|
||||
expect(controlsManager.controlsInOrder$.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('should restore deleted control on reset', () => {
|
||||
const intialControlsState = {
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
};
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>(intialControlsState);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
controlsManager.setControlApi('alpha', {} as unknown as DefaultControlApi);
|
||||
|
||||
// delete control
|
||||
controlsManager.api.removePanel('alpha');
|
||||
|
||||
// deleted control should exist on reset
|
||||
controlsManager.resetControlsUnsavedChanges();
|
||||
expect(controlsManager.controlsInOrder$.value).toEqual([
|
||||
{
|
||||
id: 'alpha',
|
||||
type: 'testControl',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should restore controls to last saved state', () => {
|
||||
const intialControlsState = {};
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>(intialControlsState);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
|
||||
// add control
|
||||
controlsManager.api.addNewPanel({ panelType: 'testControl' });
|
||||
controlsManager.setControlApi('delta', {
|
||||
snapshotRuntimeState: () => {
|
||||
return {};
|
||||
},
|
||||
} as unknown as DefaultControlApi);
|
||||
|
||||
// simulate save
|
||||
lastSavedControlsState$.next(controlsManager.snapshotControlsRuntimeState());
|
||||
|
||||
// saved control should exist on reset
|
||||
controlsManager.resetControlsUnsavedChanges();
|
||||
expect(controlsManager.controlsInOrder$.value).toEqual([
|
||||
{
|
||||
id: 'delta',
|
||||
type: 'testControl',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Test edge case where adding a panel and resetting left orphaned control in children$
|
||||
test('should remove orphaned children on reset', () => {
|
||||
// baseline last saved state contains a single control
|
||||
const intialControlsState = {
|
||||
alpha: { type: 'testControl', order: 0 },
|
||||
};
|
||||
const lastSavedControlsState$ = new BehaviorSubject<ControlPanelsState>(intialControlsState);
|
||||
const controlsManager = initControlsManager(intialControlsState, lastSavedControlsState$);
|
||||
controlsManager.setControlApi('alpha', {} as unknown as DefaultControlApi);
|
||||
|
||||
// add another control
|
||||
controlsManager.api.addNewPanel({ panelType: 'testControl' });
|
||||
controlsManager.setControlApi('delta', {} as unknown as DefaultControlApi);
|
||||
expect(Object.keys(controlsManager.api.children$.value).length).toBe(2);
|
||||
|
||||
// reset to lastSavedControlsState
|
||||
controlsManager.resetControlsUnsavedChanges();
|
||||
// children$ should no longer contain control removed by resetting back to original control baseline
|
||||
expect(Object.keys(controlsManager.api.children$.value).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewControlState', () => {
|
||||
test('should contain defaults when there are no existing controls', () => {
|
||||
const controlsManager = initControlsManager({}, DEFAULT_DATA_VIEW_ID);
|
||||
const controlsManager = initControlsManager({}, new BehaviorSubject<ControlPanelsState>({}));
|
||||
expect(controlsManager.getNewControlState()).toEqual({
|
||||
grow: true,
|
||||
width: 'medium',
|
||||
dataViewId: DEFAULT_DATA_VIEW_ID,
|
||||
dataViewId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should start with defaults if there are existing controls', () => {
|
||||
const intialControlsState = {
|
||||
alpha: {
|
||||
type: 'testControl',
|
||||
order: 1,
|
||||
dataViewId: 'myOtherDataViewId',
|
||||
width: 'small',
|
||||
grow: false,
|
||||
} as ControlPanelState & Pick<DefaultDataControlState, 'dataViewId'>,
|
||||
};
|
||||
const controlsManager = initControlsManager(
|
||||
{
|
||||
alpha: {
|
||||
type: 'testControl',
|
||||
order: 1,
|
||||
dataViewId: 'myOtherDataViewId',
|
||||
width: 'small',
|
||||
grow: false,
|
||||
} as ControlPanelState & Pick<DefaultDataControlState, 'dataViewId'>,
|
||||
},
|
||||
DEFAULT_DATA_VIEW_ID
|
||||
intialControlsState,
|
||||
new BehaviorSubject<ControlPanelsState>(intialControlsState)
|
||||
);
|
||||
expect(controlsManager.getNewControlState()).toEqual({
|
||||
grow: true,
|
||||
|
@ -221,7 +291,7 @@ describe('getNewControlState', () => {
|
|||
});
|
||||
|
||||
test('should contain values of last added control', () => {
|
||||
const controlsManager = initControlsManager({}, DEFAULT_DATA_VIEW_ID);
|
||||
const controlsManager = initControlsManager({}, new BehaviorSubject<ControlPanelsState>({}));
|
||||
controlsManager.api.addNewPanel({
|
||||
panelType: 'testControl',
|
||||
initialState: {
|
||||
|
|
|
@ -38,22 +38,25 @@ export function getControlsInOrder(initialControlPanelsState: ControlPanelsState
|
|||
}
|
||||
|
||||
export function initControlsManager(
|
||||
initialControlPanelsState: ControlPanelsState,
|
||||
defaultDataViewId: string | null
|
||||
/**
|
||||
* Composed from last saved controls state and previous sessions's unsaved changes to controls state
|
||||
*/
|
||||
initialControlsState: ControlPanelsState,
|
||||
/**
|
||||
* Observable that publishes last saved controls state only
|
||||
*/
|
||||
lastSavedControlsState$: PublishingSubject<ControlPanelsState>
|
||||
) {
|
||||
const lastSavedControlsPanelState$ = new BehaviorSubject(initialControlPanelsState);
|
||||
const initialControlIds = Object.keys(initialControlPanelsState);
|
||||
const initialControlIds = Object.keys(initialControlsState);
|
||||
const children$ = new BehaviorSubject<{ [key: string]: DefaultControlApi }>({});
|
||||
let controlsPanelState: { [panelId: string]: DefaultControlState } = {
|
||||
...initialControlPanelsState,
|
||||
let currentControlsState: { [panelId: string]: DefaultControlState } = {
|
||||
...initialControlsState,
|
||||
};
|
||||
const controlsInOrder$ = new BehaviorSubject<ControlsInOrder>(
|
||||
getControlsInOrder(initialControlPanelsState)
|
||||
getControlsInOrder(initialControlsState)
|
||||
);
|
||||
const lastUsedDataViewId$ = new BehaviorSubject<string | undefined>(
|
||||
getLastUsedDataViewId(controlsInOrder$.value, initialControlPanelsState) ??
|
||||
defaultDataViewId ??
|
||||
undefined
|
||||
getLastUsedDataViewId(controlsInOrder$.value, initialControlsState)
|
||||
);
|
||||
const lastUsedWidth$ = new BehaviorSubject<ControlWidth>(DEFAULT_CONTROL_WIDTH);
|
||||
const lastUsedGrow$ = new BehaviorSubject<boolean>(DEFAULT_CONTROL_GROW);
|
||||
|
@ -108,12 +111,12 @@ export function initControlsManager(
|
|||
type: panelType,
|
||||
});
|
||||
controlsInOrder$.next(nextControlsInOrder);
|
||||
controlsPanelState[id] = initialState ?? {};
|
||||
currentControlsState[id] = initialState ?? {};
|
||||
return await untilControlLoaded(id);
|
||||
}
|
||||
|
||||
function removePanel(panelId: string) {
|
||||
delete controlsPanelState[panelId];
|
||||
delete currentControlsState[panelId];
|
||||
controlsInOrder$.next(controlsInOrder$.value.filter(({ id }) => id !== panelId));
|
||||
children$.next(omit(children$.value, panelId));
|
||||
}
|
||||
|
@ -161,7 +164,7 @@ export function initControlsManager(
|
|||
type: controlApi.type,
|
||||
width,
|
||||
/** Re-add the `explicitInput` layer on serialize so control group saved object retains shape */
|
||||
explicitInput: rest,
|
||||
explicitInput: { id, ...rest },
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -184,9 +187,30 @@ export function initControlsManager(
|
|||
});
|
||||
return controlsRuntimeState;
|
||||
},
|
||||
resetControlsUnsavedChanges: () => {
|
||||
currentControlsState = {
|
||||
...lastSavedControlsState$.value,
|
||||
};
|
||||
const nextControlsInOrder = getControlsInOrder(currentControlsState as ControlPanelsState);
|
||||
controlsInOrder$.next(nextControlsInOrder);
|
||||
|
||||
const nextControlIds = nextControlsInOrder.map(({ id }) => id);
|
||||
const children = { ...children$.value };
|
||||
let modifiedChildren = false;
|
||||
Object.keys(children).forEach((controlId) => {
|
||||
if (!nextControlIds.includes(controlId)) {
|
||||
// remove children that no longer exist after reset
|
||||
delete children[controlId];
|
||||
modifiedChildren = true;
|
||||
}
|
||||
});
|
||||
if (modifiedChildren) {
|
||||
children$.next(children);
|
||||
}
|
||||
},
|
||||
api: {
|
||||
getSerializedStateForChild: (childId: string) => {
|
||||
const controlPanelState = controlsPanelState[childId];
|
||||
const controlPanelState = currentControlsState[childId];
|
||||
return controlPanelState ? { rawState: controlPanelState } : undefined;
|
||||
},
|
||||
children$: children$ as PublishingSubject<{
|
||||
|
@ -230,26 +254,10 @@ export function initControlsManager(
|
|||
comparators: {
|
||||
controlsInOrder: [
|
||||
controlsInOrder$,
|
||||
(next: ControlsInOrder) => controlsInOrder$.next(next),
|
||||
(next: ControlsInOrder) => {}, // setter does nothing, controlsInOrder$ reset by resetControlsRuntimeState
|
||||
fastIsEqual,
|
||||
],
|
||||
// Control state differences tracked by controlApi comparators
|
||||
// Control ordering differences tracked by controlsInOrder comparator
|
||||
// initialChildControlState comparatator exists to reset controls manager to last saved state
|
||||
initialChildControlState: [
|
||||
lastSavedControlsPanelState$,
|
||||
(lastSavedControlPanelsState: ControlPanelsState) => {
|
||||
lastSavedControlsPanelState$.next(lastSavedControlPanelsState);
|
||||
controlsPanelState = {
|
||||
...lastSavedControlPanelsState,
|
||||
};
|
||||
controlsInOrder$.next(getControlsInOrder(lastSavedControlPanelsState));
|
||||
},
|
||||
() => true,
|
||||
],
|
||||
} as StateComparators<
|
||||
Pick<ControlGroupComparatorState, 'controlsInOrder' | 'initialChildControlState'>
|
||||
>,
|
||||
} as StateComparators<Pick<ControlGroupComparatorState, 'controlsInOrder'>>,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export const openEditControlGroupFlyout = (
|
|||
Object.keys(controlGroupApi.children$.getValue()).forEach((childId) => {
|
||||
controlGroupApi.removePanel(childId);
|
||||
});
|
||||
ref.close();
|
||||
closeOverlay(ref);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { SerializedPanelState } from '@kbn/presentation-containers';
|
||||
import { omit } from 'lodash';
|
||||
import { ControlGroupRuntimeState, ControlGroupSerializedState } from './types';
|
||||
import { parseReferenceName } from '../controls/data_controls/reference_name_utils';
|
||||
|
||||
export const deserializeControlGroup = (
|
||||
state: SerializedPanelState<ControlGroupSerializedState>
|
||||
|
@ -20,9 +21,9 @@ export const deserializeControlGroup = (
|
|||
const references = state.references ?? [];
|
||||
references.forEach((reference) => {
|
||||
const referenceName = reference.name;
|
||||
const panelId = referenceName.substring('controlGroup_'.length, referenceName.lastIndexOf(':'));
|
||||
if (panels[panelId]) {
|
||||
panels[panelId].dataViewId = reference.id;
|
||||
const { controlId } = parseReferenceName(referenceName);
|
||||
if (panels[controlId]) {
|
||||
panels[controlId].dataViewId = reference.id;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -65,8 +65,9 @@ export type ControlGroupApi = PresentationContainer &
|
|||
ignoreParentSettings$: PublishingSubject<ParentIgnoreSettings | undefined>;
|
||||
allowExpensiveQueries$: PublishingSubject<boolean>;
|
||||
untilInitialized: () => Promise<void>;
|
||||
openAddDataControlFlyout: (settings?: {
|
||||
openAddDataControlFlyout: (options?: {
|
||||
controlInputTransform?: ControlInputTransform;
|
||||
onSave?: () => void;
|
||||
}) => void;
|
||||
labelPosition: PublishingSubject<ControlStyle>;
|
||||
};
|
||||
|
|
|
@ -51,6 +51,7 @@ describe('initializeDataControl', () => {
|
|||
dataControl = initializeDataControl(
|
||||
'myControlId',
|
||||
'myControlType',
|
||||
'referenceNameSuffix',
|
||||
dataControlState,
|
||||
editorStateManager,
|
||||
controlGroupApi,
|
||||
|
@ -82,6 +83,7 @@ describe('initializeDataControl', () => {
|
|||
dataControl = initializeDataControl(
|
||||
'myControlId',
|
||||
'myControlType',
|
||||
'referenceNameSuffix',
|
||||
{
|
||||
...dataControlState,
|
||||
dataViewId: 'notGonnaFindMeDataViewId',
|
||||
|
@ -120,6 +122,7 @@ describe('initializeDataControl', () => {
|
|||
dataControl = initializeDataControl(
|
||||
'myControlId',
|
||||
'myControlType',
|
||||
'referenceNameSuffix',
|
||||
{
|
||||
...dataControlState,
|
||||
fieldName: 'notGonnaFindMeFieldName',
|
||||
|
|
|
@ -26,10 +26,12 @@ import { initializeDefaultControlApi } from '../initialize_default_control_api';
|
|||
import { ControlApiInitialization, ControlStateManager, DefaultControlState } from '../types';
|
||||
import { openDataControlEditor } from './open_data_control_editor';
|
||||
import { DataControlApi, DataControlFieldFormatter, DefaultDataControlState } from './types';
|
||||
import { getReferenceName } from './reference_name_utils';
|
||||
|
||||
export const initializeDataControl = <EditorState extends object = {}>(
|
||||
controlId: string,
|
||||
controlType: string,
|
||||
referenceNameSuffix: string,
|
||||
state: DefaultDataControlState,
|
||||
/**
|
||||
* `This state manager` should only include the state that the data control editor is
|
||||
|
@ -242,7 +244,7 @@ export const initializeDataControl = <EditorState extends object = {}>(
|
|||
},
|
||||
references: [
|
||||
{
|
||||
name: `controlGroup_${controlId}:${controlType}DataView`,
|
||||
name: getReferenceName(controlId, referenceNameSuffix),
|
||||
type: DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
id: dataViewId.getValue(),
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React, { useEffect } from 'react';
|
||||
import { BehaviorSubject, combineLatest, debounceTime, filter, skip } from 'rxjs';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
|
||||
import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter, Filter } from '@kbn/es-query';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
|
@ -87,6 +88,7 @@ export const getOptionsListControlFactory = (
|
|||
>(
|
||||
uuid,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
'optionsListDataView',
|
||||
initialState,
|
||||
{ searchTechnique: searchTechnique$, singleSelect: singleSelect$ },
|
||||
controlGroupApi,
|
||||
|
@ -243,7 +245,7 @@ export const getOptionsListControlFactory = (
|
|||
searchTechnique: searchTechnique$.getValue(),
|
||||
runPastTimeout: runPastTimeout$.getValue(),
|
||||
singleSelect: singleSelect$.getValue(),
|
||||
selections: selections.selectedOptions$.getValue(),
|
||||
selectedOptions: selections.selectedOptions$.getValue(),
|
||||
sort: sort$.getValue(),
|
||||
existsSelected: selections.existsSelected$.getValue(),
|
||||
exclude: selections.exclude$.getValue(),
|
||||
|
@ -277,7 +279,7 @@ export const getOptionsListControlFactory = (
|
|||
sort: [
|
||||
sort$,
|
||||
(sort) => sort$.next(sort),
|
||||
(a, b) => (a ?? OPTIONS_LIST_DEFAULT_SORT) === (b ?? OPTIONS_LIST_DEFAULT_SORT),
|
||||
(a, b) => fastIsEqual(a ?? OPTIONS_LIST_DEFAULT_SORT, b ?? OPTIONS_LIST_DEFAULT_SORT),
|
||||
],
|
||||
|
||||
/** This state cannot currently be changed after the control is created */
|
||||
|
|
|
@ -63,6 +63,7 @@ export const getRangesliderControlFactory = (
|
|||
const dataControl = initializeDataControl<Pick<RangesliderControlState, 'step'>>(
|
||||
uuid,
|
||||
RANGE_SLIDER_CONTROL,
|
||||
'rangeSliderDataView',
|
||||
initialState,
|
||||
{
|
||||
step: step$,
|
||||
|
@ -158,8 +159,8 @@ export const getRangesliderControlFactory = (
|
|||
if (error) {
|
||||
dataControl.api.setBlockingError(error);
|
||||
}
|
||||
max$.next(max);
|
||||
min$.next(min);
|
||||
max$.next(max !== undefined ? Math.ceil(max) : undefined);
|
||||
min$.next(min !== undefined ? Math.floor(min) : undefined);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const REFERENCE_NAME_PREFIX = 'controlGroup_';
|
||||
|
||||
export function getReferenceName(controlId: string, referenceNameSuffix: string) {
|
||||
return `${REFERENCE_NAME_PREFIX}${controlId}:${referenceNameSuffix}`;
|
||||
}
|
||||
|
||||
export function parseReferenceName(referenceName: string) {
|
||||
return {
|
||||
controlId: referenceName.substring(
|
||||
REFERENCE_NAME_PREFIX.length,
|
||||
referenceName.lastIndexOf(':')
|
||||
),
|
||||
};
|
||||
}
|
|
@ -35,16 +35,17 @@ import './components/index.scss';
|
|||
import { TimeSliderPrepend } from './components/time_slider_prepend';
|
||||
import { TIME_SLIDER_CONTROL } from '../../../../common';
|
||||
|
||||
const displayName = i18n.translate('controls.timesliderControl.displayName', {
|
||||
defaultMessage: 'Time slider',
|
||||
});
|
||||
|
||||
export const getTimesliderControlFactory = (
|
||||
services: Services
|
||||
): ControlFactory<TimesliderControlState, TimesliderControlApi> => {
|
||||
return {
|
||||
type: TIME_SLIDER_CONTROL,
|
||||
getIconType: () => 'search',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('controls.timesliderControl.displayName', {
|
||||
defaultMessage: 'Time slider',
|
||||
}),
|
||||
getDisplayName: () => displayName,
|
||||
buildControl: async (initialState, buildApi, uuid, controlGroupApi) => {
|
||||
const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } =
|
||||
initTimeRangeSubscription(controlGroupApi, services);
|
||||
|
@ -203,6 +204,7 @@ export const getTimesliderControlFactory = (
|
|||
const api = buildApi(
|
||||
{
|
||||
...defaultControl.api,
|
||||
defaultPanelTitle: new BehaviorSubject<string | undefined>(displayName),
|
||||
timeslice$,
|
||||
serializeState: () => {
|
||||
const { rawState: defaultControlState } = defaultControl.serialize();
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { PublishesTimeslice } from '@kbn/presentation-publishing';
|
||||
import type { PublishesPanelTitle, PublishesTimeslice } from '@kbn/presentation-publishing';
|
||||
import type { DefaultControlApi, DefaultControlState } from '../types';
|
||||
|
||||
export type Timeslice = [number, number];
|
||||
|
@ -20,7 +20,9 @@ export interface TimesliderControlState extends DefaultControlState {
|
|||
timesliceEndAsPercentageOfTimeRange?: number;
|
||||
}
|
||||
|
||||
export type TimesliderControlApi = DefaultControlApi & PublishesTimeslice;
|
||||
export type TimesliderControlApi = DefaultControlApi &
|
||||
Pick<PublishesPanelTitle, 'defaultPanelTitle'> &
|
||||
PublishesTimeslice;
|
||||
|
||||
export interface Services {
|
||||
core: CoreStart;
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { CONTROL_GROUP_TYPE, PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import {
|
||||
EmbeddableInput,
|
||||
EmbeddablePersistableStateService,
|
||||
|
@ -23,6 +22,10 @@ export const getReferencesForPanelId = (id: string, references: Reference[]): Re
|
|||
return filteredReferences;
|
||||
};
|
||||
|
||||
export const getReferencesForControls = (references: Reference[]): Reference[] => {
|
||||
return references.filter((reference) => reference.name.startsWith(controlGroupReferencePrefix));
|
||||
};
|
||||
|
||||
export const prefixReferencesFromPanel = (id: string, references: Reference[]): Reference[] => {
|
||||
const prefix = `${id}:`;
|
||||
return references
|
||||
|
@ -34,7 +37,6 @@ export const prefixReferencesFromPanel = (id: string, references: Reference[]):
|
|||
};
|
||||
|
||||
const controlGroupReferencePrefix = 'controlGroup_';
|
||||
const controlGroupId = 'dashboard_control_group';
|
||||
|
||||
export const createInject = (
|
||||
persistableStateService: EmbeddablePersistableStateService
|
||||
|
@ -90,27 +92,6 @@ export const createInject = (
|
|||
}
|
||||
}
|
||||
|
||||
// since the controlGroup is not part of the panels array, its references need to be injected separately
|
||||
if ('controlGroupInput' in workingState && workingState.controlGroupInput) {
|
||||
const controlGroupReferences = references
|
||||
.filter((reference) => reference.name.indexOf(controlGroupReferencePrefix) === 0)
|
||||
.map((reference) => ({
|
||||
...reference,
|
||||
name: reference.name.replace(controlGroupReferencePrefix, ''),
|
||||
}));
|
||||
|
||||
const { type, ...injectedControlGroupState } = persistableStateService.inject(
|
||||
{
|
||||
...workingState.controlGroupInput,
|
||||
type: CONTROL_GROUP_TYPE,
|
||||
id: controlGroupId,
|
||||
},
|
||||
controlGroupReferences
|
||||
);
|
||||
workingState.controlGroupInput =
|
||||
injectedControlGroupState as unknown as PersistableControlGroupInput;
|
||||
}
|
||||
|
||||
return workingState as EmbeddableStateWithType;
|
||||
};
|
||||
};
|
||||
|
@ -160,23 +141,6 @@ export const createExtract = (
|
|||
}
|
||||
}
|
||||
|
||||
// since the controlGroup is not part of the panels array, its references need to be extracted separately
|
||||
if ('controlGroupInput' in workingState && workingState.controlGroupInput) {
|
||||
const { state: extractedControlGroupState, references: controlGroupReferences } =
|
||||
persistableStateService.extract({
|
||||
...workingState.controlGroupInput,
|
||||
type: CONTROL_GROUP_TYPE,
|
||||
id: controlGroupId,
|
||||
});
|
||||
workingState.controlGroupInput =
|
||||
extractedControlGroupState as unknown as PersistableControlGroupInput;
|
||||
const prefixedControlGroupReferences = controlGroupReferences.map((reference) => ({
|
||||
...reference,
|
||||
name: `${controlGroupReferencePrefix}${reference.name}`,
|
||||
}));
|
||||
references.push(...prefixedControlGroupReferences);
|
||||
}
|
||||
|
||||
return { state: workingState as EmbeddableStateWithType, references };
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types';
|
||||
import { rawControlGroupAttributesToControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
|
||||
import {
|
||||
convertPanelMapToSavedPanels,
|
||||
|
@ -33,9 +32,6 @@ function parseDashboardAttributesWithType(
|
|||
}
|
||||
|
||||
return {
|
||||
controlGroupInput:
|
||||
attributes.controlGroupInput &&
|
||||
rawControlGroupAttributesToControlGroupInput(attributes.controlGroupInput),
|
||||
type: 'dashboard',
|
||||
panels: convertSavedPanelsToPanelMap(parsedPanels),
|
||||
} as ParsedDashboardAttributesWithType;
|
||||
|
@ -59,13 +55,6 @@ export function injectReferences(
|
|||
panelsJSON: JSON.stringify(injectedPanels),
|
||||
} as DashboardAttributes;
|
||||
|
||||
if (attributes.controlGroupInput && injectedState.controlGroupInput) {
|
||||
newAttributes.controlGroupInput = {
|
||||
...attributes.controlGroupInput,
|
||||
panelsJSON: JSON.stringify(injectedState.controlGroupInput.panels),
|
||||
};
|
||||
}
|
||||
|
||||
return newAttributes;
|
||||
}
|
||||
|
||||
|
@ -96,13 +85,6 @@ export function extractReferences(
|
|||
panelsJSON: JSON.stringify(extractedPanels),
|
||||
} as DashboardAttributes;
|
||||
|
||||
if (attributes.controlGroupInput && extractedState.controlGroupInput) {
|
||||
newAttributes.controlGroupInput = {
|
||||
...attributes.controlGroupInput,
|
||||
panelsJSON: JSON.stringify(extractedState.controlGroupInput.panels),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
references: [...references, ...extractedReferences],
|
||||
attributes: newAttributes,
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
|
||||
import { DashboardAttributes, SavedDashboardPanel } from './content_management';
|
||||
import { DashboardContainerInput, DashboardPanelMap } from './dashboard_container/types';
|
||||
|
||||
|
@ -40,7 +38,6 @@ export type SharedDashboardState = Partial<
|
|||
* A partially parsed version of the Dashboard Attributes used for inject and extract logic for both the Dashboard Container and the Dashboard Saved Object.
|
||||
*/
|
||||
export type ParsedDashboardAttributesWithType = EmbeddableStateWithType & {
|
||||
controlGroupInput?: PersistableControlGroupInput;
|
||||
panels: DashboardPanelMap;
|
||||
type: 'dashboard';
|
||||
};
|
||||
|
|
|
@ -9,9 +9,7 @@
|
|||
import { DashboardAppLocatorDefinition } from './locator';
|
||||
import { hashedItemStore } from '@kbn/kibana-utils-plugin/public';
|
||||
import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock';
|
||||
import { mockControlGroupInput } from '@kbn/controls-plugin/common/mocks';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
|
||||
describe('dashboard locator', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -193,16 +191,18 @@ describe('dashboard locator', () => {
|
|||
useHashedUrl: false,
|
||||
getDashboardFilterFields: async (dashboardId: string) => [],
|
||||
});
|
||||
const controlGroupInput = mockControlGroupInput() as unknown as SerializableControlGroupInput;
|
||||
const controlGroupState = {
|
||||
autoApplySelections: false,
|
||||
};
|
||||
const location = await definition.getLocation({
|
||||
controlGroupInput,
|
||||
controlGroupState,
|
||||
});
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: 'dashboards',
|
||||
path: `#/create?_g=()`,
|
||||
state: {
|
||||
controlGroupInput,
|
||||
controlGroupState,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,16 +8,16 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { getAddControlButtonTitle } from '../../_dashboard_app_strings';
|
||||
import { useDashboardAPI } from '../../dashboard_app';
|
||||
|
||||
interface Props {
|
||||
closePopover: () => void;
|
||||
controlGroup: ControlGroupContainer;
|
||||
controlGroupApi?: ControlGroupApi;
|
||||
}
|
||||
|
||||
export const AddDataControlButton = ({ closePopover, controlGroup, ...rest }: Props) => {
|
||||
export const AddDataControlButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
|
||||
const dashboard = useDashboardAPI();
|
||||
const onSave = () => {
|
||||
dashboard.scrollToTop();
|
||||
|
@ -28,9 +28,10 @@ export const AddDataControlButton = ({ closePopover, controlGroup, ...rest }: Pr
|
|||
{...rest}
|
||||
icon="plusInCircle"
|
||||
data-test-subj="controls-create-button"
|
||||
disabled={!controlGroupApi}
|
||||
aria-label={getAddControlButtonTitle()}
|
||||
onClick={() => {
|
||||
controlGroup.openAddDataControlFlyout({ onSave });
|
||||
controlGroupApi?.openAddDataControlFlyout({ onSave });
|
||||
closePopover();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -7,8 +7,12 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { ControlGroupContainer, TIME_SLIDER_CONTROL } from '@kbn/controls-plugin/public';
|
||||
import type { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { TIME_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
|
||||
|
||||
import { apiHasType } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
getAddTimeSliderControlButtonTitle,
|
||||
getOnlyOneTimeSliderControlMsg,
|
||||
|
@ -17,40 +21,47 @@ import { useDashboardAPI } from '../../dashboard_app';
|
|||
|
||||
interface Props {
|
||||
closePopover: () => void;
|
||||
controlGroup: ControlGroupContainer;
|
||||
controlGroupApi?: ControlGroupApi;
|
||||
}
|
||||
|
||||
export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest }: Props) => {
|
||||
export const AddTimeSliderControlButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
|
||||
const [hasTimeSliderControl, setHasTimeSliderControl] = useState(false);
|
||||
const dashboard = useDashboardAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = controlGroup.getInput$().subscribe(() => {
|
||||
const childIds = controlGroup.getChildIds();
|
||||
const nextHasTimeSliderControl = childIds.some((id: string) => {
|
||||
const child = controlGroup.getChild(id);
|
||||
return child.type === TIME_SLIDER_CONTROL;
|
||||
if (!controlGroupApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = controlGroupApi.children$.subscribe((children) => {
|
||||
const nextHasTimeSliderControl = Object.values(children).some((controlApi) => {
|
||||
return apiHasType(controlApi) && controlApi.type === TIME_SLIDER_CONTROL;
|
||||
});
|
||||
if (nextHasTimeSliderControl !== hasTimeSliderControl) {
|
||||
setHasTimeSliderControl(nextHasTimeSliderControl);
|
||||
}
|
||||
setHasTimeSliderControl(nextHasTimeSliderControl);
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [controlGroup, hasTimeSliderControl, setHasTimeSliderControl]);
|
||||
}, [controlGroupApi]);
|
||||
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
{...rest}
|
||||
icon="timeslider"
|
||||
onClick={async () => {
|
||||
await controlGroup.addTimeSliderControl();
|
||||
controlGroupApi?.addNewPanel({
|
||||
panelType: TIME_SLIDER_CONTROL,
|
||||
initialState: {
|
||||
grow: true,
|
||||
width: 'large',
|
||||
id: uuidv4(),
|
||||
},
|
||||
});
|
||||
dashboard.scrollToTop();
|
||||
closePopover();
|
||||
}}
|
||||
data-test-subj="controls-create-timeslider-button"
|
||||
disabled={hasTimeSliderControl}
|
||||
disabled={!controlGroupApi || hasTimeSliderControl}
|
||||
toolTipContent={hasTimeSliderControl ? getOnlyOneTimeSliderControlMsg() : null}
|
||||
>
|
||||
{getAddTimeSliderControlButtonTitle()}
|
||||
|
|
|
@ -10,18 +10,18 @@ import React from 'react';
|
|||
|
||||
import { EuiContextMenuPanel, useEuiTheme } from '@elastic/eui';
|
||||
import { ToolbarPopover } from '@kbn/shared-ux-button-toolbar';
|
||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { getControlButtonTitle } from '../../_dashboard_app_strings';
|
||||
import { AddDataControlButton } from './add_data_control_button';
|
||||
import { AddTimeSliderControlButton } from './add_time_slider_control_button';
|
||||
import { EditControlGroupButton } from './edit_control_group_button';
|
||||
|
||||
export function ControlsToolbarButton({
|
||||
controlGroup,
|
||||
controlGroupApi,
|
||||
isDisabled,
|
||||
}: {
|
||||
controlGroup: ControlGroupContainer;
|
||||
controlGroupApi?: ControlGroupApi;
|
||||
isDisabled?: boolean;
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
@ -43,17 +43,17 @@ export function ControlsToolbarButton({
|
|||
items={[
|
||||
<AddDataControlButton
|
||||
key="addControl"
|
||||
controlGroup={controlGroup}
|
||||
controlGroupApi={controlGroupApi}
|
||||
closePopover={closePopover}
|
||||
/>,
|
||||
<AddTimeSliderControlButton
|
||||
key="addTimeSliderControl"
|
||||
controlGroup={controlGroup}
|
||||
controlGroupApi={controlGroupApi}
|
||||
closePopover={closePopover}
|
||||
/>,
|
||||
<EditControlGroupButton
|
||||
key="manageControls"
|
||||
controlGroup={controlGroup}
|
||||
controlGroupApi={controlGroupApi}
|
||||
closePopover={closePopover}
|
||||
/>,
|
||||
]}
|
||||
|
|
|
@ -8,23 +8,24 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiContextMenuItem } from '@elastic/eui';
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { getEditControlGroupButtonTitle } from '../../_dashboard_app_strings';
|
||||
|
||||
interface Props {
|
||||
closePopover: () => void;
|
||||
controlGroup: ControlGroupContainer;
|
||||
controlGroupApi?: ControlGroupApi;
|
||||
}
|
||||
|
||||
export const EditControlGroupButton = ({ closePopover, controlGroup, ...rest }: Props) => {
|
||||
export const EditControlGroupButton = ({ closePopover, controlGroupApi, ...rest }: Props) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
{...rest}
|
||||
icon="gear"
|
||||
data-test-subj="controls-settings-button"
|
||||
disabled={!controlGroupApi}
|
||||
aria-label={getEditControlGroupButtonTitle()}
|
||||
onClick={() => {
|
||||
controlGroup.openEditControlGroupFlyout();
|
||||
controlGroupApi?.onEdit();
|
||||
closePopover();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useEuiTheme } from '@elastic/eui';
|
|||
import { AddFromLibraryButton, Toolbar, ToolbarButton } from '@kbn/shared-ux-button-toolbar';
|
||||
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings';
|
||||
import { EditorMenu } from './editor_menu';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
|
@ -82,6 +83,7 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
|||
* dismissNotification: Optional, if not passed a toast will appear in the dashboard
|
||||
*/
|
||||
|
||||
const controlGroupApi = useStateFromPublishingSubject(dashboard.controlGroupApi$);
|
||||
const extraButtons = [
|
||||
<EditorMenu createNewVisType={createNewVisType} isDisabled={isDisabled} api={dashboard} />,
|
||||
<AddFromLibraryButton
|
||||
|
@ -90,12 +92,8 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
|
|||
data-test-subj="dashboardAddFromLibraryButton"
|
||||
isDisabled={isDisabled}
|
||||
/>,
|
||||
<ControlsToolbarButton isDisabled={isDisabled} controlGroupApi={controlGroupApi} />,
|
||||
];
|
||||
if (dashboard.controlGroup) {
|
||||
extraButtons.push(
|
||||
<ControlsToolbarButton isDisabled={isDisabled} controlGroup={dashboard.controlGroup} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import { EuiCheckboxGroup } from '@elastic/eui';
|
||||
import type { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
import { QueryState } from '@kbn/data-plugin/common';
|
||||
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
|
||||
|
@ -26,6 +25,7 @@ import { DashboardLocatorParams } from '../../../dashboard_container';
|
|||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { dashboardUrlParams } from '../../dashboard_router';
|
||||
import { shareModalStrings } from '../../_dashboard_app_strings';
|
||||
import { PANELS_CONTROL_GROUP_KEY } from '../../../services/dashboard_backup/dashboard_backup_service';
|
||||
|
||||
const showFilterBarId = 'showFilterBar';
|
||||
|
||||
|
@ -169,7 +169,9 @@ export function ShowShareModal({
|
|||
unsavedStateForLocator = {
|
||||
query: unsavedDashboardState.query,
|
||||
filters: unsavedDashboardState.filters,
|
||||
controlGroupInput: unsavedDashboardState.controlGroupInput as SerializableControlGroupInput,
|
||||
controlGroupState: panelModifications?.[
|
||||
PANELS_CONTROL_GROUP_KEY
|
||||
] as DashboardLocatorParams['controlGroupState'],
|
||||
panels: allUnsavedPanels as DashboardLocatorParams['panels'],
|
||||
|
||||
// options
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'
|
|||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
|
@ -32,6 +33,8 @@ export const useDashboardMenuItems = ({
|
|||
maybeRedirect: (result?: SaveDashboardReturn) => void;
|
||||
showResetChange?: boolean;
|
||||
}) => {
|
||||
const isMounted = useMountedState();
|
||||
|
||||
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
|
||||
|
||||
/**
|
||||
|
@ -99,6 +102,7 @@ export const useDashboardMenuItems = ({
|
|||
* (1) reset the dashboard to the last saved state, and
|
||||
* (2) if `switchToViewMode` is `true`, set the dashboard to view mode.
|
||||
*/
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const resetChanges = useCallback(
|
||||
(switchToViewMode: boolean = false) => {
|
||||
dashboard.clearOverlays();
|
||||
|
@ -113,13 +117,17 @@ export const useDashboardMenuItems = ({
|
|||
return;
|
||||
}
|
||||
confirmDiscardUnsavedChanges(() => {
|
||||
batch(() => {
|
||||
dashboard.resetToLastSavedState();
|
||||
switchModes?.();
|
||||
batch(async () => {
|
||||
setIsResetting(true);
|
||||
await dashboard.asyncResetToLastSavedState();
|
||||
if (isMounted()) {
|
||||
setIsResetting(false);
|
||||
switchModes?.();
|
||||
}
|
||||
});
|
||||
}, viewMode);
|
||||
},
|
||||
[dashboard, dashboardBackup, hasUnsavedChanges, viewMode]
|
||||
[dashboard, dashboardBackup, hasUnsavedChanges, viewMode, isMounted]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -190,7 +198,8 @@ export const useDashboardMenuItems = ({
|
|||
switchToViewMode: {
|
||||
...topNavStrings.switchToViewMode,
|
||||
id: 'cancel',
|
||||
disableButton: disableTopNav || !lastSavedId,
|
||||
disableButton: disableTopNav || !lastSavedId || isResetting,
|
||||
isLoading: isResetting,
|
||||
testId: 'dashboardViewOnlyMode',
|
||||
run: () => resetChanges(true),
|
||||
} as TopNavMenuData,
|
||||
|
@ -226,6 +235,7 @@ export const useDashboardMenuItems = ({
|
|||
dashboardBackup,
|
||||
quickSaveDashboard,
|
||||
resetChanges,
|
||||
isResetting,
|
||||
]);
|
||||
|
||||
const resetChangesMenuItem = useMemo(() => {
|
||||
|
@ -234,12 +244,22 @@ export const useDashboardMenuItems = ({
|
|||
id: 'reset',
|
||||
testId: 'dashboardDiscardChangesMenuItem',
|
||||
disableButton:
|
||||
isResetting ||
|
||||
!hasUnsavedChanges ||
|
||||
hasOverlays ||
|
||||
(viewMode === ViewMode.EDIT && (isSaveInProgress || !lastSavedId)),
|
||||
isLoading: isResetting,
|
||||
run: () => resetChanges(),
|
||||
};
|
||||
}, [hasOverlays, lastSavedId, resetChanges, viewMode, isSaveInProgress, hasUnsavedChanges]);
|
||||
}, [
|
||||
hasOverlays,
|
||||
lastSavedId,
|
||||
resetChanges,
|
||||
viewMode,
|
||||
isSaveInProgress,
|
||||
hasUnsavedChanges,
|
||||
isResetting,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Build ordered menus for view and edit mode.
|
||||
|
|
|
@ -44,7 +44,7 @@ jest.mock('./dashboard_grid_item', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const createAndMountDashboardGrid = () => {
|
||||
const createAndMountDashboardGrid = async () => {
|
||||
const dashboardContainer = buildMockDashboard({
|
||||
overrides: {
|
||||
panels: {
|
||||
|
@ -61,6 +61,7 @@ const createAndMountDashboardGrid = () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
await dashboardContainer.untilContainerInitialized();
|
||||
const component = mountWithIntl(
|
||||
<DashboardContainerContext.Provider value={dashboardContainer}>
|
||||
<DashboardGrid viewportWidth={1000} />
|
||||
|
@ -70,20 +71,20 @@ const createAndMountDashboardGrid = () => {
|
|||
};
|
||||
|
||||
test('renders DashboardGrid', async () => {
|
||||
const { component } = createAndMountDashboardGrid();
|
||||
const { component } = await createAndMountDashboardGrid();
|
||||
const panelElements = component.find('GridItem');
|
||||
expect(panelElements.length).toBe(2);
|
||||
});
|
||||
|
||||
test('renders DashboardGrid with no visualizations', async () => {
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
const { dashboardContainer, component } = await createAndMountDashboardGrid();
|
||||
dashboardContainer.updateInput({ panels: {} });
|
||||
component.update();
|
||||
expect(component.find('GridItem').length).toBe(0);
|
||||
});
|
||||
|
||||
test('DashboardGrid removes panel when removed from container', async () => {
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
const { dashboardContainer, component } = await createAndMountDashboardGrid();
|
||||
const originalPanels = dashboardContainer.getInput().panels;
|
||||
const filteredPanels = { ...originalPanels };
|
||||
delete filteredPanels['1'];
|
||||
|
@ -94,7 +95,7 @@ test('DashboardGrid removes panel when removed from container', async () => {
|
|||
});
|
||||
|
||||
test('DashboardGrid renders expanded panel', async () => {
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
const { dashboardContainer, component } = await createAndMountDashboardGrid();
|
||||
dashboardContainer.setExpandedPanelId('1');
|
||||
component.update();
|
||||
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
|
||||
|
@ -112,7 +113,7 @@ test('DashboardGrid renders expanded panel', async () => {
|
|||
});
|
||||
|
||||
test('DashboardGrid renders focused panel', async () => {
|
||||
const { dashboardContainer, component } = createAndMountDashboardGrid();
|
||||
const { dashboardContainer, component } = await createAndMountDashboardGrid();
|
||||
dashboardContainer.setFocusedPanelId('2');
|
||||
component.update();
|
||||
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
|
||||
|
|
|
@ -9,12 +9,19 @@
|
|||
import { debounce } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { EuiPortal } from '@elastic/eui';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
|
||||
|
||||
import {
|
||||
ControlGroupApi,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupSerializedState,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { useDashboardContainer } from '../../embeddable/dashboard_container';
|
||||
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
|
||||
|
@ -34,23 +41,11 @@ export const useDebouncedWidthObserver = (skipDebounce = false, wait = 100) => {
|
|||
};
|
||||
|
||||
export const DashboardViewportComponent = () => {
|
||||
const controlsRoot = useRef(null);
|
||||
|
||||
const dashboard = useDashboardContainer();
|
||||
|
||||
/**
|
||||
* Render Control group
|
||||
*/
|
||||
const controlGroup = dashboard.controlGroup;
|
||||
useEffect(() => {
|
||||
if (controlGroup && controlsRoot.current) controlGroup.render(controlsRoot.current);
|
||||
}, [controlGroup]);
|
||||
|
||||
const controlGroupApi = useStateFromPublishingSubject(dashboard.controlGroupApi$);
|
||||
const panelCount = Object.keys(dashboard.select((state) => state.explicitInput.panels)).length;
|
||||
const controlCount = Object.keys(
|
||||
controlGroup?.select((state) => state.explicitInput.panels) ?? {}
|
||||
).length;
|
||||
|
||||
const [hasControls, setHasControls] = useState(false);
|
||||
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
|
||||
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
|
||||
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
|
||||
|
@ -65,17 +60,59 @@ export const DashboardViewportComponent = () => {
|
|||
'dshDashboardViewport--panelExpanded': Boolean(expandedPanelId),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!controlGroupApi) {
|
||||
return;
|
||||
}
|
||||
const subscription = controlGroupApi.children$.subscribe((children) => {
|
||||
setHasControls(Object.keys(children).length > 0);
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [controlGroupApi]);
|
||||
|
||||
const [dashboardInitialized, setDashboardInitialized] = useState(false);
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
dashboard.untilContainerInitialized().then(() => {
|
||||
if (!ignore) {
|
||||
setDashboardInitialized(true);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [dashboard]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('dshDashboardViewportWrapper', {
|
||||
'dshDashboardViewportWrapper--defaultBg': !useMargins,
|
||||
})}
|
||||
>
|
||||
{controlGroup && viewMode !== ViewMode.PRINT ? (
|
||||
<div
|
||||
className={controlCount > 0 ? 'dshDashboardViewport-controls' : ''}
|
||||
ref={controlsRoot}
|
||||
/>
|
||||
{viewMode !== ViewMode.PRINT ? (
|
||||
<div className={hasControls ? 'dshDashboardViewport-controls' : ''}>
|
||||
<ReactEmbeddableRenderer<
|
||||
ControlGroupSerializedState,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupApi
|
||||
>
|
||||
key={dashboard.getInput().id}
|
||||
hidePanelChrome={true}
|
||||
panelProps={{ hideLoader: true }}
|
||||
type={CONTROL_GROUP_TYPE}
|
||||
maybeId={'control_group'}
|
||||
getParentApi={() => {
|
||||
return {
|
||||
...dashboard,
|
||||
getSerializedStateForChild: dashboard.getSerializedStateForControlGroup,
|
||||
getRuntimeStateForChild: dashboard.getRuntimeStateForControlGroup,
|
||||
};
|
||||
}}
|
||||
onApiAvailable={(api) => dashboard.setControlGroupApi(api)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{panelCount === 0 && <DashboardEmptyScreen />}
|
||||
<div
|
||||
|
@ -88,7 +125,9 @@ export const DashboardViewportComponent = () => {
|
|||
>
|
||||
{/* Wait for `viewportWidth` to actually be set before rendering the dashboard grid -
|
||||
otherwise, there is a race condition where the panels can end up being squashed */}
|
||||
{viewportWidth !== 0 && <DashboardGrid viewportWidth={viewportWidth} />}
|
||||
{viewportWidth !== 0 && dashboardInitialized && (
|
||||
<DashboardGrid viewportWidth={viewportWidth} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import type { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import {
|
||||
EmbeddableInput,
|
||||
|
@ -89,13 +88,17 @@ export async function runQuickSave(this: DashboardContainer) {
|
|||
const { panels: nextPanels, references } = await serializeAllPanelState(this);
|
||||
const dashboardStateToSave: DashboardContainerInput = { ...currentState, panels: nextPanels };
|
||||
let stateToSave: SavedDashboardInput = dashboardStateToSave;
|
||||
let persistableControlGroupInput: PersistableControlGroupInput | undefined;
|
||||
if (this.controlGroup) {
|
||||
persistableControlGroupInput = this.controlGroup.getPersistableInput();
|
||||
stateToSave = { ...stateToSave, controlGroupInput: persistableControlGroupInput };
|
||||
const controlGroupApi = this.controlGroupApi$.value;
|
||||
let controlGroupReferences: Reference[] | undefined;
|
||||
if (controlGroupApi) {
|
||||
const { rawState: controlGroupSerializedState, references: extractedReferences } =
|
||||
await controlGroupApi.serializeState();
|
||||
controlGroupReferences = extractedReferences;
|
||||
stateToSave = { ...stateToSave, controlGroupInput: controlGroupSerializedState };
|
||||
}
|
||||
|
||||
const saveResult = await saveDashboardState({
|
||||
controlGroupReferences,
|
||||
panelReferences: references,
|
||||
currentState: stateToSave,
|
||||
saveOptions: {},
|
||||
|
@ -105,9 +108,6 @@ export async function runQuickSave(this: DashboardContainer) {
|
|||
this.savedObjectReferences = saveResult.references ?? [];
|
||||
this.dispatch.setLastSavedInput(dashboardStateToSave);
|
||||
this.saveNotification$.next();
|
||||
if (this.controlGroup && persistableControlGroupInput) {
|
||||
this.controlGroup.setSavedState(persistableControlGroupInput);
|
||||
}
|
||||
|
||||
return saveResult;
|
||||
}
|
||||
|
@ -180,19 +180,20 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
|
|||
stateFromSaveModal.tags = newTags;
|
||||
}
|
||||
|
||||
let dashboardStateToSave: DashboardContainerInput & {
|
||||
controlGroupInput?: PersistableControlGroupInput;
|
||||
} = {
|
||||
let dashboardStateToSave: SavedDashboardInput = {
|
||||
...currentState,
|
||||
...stateFromSaveModal,
|
||||
};
|
||||
|
||||
let persistableControlGroupInput: PersistableControlGroupInput | undefined;
|
||||
if (this.controlGroup) {
|
||||
persistableControlGroupInput = this.controlGroup.getPersistableInput();
|
||||
const controlGroupApi = this.controlGroupApi$.value;
|
||||
let controlGroupReferences: Reference[] | undefined;
|
||||
if (controlGroupApi) {
|
||||
const { rawState: controlGroupSerializedState, references } =
|
||||
await controlGroupApi.serializeState();
|
||||
controlGroupReferences = references;
|
||||
dashboardStateToSave = {
|
||||
...dashboardStateToSave,
|
||||
controlGroupInput: persistableControlGroupInput,
|
||||
controlGroupInput: controlGroupSerializedState,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -225,6 +226,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
|
|||
const beforeAddTime = window.performance.now();
|
||||
|
||||
const saveResult = await saveDashboardState({
|
||||
controlGroupReferences,
|
||||
panelReferences: references,
|
||||
saveOptions,
|
||||
currentState: {
|
||||
|
@ -251,9 +253,6 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
|
|||
batch(() => {
|
||||
this.dispatch.setStateFromSaveModal(stateFromSaveModal);
|
||||
this.dispatch.setLastSavedInput(dashboardStateToSave);
|
||||
if (this.controlGroup && persistableControlGroupInput) {
|
||||
this.controlGroup.setSavedState(persistableControlGroupInput);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { mockControlGroupInput } from '@kbn/controls-plugin/common/mocks';
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public/control_group/embeddable/control_group_container';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { combineDashboardFiltersWithControlGroupFilters } from './dashboard_control_group_integration';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
jest.mock('@kbn/controls-plugin/public/control_group/embeddable/control_group_container');
|
||||
|
||||
|
@ -51,46 +49,41 @@ const testFilter3: Filter = {
|
|||
},
|
||||
};
|
||||
|
||||
const mockControlGroupContainer = new ControlGroupContainer(
|
||||
{ getTools: () => {} } as unknown as ReduxToolsPackage,
|
||||
mockControlGroupInput()
|
||||
);
|
||||
describe('combineDashboardFiltersWithControlGroupFilters', () => {
|
||||
it('Combined filter pills do not get overwritten', async () => {
|
||||
const dashboardFilterPills = [testFilter1, testFilter2];
|
||||
const mockControlGroupApi = {
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>([]),
|
||||
};
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
dashboardFilterPills,
|
||||
mockControlGroupApi
|
||||
);
|
||||
expect(combinedFilters).toEqual(dashboardFilterPills);
|
||||
});
|
||||
|
||||
describe('Test dashboard control group', () => {
|
||||
describe('Combine dashboard filters with control group filters test', () => {
|
||||
it('Combined filter pills do not get overwritten', async () => {
|
||||
const dashboardFilterPills = [testFilter1, testFilter2];
|
||||
mockControlGroupContainer.getOutput = jest.fn().mockReturnValue({ filters: [] });
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
dashboardFilterPills,
|
||||
mockControlGroupContainer
|
||||
);
|
||||
expect(combinedFilters).toEqual(dashboardFilterPills);
|
||||
});
|
||||
it('Combined control filters do not get overwritten', async () => {
|
||||
const controlGroupFilters = [testFilter1, testFilter2];
|
||||
const mockControlGroupApi = {
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>(controlGroupFilters),
|
||||
};
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
[] as Filter[],
|
||||
mockControlGroupApi
|
||||
);
|
||||
expect(combinedFilters).toEqual(controlGroupFilters);
|
||||
});
|
||||
|
||||
it('Combined control filters do not get overwritten', async () => {
|
||||
const controlGroupFilters = [testFilter1, testFilter2];
|
||||
mockControlGroupContainer.getOutput = jest
|
||||
.fn()
|
||||
.mockReturnValue({ filters: controlGroupFilters });
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
[] as Filter[],
|
||||
mockControlGroupContainer
|
||||
);
|
||||
expect(combinedFilters).toEqual(controlGroupFilters);
|
||||
});
|
||||
|
||||
it('Combined dashboard filter pills and control filters do not get overwritten', async () => {
|
||||
const dashboardFilterPills = [testFilter1, testFilter2];
|
||||
const controlGroupFilters = [testFilter3];
|
||||
mockControlGroupContainer.getOutput = jest
|
||||
.fn()
|
||||
.mockReturnValue({ filters: controlGroupFilters });
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
dashboardFilterPills,
|
||||
mockControlGroupContainer
|
||||
);
|
||||
expect(combinedFilters).toEqual(dashboardFilterPills.concat(controlGroupFilters));
|
||||
});
|
||||
it('Combined dashboard filter pills and control filters do not get overwritten', async () => {
|
||||
const dashboardFilterPills = [testFilter1, testFilter2];
|
||||
const controlGroupFilters = [testFilter3];
|
||||
const mockControlGroupApi = {
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>(controlGroupFilters),
|
||||
};
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
dashboardFilterPills,
|
||||
mockControlGroupApi
|
||||
);
|
||||
expect(combinedFilters).toEqual(dashboardFilterPills.concat(controlGroupFilters));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,114 +6,95 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
|
||||
import { combineCompatibleChildrenApis } from '@kbn/presentation-containers';
|
||||
import { apiPublishesDataLoading, PublishesDataLoading } from '@kbn/presentation-publishing';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { isEqual } from 'lodash';
|
||||
import { distinctUntilChanged, Observable, skip } from 'rxjs';
|
||||
import { DashboardContainerInput } from '../../../../../common';
|
||||
import { COMPARE_ALL_OPTIONS, compareFilters, type Filter } from '@kbn/es-query';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
of,
|
||||
skip,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
import { PublishesFilters, PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { DashboardContainer } from '../../dashboard_container';
|
||||
|
||||
interface DiffChecks {
|
||||
[key: string]: (a?: unknown, b?: unknown) => boolean;
|
||||
}
|
||||
export function startSyncingDashboardControlGroup(dashboard: DashboardContainer) {
|
||||
const controlGroupFilters$ = dashboard.controlGroupApi$.pipe(
|
||||
switchMap((controlGroupApi) => (controlGroupApi ? controlGroupApi.filters$ : of(undefined)))
|
||||
);
|
||||
const controlGroupTimeslice$ = dashboard.controlGroupApi$.pipe(
|
||||
switchMap((controlGroupApi) => (controlGroupApi ? controlGroupApi.timeslice$ : of(undefined)))
|
||||
);
|
||||
|
||||
const distinctUntilDiffCheck = <T extends {}>(a: T, b: T, diffChecks: DiffChecks) =>
|
||||
!(Object.keys(diffChecks) as Array<keyof T>)
|
||||
.map((key) => deepEqual(a[key], b[key]))
|
||||
.includes(false);
|
||||
|
||||
type DashboardControlGroupCommonKeys = keyof Pick<
|
||||
DashboardContainerInput | ControlGroupInput,
|
||||
'filters' | 'lastReloadRequestTime' | 'timeRange' | 'query'
|
||||
>;
|
||||
|
||||
export function startSyncingDashboardControlGroup(this: DashboardContainer) {
|
||||
if (!this.controlGroup) return;
|
||||
|
||||
const compareAllFilters = (a?: Filter[], b?: Filter[]) =>
|
||||
compareFilters(a ?? [], b ?? [], COMPARE_ALL_OPTIONS);
|
||||
|
||||
const dashboardRefetchDiff: DiffChecks = {
|
||||
filters: (a, b) => compareAllFilters(a as Filter[], b as Filter[]),
|
||||
timeRange: deepEqual,
|
||||
query: deepEqual,
|
||||
viewMode: deepEqual,
|
||||
};
|
||||
|
||||
// pass down any pieces of input needed to refetch or force refetch data for the controls
|
||||
this.integrationSubscriptions.add(
|
||||
(this.getInput$() as Readonly<Observable<DashboardContainerInput>>)
|
||||
// --------------------------------------------------------------------------------------
|
||||
// dashboard.unifiedSearchFilters$
|
||||
// --------------------------------------------------------------------------------------
|
||||
const unifiedSearchFilters$ = new BehaviorSubject<Filter[] | undefined>(
|
||||
dashboard.getInput().filters
|
||||
);
|
||||
dashboard.unifiedSearchFilters$ = unifiedSearchFilters$ as PublishingSubject<
|
||||
Filter[] | undefined
|
||||
>;
|
||||
dashboard.publishingSubscription.add(
|
||||
dashboard
|
||||
.getInput$()
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) =>
|
||||
distinctUntilDiffCheck<DashboardContainerInput>(a, b, dashboardRefetchDiff)
|
||||
)
|
||||
startWith(dashboard.getInput()),
|
||||
map((input) => input.filters),
|
||||
distinctUntilChanged((previous, current) => {
|
||||
return compareFilters(previous ?? [], current ?? [], COMPARE_ALL_OPTIONS);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
const newInput: { [key: string]: unknown } = {};
|
||||
(Object.keys(dashboardRefetchDiff) as DashboardControlGroupCommonKeys[]).forEach((key) => {
|
||||
if (
|
||||
!dashboardRefetchDiff[key]?.(this.getInput()[key], this.controlGroup!.getInput()[key])
|
||||
) {
|
||||
newInput[key] = this.getInput()[key];
|
||||
}
|
||||
});
|
||||
if (Object.keys(newInput).length > 0) {
|
||||
this.controlGroup!.updateInput(newInput);
|
||||
}
|
||||
.subscribe((unifiedSearchFilters) => {
|
||||
unifiedSearchFilters$.next(unifiedSearchFilters);
|
||||
})
|
||||
);
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set dashboard.filters$ to include unified search filters and control group filters
|
||||
// --------------------------------------------------------------------------------------
|
||||
function getCombinedFilters() {
|
||||
return combineDashboardFiltersWithControlGroupFilters(
|
||||
dashboard.getInput().filters ?? [],
|
||||
dashboard.controlGroupApi$.value
|
||||
);
|
||||
}
|
||||
|
||||
const filters$ = new BehaviorSubject<Filter[] | undefined>(getCombinedFilters());
|
||||
dashboard.filters$ = filters$;
|
||||
|
||||
dashboard.publishingSubscription.add(
|
||||
combineLatest([dashboard.unifiedSearchFilters$, controlGroupFilters$]).subscribe(() => {
|
||||
filters$.next(getCombinedFilters());
|
||||
})
|
||||
);
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// when control group outputs filters, force a refresh!
|
||||
this.integrationSubscriptions.add(
|
||||
this.controlGroup
|
||||
.getOutput$()
|
||||
// --------------------------------------------------------------------------------------
|
||||
dashboard.publishingSubscription.add(
|
||||
controlGroupFilters$
|
||||
.pipe(
|
||||
distinctUntilChanged(({ filters: filtersA }, { filters: filtersB }) =>
|
||||
compareAllFilters(filtersA, filtersB)
|
||||
),
|
||||
skip(1) // skip first filter output because it will have been applied in initialize
|
||||
)
|
||||
.subscribe(() => this.forceRefresh(false)) // we should not reload the control group when the control group output changes - otherwise, performance is severely impacted
|
||||
.subscribe(() => dashboard.forceRefresh(false)) // we should not reload the control group when the control group output changes - otherwise, performance is severely impacted
|
||||
);
|
||||
|
||||
this.integrationSubscriptions.add(
|
||||
this.controlGroup
|
||||
.getOutput$()
|
||||
.pipe(
|
||||
distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) =>
|
||||
isEqual(timesliceA, timesliceB)
|
||||
)
|
||||
)
|
||||
.subscribe(({ timeslice }) => {
|
||||
if (!isEqual(timeslice, this.getInput().timeslice)) {
|
||||
this.dispatch.setTimeslice(timeslice);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// the Control Group needs to know when any dashboard children are loading in order to know when to move on to the next time slice when playing.
|
||||
this.integrationSubscriptions.add(
|
||||
combineCompatibleChildrenApis<PublishesDataLoading, boolean>(
|
||||
this,
|
||||
'dataLoading',
|
||||
apiPublishesDataLoading,
|
||||
false,
|
||||
(childrenLoading) => childrenLoading.some(Boolean)
|
||||
)
|
||||
.pipe(skip(1)) // skip the initial output of "false"
|
||||
.subscribe((anyChildLoading) =>
|
||||
this.controlGroup?.anyControlOutputConsumerLoading$.next(anyChildLoading)
|
||||
)
|
||||
// --------------------------------------------------------------------------------------
|
||||
// when control group outputs timeslice, dispatch timeslice
|
||||
// --------------------------------------------------------------------------------------
|
||||
dashboard.publishingSubscription.add(
|
||||
controlGroupTimeslice$.subscribe((timeslice) => {
|
||||
dashboard.dispatch.setTimeslice(timeslice);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const combineDashboardFiltersWithControlGroupFilters = (
|
||||
dashboardFilters: Filter[],
|
||||
controlGroup: ControlGroupContainer
|
||||
controlGroupApi?: PublishesFilters
|
||||
): Filter[] => {
|
||||
return [...dashboardFilters, ...(controlGroup.getOutput().filters ?? [])];
|
||||
return [...dashboardFilters, ...(controlGroupApi?.filters$.value ?? [])];
|
||||
};
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import {
|
||||
ContactCardEmbeddable,
|
||||
ContactCardEmbeddableFactory,
|
||||
|
@ -15,11 +13,6 @@ import {
|
|||
ContactCardEmbeddableOutput,
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
} from '@kbn/embeddable-plugin/public/lib/test_samples';
|
||||
import {
|
||||
ControlGroupInput,
|
||||
ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -29,6 +22,7 @@ import { getSampleDashboardPanel } from '../../../mocks';
|
|||
import { pluginServices } from '../../../services/plugin_services';
|
||||
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
||||
import { DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
|
||||
import { mockControlGroupApi } from '../../../mocks';
|
||||
|
||||
test("doesn't throw error when no data views are available", async () => {
|
||||
pluginServices.getServices().data.dataViews.defaultDataViewExists = jest
|
||||
|
@ -416,6 +410,7 @@ test('creates new embeddable with incoming embeddable if id does not match exist
|
|||
},
|
||||
}),
|
||||
});
|
||||
dashboard?.setControlGroupApi(mockControlGroupApi);
|
||||
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
@ -476,6 +471,7 @@ test('creates new embeddable with specified size if size is provided', async ()
|
|||
},
|
||||
}),
|
||||
});
|
||||
dashboard?.setControlGroupApi(mockControlGroupApi);
|
||||
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
|
@ -497,42 +493,6 @@ test('creates new embeddable with specified size if size is provided', async ()
|
|||
expect(dashboard!.getState().explicitInput.panels.new_panel.gridData.h).toBe(1);
|
||||
});
|
||||
|
||||
test('creates a control group from the control group factory', async () => {
|
||||
const mockControlGroupContainer = {
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
updateInput: jest.fn(),
|
||||
getInput: jest.fn().mockReturnValue({}),
|
||||
getInput$: jest.fn().mockReturnValue(new Observable()),
|
||||
getOutput: jest.fn().mockReturnValue({}),
|
||||
getOutput$: jest.fn().mockReturnValue(new Observable()),
|
||||
onFiltersPublished$: new Observable(),
|
||||
unsavedChanges: new BehaviorSubject(undefined),
|
||||
} as unknown as ControlGroupContainer;
|
||||
const mockControlGroupFactory = {
|
||||
create: jest.fn().mockReturnValue(mockControlGroupContainer),
|
||||
} as unknown as ControlGroupContainerFactory;
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockControlGroupFactory);
|
||||
await createDashboard({
|
||||
useControlGroupIntegration: true,
|
||||
getInitialInput: () => ({
|
||||
controlGroupInput: { controlStyle: 'twoLine' } as unknown as ControlGroupInput,
|
||||
}),
|
||||
});
|
||||
// flush promises
|
||||
await new Promise((r) => setTimeout(r, 1));
|
||||
expect(pluginServices.getServices().embeddable.getEmbeddableFactory).toHaveBeenCalledWith(
|
||||
'control_group'
|
||||
);
|
||||
expect(mockControlGroupFactory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ controlStyle: 'twoLine' }),
|
||||
undefined,
|
||||
{ lastSavedInput: expect.objectContaining({ controlStyle: 'oneLine' }) }
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* dashboard.getInput$() subscriptions are used to update:
|
||||
* 1) dashboard instance searchSessionId state
|
||||
|
@ -567,6 +527,7 @@ test('searchSessionId is updated prior to child embeddable parent subscription e
|
|||
createSessionRestorationDataProvider: () => {},
|
||||
} as unknown as DashboardCreationOptions['searchSessionSettings'],
|
||||
});
|
||||
dashboard?.setControlGroupApi(mockControlGroupApi);
|
||||
expect(dashboard).toBeDefined();
|
||||
const embeddable = await dashboard!.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
|
|
@ -5,38 +5,13 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import {
|
||||
ControlGroupInput,
|
||||
CONTROL_GROUP_TYPE,
|
||||
getDefaultControlGroupInput,
|
||||
getDefaultControlGroupPersistableInput,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import {
|
||||
ControlGroupContainerFactory,
|
||||
ControlGroupOutput,
|
||||
type ControlGroupContainer,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
|
||||
import { GlobalQueryStateFromUrl, syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public';
|
||||
import { EmbeddableFactory, isErrorEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
AggregateQuery,
|
||||
compareFilters,
|
||||
COMPARE_ALL_OPTIONS,
|
||||
Filter,
|
||||
Query,
|
||||
TimeRange,
|
||||
} from '@kbn/es-query';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { cloneDeep, identity, omit, pickBy } from 'lodash';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
startWith,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { cloneDeep, omit } from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
DashboardContainerInput,
|
||||
|
@ -60,14 +35,11 @@ import { startDiffingDashboardState } from '../../state/diffing/dashboard_diffin
|
|||
import { DashboardPublicState, UnsavedPanelState } from '../../types';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { DashboardCreationOptions } from '../dashboard_container_factory';
|
||||
import {
|
||||
combineDashboardFiltersWithControlGroupFilters,
|
||||
startSyncingDashboardControlGroup,
|
||||
} from './controls/dashboard_control_group_integration';
|
||||
import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data_views';
|
||||
import { startQueryPerformanceTracking } from './performance/query_performance_tracking';
|
||||
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
|
||||
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
|
||||
import { PANELS_CONTROL_GROUP_KEY } from '../../../services/dashboard_backup/dashboard_backup_service';
|
||||
|
||||
/**
|
||||
* Builds a new Dashboard from scratch.
|
||||
|
@ -162,16 +134,13 @@ export const initializeDashboard = async ({
|
|||
loadDashboardReturn,
|
||||
untilDashboardReady,
|
||||
creationOptions,
|
||||
controlGroup,
|
||||
}: {
|
||||
loadDashboardReturn: LoadDashboardReturn;
|
||||
untilDashboardReady: () => Promise<DashboardContainer>;
|
||||
creationOptions?: DashboardCreationOptions;
|
||||
controlGroup?: ControlGroupContainer;
|
||||
}) => {
|
||||
const {
|
||||
dashboardBackup,
|
||||
embeddable: { getEmbeddableFactory },
|
||||
dashboardCapabilities: { showWriteControls },
|
||||
embeddable: { reactEmbeddableRegistryHasKey },
|
||||
data: {
|
||||
|
@ -191,7 +160,6 @@ export const initializeDashboard = async ({
|
|||
searchSessionSettings,
|
||||
unifiedSearchSettings,
|
||||
validateLoadedSavedObject,
|
||||
useControlGroupIntegration,
|
||||
useUnifiedSearchIntegration,
|
||||
useSessionStorageIntegration,
|
||||
} = creationOptions ?? {};
|
||||
|
@ -291,11 +259,6 @@ export const initializeDashboard = async ({
|
|||
cloneDeep(combinedOverrideInput),
|
||||
'controlGroupInput'
|
||||
);
|
||||
const initialControlGroupInput: ControlGroupInput | {} = {
|
||||
...(loadDashboardReturn?.dashboardInput?.controlGroupInput ?? {}),
|
||||
...(sessionStorageInput?.controlGroupInput ?? {}),
|
||||
...(overrideInput?.controlGroupInput ?? {}),
|
||||
};
|
||||
|
||||
// Back up any view mode passed in explicitly.
|
||||
if (overrideInput?.viewMode) {
|
||||
|
@ -312,6 +275,7 @@ export const initializeDashboard = async ({
|
|||
// --------------------------------------------------------------------------------------
|
||||
untilDashboardReady().then((dashboard) => {
|
||||
dashboard.savedObjectReferences = loadDashboardReturn?.references;
|
||||
dashboard.controlGroupInput = loadDashboardReturn?.dashboardInput?.controlGroupInput;
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -474,6 +438,13 @@ export const initializeDashboard = async ({
|
|||
// Set restored runtime state for react embeddables.
|
||||
// --------------------------------------------------------------------------------------
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
if (overrideInput?.controlGroupState) {
|
||||
dashboardContainer.setRuntimeStateForChild(
|
||||
PANELS_CONTROL_GROUP_KEY,
|
||||
overrideInput.controlGroupState
|
||||
);
|
||||
}
|
||||
|
||||
for (const idWithRuntimeState of Object.keys(runtimePanelsToRestore)) {
|
||||
const restoredRuntimeStateForChild = runtimePanelsToRestore[idWithRuntimeState];
|
||||
if (!restoredRuntimeStateForChild) continue;
|
||||
|
@ -481,52 +452,6 @@ export const initializeDashboard = async ({
|
|||
}
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Start the control group integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
if (useControlGroupIntegration) {
|
||||
const controlsGroupFactory = getEmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
>(CONTROL_GROUP_TYPE) as EmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
> & {
|
||||
create: ControlGroupContainerFactory['create'];
|
||||
};
|
||||
const { filters, query, timeRange, viewMode, id } = initialDashboardInput;
|
||||
const fullControlGroupInput = {
|
||||
id: `control_group_${id ?? 'new_dashboard'}`,
|
||||
...getDefaultControlGroupInput(),
|
||||
...pickBy(initialControlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
|
||||
timeRange,
|
||||
viewMode,
|
||||
filters,
|
||||
query,
|
||||
};
|
||||
|
||||
if (controlGroup) {
|
||||
controlGroup.updateInputAndReinitialize(fullControlGroupInput);
|
||||
} else {
|
||||
const newControlGroup = await controlsGroupFactory?.create(fullControlGroupInput, this, {
|
||||
lastSavedInput:
|
||||
loadDashboardReturn?.dashboardInput?.controlGroupInput ??
|
||||
getDefaultControlGroupPersistableInput(),
|
||||
});
|
||||
if (!newControlGroup || isErrorEmbeddable(newControlGroup)) {
|
||||
throw new Error('Error in control group startup');
|
||||
}
|
||||
controlGroup = newControlGroup;
|
||||
}
|
||||
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
dashboardContainer.controlGroup = controlGroup;
|
||||
startSyncingDashboardControlGroup.bind(dashboardContainer)();
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Start the data views integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -552,63 +477,6 @@ export const initializeDashboard = async ({
|
|||
setTimeout(() => dashboard.dispatch.setAnimatePanelTransforms(true), 500)
|
||||
);
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set parentApi.filters$ to include dashboardContainer filters and control group filters
|
||||
// --------------------------------------------------------------------------------------
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
if (!dashboardContainer.controlGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
function getCombinedFilters() {
|
||||
return combineDashboardFiltersWithControlGroupFilters(
|
||||
dashboardContainer.getInput().filters ?? [],
|
||||
dashboardContainer.controlGroup!
|
||||
);
|
||||
}
|
||||
|
||||
const filters$ = new BehaviorSubject<Filter[] | undefined>(getCombinedFilters());
|
||||
dashboardContainer.filters$ = filters$;
|
||||
|
||||
const inputFilters$ = dashboardContainer.getInput$().pipe(
|
||||
startWith(dashboardContainer.getInput()),
|
||||
map((input) => input.filters),
|
||||
distinctUntilChanged((previous, current) => {
|
||||
return compareFilters(previous ?? [], current ?? [], COMPARE_ALL_OPTIONS);
|
||||
})
|
||||
);
|
||||
|
||||
// Can not use onFiltersPublished$ directly since it does not have an intial value and
|
||||
// combineLatest will not emit until each observable emits at least one value
|
||||
const controlGroupFilters$ = dashboardContainer.controlGroup.onFiltersPublished$.pipe(
|
||||
startWith(dashboardContainer.controlGroup.getOutput().filters)
|
||||
);
|
||||
|
||||
dashboardContainer.integrationSubscriptions.add(
|
||||
combineLatest([inputFilters$, controlGroupFilters$]).subscribe(() => {
|
||||
filters$.next(getCombinedFilters());
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set up parentApi.query$
|
||||
// Can not use legacyEmbeddableToApi since query$ setting is delayed
|
||||
// --------------------------------------------------------------------------------------
|
||||
untilDashboardReady().then((dashboardContainer) => {
|
||||
const query$ = new BehaviorSubject<Query | AggregateQuery | undefined>(
|
||||
dashboardContainer.getInput().query
|
||||
);
|
||||
dashboardContainer.query$ = query$;
|
||||
dashboardContainer.integrationSubscriptions.add(
|
||||
dashboardContainer.getInput$().subscribe((input) => {
|
||||
if (!deepEqual(query$.getValue() ?? [], input.query)) {
|
||||
query$.next(input.query);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set up search sessions integration.
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -629,7 +497,8 @@ export const initializeDashboard = async ({
|
|||
sessionIdToRestore ??
|
||||
(existingSession && incomingEmbeddable ? existingSession : session.start());
|
||||
|
||||
untilDashboardReady().then((container) => {
|
||||
untilDashboardReady().then(async (container) => {
|
||||
await container.untilContainerInitialized();
|
||||
startDashboardSearchSessionIntegration.bind(container)(
|
||||
creationOptions?.searchSessionSettings
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
|
|||
import { combineCompatibleChildrenApis } from '@kbn/presentation-containers';
|
||||
import { apiPublishesDataViews, PublishesDataViews } from '@kbn/presentation-publishing';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { combineLatest, map, Observable, of, switchMap } from 'rxjs';
|
||||
import { combineLatest, Observable, of, switchMap } from 'rxjs';
|
||||
import { pluginServices } from '../../../../services/plugin_services';
|
||||
import { DashboardContainer } from '../../dashboard_container';
|
||||
|
||||
|
@ -19,19 +19,11 @@ export function startSyncingDashboardDataViews(this: DashboardContainer) {
|
|||
data: { dataViews },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const controlGroupDataViewsPipe: Observable<DataView[]> = this.controlGroup
|
||||
? this.controlGroup.getOutput$().pipe(
|
||||
map((output) => output.dataViewIds ?? []),
|
||||
switchMap(
|
||||
(dataViewIds) =>
|
||||
new Promise<DataView[]>((resolve) =>
|
||||
Promise.all(dataViewIds.map((id) => dataViews.get(id))).then((nextDataViews) =>
|
||||
resolve(nextDataViews)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: of([]);
|
||||
const controlGroupDataViewsPipe: Observable<DataView[] | undefined> = this.controlGroupApi$.pipe(
|
||||
switchMap((controlGroupApi) => {
|
||||
return controlGroupApi ? controlGroupApi.dataViews : of([]);
|
||||
})
|
||||
);
|
||||
|
||||
const childDataViewsPipe = combineCompatibleChildrenApis<PublishesDataViews, DataView[]>(
|
||||
this,
|
||||
|
@ -43,7 +35,10 @@ export function startSyncingDashboardDataViews(this: DashboardContainer) {
|
|||
return combineLatest([controlGroupDataViewsPipe, childDataViewsPipe])
|
||||
.pipe(
|
||||
switchMap(([controlGroupDataViews, childDataViews]) => {
|
||||
const allDataViews = controlGroupDataViews.concat(childDataViews);
|
||||
const allDataViews = [
|
||||
...(controlGroupDataViews ? controlGroupDataViews : []),
|
||||
...childDataViews,
|
||||
];
|
||||
if (allDataViews.length === 0) {
|
||||
return (async () => {
|
||||
const defaultDataViewId = await dataViews.getDefaultId();
|
||||
|
@ -54,7 +49,6 @@ export function startSyncingDashboardDataViews(this: DashboardContainer) {
|
|||
})
|
||||
)
|
||||
.subscribe((newDataViews) => {
|
||||
if (newDataViews[0].id) this.controlGroup?.setRelevantDataViewId(newDataViews[0].id);
|
||||
this.setAllDataViews(newDataViews);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,7 +18,12 @@ import {
|
|||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
|
||||
import { buildMockDashboard, getSampleDashboardInput, getSampleDashboardPanel } from '../../mocks';
|
||||
import {
|
||||
buildMockDashboard,
|
||||
getSampleDashboardInput,
|
||||
getSampleDashboardPanel,
|
||||
mockControlGroupApi,
|
||||
} from '../../mocks';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { DashboardContainer } from './dashboard_container';
|
||||
|
||||
|
@ -170,6 +175,7 @@ test('searchSessionId propagates to children', async () => {
|
|||
undefined,
|
||||
{ lastSavedInput: sampleInput }
|
||||
);
|
||||
container?.setControlGroupApi(mockControlGroupApi);
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
|
@ -189,11 +195,10 @@ describe('getInheritedInput', () => {
|
|||
const dashboardTimeslice = [1688061910000, 1688062209000] as [number, number];
|
||||
|
||||
test('Should pass dashboard timeRange and timeslice to panel when panel does not have custom time range', async () => {
|
||||
const container = buildMockDashboard({
|
||||
overrides: {
|
||||
timeRange: dashboardTimeRange,
|
||||
timeslice: dashboardTimeslice,
|
||||
},
|
||||
const container = buildMockDashboard();
|
||||
container.updateInput({
|
||||
timeRange: dashboardTimeRange,
|
||||
timeslice: dashboardTimeslice,
|
||||
});
|
||||
const embeddable = await container.addNewEmbeddable<ContactCardEmbeddableInput>(
|
||||
CONTACT_CARD_EMBEDDABLE,
|
||||
|
@ -214,11 +219,10 @@ describe('getInheritedInput', () => {
|
|||
});
|
||||
|
||||
test('Should not pass dashboard timeRange and timeslice to panel when panel has custom time range', async () => {
|
||||
const container = buildMockDashboard({
|
||||
overrides: {
|
||||
timeRange: dashboardTimeRange,
|
||||
timeslice: dashboardTimeslice,
|
||||
},
|
||||
const container = buildMockDashboard();
|
||||
container.updateInput({
|
||||
timeRange: dashboardTimeRange,
|
||||
timeslice: dashboardTimeslice,
|
||||
});
|
||||
const embeddableTimeRange = {
|
||||
to: 'now',
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import type { I18nStart, KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
|
||||
import {
|
||||
type PublishingSubject,
|
||||
|
@ -16,6 +15,8 @@ import {
|
|||
apiPublishesUnsavedChanges,
|
||||
getPanelTitle,
|
||||
PublishesViewMode,
|
||||
PublishesDataLoading,
|
||||
apiPublishesDataLoading,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
@ -32,7 +33,7 @@ import {
|
|||
type EmbeddableOutput,
|
||||
type IEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import {
|
||||
HasRuntimeChildState,
|
||||
|
@ -40,6 +41,7 @@ import {
|
|||
HasSerializedChildState,
|
||||
TrackContentfulRender,
|
||||
TracksQueryPerformance,
|
||||
combineCompatibleChildrenApis,
|
||||
} from '@kbn/presentation-containers';
|
||||
import { PanelPackage } from '@kbn/presentation-containers';
|
||||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
|
@ -50,14 +52,18 @@ import { omit } from 'lodash';
|
|||
import React, { createContext, useContext } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { batch } from 'react-redux';
|
||||
import { BehaviorSubject, Subject, Subscription } from 'rxjs';
|
||||
import { BehaviorSubject, Subject, Subscription, first, skipWhile, switchMap } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
import { PublishesSettings } from '@kbn/presentation-containers/interfaces/publishes_settings';
|
||||
import { apiHasSerializableState } from '@kbn/presentation-containers/interfaces/serialized_state';
|
||||
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
|
||||
import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||
import { DashboardContainerInput, DashboardPanelState } from '../../../common';
|
||||
import { getReferencesForPanelId } from '../../../common/dashboard_container/persistable_state/dashboard_container_references';
|
||||
import { DashboardAttributes, DashboardContainerInput, DashboardPanelState } from '../../../common';
|
||||
import {
|
||||
getReferencesForControls,
|
||||
getReferencesForPanelId,
|
||||
} from '../../../common/dashboard_container/persistable_state/dashboard_container_references';
|
||||
import {
|
||||
DASHBOARD_APP_ID,
|
||||
DASHBOARD_UI_METRIC_ID,
|
||||
|
@ -84,7 +90,10 @@ import {
|
|||
showSettings,
|
||||
} from './api';
|
||||
import { duplicateDashboardPanel } from './api/duplicate_dashboard_panel';
|
||||
import { combineDashboardFiltersWithControlGroupFilters } from './create/controls/dashboard_control_group_integration';
|
||||
import {
|
||||
combineDashboardFiltersWithControlGroupFilters,
|
||||
startSyncingDashboardControlGroup,
|
||||
} from './create/controls/dashboard_control_group_integration';
|
||||
import { initializeDashboard } from './create/create_dashboard';
|
||||
import {
|
||||
DashboardCreationOptions,
|
||||
|
@ -92,6 +101,7 @@ import {
|
|||
dashboardTypeDisplayName,
|
||||
} from './dashboard_container_factory';
|
||||
import { getPanelAddedSuccessString } from '../../dashboard_app/_dashboard_app_strings';
|
||||
import { PANELS_CONTROL_GROUP_KEY } from '../../services/dashboard_backup/dashboard_backup_service';
|
||||
|
||||
export interface InheritedChildInput {
|
||||
filters: Filter[];
|
||||
|
@ -147,7 +157,7 @@ export class DashboardContainer
|
|||
public integrationSubscriptions: Subscription = new Subscription();
|
||||
public publishingSubscription: Subscription = new Subscription();
|
||||
public diffingSubscription: Subscription = new Subscription();
|
||||
public controlGroup?: ControlGroupContainer;
|
||||
public controlGroupApi$: PublishingSubject<ControlGroupApi | undefined>;
|
||||
public settings: Record<string, PublishingSubject<boolean | undefined>>;
|
||||
|
||||
public searchSessionId?: string;
|
||||
|
@ -156,6 +166,7 @@ export class DashboardContainer
|
|||
public reload$ = new Subject<void>();
|
||||
public timeRestore$: BehaviorSubject<boolean | undefined>;
|
||||
public timeslice$: BehaviorSubject<[number, number] | undefined>;
|
||||
public unifiedSearchFilters$?: PublishingSubject<Filter[] | undefined>;
|
||||
public locator?: Pick<LocatorPublic<DashboardLocatorParams>, 'navigate' | 'getRedirectUrl'>;
|
||||
|
||||
public readonly executionContext: KibanaExecutionContext;
|
||||
|
@ -172,6 +183,9 @@ export class DashboardContainer
|
|||
private hadContentfulRender = false;
|
||||
private scrollPosition?: number;
|
||||
|
||||
// setup
|
||||
public untilContainerInitialized: () => Promise<void>;
|
||||
|
||||
// cleanup
|
||||
public stopSyncingWithUnifiedSearch?: () => void;
|
||||
private cleanupStateTools: () => void;
|
||||
|
@ -197,6 +211,7 @@ export class DashboardContainer
|
|||
| undefined;
|
||||
// new embeddable framework
|
||||
public savedObjectReferences: Reference[] = [];
|
||||
public controlGroupInput: DashboardAttributes['controlGroupInput'] | undefined;
|
||||
|
||||
constructor(
|
||||
initialInput: DashboardContainerInput,
|
||||
|
@ -207,19 +222,43 @@ export class DashboardContainer
|
|||
creationOptions?: DashboardCreationOptions,
|
||||
initialComponentState?: DashboardPublicState
|
||||
) {
|
||||
const controlGroupApi$ = new BehaviorSubject<ControlGroupApi | undefined>(undefined);
|
||||
async function untilContainerInitialized(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
controlGroupApi$
|
||||
.pipe(
|
||||
skipWhile((controlGroupApi) => !controlGroupApi),
|
||||
switchMap(async (controlGroupApi) => {
|
||||
await controlGroupApi?.untilInitialized();
|
||||
}),
|
||||
first()
|
||||
)
|
||||
.subscribe(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
usageCollection,
|
||||
embeddable: { getEmbeddableFactory },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
super(
|
||||
{
|
||||
...initialInput,
|
||||
},
|
||||
{ embeddableLoaded: {} },
|
||||
getEmbeddableFactory,
|
||||
parent
|
||||
parent,
|
||||
{
|
||||
untilContainerInitialized,
|
||||
}
|
||||
);
|
||||
|
||||
this.controlGroupApi$ = controlGroupApi$;
|
||||
this.untilContainerInitialized = untilContainerInitialized;
|
||||
|
||||
this.trackPanelAddMetric = usageCollection.reportUiCounter?.bind(
|
||||
usageCollection,
|
||||
DASHBOARD_UI_METRIC_ID
|
||||
|
@ -311,7 +350,41 @@ export class DashboardContainer
|
|||
DashboardContainerInput
|
||||
>(this.publishingSubscription, this, 'lastReloadRequestTime');
|
||||
|
||||
startSyncingDashboardControlGroup(this);
|
||||
|
||||
this.executionContext = initialInput.executionContext;
|
||||
|
||||
this.dataLoading = new BehaviorSubject<boolean | undefined>(false);
|
||||
this.publishingSubscription.add(
|
||||
combineCompatibleChildrenApis<PublishesDataLoading, boolean | undefined>(
|
||||
this,
|
||||
'dataLoading',
|
||||
apiPublishesDataLoading,
|
||||
undefined,
|
||||
// flatten method
|
||||
(values) => {
|
||||
return values.some((isLoading) => isLoading);
|
||||
}
|
||||
).subscribe((isAtLeastOneChildLoading) => {
|
||||
(this.dataLoading as BehaviorSubject<boolean | undefined>).next(isAtLeastOneChildLoading);
|
||||
})
|
||||
);
|
||||
|
||||
this.dataViews = new BehaviorSubject<DataView[] | undefined>(this.getAllDataViews());
|
||||
|
||||
const query$ = new BehaviorSubject<Query | AggregateQuery | undefined>(this.getInput().query);
|
||||
this.query$ = query$;
|
||||
this.publishingSubscription.add(
|
||||
this.getInput$().subscribe((input) => {
|
||||
if (!deepEqual(query$.getValue() ?? [], input.query)) {
|
||||
query$.next(input.query);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public setControlGroupApi(controlGroupApi: ControlGroupApi) {
|
||||
(this.controlGroupApi$ as BehaviorSubject<ControlGroupApi | undefined>).next(controlGroupApi);
|
||||
}
|
||||
|
||||
public getAppContext() {
|
||||
|
@ -397,10 +470,10 @@ export class DashboardContainer
|
|||
panels,
|
||||
} = this.input;
|
||||
|
||||
let combinedFilters = filters;
|
||||
if (this.controlGroup) {
|
||||
combinedFilters = combineDashboardFiltersWithControlGroupFilters(filters, this.controlGroup);
|
||||
}
|
||||
const combinedFilters = combineDashboardFiltersWithControlGroupFilters(
|
||||
filters,
|
||||
this.controlGroupApi$?.value
|
||||
);
|
||||
const hasCustomTimeRange = Boolean(
|
||||
(panels[id]?.explicitInput as Partial<InheritedChildInput>)?.timeRange
|
||||
);
|
||||
|
@ -429,7 +502,6 @@ export class DashboardContainer
|
|||
public destroy() {
|
||||
super.destroy();
|
||||
this.cleanupStateTools();
|
||||
this.controlGroup?.destroy();
|
||||
this.diffingSubscription.unsubscribe();
|
||||
this.publishingSubscription.unsubscribe();
|
||||
this.integrationSubscriptions.unsubscribe();
|
||||
|
@ -615,16 +687,12 @@ export class DashboardContainer
|
|||
public forceRefresh(refreshControlGroup: boolean = true) {
|
||||
this.dispatch.setLastReloadRequestTimeToNow({});
|
||||
if (refreshControlGroup) {
|
||||
this.controlGroup?.reload();
|
||||
|
||||
// only reload all panels if this refresh does not come from the control group.
|
||||
this.reload$.next();
|
||||
}
|
||||
}
|
||||
|
||||
public onDataViewsUpdate$ = new Subject<DataView[]>();
|
||||
|
||||
public resetToLastSavedState() {
|
||||
public async asyncResetToLastSavedState() {
|
||||
this.dispatch.resetToLastSavedInput({});
|
||||
const {
|
||||
explicitInput: { timeRange, refreshInterval },
|
||||
|
@ -633,8 +701,8 @@ export class DashboardContainer
|
|||
},
|
||||
} = this.getState();
|
||||
|
||||
if (this.controlGroup) {
|
||||
this.controlGroup.resetToLastSavedState();
|
||||
if (this.controlGroupApi$.value) {
|
||||
await this.controlGroupApi$.value.asyncResetUnsavedChanges();
|
||||
}
|
||||
|
||||
// if we are using the unified search integration, we need to force reset the time picker.
|
||||
|
@ -679,7 +747,6 @@ export class DashboardContainer
|
|||
|
||||
const initializeResult = await initializeDashboard({
|
||||
creationOptions: this.creationOptions,
|
||||
controlGroup: this.controlGroup,
|
||||
untilDashboardReady,
|
||||
loadDashboardReturn,
|
||||
});
|
||||
|
@ -694,9 +761,6 @@ export class DashboardContainer
|
|||
omit(loadDashboardReturn?.dashboardInput, 'controlGroupInput')
|
||||
);
|
||||
this.dispatch.setManaged(loadDashboardReturn?.managed);
|
||||
if (this.controlGroup) {
|
||||
this.controlGroup.setSavedState(loadDashboardReturn.dashboardInput?.controlGroupInput);
|
||||
}
|
||||
this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate.
|
||||
this.dispatch.setLastSavedId(newSavedObjectId);
|
||||
this.setExpandedPanelId(undefined);
|
||||
|
@ -720,7 +784,7 @@ export class DashboardContainer
|
|||
*/
|
||||
public setAllDataViews = (newDataViews: DataView[]) => {
|
||||
this.allDataViews = newDataViews;
|
||||
this.onDataViewsUpdate$.next(newDataViews);
|
||||
(this.dataViews as BehaviorSubject<DataView[] | undefined>).next(newDataViews);
|
||||
};
|
||||
|
||||
public getExpandedPanelId = () => {
|
||||
|
@ -743,7 +807,6 @@ export class DashboardContainer
|
|||
public clearOverlays = () => {
|
||||
this.dispatch.setHasOverlays(false);
|
||||
this.dispatch.setFocusedPanelId(undefined);
|
||||
this.controlGroup?.closeAllFlyouts();
|
||||
this.overlayRef?.close();
|
||||
};
|
||||
|
||||
|
@ -848,6 +911,22 @@ export class DashboardContainer
|
|||
};
|
||||
};
|
||||
|
||||
public getSerializedStateForControlGroup = () => {
|
||||
return {
|
||||
rawState: this.controlGroupInput
|
||||
? (this.controlGroupInput as ControlGroupSerializedState)
|
||||
: ({
|
||||
controlStyle: 'oneLine',
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
showApplySelections: false,
|
||||
panelsJSON: '{}',
|
||||
ignoreParentSettingsJSON:
|
||||
'{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
|
||||
} as ControlGroupSerializedState),
|
||||
references: getReferencesForControls(this.savedObjectReferences),
|
||||
};
|
||||
};
|
||||
|
||||
private restoredRuntimeState: UnsavedPanelState | undefined = undefined;
|
||||
public setRuntimeStateForChild = (childId: string, state: object) => {
|
||||
const runtimeState = this.restoredRuntimeState ?? {};
|
||||
|
@ -858,6 +937,10 @@ export class DashboardContainer
|
|||
return this.restoredRuntimeState?.[childId];
|
||||
};
|
||||
|
||||
public getRuntimeStateForControlGroup = () => {
|
||||
return this.getRuntimeStateForChild(PANELS_CONTROL_GROUP_KEY);
|
||||
};
|
||||
|
||||
public removePanel(id: string) {
|
||||
const {
|
||||
embeddable: { reactEmbeddableRegistryHasKey },
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { childrenUnsavedChanges$ } from '@kbn/presentation-containers';
|
||||
import { omit } from 'lodash';
|
||||
import { AnyAction, Middleware } from 'redux';
|
||||
import { combineLatest, debounceTime, Observable, of, startWith, switchMap } from 'rxjs';
|
||||
import { combineLatest, debounceTime, skipWhile, startWith, switchMap } from 'rxjs';
|
||||
import { DashboardContainer, DashboardCreationOptions } from '../..';
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../../dashboard_constants';
|
||||
|
@ -17,6 +16,7 @@ import { pluginServices } from '../../../services/plugin_services';
|
|||
import { UnsavedPanelState } from '../../types';
|
||||
import { dashboardContainerReducers } from '../dashboard_container_reducers';
|
||||
import { isKeyEqualAsync, unsavedChangesDiffingFunctions } from './dashboard_diffing_functions';
|
||||
import { PANELS_CONTROL_GROUP_KEY } from '../../../services/dashboard_backup/dashboard_backup_service';
|
||||
|
||||
/**
|
||||
* An array of reducers which cannot cause unsaved changes. Unsaved changes only compares the explicit input
|
||||
|
@ -111,8 +111,12 @@ export function startDiffingDashboardState(
|
|||
combineLatest([
|
||||
dashboardUnsavedChanges,
|
||||
childrenUnsavedChanges$(this.children$),
|
||||
this.controlGroup?.unsavedChanges ??
|
||||
(of(undefined) as Observable<PersistableControlGroupInput | undefined>),
|
||||
this.controlGroupApi$.pipe(
|
||||
skipWhile((controlGroupApi) => !controlGroupApi),
|
||||
switchMap((controlGroupApi) => {
|
||||
return controlGroupApi!.unsavedChanges;
|
||||
})
|
||||
),
|
||||
]).subscribe(([dashboardChanges, unsavedPanelState, controlGroupChanges]) => {
|
||||
// calculate unsaved changes
|
||||
const hasUnsavedChanges =
|
||||
|
@ -125,11 +129,11 @@ export function startDiffingDashboardState(
|
|||
|
||||
// backup unsaved changes if configured to do so
|
||||
if (creationOptions?.useSessionStorageIntegration) {
|
||||
backupUnsavedChanges.bind(this)(
|
||||
dashboardChanges,
|
||||
unsavedPanelState ? unsavedPanelState : {},
|
||||
controlGroupChanges
|
||||
);
|
||||
const reactEmbeddableChanges = unsavedPanelState ? { ...unsavedPanelState } : {};
|
||||
if (controlGroupChanges) {
|
||||
reactEmbeddableChanges[PANELS_CONTROL_GROUP_KEY] = controlGroupChanges;
|
||||
}
|
||||
backupUnsavedChanges.bind(this)(dashboardChanges, reactEmbeddableChanges);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -181,8 +185,7 @@ export async function getDashboardUnsavedChanges(
|
|||
function backupUnsavedChanges(
|
||||
this: DashboardContainer,
|
||||
dashboardChanges: Partial<DashboardContainerInput>,
|
||||
reactEmbeddableChanges: UnsavedPanelState,
|
||||
controlGroupChanges: PersistableControlGroupInput | undefined
|
||||
reactEmbeddableChanges: UnsavedPanelState
|
||||
) {
|
||||
const { dashboardBackup } = pluginServices.getServices();
|
||||
const dashboardStateToBackup = omit(dashboardChanges, keysToOmitFromSessionStorage);
|
||||
|
@ -192,7 +195,6 @@ function backupUnsavedChanges(
|
|||
{
|
||||
...dashboardStateToBackup,
|
||||
panels: dashboardChanges.panels,
|
||||
controlGroupInput: controlGroupChanges,
|
||||
},
|
||||
reactEmbeddableChanges
|
||||
);
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SerializableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import type { ContainerOutput } from '@kbn/embeddable-plugin/public';
|
||||
import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import type { DashboardContainerInput, DashboardOptions } from '../../common';
|
||||
import { SavedDashboardPanel } from '../../common/content_management';
|
||||
|
||||
|
@ -125,7 +125,7 @@ export type DashboardLocatorParams = Partial<
|
|||
panels?: Array<SavedDashboardPanel & SerializableRecord>; // used SerializableRecord here to force the GridData type to be read as serializable
|
||||
|
||||
/**
|
||||
* Control group input
|
||||
* Control group changes
|
||||
*/
|
||||
controlGroupInput?: SerializableControlGroupInput;
|
||||
controlGroupState?: Partial<ControlGroupRuntimeState> & SerializableRecord; // used SerializableRecord here to force the GridData type to be read as serializable
|
||||
};
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
getContextProvider as getPresentationUtilContextProvider,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
import {
|
||||
EuiBreadcrumb,
|
||||
|
@ -29,6 +28,7 @@ import {
|
|||
import { MountPoint } from '@kbn/core/public';
|
||||
import { getManagedContentBadge } from '@kbn/managed-content-badge';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
getDashboardTitle,
|
||||
leaveConfirmStrings,
|
||||
|
@ -113,16 +113,8 @@ export function InternalDashboardTopNav({
|
|||
const query = dashboard.select((state) => state.explicitInput.query);
|
||||
const title = dashboard.select((state) => state.explicitInput.title);
|
||||
|
||||
// store data views in state & subscribe to dashboard data view changes.
|
||||
const [allDataViews, setAllDataViews] = useState<DataView[]>([]);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
setAllDataViews(dashboard.getAllDataViews());
|
||||
const subscription = dashboard.onDataViewsUpdate$.subscribe((dataViews) =>
|
||||
setAllDataViews(dataViews)
|
||||
);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [dashboard]);
|
||||
const allDataViews = useStateFromPublishingSubject(dashboard.dataViews);
|
||||
|
||||
const dashboardTitle = useMemo(() => {
|
||||
return getDashboardTitle(title, viewMode, !lastSavedId);
|
||||
|
@ -411,7 +403,7 @@ export function InternalDashboardTopNav({
|
|||
screenTitle={title}
|
||||
useDefaultBehaviors={true}
|
||||
savedQueryId={savedQueryId}
|
||||
indexPatterns={allDataViews}
|
||||
indexPatterns={allDataViews ?? []}
|
||||
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
|
||||
appName={LEGACY_DASHBOARD_APP_ID}
|
||||
visible={viewMode !== ViewMode.PRINT}
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
import { EmbeddableInput, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public/mocks';
|
||||
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DashboardContainerInput, DashboardPanelState } from '../common';
|
||||
import { DashboardContainer } from './dashboard_container/embeddable/dashboard_container';
|
||||
import { DashboardStart } from './plugin';
|
||||
|
@ -72,6 +74,15 @@ export function setupIntersectionObserverMock({
|
|||
});
|
||||
}
|
||||
|
||||
export const mockControlGroupApi = {
|
||||
untilInitialized: async () => {},
|
||||
filters$: new BehaviorSubject(undefined),
|
||||
query$: new BehaviorSubject(undefined),
|
||||
timeslice$: new BehaviorSubject(undefined),
|
||||
dataViews: new BehaviorSubject(undefined),
|
||||
unsavedChanges: new BehaviorSubject(undefined),
|
||||
} as unknown as ControlGroupApi;
|
||||
|
||||
export function buildMockDashboard({
|
||||
overrides,
|
||||
savedObjectId,
|
||||
|
@ -89,6 +100,7 @@ export function buildMockDashboard({
|
|||
undefined,
|
||||
{ lastSavedInput: initialInput, lastSavedId: savedObjectId }
|
||||
);
|
||||
dashboardContainer?.setControlGroupApi(mockControlGroupApi);
|
||||
return dashboardContainer;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { backupServiceStrings } from '../../dashboard_container/_dashboard_conta
|
|||
import { UnsavedPanelState } from '../../dashboard_container/types';
|
||||
|
||||
export const DASHBOARD_PANELS_UNSAVED_ID = 'unsavedDashboard';
|
||||
export const PANELS_CONTROL_GROUP_KEY = 'controlGroup';
|
||||
const DASHBOARD_PANELS_SESSION_KEY = 'dashboardPanels';
|
||||
const DASHBOARD_VIEWMODE_LOCAL_KEY = 'dashboardViewMode';
|
||||
|
||||
|
@ -112,6 +113,7 @@ class DashboardBackupService implements DashboardBackupServiceType {
|
|||
const panels = this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[this.activeSpaceId]?.[
|
||||
id
|
||||
] as UnsavedPanelState | undefined;
|
||||
|
||||
return { dashboardState, panels };
|
||||
} catch (e) {
|
||||
this.notifications.toasts.addDanger({
|
||||
|
|
|
@ -56,8 +56,15 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
|
|||
contentManagement,
|
||||
savedObjectsTagging,
|
||||
}),
|
||||
saveDashboardState: ({ currentState, saveOptions, lastSavedId, panelReferences }) =>
|
||||
saveDashboardState: ({
|
||||
controlGroupReferences,
|
||||
currentState,
|
||||
saveOptions,
|
||||
lastSavedId,
|
||||
panelReferences,
|
||||
}) =>
|
||||
saveDashboardState({
|
||||
controlGroupReferences,
|
||||
data,
|
||||
embeddable,
|
||||
saveOptions,
|
||||
|
|
|
@ -12,7 +12,6 @@ import { Filter, Query } from '@kbn/es-query';
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
||||
import { rawControlGroupAttributesToControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { parseSearchSourceJSON, injectSearchSourceReferences } from '@kbn/data-plugin/public';
|
||||
|
||||
import {
|
||||
|
@ -187,9 +186,7 @@ export const loadDashboardState = async ({
|
|||
viewMode: ViewMode.VIEW, // dashboards loaded from saved object default to view mode. If it was edited recently, the view mode from session storage will override this.
|
||||
tags: savedObjectsTagging.getTagIdsFromReferences?.(references) ?? [],
|
||||
|
||||
controlGroupInput:
|
||||
attributes.controlGroupInput &&
|
||||
rawControlGroupAttributesToControlGroupInput(attributes.controlGroupInput),
|
||||
controlGroupInput: attributes.controlGroupInput,
|
||||
|
||||
version: convertNumberToDashboardVersion(version),
|
||||
},
|
||||
|
|
|
@ -6,9 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||
|
||||
import { getSampleDashboardInput, getSampleDashboardPanel } from '../../../mocks';
|
||||
import { DashboardEmbeddableService } from '../../embeddable/types';
|
||||
import { SavedDashboardInput } from '../types';
|
||||
|
@ -32,23 +29,6 @@ describe('Migrate dashboard input', () => {
|
|||
panel3: getSampleDashboardPanel({ type: 'ultraDiscover', explicitInput: { id: 'panel3' } }),
|
||||
panel4: getSampleDashboardPanel({ type: 'ultraDiscover', explicitInput: { id: 'panel4' } }),
|
||||
};
|
||||
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
|
||||
controlGroupInputBuilder.addOptionsListControl(controlGroupInput, {
|
||||
dataViewId: 'positions-remain-fixed',
|
||||
title: 'Results can be mixed',
|
||||
fieldName: 'theres-a-stasis',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
controlGroupInputBuilder.addRangeSliderControl(controlGroupInput, {
|
||||
dataViewId: 'an-object-set-in-motion',
|
||||
title: 'The arbiter of time',
|
||||
fieldName: 'unexpressed-emotion',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
controlGroupInputBuilder.addTimeSliderControl(controlGroupInput);
|
||||
dashboardInput.controlGroupInput = controlGroupInput;
|
||||
|
||||
const embeddableService: DashboardEmbeddableService = {
|
||||
getEmbeddableFactory: jest.fn(() => ({
|
||||
|
@ -62,11 +42,8 @@ describe('Migrate dashboard input', () => {
|
|||
// migration run should be true because the runEmbeddableFactoryMigrations mock above returns true.
|
||||
expect(result.anyMigrationRun).toBe(true);
|
||||
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledTimes(7); // should be called 4 times for the panels, and 3 times for the controls
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledTimes(4); // should be called 4 times for the panels, and 3 times for the controls
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith('superLens');
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith('ultraDiscover');
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith('optionsListControl');
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith('rangeSliderControl');
|
||||
expect(embeddableService.getEmbeddableFactory).toHaveBeenCalledWith('timeSlider');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import {
|
||||
EmbeddableFactoryNotFoundError,
|
||||
runEmbeddableFactoryMigrations,
|
||||
|
@ -31,28 +30,7 @@ export const migrateDashboardInput = (
|
|||
} = pluginServices.getServices();
|
||||
let anyMigrationRun = false;
|
||||
if (!dashboardInput) return dashboardInput;
|
||||
if (dashboardInput.controlGroupInput) {
|
||||
/**
|
||||
* If any Control Group migrations are required, we will need to start storing a Control Group Input version
|
||||
* string in Dashboard Saved Objects and then running the whole Control Group input through the embeddable
|
||||
* factory migrations here.
|
||||
*/
|
||||
|
||||
// Migrate all of the Control children as well.
|
||||
const migratedControls: ControlGroupInput['panels'] = {};
|
||||
|
||||
Object.entries(dashboardInput.controlGroupInput.panels).forEach(([id, panel]) => {
|
||||
const factory = embeddable.getEmbeddableFactory(panel.type);
|
||||
if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type);
|
||||
const { input: newInput, migrationRun: controlMigrationRun } = runEmbeddableFactoryMigrations(
|
||||
panel.explicitInput,
|
||||
factory
|
||||
);
|
||||
if (controlMigrationRun) anyMigrationRun = true;
|
||||
panel.explicitInput = newInput as DashboardPanelState['explicitInput'];
|
||||
migratedControls[id] = panel;
|
||||
});
|
||||
}
|
||||
const migratedPanels: DashboardContainerInput['panels'] = {};
|
||||
for (const [id, panel] of Object.entries(dashboardInput.panels)) {
|
||||
// if the panel type is registered in the new embeddable system, we do not need to run migrations for it.
|
||||
|
|
|
@ -9,12 +9,6 @@
|
|||
import { pick } from 'lodash';
|
||||
import moment, { Moment } from 'moment';
|
||||
|
||||
import {
|
||||
controlGroupInputToRawControlGroupAttributes,
|
||||
generateNewControlIds,
|
||||
getDefaultControlGroupInput,
|
||||
persistableControlGroupInputIsEqual,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { extractSearchSourceReferences, RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import { isFilterPinned } from '@kbn/es-query';
|
||||
|
||||
|
@ -29,24 +23,10 @@ import {
|
|||
DashboardContentManagementRequiredServices,
|
||||
SaveDashboardProps,
|
||||
SaveDashboardReturn,
|
||||
SavedDashboardInput,
|
||||
} from '../types';
|
||||
import { convertDashboardVersionToNumber } from './dashboard_versioning';
|
||||
import { generateNewPanelIds } from '../../../../common/lib/dashboard_panel_converters';
|
||||
|
||||
export const serializeControlGroupInput = (
|
||||
controlGroupInput: SavedDashboardInput['controlGroupInput']
|
||||
) => {
|
||||
// only save to saved object if control group is not default
|
||||
if (
|
||||
!controlGroupInput ||
|
||||
persistableControlGroupInputIsEqual(controlGroupInput, getDefaultControlGroupInput())
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return controlGroupInputToRawControlGroupAttributes(controlGroupInput);
|
||||
};
|
||||
|
||||
export const convertTimeToUTCString = (time?: string | Moment): undefined | string => {
|
||||
if (moment(time).isValid()) {
|
||||
return moment(time).utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
|
||||
|
@ -68,6 +48,7 @@ type SaveDashboardStateProps = SaveDashboardProps & {
|
|||
};
|
||||
|
||||
export const saveDashboardState = async ({
|
||||
controlGroupReferences,
|
||||
data,
|
||||
embeddable,
|
||||
lastSavedId,
|
||||
|
@ -100,9 +81,10 @@ export const saveDashboardState = async ({
|
|||
syncCursor,
|
||||
syncTooltips,
|
||||
hidePanelTitles,
|
||||
controlGroupInput,
|
||||
} = currentState;
|
||||
|
||||
let { panels, controlGroupInput } = currentState;
|
||||
let { panels } = currentState;
|
||||
let prefixedPanelReferences = panelReferences;
|
||||
if (saveOptions.saveAsCopy) {
|
||||
const { panels: newPanels, references: newPanelReferences } = generateNewPanelIds(
|
||||
|
@ -111,7 +93,10 @@ export const saveDashboardState = async ({
|
|||
);
|
||||
panels = newPanels;
|
||||
prefixedPanelReferences = newPanelReferences;
|
||||
controlGroupInput = generateNewControlIds(controlGroupInput);
|
||||
//
|
||||
// do not need to generate new ids for controls.
|
||||
// ControlGroup Component is keyed on dashboard id so changing dashboard id mounts new ControlGroup Component.
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -159,7 +144,7 @@ export const saveDashboardState = async ({
|
|||
|
||||
const rawDashboardAttributes: DashboardAttributes = {
|
||||
version: convertDashboardVersionToNumber(LATEST_DASHBOARD_CONTAINER_VERSION),
|
||||
controlGroupInput: serializeControlGroupInput(controlGroupInput),
|
||||
controlGroupInput,
|
||||
kibanaSavedObjectMeta: { searchSourceJSON },
|
||||
description: description ?? '',
|
||||
refreshInterval,
|
||||
|
@ -186,7 +171,11 @@ export const saveDashboardState = async ({
|
|||
? savedObjectsTagging.updateTagsReferences(dashboardReferences, tags)
|
||||
: dashboardReferences;
|
||||
|
||||
const allReferences = [...references, ...(prefixedPanelReferences ?? [])];
|
||||
const allReferences = [
|
||||
...references,
|
||||
...(prefixedPanelReferences ?? []),
|
||||
...(controlGroupReferences ?? []),
|
||||
];
|
||||
|
||||
/**
|
||||
* Save the saved object using the content management
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
*/
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { PersistableControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
|
||||
|
||||
import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import { DashboardCrudTypes } from '../../../common/content_management';
|
||||
import { DashboardAttributes, DashboardCrudTypes } from '../../../common/content_management';
|
||||
import { DashboardStartDependencies } from '../../plugin';
|
||||
import { DashboardBackupServiceType } from '../dashboard_backup/types';
|
||||
import { DashboardDataService } from '../data/types';
|
||||
|
@ -64,7 +64,17 @@ export interface LoadDashboardFromSavedObjectProps {
|
|||
type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta'];
|
||||
|
||||
export type SavedDashboardInput = DashboardContainerInput & {
|
||||
controlGroupInput?: PersistableControlGroupInput;
|
||||
/**
|
||||
* Serialized control group state.
|
||||
* Contains state loaded from dashboard saved object
|
||||
*/
|
||||
controlGroupInput?: DashboardAttributes['controlGroupInput'] | undefined;
|
||||
/**
|
||||
* Runtime control group state.
|
||||
* Contains state passed from dashboard locator
|
||||
* Use runtime state when building input for portable dashboards
|
||||
*/
|
||||
controlGroupState?: Partial<ControlGroupRuntimeState>;
|
||||
};
|
||||
|
||||
export interface LoadDashboardReturn {
|
||||
|
@ -89,6 +99,7 @@ export interface LoadDashboardReturn {
|
|||
export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { saveAsCopy?: boolean };
|
||||
|
||||
export interface SaveDashboardProps {
|
||||
controlGroupReferences?: Reference[];
|
||||
currentState: SavedDashboardInput;
|
||||
saveOptions: SavedDashboardSaveOpts;
|
||||
panelReferences?: Reference[];
|
||||
|
|
|
@ -82,6 +82,9 @@ export abstract class Container<
|
|||
const init$ = this.getInput$().pipe(
|
||||
take(1),
|
||||
mergeMap(async (currentInput) => {
|
||||
if (settings?.untilContainerInitialized) {
|
||||
await settings.untilContainerInitialized();
|
||||
}
|
||||
const initPromise = this.initializeChildEmbeddables(currentInput, settings);
|
||||
if (awaitingInitialize) await initPromise;
|
||||
})
|
||||
|
|
|
@ -37,6 +37,8 @@ export interface EmbeddableContainerSettings {
|
|||
* Initialise children in the order specified. If an ID does not match it will be skipped and if a child is not included it will be initialized in the default order after the list of provided IDs.
|
||||
*/
|
||||
childIdInitializeOrder?: string[];
|
||||
|
||||
untilContainerInitialized?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IContainer<
|
||||
|
|
|
@ -51,7 +51,11 @@ export const embeddableInputToSubject = <
|
|||
subscription.add(
|
||||
embeddable
|
||||
.getInput$()
|
||||
.pipe(distinctUntilKeyChanged(key))
|
||||
.pipe(
|
||||
distinctUntilKeyChanged(key, (prev, current) => {
|
||||
return deepEqual(prev, current);
|
||||
})
|
||||
)
|
||||
.subscribe(() => subject.next(embeddable.getInput()?.[key] as ValueType))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -58,16 +58,19 @@ export const genericEmbeddableInputIsEqual = (
|
|||
const {
|
||||
title: currentTitle,
|
||||
hidePanelTitles: currentHidePanelTitles,
|
||||
enhancements: currentEnhancements,
|
||||
...current
|
||||
} = pick(currentInput as GenericEmbedableInputToCompare, genericInputKeysToCompare);
|
||||
const {
|
||||
title: lastTitle,
|
||||
hidePanelTitles: lastHidePanelTitles,
|
||||
enhancements: lastEnhancements,
|
||||
...last
|
||||
} = pick(lastInput as GenericEmbedableInputToCompare, genericInputKeysToCompare);
|
||||
|
||||
if (currentTitle !== lastTitle) return false;
|
||||
if (Boolean(currentHidePanelTitles) !== Boolean(lastHidePanelTitles)) return false;
|
||||
if (!fastIsEqual(currentEnhancements ?? {}, lastEnhancements ?? {})) return false;
|
||||
if (!fastIsEqual(current, last)) return false;
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
@ -14,77 +13,25 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
|
|||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const pieChart = getService('pieChart');
|
||||
const elasticChart = getService('elasticChart');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
|
||||
const { dashboard, header, dashboardControls, timePicker } = getPageObjects([
|
||||
const { dashboard, header, dashboardControls } = getPageObjects([
|
||||
'dashboardControls',
|
||||
'timePicker',
|
||||
'dashboard',
|
||||
'header',
|
||||
]);
|
||||
|
||||
describe('Dashboard control group apply button', () => {
|
||||
let controlIds: string[];
|
||||
const optionsListId = '41827e70-5285-4d44-8375-4c498449b9a7';
|
||||
const rangeSliderId = '515e7b9f-4f1b-4a06-beec-763810e4951a';
|
||||
|
||||
before(async () => {
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.gotoDashboardLandingPage();
|
||||
await dashboard.clickNewDashboard();
|
||||
await timePicker.setDefaultDataRange();
|
||||
|
||||
await dashboard.loadSavedDashboard('Test Control Group Apply Button');
|
||||
await dashboard.switchToEditMode();
|
||||
await elasticChart.setNewChartUiDebugFlag();
|
||||
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
|
||||
|
||||
// save the dashboard before adding controls
|
||||
await dashboard.saveDashboard('Test Control Group Apply Button', {
|
||||
exitFromEditMode: false,
|
||||
saveAsNew: true,
|
||||
});
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
|
||||
// populate an initial set of controls and get their ids.
|
||||
await dashboardControls.createControl({
|
||||
controlType: OPTIONS_LIST_CONTROL,
|
||||
dataViewTitle: 'animals-*',
|
||||
fieldName: 'animal.keyword',
|
||||
title: 'Animal',
|
||||
});
|
||||
await dashboardControls.createControl({
|
||||
controlType: RANGE_SLIDER_CONTROL,
|
||||
dataViewTitle: 'animals-*',
|
||||
fieldName: 'weightLbs',
|
||||
title: 'Animal Name',
|
||||
});
|
||||
await dashboardControls.createTimeSliderControl();
|
||||
|
||||
// wait for all controls to finish loading before saving
|
||||
controlIds = await dashboardControls.getAllControlIds();
|
||||
await dashboardControls.optionsListWaitForLoading(controlIds[0]);
|
||||
await dashboardControls.rangeSliderWaitForLoading(controlIds[1]);
|
||||
|
||||
// re-save the dashboard
|
||||
await dashboard.clickQuickSave();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
});
|
||||
|
||||
it('able to set apply button setting', async () => {
|
||||
await dashboardControls.updateShowApplyButtonSetting(true);
|
||||
await testSubjects.existOrFail('controlGroup--applyFiltersButton');
|
||||
await dashboard.expectUnsavedChangesBadge();
|
||||
|
||||
await dashboard.clickQuickSave();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
});
|
||||
|
||||
it('renabling auto-apply forces filters to be published', async () => {
|
||||
const optionsListId = controlIds[0];
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
await dashboardControls.optionsListOpenPopover(optionsListId);
|
||||
await dashboardControls.optionsListPopoverSelectOption('cat');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(optionsListId);
|
||||
|
@ -101,14 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('options list selections', () => {
|
||||
let optionsListId: string;
|
||||
|
||||
before(async () => {
|
||||
optionsListId = controlIds[0];
|
||||
});
|
||||
|
||||
it('making selection enables apply button', async () => {
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
await dashboardControls.optionsListOpenPopover(optionsListId);
|
||||
await dashboardControls.optionsListPopoverSelectOption('cat');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(optionsListId);
|
||||
|
@ -117,7 +57,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('waits to apply filters until button is pressed', async () => {
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
expect(await pieChart.getPieSliceCount()).to.be(5);
|
||||
|
||||
await dashboardControls.clickApplyButton();
|
||||
|
@ -139,27 +78,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
expect(await pieChart.getPieSliceCount()).to.be(5);
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
expect(await dashboardControls.optionsListGetSelectionsString(optionsListId)).to.be('Any');
|
||||
});
|
||||
});
|
||||
|
||||
describe('range slider selections', () => {
|
||||
let rangeSliderId: string;
|
||||
|
||||
before(async () => {
|
||||
rangeSliderId = controlIds[1];
|
||||
});
|
||||
|
||||
it('making selection enables apply button', async () => {
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
await dashboardControls.rangeSliderSetUpperBound(rangeSliderId, '30');
|
||||
await dashboardControls.verifyApplyButtonEnabled();
|
||||
});
|
||||
|
||||
it('waits to apply filters until apply button is pressed', async () => {
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
expect(await pieChart.getPieSliceCount()).to.be(5);
|
||||
|
||||
await dashboardControls.clickApplyButton();
|
||||
|
@ -180,8 +111,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
expect(await pieChart.getPieSliceCount()).to.be(5);
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
expect(
|
||||
await dashboardControls.rangeSliderGetLowerBoundAttribute(rangeSliderId, 'value')
|
||||
).to.be('');
|
||||
|
@ -199,7 +130,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('making selection enables apply button', async () => {
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
await dashboardControls.gotoNextTimeSlice();
|
||||
await dashboardControls.gotoNextTimeSlice(); // go to an empty timeslice
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
@ -207,7 +137,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('waits to apply timeslice until apply button is pressed', async () => {
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
expect(await pieChart.getPieSliceCount()).to.be(5);
|
||||
|
||||
await dashboardControls.clickApplyButton();
|
||||
|
@ -226,8 +155,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
await dashboard.expectMissingUnsavedChangesBadge();
|
||||
expect(await pieChart.getPieSliceCount()).to.be(5);
|
||||
await dashboardControls.verifyApplyButtonEnabled(false);
|
||||
const valueNow = await dashboardControls.getTimeSliceFromTimeSlider();
|
||||
expect(valueNow).to.equal(valueBefore);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
@ -17,17 +16,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const kibanaServer = getService('kibanaServer');
|
||||
const filterBar = getService('filterBar');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const { common, dashboard, dashboardControls } = getPageObjects([
|
||||
const { dashboard, dashboardControls } = getPageObjects([
|
||||
'dashboardControls',
|
||||
'dashboard',
|
||||
'console',
|
||||
'common',
|
||||
'header',
|
||||
]);
|
||||
|
||||
describe('Dashboard control group with multiple data views', () => {
|
||||
let controlIds: string[];
|
||||
// Controls from flights data view
|
||||
const carrierControlId = '265b6a28-9ccb-44ae-83c9-3d7a7cac1961';
|
||||
const ticketPriceControlId = 'ed2b93e2-da37-482b-ae43-586a41cc2399';
|
||||
// Controls from logstash-* data view
|
||||
const osControlId = '5e1b146b-8a8b-4117-9218-c4aeaee7bc9a';
|
||||
const bytesControlId = 'c4760951-e793-45d5-a6b7-c72c145af7f9';
|
||||
|
||||
async function waitForAllConrolsLoading() {
|
||||
await Promise.all([
|
||||
dashboardControls.optionsListWaitForLoading(carrierControlId),
|
||||
dashboardControls.rangeSliderWaitForLoading(ticketPriceControlId),
|
||||
dashboardControls.optionsListWaitForLoading(osControlId),
|
||||
dashboardControls.rangeSliderWaitForLoading(bytesControlId),
|
||||
]);
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']);
|
||||
|
@ -39,50 +50,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.load(
|
||||
'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern'
|
||||
);
|
||||
await kibanaServer.importExport.load(
|
||||
'test/functional/fixtures/kbn_archiver/dashboard/current/multi_data_view_kibana'
|
||||
);
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
|
||||
'courier:ignoreFilterIfFieldNotInIndex': true,
|
||||
});
|
||||
|
||||
await common.setTime({
|
||||
from: 'Apr 10, 2018 @ 00:00:00.000',
|
||||
to: 'Nov 15, 2018 @ 00:00:00.000',
|
||||
});
|
||||
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.clickNewDashboard();
|
||||
|
||||
await dashboardControls.createControl({
|
||||
controlType: OPTIONS_LIST_CONTROL,
|
||||
dataViewTitle: 'kibana_sample_data_flights',
|
||||
fieldName: 'Carrier',
|
||||
title: 'Carrier',
|
||||
});
|
||||
|
||||
await dashboardControls.createControl({
|
||||
controlType: RANGE_SLIDER_CONTROL,
|
||||
dataViewTitle: 'kibana_sample_data_flights',
|
||||
fieldName: 'AvgTicketPrice',
|
||||
title: 'Average Ticket Price',
|
||||
});
|
||||
|
||||
await dashboardControls.createControl({
|
||||
controlType: OPTIONS_LIST_CONTROL,
|
||||
dataViewTitle: 'logstash-*',
|
||||
fieldName: 'machine.os.raw',
|
||||
title: 'Operating System',
|
||||
});
|
||||
|
||||
await dashboardControls.createControl({
|
||||
controlType: RANGE_SLIDER_CONTROL,
|
||||
dataViewTitle: 'logstash-*',
|
||||
fieldName: 'bytes',
|
||||
title: 'Bytes',
|
||||
});
|
||||
|
||||
await dashboardAddPanel.addSavedSearch('logstash hits');
|
||||
|
||||
controlIds = await dashboardControls.getAllControlIds();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -93,96 +66,169 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.unload(
|
||||
'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'test/functional/fixtures/kbn_archiver/dashboard/current/multi_data_view_kibana'
|
||||
);
|
||||
await security.testUser.restoreDefaults();
|
||||
await kibanaServer.uiSettings.unset('courier:ignoreFilterIfFieldNotInIndex');
|
||||
await kibanaServer.uiSettings.unset('defaultIndex');
|
||||
});
|
||||
|
||||
it('ignores global filters on controls using a data view without the filter field', async () => {
|
||||
await filterBar.addFilter({ field: 'Carrier', operation: 'exists' });
|
||||
describe('courier:ignoreFilterIfFieldNotInIndex enabled', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.uiSettings.replace({
|
||||
'courier:ignoreFilterIfFieldNotInIndex': true,
|
||||
});
|
||||
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[0]);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('4');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.loadSavedDashboard('Test Control Group With Multiple Data Views');
|
||||
});
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[1], '100', '1200');
|
||||
after(async () => {
|
||||
await kibanaServer.uiSettings.unset('courier:ignoreFilterIfFieldNotInIndex');
|
||||
});
|
||||
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[2]);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('5');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
|
||||
describe('global filters', () => {
|
||||
before(async () => {
|
||||
await filterBar.addFilter({
|
||||
field: 'Carrier',
|
||||
operation: 'is',
|
||||
value: 'Kibana Airlines',
|
||||
});
|
||||
await waitForAllConrolsLoading();
|
||||
});
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[3], '0', '19979');
|
||||
after(async () => {
|
||||
await dashboard.clickDiscardChanges();
|
||||
});
|
||||
|
||||
it('applies global filters to controls with data view of filter field', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(carrierControlId);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('1');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(carrierControlId);
|
||||
|
||||
await dashboardControls.validateRange('placeholder', ticketPriceControlId, '100', '1196');
|
||||
});
|
||||
|
||||
it('ignores global filters to controls without data view of filter field', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(osControlId);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('5');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(osControlId);
|
||||
|
||||
await dashboardControls.validateRange('placeholder', bytesControlId, '0', '19979');
|
||||
});
|
||||
});
|
||||
|
||||
describe('control filters', () => {
|
||||
before(async () => {
|
||||
await dashboardControls.optionsListOpenPopover(carrierControlId);
|
||||
await dashboardControls.optionsListPopoverSelectOption('Kibana Airlines');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(carrierControlId);
|
||||
await waitForAllConrolsLoading();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await dashboard.clickDiscardChanges();
|
||||
});
|
||||
|
||||
it('applies control filters to controls with data view of control filter', async () => {
|
||||
await dashboardControls.validateRange('placeholder', ticketPriceControlId, '100', '1196');
|
||||
});
|
||||
|
||||
it('ignores control filters on controls without data view of control filter', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(osControlId);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('5');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(osControlId);
|
||||
|
||||
await dashboardControls.validateRange('placeholder', bytesControlId, '0', '19979');
|
||||
});
|
||||
|
||||
it('ignores control filters on panels without data view of control filter', async () => {
|
||||
const logstashSavedSearchPanel = await testSubjects.find('embeddedSavedSearchDocTable');
|
||||
expect(
|
||||
await (
|
||||
await logstashSavedSearchPanel.findByCssSelector('[data-document-number]')
|
||||
).getAttribute('data-document-number')
|
||||
).to.not.be('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores controls on other controls and panels using a data view without the control field by default', async () => {
|
||||
await filterBar.removeFilter('Carrier');
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[0]);
|
||||
await dashboardControls.optionsListPopoverSelectOption('Kibana Airlines');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
|
||||
describe('courier:ignoreFilterIfFieldNotInIndex disabled', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.uiSettings.replace({
|
||||
'courier:ignoreFilterIfFieldNotInIndex': false,
|
||||
});
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[1], '100', '1196');
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.loadSavedDashboard('Test Control Group With Multiple Data Views');
|
||||
});
|
||||
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[2]);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('5');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
|
||||
after(async () => {
|
||||
await kibanaServer.uiSettings.unset('courier:ignoreFilterIfFieldNotInIndex');
|
||||
});
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[3], '0', '19979');
|
||||
describe('global filters', () => {
|
||||
before(async () => {
|
||||
await filterBar.addFilter({
|
||||
field: 'Carrier',
|
||||
operation: 'is',
|
||||
value: 'Kibana Airlines',
|
||||
});
|
||||
await waitForAllConrolsLoading();
|
||||
});
|
||||
|
||||
const logstashSavedSearchPanel = await testSubjects.find('embeddedSavedSearchDocTable');
|
||||
expect(
|
||||
await (
|
||||
await logstashSavedSearchPanel.findByCssSelector('[data-document-number]')
|
||||
).getAttribute('data-document-number')
|
||||
).to.not.be('0');
|
||||
});
|
||||
after(async () => {
|
||||
await dashboard.clickDiscardChanges();
|
||||
});
|
||||
|
||||
it('applies global filters on controls using data view a without the filter field', async () => {
|
||||
await kibanaServer.uiSettings.update({ 'courier:ignoreFilterIfFieldNotInIndex': false });
|
||||
await common.navigateToApp('dashboard');
|
||||
await testSubjects.click('edit-unsaved-New-Dashboard');
|
||||
await filterBar.addFilter({ field: 'Carrier', operation: 'exists' });
|
||||
it('applies global filters to controls without data view of filter field', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(osControlId);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('0');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(osControlId);
|
||||
|
||||
await Promise.all([
|
||||
dashboardControls.optionsListWaitForLoading(controlIds[0]),
|
||||
dashboardControls.rangeSliderWaitForLoading(controlIds[1]),
|
||||
dashboardControls.optionsListWaitForLoading(controlIds[2]),
|
||||
dashboardControls.rangeSliderWaitForLoading(controlIds[3]),
|
||||
]);
|
||||
await dashboardControls.validateRange(
|
||||
'placeholder',
|
||||
bytesControlId,
|
||||
'-Infinity',
|
||||
'Infinity'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await dashboardControls.clearControlSelections(controlIds[0]);
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[0]);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('4');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
|
||||
describe('control filters', () => {
|
||||
before(async () => {
|
||||
await dashboardControls.optionsListOpenPopover(carrierControlId);
|
||||
await dashboardControls.optionsListPopoverSelectOption('Kibana Airlines');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(carrierControlId);
|
||||
await waitForAllConrolsLoading();
|
||||
});
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[1], '100', '1200');
|
||||
after(async () => {
|
||||
await dashboard.clickDiscardChanges();
|
||||
});
|
||||
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[2]);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('0');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
|
||||
it('applies control filters on controls without data view of control filter', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(osControlId);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('0');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(osControlId);
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[3], '0', '0');
|
||||
});
|
||||
await dashboardControls.validateRange(
|
||||
'placeholder',
|
||||
bytesControlId,
|
||||
'-Infinity',
|
||||
'Infinity'
|
||||
);
|
||||
});
|
||||
|
||||
it('applies global filters on controls using a data view without the filter field', async () => {
|
||||
await filterBar.removeFilter('Carrier');
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[0]);
|
||||
await dashboardControls.optionsListPopoverSelectOption('Kibana Airlines');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[1], '100', '1196');
|
||||
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[2]);
|
||||
expect(await dashboardControls.optionsListGetCardinalityValue()).to.be('0');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
|
||||
|
||||
await dashboardControls.validateRange('placeholder', controlIds[3], '0', '0');
|
||||
|
||||
const logstashSavedSearchPanel = await testSubjects.find('embeddedSavedSearchDocTable');
|
||||
expect(
|
||||
await (
|
||||
await logstashSavedSearchPanel.findByCssSelector('[data-document-number]')
|
||||
).getAttribute('data-document-number')
|
||||
).to.be('0');
|
||||
it('applies control filters on panels without data view of control filter', async () => {
|
||||
const logstashSavedSearchPanel = await testSubjects.find('embeddedSavedSearchDocTable');
|
||||
expect(
|
||||
await (
|
||||
await logstashSavedSearchPanel.findByCssSelector('[data-document-number]')
|
||||
).getAttribute('data-document-number')
|
||||
).to.be('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -35,15 +35,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
const replaceWithOptionsList = async (controlId: string, field: string) => {
|
||||
await changeFieldType(controlId, field, OPTIONS_LIST_CONTROL);
|
||||
await testSubjects.waitForEnabled(`optionsList-control-${controlId}`);
|
||||
await dashboardControls.verifyControlType(controlId, 'optionsList-control');
|
||||
const newControlId: string = (await dashboardControls.getAllControlIds())[0];
|
||||
await testSubjects.waitForEnabled(`optionsList-control-${newControlId}`);
|
||||
await dashboardControls.verifyControlType(newControlId, 'optionsList-control');
|
||||
};
|
||||
|
||||
const replaceWithRangeSlider = async (controlId: string, field: string) => {
|
||||
await changeFieldType(controlId, field, RANGE_SLIDER_CONTROL);
|
||||
await retry.try(async () => {
|
||||
await dashboardControls.rangeSliderWaitForLoading(controlId);
|
||||
await dashboardControls.verifyControlType(controlId, 'range-slider-control');
|
||||
const newControlId: string = (await dashboardControls.getAllControlIds())[0];
|
||||
await dashboardControls.rangeSliderWaitForLoading(newControlId);
|
||||
await dashboardControls.verifyControlType(newControlId, 'range-slider-control');
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -68,7 +70,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('Replace options list', () => {
|
||||
beforeEach(async () => {
|
||||
await dashboardControls.clearAllControls();
|
||||
await dashboardControls.createControl({
|
||||
controlType: OPTIONS_LIST_CONTROL,
|
||||
dataViewTitle: 'animals-*',
|
||||
|
@ -78,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dashboard.clearUnsavedChanges();
|
||||
await dashboardControls.clearAllControls();
|
||||
});
|
||||
|
||||
it('with range slider - default title', async () => {
|
||||
|
@ -100,7 +101,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('Replace range slider', () => {
|
||||
beforeEach(async () => {
|
||||
await dashboardControls.clearAllControls();
|
||||
await dashboardControls.createControl({
|
||||
controlType: RANGE_SLIDER_CONTROL,
|
||||
dataViewTitle: 'animals-*',
|
||||
|
@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dashboard.clearUnsavedChanges();
|
||||
await dashboardControls.clearAllControls();
|
||||
});
|
||||
|
||||
it('with options list - default title', async () => {
|
||||
|
|
|
@ -17,11 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
const { dashboardControls, dashboard, header } = getPageObjects([
|
||||
'dashboardControls',
|
||||
'timePicker',
|
||||
'dashboard',
|
||||
'settings',
|
||||
'console',
|
||||
'common',
|
||||
'header',
|
||||
]);
|
||||
|
||||
|
@ -52,6 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
after(async () => {
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
|
||||
await dashboard.clickDiscardChanges();
|
||||
});
|
||||
|
||||
it('sort alphabetically - descending', async () => {
|
||||
|
@ -133,12 +130,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
for (let i = 0; i < sortedSuggestions.length - 1; i++) {
|
||||
expect(sortedSuggestions[i]).to.be.lessThan(sortedSuggestions[i + 1]);
|
||||
}
|
||||
|
||||
// revert to the old field name to keep state consistent for other tests
|
||||
await dashboardControls.editExistingControl(controlId);
|
||||
await dashboardControls.controlsEditorSetfield('sound.keyword');
|
||||
await dashboardControls.optionsListSetAdditionalSettings({ searchTechnique: 'prefix' });
|
||||
await dashboardControls.controlEditorSave();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { pick } from 'lodash';
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
|
||||
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import { OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS } from '../../../../page_objects/dashboard_page_controls';
|
||||
|
@ -18,8 +17,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const queryBar = getService('queryBar');
|
||||
const pieChart = getService('pieChart');
|
||||
const filterBar = getService('filterBar');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
|
||||
const { dashboardControls, dashboard, header } = getPageObjects([
|
||||
'dashboardControls',
|
||||
|
@ -32,41 +29,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
]);
|
||||
|
||||
describe('Dashboard options list validation', () => {
|
||||
let controlId: string;
|
||||
const controlId = 'cd881630-fd28-4e9c-aec5-ae9711d48369';
|
||||
|
||||
before(async () => {
|
||||
await dashboard.loadSavedDashboard('Test Options List Validation');
|
||||
await dashboard.ensureDashboardIsInEditMode();
|
||||
await dashboardControls.createControl({
|
||||
controlType: OPTIONS_LIST_CONTROL,
|
||||
dataViewTitle: 'animals-*',
|
||||
fieldName: 'sound.keyword',
|
||||
title: 'Animal Sounds',
|
||||
});
|
||||
controlId = (await dashboardControls.getAllControlIds())[0];
|
||||
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
|
||||
await dashboard.clickQuickSave();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await filterBar.removeAllFilters();
|
||||
await dashboardControls.deleteAllControls();
|
||||
await dashboardPanelActions.removePanelByTitle('Rendering Test: animal sounds pie');
|
||||
await dashboard.clickQuickSave();
|
||||
});
|
||||
|
||||
describe('Options List dashboard validation', () => {
|
||||
before(async () => {
|
||||
await dashboardControls.optionsListOpenPopover(controlId);
|
||||
await dashboardControls.optionsListPopoverSelectOption('meow');
|
||||
await dashboardControls.optionsListPopoverSelectOption('bark');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await dashboardControls.clearControlSelections(controlId);
|
||||
// Instead of reset, filter must be manually deleted to avoid
|
||||
// https://github.com/elastic/kibana/issues/191675
|
||||
await filterBar.removeAllFilters();
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
});
|
||||
|
||||
it('Can mark selections invalid with Query', async () => {
|
||||
|
@ -118,13 +92,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('Options List dashboard no validation', () => {
|
||||
before(async () => {
|
||||
await dashboardControls.optionsListOpenPopover(controlId);
|
||||
await dashboardControls.optionsListPopoverSelectOption('meow');
|
||||
await dashboardControls.optionsListPopoverSelectOption('bark');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
|
||||
await dashboardControls.updateValidationSetting(false);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await dashboard.clickDiscardChanges();
|
||||
});
|
||||
|
||||
it('Does not mark selections invalid with Query', async () => {
|
||||
await queryBar.setQuery('NOT animal.keyword : "dog" ');
|
||||
await queryBar.submitQuery();
|
||||
|
|
|
@ -3225,3 +3225,108 @@
|
|||
"coreMigrationVersion": "8.8.0",
|
||||
"typeMigrationVersion": "10.2.0"
|
||||
}
|
||||
|
||||
{
|
||||
"id": "55bc0b4b-a50f-46bf-b154-dd156067eea5",
|
||||
"type": "dashboard",
|
||||
"namespaces": [
|
||||
"default"
|
||||
],
|
||||
"updated_at": "2024-08-26T13:30:47.442Z",
|
||||
"created_at": "2024-08-26T13:29:23.580Z",
|
||||
"version": "WzEwNiwxXQ==",
|
||||
"attributes": {
|
||||
"version": 2,
|
||||
"controlGroupInput": {
|
||||
"chainingSystem": "HIERARCHICAL",
|
||||
"controlStyle": "oneLine",
|
||||
"showApplySelections": true,
|
||||
"ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}",
|
||||
"panelsJSON": "{\"41827e70-5285-4d44-8375-4c498449b9a7\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"a0f483a0-3dc9-11e8-8660-4d65aa086b3c\",\"fieldName\":\"animal.keyword\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"}}},\"515e7b9f-4f1b-4a06-beec-763810e4951a\":{\"grow\":true,\"order\":1,\"type\":\"rangeSliderControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"a0f483a0-3dc9-11e8-8660-4d65aa086b3c\",\"fieldName\":\"weightLbs\",\"step\":1}},\"b33b103a-84e2-4c2f-b4bd-be143dbd7e8a\":{\"grow\":true,\"order\":2,\"type\":\"timeSlider\",\"width\":\"large\",\"explicitInput\":{}}}"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
|
||||
},
|
||||
"description": "",
|
||||
"refreshInterval": {
|
||||
"pause": true,
|
||||
"value": 60000
|
||||
},
|
||||
"timeRestore": true,
|
||||
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
|
||||
"panelsJSON": "[{\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ffc13252-56b4-4e3f-847e-61373fa0be86\"},\"panelIndex\":\"ffc13252-56b4-4e3f-847e-61373fa0be86\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_ffc13252-56b4-4e3f-847e-61373fa0be86\"}]",
|
||||
"timeFrom": "2018-01-01T00:00:00.000Z",
|
||||
"title": "Test Control Group Apply Button",
|
||||
"timeTo": "2018-04-13T00:00:00.000Z"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"name": "ffc13252-56b4-4e3f-847e-61373fa0be86:panel_ffc13252-56b4-4e3f-847e-61373fa0be86",
|
||||
"type": "visualization",
|
||||
"id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_41827e70-5285-4d44-8375-4c498449b9a7:optionsListControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_515e7b9f-4f1b-4a06-beec-763810e4951a:rangeSliderControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c"
|
||||
}
|
||||
],
|
||||
"managed": false,
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"typeMigrationVersion": "10.2.0"
|
||||
}
|
||||
|
||||
{
|
||||
"id": "0b61857d-b7d3-4b4b-aa6b-773808361cd6",
|
||||
"type": "dashboard",
|
||||
"namespaces": [
|
||||
"default"
|
||||
],
|
||||
"updated_at": "2024-08-26T15:23:33.053Z",
|
||||
"created_at": "2024-08-26T15:22:39.194Z",
|
||||
"version": "WzE1MTksMV0=",
|
||||
"attributes": {
|
||||
"version": 2,
|
||||
"controlGroupInput": {
|
||||
"chainingSystem": "HIERARCHICAL",
|
||||
"controlStyle": "oneLine",
|
||||
"showApplySelections": false,
|
||||
"ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}",
|
||||
"panelsJSON": "{\"cd881630-fd28-4e9c-aec5-ae9711d48369\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"a0f483a0-3dc9-11e8-8660-4d65aa086b3c\",\"fieldName\":\"sound.keyword\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[\"meow\",\"bark\"],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"}}}}"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
|
||||
},
|
||||
"description": "",
|
||||
"refreshInterval": {
|
||||
"pause": true,
|
||||
"value": 60000
|
||||
},
|
||||
"timeRestore": true,
|
||||
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
|
||||
"panelsJSON": "[{\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"12415efc-008a-4f02-bad4-5c1f0d9ba1c6\"},\"panelIndex\":\"12415efc-008a-4f02-bad4-5c1f0d9ba1c6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12415efc-008a-4f02-bad4-5c1f0d9ba1c6\"}]",
|
||||
"timeFrom": "2018-01-01T00:00:00.000Z",
|
||||
"title": "Test Options List Validation",
|
||||
"timeTo": "2018-04-13T00:00:00.000Z"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"name": "12415efc-008a-4f02-bad4-5c1f0d9ba1c6:panel_12415efc-008a-4f02-bad4-5c1f0d9ba1c6",
|
||||
"type": "visualization",
|
||||
"id": "50643b60-3dd3-11e8-b2b9-5d5dc1715159"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_cd881630-fd28-4e9c-aec5-ae9711d48369:optionsListControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c"
|
||||
}
|
||||
],
|
||||
"managed": false,
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"typeMigrationVersion": "10.2.0"
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"id": "2af8906f-143b-4152-9f74-4994fb9c7b3e",
|
||||
"type": "dashboard",
|
||||
"namespaces": [
|
||||
"default"
|
||||
],
|
||||
"updated_at": "2024-08-27T16:43:33.847Z",
|
||||
"created_at": "2024-08-27T16:43:33.847Z",
|
||||
"version": "WzIwNSwxXQ==",
|
||||
"attributes": {
|
||||
"version": 2,
|
||||
"controlGroupInput": {
|
||||
"chainingSystem": "HIERARCHICAL",
|
||||
"controlStyle": "oneLine",
|
||||
"showApplySelections": false,
|
||||
"ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}",
|
||||
"panelsJSON": "{\"265b6a28-9ccb-44ae-83c9-3d7a7cac1961\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"fieldName\":\"Carrier\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"}}},\"ed2b93e2-da37-482b-ae43-586a41cc2399\":{\"grow\":true,\"order\":1,\"type\":\"rangeSliderControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"fieldName\":\"AvgTicketPrice\",\"title\":\"Average Ticket Price\",\"step\":1}},\"5e1b146b-8a8b-4117-9218-c4aeaee7bc9a\":{\"grow\":true,\"order\":2,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"0bf35f60-3dc9-11e8-8660-4d65aa086b3c\",\"fieldName\":\"machine.os.raw\",\"title\":\"Operating System\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"}}},\"c4760951-e793-45d5-a6b7-c72c145af7f9\":{\"grow\":true,\"order\":3,\"type\":\"rangeSliderControl\",\"width\":\"medium\",\"explicitInput\":{\"dataViewId\":\"0bf35f60-3dc9-11e8-8660-4d65aa086b3c\",\"fieldName\":\"bytes\",\"title\":\"Bytes\",\"step\":1}}}"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
|
||||
},
|
||||
"description": "",
|
||||
"refreshInterval": {
|
||||
"pause": true,
|
||||
"value": 60000
|
||||
},
|
||||
"timeRestore": true,
|
||||
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
|
||||
"panelsJSON": "[{\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"d75a68e9-67d9-4bed-9dba-85490d3eec37\"},\"panelIndex\":\"d75a68e9-67d9-4bed-9dba-85490d3eec37\",\"embeddableConfig\":{\"description\":\"\",\"enhancements\":{}},\"title\":\"logstash hits\",\"panelRefName\":\"panel_d75a68e9-67d9-4bed-9dba-85490d3eec37\"}]",
|
||||
"timeFrom": "2018-04-10T00:00:00.000Z",
|
||||
"title": "Test Control Group With Multiple Data Views",
|
||||
"timeTo": "2018-11-15T00:00:00.000Z"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"name": "d75a68e9-67d9-4bed-9dba-85490d3eec37:panel_d75a68e9-67d9-4bed-9dba-85490d3eec37",
|
||||
"type": "search",
|
||||
"id": "2b9247e0-6458-11ed-9957-e76caeeb9f75"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_265b6a28-9ccb-44ae-83c9-3d7a7cac1961:optionsListControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "d3d7af60-4c81-11e8-b3d7-01146121b73d"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_ed2b93e2-da37-482b-ae43-586a41cc2399:rangeSliderControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "d3d7af60-4c81-11e8-b3d7-01146121b73d"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_5e1b146b-8a8b-4117-9218-c4aeaee7bc9a:optionsListControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c"
|
||||
},
|
||||
{
|
||||
"name": "controlGroup_c4760951-e793-45d5-a6b7-c72c145af7f9:rangeSliderControlDataView",
|
||||
"type": "index-pattern",
|
||||
"id": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c"
|
||||
}
|
||||
],
|
||||
"managed": false,
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"typeMigrationVersion": "10.2.0"
|
||||
}
|
|
@ -475,7 +475,11 @@ export class DashboardPageControls extends FtrService {
|
|||
await this.optionsListWaitForLoading(controlId);
|
||||
if (!skipOpen) await this.optionsListOpenPopover(controlId);
|
||||
await this.retry.try(async () => {
|
||||
expect(await this.optionsListPopoverGetAvailableOptions()).to.eql(expectation);
|
||||
const availableOptions = await this.optionsListPopoverGetAvailableOptions();
|
||||
expect(availableOptions.suggestions).to.eql(expectation.suggestions);
|
||||
expect(availableOptions.invalidSelections.sort()).to.eql(
|
||||
expectation.invalidSelections.sort()
|
||||
);
|
||||
});
|
||||
if (await this.testSubjects.exists('optionsList-cardinality-label')) {
|
||||
expect(await this.optionsListGetCardinalityValue()).to.be(
|
||||
|
@ -496,7 +500,9 @@ export class DashboardPageControls extends FtrService {
|
|||
public async optionsListPopoverSearchForOption(search: string) {
|
||||
this.log.debug(`searching for ${search} in options list`);
|
||||
await this.optionsListPopoverAssertOpen();
|
||||
await this.testSubjects.setValue(`optionsList-control-search-input`, search);
|
||||
await this.testSubjects.setValue(`optionsList-control-search-input`, search, {
|
||||
typeCharByChar: true,
|
||||
});
|
||||
await this.optionsListPopoverWaitForLoading();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,7 @@ import {
|
|||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { buildExistsFilter, buildPhraseFilter, Filter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { controlGroupStateBuilder } from '@kbn/controls-plugin/public';
|
||||
import { NotificationsStart } from '@kbn/core/public';
|
||||
import {
|
||||
ENVIRONMENT_ALL,
|
||||
|
@ -71,10 +70,9 @@ async function getCreationOptions(
|
|||
dataView: DataView
|
||||
): Promise<DashboardCreationOptions> {
|
||||
try {
|
||||
const builder = controlGroupInputBuilder;
|
||||
const controlGroupInput = getDefaultControlGroupInput();
|
||||
const controlGroupState = {};
|
||||
|
||||
await builder.addDataControlFromField(controlGroupInput, {
|
||||
await controlGroupStateBuilder.addDataControlFromField(controlGroupState, {
|
||||
dataViewId: dataView.id ?? '',
|
||||
title: 'Node name',
|
||||
fieldName: 'service.node.name',
|
||||
|
@ -92,7 +90,7 @@ async function getCreationOptions(
|
|||
getInitialInput: () => ({
|
||||
viewMode: ViewMode.VIEW,
|
||||
panels,
|
||||
controlGroupInput,
|
||||
controlGroupState,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue