mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Embeddable Rebuild] Migrate ControlGroupRenderer
to new embeddable framework (#190561)
Closes https://github.com/elastic/kibana/issues/189820 ### Summary This PR converts the `ControlGroupRenderer` to use the new control group embeddable, which is built on the new React embeddable framework. With this conversion, there should not be **any** changes in user-facing behaviour - therefore, testing of this PR should be focused on ensuring that no behaviour is changed and/or broken with this refactor. **Notes to Solution Reviewers:** - There should be minimal changes to your uses of `ControlGroupRenderer` - our goal here was to keep the exposed API more-or-less consistent with this refactor. Therefore, most changes are simply renames + changes of imports. - That being said, `updateInput` and `getInput$` are **very much** tied to the old embeddable infrastructure - so while they will continue to work for now, they have been deprecated in favour of adding setters/getters for the parts of the control group state that you need to update / respond to. **Notes to Presentation Reviewer:** - The bundle size was originally being increased by this PR, so I decided to remove a bunch of the public exports that are no longer necessary as a final cleanup - this resulted in changes to imports in a few files, but it was worth doing in this PR IMO so that we didn't have to increase the Controls bundle limit. Now, this PR shrinks the bundle size 🎉 - I fixed a small bug with the default value of `showApplySelections` in this PR - since it was a one-line change, if felt like overkill to separate it out. See https://github.com/elastic/kibana/pull/190561/files#r1733253015 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3ac27f4ba3
commit
77e4728a34
75 changed files with 1361 additions and 1481 deletions
|
@ -13,8 +13,6 @@ import useAsync from 'react-use/lib/useAsync';
|
|||
import { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { SearchExample } from './control_group_renderer_examples/search_example';
|
||||
import { EditExample } from './control_group_renderer_examples/edit_example';
|
||||
import { BasicReduxExample } from './control_group_renderer_examples/basic_redux_example';
|
||||
import { AddButtonExample } from './control_group_renderer_examples/add_button_example';
|
||||
import { ControlsExampleStartDeps } from '../plugin';
|
||||
|
||||
export const ControlGroupRendererExamples = ({
|
||||
|
@ -36,10 +34,6 @@ export const ControlGroupRendererExamples = ({
|
|||
<SearchExample dataView={dataViews[0]} navigation={navigation} data={data} />
|
||||
<EuiSpacer size="xl" />
|
||||
<EditExample />
|
||||
<EuiSpacer size="xl" />
|
||||
<BasicReduxExample dataViewId={dataViews[0].id!} />
|
||||
<EuiSpacer size="xl" />
|
||||
<AddButtonExample dataViewId={dataViews[0].id!} />
|
||||
</>
|
||||
) : (
|
||||
<EuiText>{'Install web logs sample data to run controls examples.'}</EuiText>
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
|
||||
export const AddButtonExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Add button example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>
|
||||
Use the built in add button to add controls to a control group based on a hardcoded
|
||||
dataViewId and a simplified editor flyout
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={async (initialInput, builder) => {
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
dataViewId,
|
||||
title: 'Destintion',
|
||||
fieldName: 'geo.dest',
|
||||
grow: false,
|
||||
width: 'small',
|
||||
});
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
dataViewId,
|
||||
fieldName: 'geo.src',
|
||||
grow: false,
|
||||
title: 'Source',
|
||||
width: 'small',
|
||||
});
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.EDIT,
|
||||
defaultControlGrow: false,
|
||||
defaultControlWidth: 'small',
|
||||
},
|
||||
settings: {
|
||||
showAddButton: true,
|
||||
staticDataViewId: dataViewId,
|
||||
editorConfig: {
|
||||
hideAdditionalSettings: true,
|
||||
hideDataViewSelector: true,
|
||||
hideWidthSettings: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { EuiButtonGroup, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { ControlGroupRenderer, ControlStyle, ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||
import { AwaitingControlGroupAPI } from '@kbn/controls-plugin/public/control_group';
|
||||
|
||||
export const BasicReduxExample = ({ dataViewId }: { dataViewId: string }) => {
|
||||
const [controlGroupAPI, setControlGroupApi] = useState<AwaitingControlGroupAPI>(null);
|
||||
|
||||
const Buttons = ({ api }: { api: ControlGroupAPI }) => {
|
||||
const controlStyle = api.select((state) => state.explicitInput.controlStyle);
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
legend="Text style"
|
||||
options={[
|
||||
{
|
||||
id: `oneLine`,
|
||||
label: 'One line',
|
||||
value: 'oneLine' as ControlStyle,
|
||||
},
|
||||
{
|
||||
id: `twoLine`,
|
||||
label: 'Two lines',
|
||||
value: 'twoLine' as ControlStyle,
|
||||
},
|
||||
]}
|
||||
idSelected={controlStyle}
|
||||
onChange={(id, value) => api.dispatch.setControlStyle(value)}
|
||||
type="single"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Redux example</h2>
|
||||
</EuiTitle>
|
||||
<EuiText>
|
||||
<p>Use the redux context from the control group to set layout style.</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
{controlGroupAPI && <Buttons api={controlGroupAPI} />}
|
||||
|
||||
<ControlGroupRenderer
|
||||
ref={setControlGroupApi}
|
||||
getCreationOptions={async (initialInput, builder) => {
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
dataViewId,
|
||||
title: 'Destintion country',
|
||||
fieldName: 'geo.dest',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
dataViewId,
|
||||
fieldName: 'bytes',
|
||||
width: 'medium',
|
||||
grow: true,
|
||||
title: 'Bytes',
|
||||
});
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.VIEW,
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { pickBy } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -23,50 +23,51 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
|
||||
import { ControlGroupRuntimeState, ControlStateTransform } from '@kbn/controls-plugin/public';
|
||||
import {
|
||||
type ControlGroupInput,
|
||||
ControlGroupRenderer,
|
||||
AwaitingControlGroupAPI,
|
||||
ACTION_EDIT_CONTROL,
|
||||
ACTION_DELETE_CONTROL,
|
||||
ACTION_EDIT_CONTROL,
|
||||
ControlGroupRenderer,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { ControlInputTransform } from '@kbn/controls-plugin/common/types';
|
||||
import { ControlGroupRendererApi } from '@kbn/controls-plugin/public';
|
||||
|
||||
const INPUT_KEY = 'kbnControls:saveExample:input';
|
||||
|
||||
const WITH_CUSTOM_PLACEHOLDER = 'Custom Placeholder';
|
||||
|
||||
type StoredState = ControlGroupRuntimeState & { disabledActions: string[] };
|
||||
|
||||
export const EditExample = () => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>(null);
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<ControlGroupRendererApi | undefined>();
|
||||
const [toggleIconIdToSelectedMapIcon, setToggleIconIdToSelectedMapIcon] = useState<{
|
||||
[id: string]: boolean;
|
||||
}>({});
|
||||
|
||||
function onChangeIconsMultiIcons(optionId: string) {
|
||||
const newToggleIconIdToSelectedMapIcon = {
|
||||
...toggleIconIdToSelectedMapIcon,
|
||||
...{
|
||||
[optionId]: !toggleIconIdToSelectedMapIcon[optionId],
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (controlGroupAPI) {
|
||||
const disabledActions: string[] = Object.keys(
|
||||
pickBy(newToggleIconIdToSelectedMapIcon, (value) => value)
|
||||
pickBy(
|
||||
toggleIconIdToSelectedMapIcon,
|
||||
(value, key) => value && key !== WITH_CUSTOM_PLACEHOLDER
|
||||
)
|
||||
);
|
||||
controlGroupAPI.updateInput({ disabledActions });
|
||||
controlGroupAPI.setDisabledActionIds(disabledActions);
|
||||
}
|
||||
|
||||
setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon);
|
||||
}
|
||||
}, [controlGroupAPI, toggleIconIdToSelectedMapIcon]);
|
||||
|
||||
async function onSave() {
|
||||
if (!controlGroupAPI) return;
|
||||
|
||||
setIsSaving(true);
|
||||
localStorage.setItem(INPUT_KEY, JSON.stringify(controlGroupAPI.getInput()));
|
||||
localStorage.setItem(
|
||||
INPUT_KEY,
|
||||
JSON.stringify({
|
||||
...controlGroupAPI.snapshotRuntimeState(),
|
||||
disabledActions: controlGroupAPI.disabledActionIds.getValue(), // not part of runtime
|
||||
})
|
||||
);
|
||||
|
||||
// simulated async save await
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
@ -80,12 +81,12 @@ export const EditExample = () => {
|
|||
// simulated async load await
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
|
||||
let input: Partial<ControlGroupInput> = {};
|
||||
let state: Partial<StoredState> = {};
|
||||
let disabledActions = [];
|
||||
const inputAsString = localStorage.getItem(INPUT_KEY);
|
||||
if (inputAsString) {
|
||||
try {
|
||||
input = JSON.parse(inputAsString);
|
||||
const disabledActions = input.disabledActions ?? [];
|
||||
({ disabledActions, ...state } = JSON.parse(inputAsString));
|
||||
setToggleIconIdToSelectedMapIcon({
|
||||
[ACTION_EDIT_CONTROL]: disabledActions.includes(ACTION_EDIT_CONTROL),
|
||||
[ACTION_DELETE_CONTROL]: disabledActions.includes(ACTION_DELETE_CONTROL),
|
||||
|
@ -96,10 +97,10 @@ export const EditExample = () => {
|
|||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
return input;
|
||||
return state;
|
||||
}
|
||||
|
||||
const controlInputTransform: ControlInputTransform = (newState, type) => {
|
||||
const controlStateTransform: ControlStateTransform = (newState, type) => {
|
||||
if (type === OPTIONS_LIST_CONTROL && toggleIconIdToSelectedMapIcon[WITH_CUSTOM_PLACEHOLDER]) {
|
||||
return {
|
||||
...newState,
|
||||
|
@ -134,7 +135,8 @@ export const EditExample = () => {
|
|||
iconType="plusInCircle"
|
||||
isDisabled={controlGroupAPI === undefined}
|
||||
onClick={() => {
|
||||
controlGroupAPI!.openAddDataControlFlyout({ controlInputTransform });
|
||||
if (!controlGroupAPI) return;
|
||||
controlGroupAPI.openAddDataControlFlyout({ controlStateTransform });
|
||||
}}
|
||||
>
|
||||
Add control
|
||||
|
@ -163,7 +165,15 @@ export const EditExample = () => {
|
|||
]}
|
||||
idToSelectedMap={toggleIconIdToSelectedMapIcon}
|
||||
type="multi"
|
||||
onChange={(id: string) => onChangeIconsMultiIcons(id)}
|
||||
onChange={(optionId: string) => {
|
||||
const newToggleIconIdToSelectedMapIcon = {
|
||||
...toggleIconIdToSelectedMapIcon,
|
||||
...{
|
||||
[optionId]: !toggleIconIdToSelectedMapIcon[optionId],
|
||||
},
|
||||
};
|
||||
setToggleIconIdToSelectedMapIcon(newToggleIconIdToSelectedMapIcon);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -185,17 +195,17 @@ export const EditExample = () => {
|
|||
</>
|
||||
) : null}
|
||||
<ControlGroupRenderer
|
||||
ref={setControlGroupAPI}
|
||||
getCreationOptions={async (initialInput, builder) => {
|
||||
const persistedInput = await onLoad();
|
||||
onApiAvailable={setControlGroupAPI}
|
||||
getCreationOptions={async (initialState, builder) => {
|
||||
const persistedState = await onLoad();
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
...persistedInput,
|
||||
viewMode: ViewMode.EDIT,
|
||||
initialState: {
|
||||
...initialState,
|
||||
...persistedState,
|
||||
},
|
||||
};
|
||||
}}
|
||||
viewMode={ViewMode.EDIT}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
|
|
|
@ -14,7 +14,6 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiLoadingSpinner,
|
||||
|
@ -23,7 +22,7 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupRenderer, ControlGroupRendererApi } from '@kbn/controls-plugin/public';
|
||||
import { PLUGIN_ID } from '../../constants';
|
||||
|
||||
interface Props {
|
||||
|
@ -34,7 +33,7 @@ interface Props {
|
|||
|
||||
export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
||||
const [controlFilters, setControlFilters] = useState<Filter[]>([]);
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>();
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<ControlGroupRendererApi | undefined>();
|
||||
const [hits, setHits] = useState(0);
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
@ -48,8 +47,8 @@ export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
|||
if (!controlGroupAPI) {
|
||||
return;
|
||||
}
|
||||
const subscription = controlGroupAPI.onFiltersPublished$.subscribe((newFilters) => {
|
||||
setControlFilters([...newFilters]);
|
||||
const subscription = controlGroupAPI.filters$.subscribe((newFilters) => {
|
||||
setControlFilters(newFilters ?? []);
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
|
@ -131,15 +130,15 @@ export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
|||
/>
|
||||
<ControlGroupRenderer
|
||||
filters={filters}
|
||||
getCreationOptions={async (initialInput, builder) => {
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
getCreationOptions={async (initialState, builder) => {
|
||||
await builder.addDataControlFromField(initialState, {
|
||||
dataViewId: dataView.id!,
|
||||
title: 'Destintion country',
|
||||
fieldName: 'geo.dest',
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
});
|
||||
await builder.addDataControlFromField(initialInput, {
|
||||
await builder.addDataControlFromField(initialState, {
|
||||
dataViewId: dataView.id!,
|
||||
fieldName: 'bytes',
|
||||
width: 'medium',
|
||||
|
@ -147,14 +146,11 @@ export const SearchExample = ({ data, dataView, navigation }: Props) => {
|
|||
title: 'Bytes',
|
||||
});
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.VIEW,
|
||||
},
|
||||
initialState,
|
||||
};
|
||||
}}
|
||||
query={query}
|
||||
ref={setControlGroupAPI}
|
||||
onApiAvailable={setControlGroupAPI}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<EuiCallOut title="Search results">
|
||||
|
|
|
@ -26,9 +26,8 @@ import type {
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupRendererApi, ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
import { css } from '@emotion/react';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { ControlsPanels } from '@kbn/controls-plugin/common';
|
||||
import { Route, Router, Routes } from '@kbn/shared-ux-router';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
@ -343,7 +342,9 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
|
|||
);
|
||||
},
|
||||
PrependFilterBar: () => {
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>();
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<
|
||||
ControlGroupRendererApi | undefined
|
||||
>();
|
||||
const stateStorage = stateContainer.stateStorage;
|
||||
const dataView = useObservable(
|
||||
stateContainer.internalState.state$,
|
||||
|
@ -357,18 +358,19 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
|
|||
|
||||
const stateSubscription = stateStorage
|
||||
.change$<ControlsPanels>('controlPanels')
|
||||
.subscribe((panels) => controlGroupAPI.updateInput({ panels: panels ?? undefined }));
|
||||
.subscribe((panels) =>
|
||||
controlGroupAPI.updateInput({ initialChildControlState: panels ?? undefined })
|
||||
);
|
||||
|
||||
const inputSubscription = controlGroupAPI.getInput$().subscribe((input) => {
|
||||
if (input && input.panels) stateStorage.set('controlPanels', input.panels);
|
||||
if (input && input.initialChildControlState)
|
||||
stateStorage.set('controlPanels', input.initialChildControlState);
|
||||
});
|
||||
|
||||
const filterSubscription = controlGroupAPI.onFiltersPublished$.subscribe(
|
||||
(newFilters) => {
|
||||
stateContainer.internalState.transitions.setCustomFilters(newFilters);
|
||||
stateContainer.actions.fetchData();
|
||||
}
|
||||
);
|
||||
const filterSubscription = controlGroupAPI.filters$.subscribe((newFilters = []) => {
|
||||
stateContainer.internalState.transitions.setCustomFilters(newFilters);
|
||||
stateContainer.actions.fetchData();
|
||||
});
|
||||
|
||||
return () => {
|
||||
stateSubscription.unsubscribe();
|
||||
|
@ -406,12 +408,12 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
|
|||
`}
|
||||
>
|
||||
<ControlGroupRenderer
|
||||
ref={setControlGroupAPI}
|
||||
getCreationOptions={async (initialInput, builder) => {
|
||||
onApiAvailable={setControlGroupAPI}
|
||||
getCreationOptions={async (initialState, builder) => {
|
||||
const panels = stateStorage.get<ControlsPanels>('controlPanels');
|
||||
|
||||
if (!panels) {
|
||||
await builder.addOptionsListControl(initialInput, {
|
||||
builder.addOptionsListControl(initialState, {
|
||||
dataViewId: dataView?.id!,
|
||||
title: fieldToFilterOn.name.split('.')[0],
|
||||
fieldName: fieldToFilterOn.name,
|
||||
|
@ -421,14 +423,13 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin {
|
|||
}
|
||||
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
panels: panels ?? initialInput.panels,
|
||||
viewMode: ViewMode.VIEW,
|
||||
filters: stateContainer.appState.get().filters ?? [],
|
||||
initialState: {
|
||||
...initialState,
|
||||
initialChildControlState: panels ?? initialState.initialChildControlState,
|
||||
},
|
||||
};
|
||||
}}
|
||||
filters={stateContainer.appState.get().filters ?? []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
"@kbn/discover-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { AddOptionsListControlProps } from '@kbn/controls-plugin/public';
|
||||
import { ALERT_RULE_NAME, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import { OptionsListControlState } from '@kbn/controls-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FilterControlConfig } from './types';
|
||||
|
||||
|
@ -65,14 +65,11 @@ export const TEST_IDS = {
|
|||
},
|
||||
};
|
||||
|
||||
export const COMMON_OPTIONS_LIST_CONTROL_INPUTS: Partial<AddOptionsListControlProps> = {
|
||||
export const COMMON_OPTIONS_LIST_CONTROL_INPUTS: Partial<OptionsListControlState> = {
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
hidePanelTitles: true,
|
||||
placeholder: '',
|
||||
ignoreParentSettings: {
|
||||
ignoreValidations: true,
|
||||
},
|
||||
width: 'small',
|
||||
};
|
||||
|
||||
export const TIMEOUTS = {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { controlGroupStateBuilder } from '@kbn/controls-plugin/public';
|
||||
import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { COMMON_OPTIONS_LIST_CONTROL_INPUTS, TEST_IDS } from './constants';
|
||||
|
@ -24,7 +25,7 @@ export const FilterGroupContextMenu = () => {
|
|||
|
||||
const {
|
||||
isViewMode,
|
||||
controlGroupInputUpdates,
|
||||
controlGroupStateUpdates,
|
||||
controlGroup,
|
||||
switchToViewMode,
|
||||
switchToEditMode,
|
||||
|
@ -51,29 +52,31 @@ export const FilterGroupContextMenu = () => {
|
|||
);
|
||||
|
||||
const resetSelection = useCallback(async () => {
|
||||
if (!controlGroupInputUpdates) return;
|
||||
|
||||
if (!controlGroupStateUpdates) return;
|
||||
// remove existing embeddables
|
||||
controlGroup?.updateInput({
|
||||
panels: {},
|
||||
});
|
||||
|
||||
const newInput = { initialChildControlState: {} };
|
||||
|
||||
for (let counter = 0; counter < initialControls.length; counter++) {
|
||||
const control = initialControls[counter];
|
||||
await controlGroup?.addOptionsListControl({
|
||||
controlId: String(counter),
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
// option List controls will handle an invalid dataview
|
||||
// & display an appropriate message
|
||||
dataViewId: dataViewId ?? '',
|
||||
...control,
|
||||
});
|
||||
controlGroupStateBuilder.addOptionsListControl(
|
||||
newInput,
|
||||
{
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
// option List controls will handle an invalid dataview
|
||||
// & display an appropriate message
|
||||
dataViewId: dataViewId ?? '',
|
||||
...control,
|
||||
},
|
||||
String(counter)
|
||||
);
|
||||
controlGroup?.updateInput(newInput);
|
||||
}
|
||||
|
||||
switchToViewMode();
|
||||
setShowFiltersChangedBanner(false);
|
||||
}, [
|
||||
controlGroupInputUpdates,
|
||||
controlGroupStateUpdates,
|
||||
controlGroup,
|
||||
initialControls,
|
||||
dataViewId,
|
||||
|
|
|
@ -11,13 +11,9 @@ import { FilterGroup } from './filter_group';
|
|||
import { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
ControlGroupOutput,
|
||||
ControlGroupInput,
|
||||
ControlGroupContainer,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupRendererApi, ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
|
||||
import { initialInputData, sampleOutputData } from './mocks/data';
|
||||
import { ControlGroupOutput, initialInputData, sampleOutputData } from './mocks/data';
|
||||
import {
|
||||
COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
DEFAULT_CONTROLS,
|
||||
|
@ -25,11 +21,14 @@ import {
|
|||
URL_PARAM_KEY,
|
||||
} from './constants';
|
||||
import {
|
||||
controlGroupFilterInputMock$,
|
||||
controlGroupFilterOutputMock$,
|
||||
controlGroupFilterStateMock$,
|
||||
getControlGroupMock,
|
||||
} from './mocks/control_group';
|
||||
import { getMockedControlGroupRenderer } from './mocks/control_group_renderer';
|
||||
import {
|
||||
addOptionsListControlMock,
|
||||
getMockedControlGroupRenderer,
|
||||
} from './mocks/control_group_renderer';
|
||||
import { URL_PARAM_ARRAY_EXCEPTION_MSG } from './translations';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -41,19 +40,19 @@ const LOCAL_STORAGE_KEY = `${featureIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`;
|
|||
|
||||
const controlGroupMock = getControlGroupMock();
|
||||
|
||||
const updateControlGroupInputMock = (newInput: ControlGroupInput) => {
|
||||
const updateControlGroupInputMock = (newState: ControlGroupRuntimeState) => {
|
||||
act(() => {
|
||||
controlGroupFilterInputMock$.next(newInput);
|
||||
controlGroupMock.getInput.mockReturnValue(newInput);
|
||||
controlGroupMock.snapshotRuntimeState.mockReturnValue(newState);
|
||||
controlGroupFilterStateMock$.next(newState);
|
||||
});
|
||||
};
|
||||
|
||||
const updateControlGroupOutputMock = (newOutput: ControlGroupOutput) => {
|
||||
controlGroupFilterOutputMock$.next(newOutput);
|
||||
controlGroupFilterOutputMock$.next(newOutput.filters);
|
||||
};
|
||||
|
||||
const MockedControlGroupRenderer = getMockedControlGroupRenderer(
|
||||
controlGroupMock as unknown as ControlGroupContainer
|
||||
controlGroupMock as unknown as ControlGroupRendererApi
|
||||
);
|
||||
|
||||
const onFilterChangeMock = jest.fn();
|
||||
|
@ -100,6 +99,7 @@ describe(' Filter Group Component ', () => {
|
|||
jest.clearAllMocks();
|
||||
global.localStorage.clear();
|
||||
});
|
||||
|
||||
it('should render', async () => {
|
||||
render(<TestComponent />);
|
||||
expect(screen.getByTestId(TEST_IDS.MOCKED_CONTROL)).toBeVisible();
|
||||
|
@ -122,7 +122,7 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
it('should go into edit mode without any issues', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
await openContextMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.CONTEXT_MENU.EDIT));
|
||||
await waitFor(() => {
|
||||
|
@ -135,7 +135,7 @@ describe(' Filter Group Component ', () => {
|
|||
it('should have add button disable/enable when controls are more/less than max', async () => {
|
||||
render(<TestComponent maxControls={4} />);
|
||||
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
|
||||
|
@ -147,10 +147,10 @@ describe(' Filter Group Component ', () => {
|
|||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
|
@ -165,7 +165,7 @@ describe(' Filter Group Component ', () => {
|
|||
it('should open flyout when clicked on ADD', async () => {
|
||||
render(<TestComponent maxControls={4} />);
|
||||
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
|
||||
|
@ -177,10 +177,10 @@ describe(' Filter Group Component ', () => {
|
|||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
|
@ -200,22 +200,22 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
it('should call controlGroupTransform which returns object WITHOUT placeholder when type != OPTION_LIST_CONTROL on opening Flyout', async () => {
|
||||
const returnValueWatcher = jest.fn();
|
||||
controlGroupMock.openAddDataControlFlyout.mockImplementationOnce(
|
||||
({ controlInputTransform }) => {
|
||||
if (controlInputTransform) {
|
||||
const returnValue = controlInputTransform({}, 'NOT_OPTIONS_LIST_CONTROL');
|
||||
(controlGroupMock as unknown as ControlGroupRendererApi).openAddDataControlFlyout = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(({ controlStateTransform }) => {
|
||||
if (controlStateTransform) {
|
||||
const returnValue = controlStateTransform({}, 'NOT_OPTIONS_LIST_CONTROL');
|
||||
returnValueWatcher(returnValue);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
render(<TestComponent />);
|
||||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
await openContextMenu();
|
||||
|
@ -237,23 +237,23 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
it('should call controlGroupTransform which returns object WITH correct placeholder value when type = OPTION_LIST_CONTROL on opening Flyout', async () => {
|
||||
const returnValueWatcher = jest.fn();
|
||||
controlGroupMock.openAddDataControlFlyout.mockImplementationOnce(
|
||||
({ controlInputTransform }) => {
|
||||
if (controlInputTransform) {
|
||||
const returnValue = controlInputTransform({}, OPTIONS_LIST_CONTROL);
|
||||
(controlGroupMock as unknown as ControlGroupRendererApi).openAddDataControlFlyout = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(({ controlStateTransform }) => {
|
||||
if (controlStateTransform) {
|
||||
const returnValue = controlStateTransform({}, OPTIONS_LIST_CONTROL);
|
||||
returnValueWatcher(returnValue);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
render(<TestComponent />);
|
||||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
|
@ -276,77 +276,80 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
it('should not rebuild controls while saving controls when controls are in desired order', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
await openContextMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.CONTEXT_MENU.EDIT));
|
||||
|
||||
// modify controls
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
initialChildControlState: {
|
||||
// status as persistent controls is first in the position with order as 0
|
||||
'0': initialInputData.panels['0'],
|
||||
'1': initialInputData.panels['1'],
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
'1': initialInputData.initialChildControlState['1'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
// clear any previous calls to the API
|
||||
controlGroupMock.addOptionsListControl.mockClear();
|
||||
controlGroupMock.updateInput.mockClear();
|
||||
addOptionsListControlMock.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.SAVE_CONTROL));
|
||||
|
||||
// edit model gone
|
||||
await waitFor(() => expect(screen.queryAllByTestId(TEST_IDS.SAVE_CONTROL)).toHaveLength(0));
|
||||
// check if upsert was called correctly
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(0);
|
||||
expect(addOptionsListControlMock.mock.calls.length).toBe(0);
|
||||
expect(controlGroupMock.updateInput.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should rebuild and save controls successfully when controls are not in desired order', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
await openContextMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.CONTEXT_MENU.EDIT));
|
||||
|
||||
// modify controls
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
initialChildControlState: {
|
||||
'0': {
|
||||
...initialInputData.panels['0'],
|
||||
...initialInputData.initialChildControlState['0'],
|
||||
// status is second in position.
|
||||
// this will force the rebuilding of controls
|
||||
order: 1,
|
||||
},
|
||||
'1': {
|
||||
...initialInputData.panels['1'],
|
||||
...initialInputData.initialChildControlState['1'],
|
||||
order: 0,
|
||||
},
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
// clear any previous calls to the API
|
||||
controlGroupMock.addOptionsListControl.mockClear();
|
||||
controlGroupMock.updateInput.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.SAVE_CONTROL));
|
||||
|
||||
// edit model gone
|
||||
await waitFor(() => expect(screen.queryAllByTestId(TEST_IDS.SAVE_CONTROL)).toHaveLength(0));
|
||||
// check if upsert was called correctly
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(2);
|
||||
// field id is not required to be passed when creating a control
|
||||
const { id, ...expectedInputData } = initialInputData.panels['0'].explicitInput;
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[0][0]).toMatchObject({
|
||||
...expectedInputData,
|
||||
expect(controlGroupMock.updateInput.mock.calls.length).toBe(1);
|
||||
expect(controlGroupMock.updateInput.mock.calls[0][0]).toMatchObject({
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
'1': initialInputData.initialChildControlState['1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add persistable controls back on save, if deleted', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.CONTEXT_MENU.EDIT));
|
||||
|
@ -354,16 +357,16 @@ describe(' Filter Group Component ', () => {
|
|||
// modify controls
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
initialChildControlState: {
|
||||
// removed persitable control i.e. status at "0" key
|
||||
'3': initialInputData.panels['3'],
|
||||
'3': initialInputData.initialChildControlState['3'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
// clear any previous calls to the API
|
||||
controlGroupMock.addOptionsListControl.mockClear();
|
||||
controlGroupMock.updateInput.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.SAVE_CONTROL));
|
||||
|
||||
|
@ -371,17 +374,12 @@ describe(' Filter Group Component ', () => {
|
|||
// edit model gone
|
||||
expect(screen.queryAllByTestId(TEST_IDS.SAVE_CONTROL)).toHaveLength(0);
|
||||
// check if upsert was called correctly
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(2);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[0][0]).toMatchObject({
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
...DEFAULT_CONTROLS[0],
|
||||
});
|
||||
|
||||
// field id is not required to be passed when creating a control
|
||||
const { id, ...expectedInputData } = initialInputData.panels['3'].explicitInput;
|
||||
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[1][0]).toMatchObject({
|
||||
...expectedInputData,
|
||||
expect(controlGroupMock.updateInput.mock.calls.length).toBe(1);
|
||||
expect(controlGroupMock.updateInput.mock.calls[0][0]).toMatchObject({
|
||||
initialChildControlState: {
|
||||
'0': { ...COMMON_OPTIONS_LIST_CONTROL_INPUTS, ...DEFAULT_CONTROLS[0] },
|
||||
'1': { ...initialInputData.initialChildControlState['3'], order: 1 },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -389,7 +387,7 @@ describe(' Filter Group Component ', () => {
|
|||
it('should have Context menu changed when pending changes', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
|
||||
|
@ -398,10 +396,10 @@ describe(' Filter Group Component ', () => {
|
|||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
|
@ -419,7 +417,7 @@ describe(' Filter Group Component ', () => {
|
|||
it('should be able to discard changes', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
|
||||
|
@ -428,16 +426,16 @@ describe(' Filter Group Component ', () => {
|
|||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
// await waitFor(() => {
|
||||
// expect(screen.getByTestId(TEST_IDS.SAVE_CHANGE_POPOVER)).toBeVisible();
|
||||
// });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(TEST_IDS.SAVE_CHANGE_POPOVER)).toBeVisible();
|
||||
});
|
||||
await openContextMenu();
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -449,15 +447,10 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(controlGroupMock.updateInput).toHaveBeenCalled();
|
||||
expect(controlGroupMock.updateInput.mock.calls.length).toBe(2);
|
||||
expect(controlGroupMock.updateInput.mock.calls.length).toBe(1);
|
||||
// discard changes
|
||||
expect(controlGroupMock.updateInput.mock.calls[0][0]).toMatchObject({
|
||||
panels: initialInputData.panels,
|
||||
});
|
||||
|
||||
// shift to view mode
|
||||
expect(controlGroupMock.updateInput.mock.calls[1][0]).toMatchObject({
|
||||
viewMode: 'view',
|
||||
initialChildControlState: initialInputData.initialChildControlState,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -465,28 +458,27 @@ describe(' Filter Group Component ', () => {
|
|||
it('should reset controls on clicking reset', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId(TEST_IDS.CONTEXT_MENU.RESET)).toBeVisible());
|
||||
|
||||
controlGroupMock.addOptionsListControl.mockClear();
|
||||
controlGroupMock.updateInput.mockClear();
|
||||
fireEvent.click(screen.getByTestId(TEST_IDS.CONTEXT_MENU.RESET));
|
||||
|
||||
// blanks the input
|
||||
await waitFor(() => expect(controlGroupMock.updateInput.mock.calls.length).toBe(2));
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(5);
|
||||
await waitFor(() => expect(controlGroupMock.updateInput.mock.calls.length).toBe(5));
|
||||
});
|
||||
|
||||
it('should restore controls saved in local storage', () => {
|
||||
it('should restore controls saved in local storage', async () => {
|
||||
addOptionsListControlMock.mockClear();
|
||||
global.localStorage.setItem(
|
||||
LOCAL_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -494,13 +486,13 @@ describe(' Filter Group Component ', () => {
|
|||
// should create one control
|
||||
//
|
||||
render(<TestComponent />);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(1);
|
||||
expect(addOptionsListControlMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should show/hide pending changes popover on mouseout/mouseover', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
|
||||
await openContextMenu();
|
||||
|
||||
|
@ -509,10 +501,10 @@ describe(' Filter Group Component ', () => {
|
|||
// delete some panels
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
} as ControlGroupInput;
|
||||
} as ControlGroupRuntimeState;
|
||||
|
||||
updateControlGroupInputMock(newInputData);
|
||||
|
||||
|
@ -531,36 +523,6 @@ describe(' Filter Group Component ', () => {
|
|||
expect(screen.queryByTestId(TEST_IDS.SAVE_CHANGE_POPOVER)).toBeVisible();
|
||||
});
|
||||
});
|
||||
it('should update controlGroup with new filters and queries when valid query is supplied', async () => {
|
||||
const validQuery = { query: { language: 'kuery', query: '' } };
|
||||
// pass an invalid query
|
||||
render(<TestComponent {...validQuery} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(controlGroupMock.updateInput).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
filters: undefined,
|
||||
query: validQuery.query,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update controlGroup with new filters and queries when invalid query is supplied', async () => {
|
||||
const invalidQuery = { query: { language: 'kuery', query: '\\' } };
|
||||
// pass an invalid query
|
||||
render(<TestComponent {...invalidQuery} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(controlGroupMock.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: [],
|
||||
query: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter Changed Banner', () => {
|
||||
|
@ -581,14 +543,16 @@ describe(' Filter Group Component ', () => {
|
|||
]}
|
||||
/>
|
||||
);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(TEST_IDS.FILTERS_CHANGED_BANNER)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use url filters if url and stored filters are not same', async () => {
|
||||
addOptionsListControlMock.mockClear();
|
||||
global.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(initialInputData));
|
||||
|
||||
render(
|
||||
<TestComponent
|
||||
controlsUrlState={[
|
||||
|
@ -598,18 +562,16 @@ describe(' Filter Group Component ', () => {
|
|||
]}
|
||||
/>
|
||||
);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(2);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[0][1]).toMatchObject({
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
expect(addOptionsListControlMock.mock.calls.length).toBe(2);
|
||||
expect(addOptionsListControlMock.mock.calls[0][1]).toMatchObject({
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
...DEFAULT_CONTROLS[0],
|
||||
});
|
||||
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[1][1]).toMatchObject({
|
||||
expect(addOptionsListControlMock.mock.calls[1][1]).toMatchObject({
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
fieldName: 'abc',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(TEST_IDS.FILTERS_CHANGED_BANNER)).toBeVisible();
|
||||
});
|
||||
|
@ -639,9 +601,10 @@ describe(' Filter Group Component ', () => {
|
|||
jest.useFakeTimers();
|
||||
global.localStorage.clear();
|
||||
});
|
||||
|
||||
it('should call onFilterChange when new filters have been published', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
updateControlGroupOutputMock(sampleOutputData);
|
||||
await waitFor(() => {
|
||||
expect(onFilterChangeMock.mock.calls.length).toBe(1);
|
||||
|
@ -658,13 +621,13 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
it('should pass empty onFilterChange as the initial state. Eg. in case of error', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
updateControlGroupOutputMock(sampleOutputData);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
updateControlGroupOutputMock({
|
||||
...sampleOutputData,
|
||||
filters: undefined,
|
||||
filters: [],
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(onFilterChangeMock.mock.calls.length).toBe(2);
|
||||
|
@ -681,7 +644,7 @@ describe(' Filter Group Component ', () => {
|
|||
|
||||
it('should not call onFilterChange if same set of filters are published twice', async () => {
|
||||
render(<TestComponent />);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
updateControlGroupOutputMock(sampleOutputData);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
@ -703,18 +666,15 @@ describe(' Filter Group Component ', () => {
|
|||
});
|
||||
|
||||
it('should restore from localstorage when one of the value is exists and exclude is false', async () => {
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupInput);
|
||||
updateControlGroupInputMock(initialInputData as ControlGroupRuntimeState);
|
||||
const savedData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
...initialInputData.panels,
|
||||
initialChildControlState: {
|
||||
...initialInputData.initialChildControlState,
|
||||
'2': {
|
||||
...initialInputData.panels['2'],
|
||||
explicitInput: {
|
||||
...initialInputData.panels['2'].explicitInput,
|
||||
existsSelected: true,
|
||||
exclude: false,
|
||||
},
|
||||
...initialInputData.initialChildControlState['2'],
|
||||
existsSelected: true,
|
||||
exclude: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -724,8 +684,8 @@ describe(' Filter Group Component ', () => {
|
|||
render(<TestComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(5);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[2][1]).toMatchObject(
|
||||
expect(addOptionsListControlMock.mock.calls.length).toBe(5);
|
||||
expect(addOptionsListControlMock.mock.calls[2][1]).toMatchObject(
|
||||
expect.objectContaining({
|
||||
existsSelected: true,
|
||||
exclude: false,
|
||||
|
@ -737,15 +697,12 @@ describe(' Filter Group Component ', () => {
|
|||
it('should restore from localstorage when one of the value has both exists and exclude true', async () => {
|
||||
const savedData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
...initialInputData.panels,
|
||||
initialChildControlState: {
|
||||
...initialInputData.initialChildControlState,
|
||||
'2': {
|
||||
...initialInputData.panels['2'],
|
||||
explicitInput: {
|
||||
...initialInputData.panels['2'].explicitInput,
|
||||
existsSelected: true,
|
||||
exclude: true,
|
||||
},
|
||||
...initialInputData.initialChildControlState['2'],
|
||||
existsSelected: true,
|
||||
exclude: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -755,8 +712,8 @@ describe(' Filter Group Component ', () => {
|
|||
render(<TestComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(5);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[2][1]).toMatchObject(
|
||||
expect(addOptionsListControlMock.mock.calls.length).toBe(5);
|
||||
expect(addOptionsListControlMock.mock.calls[2][1]).toMatchObject(
|
||||
expect.objectContaining({
|
||||
existsSelected: true,
|
||||
exclude: true,
|
||||
|
@ -764,17 +721,15 @@ describe(' Filter Group Component ', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should restore from localstorage when some value has selected options', async () => {
|
||||
const savedData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
...initialInputData.panels,
|
||||
initialChildControlState: {
|
||||
...initialInputData.initialChildControlState,
|
||||
'2': {
|
||||
...initialInputData.panels['2'],
|
||||
explicitInput: {
|
||||
...initialInputData.panels['2'].explicitInput,
|
||||
selectedOptions: ['abc'],
|
||||
},
|
||||
...initialInputData.initialChildControlState['2'],
|
||||
selectedOptions: ['abc'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -784,8 +739,8 @@ describe(' Filter Group Component ', () => {
|
|||
render(<TestComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls.length).toBe(5);
|
||||
expect(controlGroupMock.addOptionsListControl.mock.calls[2][1]).toMatchObject(
|
||||
expect(addOptionsListControlMock.mock.calls.length).toBe(5);
|
||||
expect(addOptionsListControlMock.mock.calls[2][1]).toMatchObject(
|
||||
expect.objectContaining({
|
||||
selectedOptions: ['abc'],
|
||||
})
|
||||
|
|
|
@ -9,25 +9,21 @@
|
|||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import type { ControlInputTransform } from '@kbn/controls-plugin/common';
|
||||
import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
|
||||
import { controlGroupStateBuilder } from '@kbn/controls-plugin/public';
|
||||
import type {
|
||||
ControlGroupContainer,
|
||||
ControlGroupInput,
|
||||
ControlGroupInputBuilder,
|
||||
ControlGroupOutput,
|
||||
ControlGroupRendererApi,
|
||||
ControlGroupRendererProps,
|
||||
DataControlInput,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupCreationOptions,
|
||||
ControlGroupStateBuilder,
|
||||
DefaultDataControlState,
|
||||
ControlStateTransform,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { debounce, isEqual, isEqualWith } from 'lodash';
|
||||
import type {
|
||||
ControlGroupCreationOptions,
|
||||
FieldFilterPredicate,
|
||||
} from '@kbn/controls-plugin/public/control_group/types';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
import type { FilterGroupProps, FilterControlConfig } from './types';
|
||||
import './index.scss';
|
||||
import { FilterGroupLoading } from './loading';
|
||||
|
@ -37,7 +33,7 @@ import { FilterGroupContextMenu } from './context_menu';
|
|||
import { AddControl, SaveControls } from './buttons';
|
||||
import {
|
||||
getFilterControlsComparator,
|
||||
getFilterItemObjListFromControlInput,
|
||||
getFilterItemObjListFromControlState,
|
||||
mergeControls,
|
||||
reorderControlsWithDefaultControls,
|
||||
} from './utils';
|
||||
|
@ -80,7 +76,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
[defaultControls]
|
||||
);
|
||||
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupRendererApi>();
|
||||
|
||||
const localStoragePageFilterKey = useMemo(
|
||||
() => `${featureIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`,
|
||||
|
@ -98,14 +94,13 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
switchToViewMode,
|
||||
switchToEditMode,
|
||||
setHasPendingChanges,
|
||||
} = useViewEditMode({
|
||||
controlGroup,
|
||||
});
|
||||
filterGroupMode,
|
||||
} = useViewEditMode({});
|
||||
|
||||
const {
|
||||
controlGroupInput: controlGroupInputUpdates,
|
||||
setControlGroupInput: setControlGroupInputUpdates,
|
||||
getStoredControlGroupInput: getStoredControlInput,
|
||||
controlGroupState: controlGroupStateUpdates,
|
||||
setControlGroupState: setControlGroupStateUpdates,
|
||||
getStoredControlGroupState: getStoredControlState,
|
||||
} = useControlGroupSyncToLocalStorage({
|
||||
Storage,
|
||||
storageKey: localStoragePageFilterKey,
|
||||
|
@ -113,13 +108,13 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (controlGroupInputUpdates) {
|
||||
const formattedFilters = getFilterItemObjListFromControlInput(controlGroupInputUpdates);
|
||||
if (controlGroupStateUpdates) {
|
||||
const formattedFilters = getFilterItemObjListFromControlState(controlGroupStateUpdates);
|
||||
if (!formattedFilters) return;
|
||||
if (controlGroupInputUpdates.viewMode !== 'view') return;
|
||||
if (!isViewMode) return;
|
||||
setControlsUrlState?.(formattedFilters);
|
||||
}
|
||||
}, [controlGroupInputUpdates, setControlsUrlState]);
|
||||
}, [controlGroupStateUpdates, setControlsUrlState, isViewMode]);
|
||||
|
||||
const [showFiltersChangedBanner, setShowFiltersChangedBanner] = useState(false);
|
||||
|
||||
|
@ -160,34 +155,31 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
}, [filters, query]);
|
||||
|
||||
useEffect(() => {
|
||||
controlGroup?.updateInput({
|
||||
filters: validatedFilters,
|
||||
query: validatedQuery,
|
||||
timeRange,
|
||||
chainingSystem,
|
||||
});
|
||||
}, [timeRange, chainingSystem, controlGroup, validatedQuery, validatedFilters]);
|
||||
controlGroup?.setChainingSystem(chainingSystem);
|
||||
}, [chainingSystem, controlGroup]);
|
||||
|
||||
const handleInputUpdates = useCallback(
|
||||
(newInput: ControlGroupInput) => {
|
||||
if (isEqual(getStoredControlInput(), newInput)) {
|
||||
const handleStateUpdates = useCallback(
|
||||
(newState: ControlGroupRuntimeState) => {
|
||||
if (isEqual(getStoredControlState(), newState)) {
|
||||
return;
|
||||
}
|
||||
if (!isEqual(newInput.panels, getStoredControlInput()?.panels) && !isViewMode) {
|
||||
if (
|
||||
!isEqual(
|
||||
newState?.initialChildControlState,
|
||||
getStoredControlState()?.initialChildControlState
|
||||
) &&
|
||||
!isViewMode
|
||||
) {
|
||||
setHasPendingChanges(true);
|
||||
}
|
||||
setControlGroupInputUpdates(newInput);
|
||||
setControlGroupStateUpdates(newState);
|
||||
},
|
||||
[setControlGroupInputUpdates, getStoredControlInput, isViewMode, setHasPendingChanges]
|
||||
[setControlGroupStateUpdates, getStoredControlState, isViewMode, setHasPendingChanges]
|
||||
);
|
||||
|
||||
const handleOutputFilterUpdates = useCallback(
|
||||
({ filters: newFilters, embeddableLoaded }: ControlGroupOutput) => {
|
||||
const haveAllEmbeddablesLoaded = Object.values(embeddableLoaded).every((v) =>
|
||||
Boolean(v ?? true)
|
||||
);
|
||||
(newFilters: Filter[] = []) => {
|
||||
if (isEqual(currentFiltersRef.current, newFilters)) return;
|
||||
if (!haveAllEmbeddablesLoaded) return;
|
||||
if (onFiltersChange) onFiltersChange(newFilters ?? []);
|
||||
currentFiltersRef.current = newFilters ?? [];
|
||||
},
|
||||
|
@ -204,9 +196,9 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
if (!Array.isArray(controlsUrlState)) {
|
||||
throw new Error(URL_PARAM_ARRAY_EXCEPTION_MSG);
|
||||
}
|
||||
const storedControlGroupInput = getStoredControlInput();
|
||||
if (storedControlGroupInput) {
|
||||
const panelsFormatted = getFilterItemObjListFromControlInput(storedControlGroupInput);
|
||||
const storedControlGroupState = getStoredControlState();
|
||||
if (storedControlGroupState) {
|
||||
const panelsFormatted = getFilterItemObjListFromControlState(storedControlGroupState);
|
||||
if (
|
||||
controlsUrlState.length &&
|
||||
!isEqualWith(
|
||||
|
@ -226,7 +218,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
console.error(err);
|
||||
}
|
||||
setUrlStateInitialized(true);
|
||||
}, [controlsUrlState, getStoredControlInput, switchToEditMode]);
|
||||
}, [controlsUrlState, getStoredControlState, switchToEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (controlsUrlState && !urlStateInitialized) {
|
||||
|
@ -237,12 +229,12 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
return;
|
||||
}
|
||||
|
||||
filterChangedSubscription.current = controlGroup.getOutput$().subscribe({
|
||||
filterChangedSubscription.current = controlGroup.filters$.subscribe({
|
||||
next: debouncedFilterUpdates,
|
||||
});
|
||||
|
||||
inputChangedSubscription.current = controlGroup.getInput$().subscribe({
|
||||
next: handleInputUpdates,
|
||||
next: handleStateUpdates,
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
@ -254,18 +246,18 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
controlGroup,
|
||||
controlsUrlState,
|
||||
debouncedFilterUpdates,
|
||||
getStoredControlInput,
|
||||
handleInputUpdates,
|
||||
getStoredControlState,
|
||||
handleStateUpdates,
|
||||
initializeUrlState,
|
||||
switchToEditMode,
|
||||
urlStateInitialized,
|
||||
]);
|
||||
|
||||
const onControlGroupLoadHandler = useCallback(
|
||||
(controlGroupContainer: ControlGroupContainer) => {
|
||||
if (!controlGroupContainer) return;
|
||||
if (onInit) onInit(controlGroupContainer);
|
||||
setControlGroup(controlGroupContainer);
|
||||
(controlGroupRendererApi: ControlGroupRendererApi | undefined) => {
|
||||
if (!controlGroupRendererApi) return;
|
||||
if (onInit) onInit(controlGroupRendererApi);
|
||||
setControlGroup(controlGroupRendererApi);
|
||||
},
|
||||
[onInit]
|
||||
);
|
||||
|
@ -281,9 +273,9 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
* */
|
||||
|
||||
let controlsFromLocalStorage: FilterControlConfig[] = [];
|
||||
const storedControlGroupInput = getStoredControlInput();
|
||||
if (storedControlGroupInput) {
|
||||
controlsFromLocalStorage = getFilterItemObjListFromControlInput(storedControlGroupInput);
|
||||
const storedControlGroupState = getStoredControlState();
|
||||
if (storedControlGroupState) {
|
||||
controlsFromLocalStorage = getFilterItemObjListFromControlState(storedControlGroupState);
|
||||
}
|
||||
let overridingControls = mergeControls({
|
||||
controlsWithPriority: [controlsFromUrl, controlsFromLocalStorage],
|
||||
|
@ -307,78 +299,63 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
controls: overridingControls,
|
||||
defaultControls,
|
||||
});
|
||||
}, [getStoredControlInput, controlsFromUrl, defaultControlsObj, defaultControls]);
|
||||
|
||||
const fieldFilterPredicate: FieldFilterPredicate = useCallback((f) => f.type !== 'number', []);
|
||||
}, [getStoredControlState, controlsFromUrl, defaultControlsObj, defaultControls]);
|
||||
|
||||
const getCreationOptions: ControlGroupRendererProps['getCreationOptions'] = useCallback(
|
||||
async (
|
||||
defaultInput: Partial<ControlGroupInput>,
|
||||
{ addOptionsListControl }: ControlGroupInputBuilder
|
||||
defaultState: Partial<ControlGroupRuntimeState>,
|
||||
{ addOptionsListControl }: ControlGroupStateBuilder
|
||||
) => {
|
||||
const initialInput: Partial<ControlGroupInput> = {
|
||||
...defaultInput,
|
||||
defaultControlWidth: 'small',
|
||||
viewMode: ViewMode.VIEW,
|
||||
timeRange,
|
||||
filters,
|
||||
query,
|
||||
const initialState: Partial<ControlGroupRuntimeState> = {
|
||||
...defaultState,
|
||||
chainingSystem,
|
||||
ignoreParentSettings: {
|
||||
ignoreValidations: true,
|
||||
},
|
||||
};
|
||||
|
||||
const finalControls = selectControlsWithPriority();
|
||||
|
||||
urlDataApplied.current = true;
|
||||
|
||||
finalControls.forEach((control, idx) => {
|
||||
addOptionsListControl(initialInput, {
|
||||
controlId: String(idx),
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
// option List controls will handle an invalid dataview
|
||||
// & display an appropriate message
|
||||
dataViewId: dataViewId ?? '',
|
||||
...control,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
initialInput,
|
||||
settings: {
|
||||
showAddButton: false,
|
||||
staticDataViewId: dataViewId ?? '',
|
||||
editorConfig: {
|
||||
hideWidthSettings: true,
|
||||
hideDataViewSelector: true,
|
||||
hideAdditionalSettings: true,
|
||||
addOptionsListControl(
|
||||
initialState,
|
||||
{
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
// option List controls will handle an invalid dataview
|
||||
// & display an appropriate message
|
||||
dataViewId: dataViewId ?? '',
|
||||
...control,
|
||||
},
|
||||
String(idx)
|
||||
);
|
||||
});
|
||||
return {
|
||||
initialState,
|
||||
editorConfig: {
|
||||
hideWidthSettings: true,
|
||||
hideDataViewSelector: true,
|
||||
hideAdditionalSettings: true,
|
||||
fieldFilterPredicate: (f) => f.type !== 'number',
|
||||
},
|
||||
fieldFilterPredicate,
|
||||
} as ControlGroupCreationOptions;
|
||||
},
|
||||
[
|
||||
dataViewId,
|
||||
timeRange,
|
||||
filters,
|
||||
chainingSystem,
|
||||
query,
|
||||
selectControlsWithPriority,
|
||||
fieldFilterPredicate,
|
||||
]
|
||||
[dataViewId, chainingSystem, selectControlsWithPriority]
|
||||
);
|
||||
|
||||
const discardChangesHandler = useCallback(() => {
|
||||
const discardChangesHandler = useCallback(async () => {
|
||||
if (hasPendingChanges) {
|
||||
controlGroup?.updateInput({
|
||||
panels: getStoredControlInput()?.panels,
|
||||
initialChildControlState: getStoredControlState()?.initialChildControlState,
|
||||
});
|
||||
}
|
||||
switchToViewMode();
|
||||
setShowFiltersChangedBanner(false);
|
||||
}, [controlGroup, switchToViewMode, getStoredControlInput, hasPendingChanges]);
|
||||
}, [controlGroup, switchToViewMode, getStoredControlState, hasPendingChanges]);
|
||||
|
||||
const upsertPersistableControls = useCallback(async () => {
|
||||
if (!controlGroup) return;
|
||||
const currentPanels = getFilterItemObjListFromControlInput(controlGroup.getInput());
|
||||
const currentPanels = getFilterItemObjListFromControlState(controlGroup.snapshotRuntimeState());
|
||||
|
||||
const reorderedControls = reorderControlsWithDefaultControls({
|
||||
controls: currentPanels,
|
||||
|
@ -388,19 +365,23 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
if (!isEqualWith(reorderedControls, currentPanels, getFilterControlsComparator('fieldName'))) {
|
||||
// reorder only if fields are in different order
|
||||
// or not same.
|
||||
controlGroup?.updateInput({ panels: {} });
|
||||
|
||||
for (const control of reorderedControls) {
|
||||
await controlGroup?.addOptionsListControl({
|
||||
title: control.title,
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
// option List controls will handle an invalid dataview
|
||||
// & display an appropriate message
|
||||
dataViewId: dataViewId ?? '',
|
||||
selectedOptions: control.selectedOptions,
|
||||
...control,
|
||||
});
|
||||
}
|
||||
const newInput = { initialChildControlState: {} };
|
||||
reorderedControls.forEach((control, idx) => {
|
||||
controlGroupStateBuilder.addOptionsListControl(
|
||||
newInput,
|
||||
{
|
||||
title: control.title,
|
||||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
// option List controls will handle an invalid dataview
|
||||
// & display an appropriate message
|
||||
dataViewId: dataViewId ?? '',
|
||||
selectedOptions: control.selectedOptions,
|
||||
...control,
|
||||
},
|
||||
String(idx)
|
||||
);
|
||||
});
|
||||
controlGroup?.updateInput(newInput);
|
||||
}
|
||||
}, [controlGroup, dataViewId, defaultControls]);
|
||||
|
||||
|
@ -410,7 +391,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
setShowFiltersChangedBanner(false);
|
||||
}, [switchToViewMode, upsertPersistableControls]);
|
||||
|
||||
const newControlInputTransform: ControlInputTransform = useCallback(
|
||||
const newControlStateTransform: ControlStateTransform<DefaultDataControlState> = useCallback(
|
||||
(newInput, controlType) => {
|
||||
// for any new controls, we want to avoid
|
||||
// default placeholder
|
||||
|
@ -421,10 +402,10 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
...COMMON_OPTIONS_LIST_CONTROL_INPUTS,
|
||||
};
|
||||
|
||||
if ((newInput as DataControlInput).fieldName in defaultControlsObj) {
|
||||
if ((newInput as DefaultDataControlState).fieldName in defaultControlsObj) {
|
||||
result = {
|
||||
...result,
|
||||
...defaultControlsObj[(newInput as DataControlInput).fieldName],
|
||||
...defaultControlsObj[(newInput as DefaultDataControlState).fieldName],
|
||||
// title should not be overridden by the initial controls, hence the hardcoding
|
||||
title: newInput.title ?? result.title,
|
||||
};
|
||||
|
@ -437,9 +418,9 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
|
||||
const addControlsHandler = useCallback(() => {
|
||||
controlGroup?.openAddDataControlFlyout({
|
||||
controlInputTransform: newControlInputTransform,
|
||||
controlStateTransform: newControlStateTransform,
|
||||
});
|
||||
}, [controlGroup, newControlInputTransform]);
|
||||
}, [controlGroup, newControlStateTransform]);
|
||||
|
||||
if (!spaceId) {
|
||||
return <FilterGroupLoading />;
|
||||
|
@ -452,7 +433,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
initialControls: defaultControls,
|
||||
isViewMode,
|
||||
controlGroup,
|
||||
controlGroupInputUpdates,
|
||||
controlGroupStateUpdates,
|
||||
hasPendingChanges,
|
||||
pendingChangesPopoverOpen,
|
||||
setHasPendingChanges,
|
||||
|
@ -470,8 +451,12 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
{Array.isArray(controlsFromUrl) ? (
|
||||
<EuiFlexItem grow={true} data-test-subj={TEST_IDS.FILTER_CONTROLS}>
|
||||
<ControlGroupRenderer
|
||||
ref={onControlGroupLoadHandler}
|
||||
onApiAvailable={onControlGroupLoadHandler}
|
||||
getCreationOptions={getCreationOptions}
|
||||
timeRange={timeRange}
|
||||
query={validatedQuery}
|
||||
filters={validatedFilters}
|
||||
viewMode={filterGroupMode}
|
||||
/>
|
||||
{!controlGroup ? <FilterGroupLoading /> : null}
|
||||
</EuiFlexItem>
|
||||
|
@ -482,8 +467,9 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
<AddControl
|
||||
onClick={addControlsHandler}
|
||||
isDisabled={
|
||||
controlGroupInputUpdates &&
|
||||
Object.values(controlGroupInputUpdates.panels).length >= maxControls
|
||||
controlGroupStateUpdates &&
|
||||
Object.values(controlGroupStateUpdates.initialChildControlState).length >=
|
||||
maxControls
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -7,15 +7,18 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupContainer, ControlGroupInput } from '@kbn/controls-plugin/public';
|
||||
import type {
|
||||
ControlGroupRendererApi,
|
||||
ControlGroupRuntimeState,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { createContext } from 'react';
|
||||
import type { FilterControlConfig } from './types';
|
||||
|
||||
export interface FilterGroupContextType {
|
||||
initialControls: FilterControlConfig[];
|
||||
dataViewId: string;
|
||||
controlGroup: ControlGroupContainer | undefined;
|
||||
controlGroupInputUpdates: ControlGroupInput | undefined;
|
||||
controlGroup: ControlGroupRendererApi | undefined;
|
||||
controlGroupStateUpdates: ControlGroupRuntimeState | undefined;
|
||||
isViewMode: boolean;
|
||||
hasPendingChanges: boolean;
|
||||
pendingChangesPopoverOpen: boolean;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
@ -19,9 +19,9 @@ interface UseControlGroupSyncToLocalStorageArgs {
|
|||
}
|
||||
|
||||
type UseControlGroupSyncToLocalStorage = (args: UseControlGroupSyncToLocalStorageArgs) => {
|
||||
controlGroupInput: ControlGroupInput | undefined;
|
||||
setControlGroupInput: Dispatch<SetStateAction<ControlGroupInput>>;
|
||||
getStoredControlGroupInput: () => ControlGroupInput | undefined;
|
||||
controlGroupState: ControlGroupRuntimeState | undefined;
|
||||
setControlGroupState: Dispatch<SetStateAction<ControlGroupRuntimeState>>;
|
||||
getStoredControlGroupState: () => ControlGroupRuntimeState | undefined;
|
||||
};
|
||||
|
||||
export const useControlGroupSyncToLocalStorage: UseControlGroupSyncToLocalStorage = ({
|
||||
|
@ -31,23 +31,23 @@ export const useControlGroupSyncToLocalStorage: UseControlGroupSyncToLocalStorag
|
|||
}) => {
|
||||
const storage = useRef(new Storage(localStorage));
|
||||
|
||||
const [controlGroupInput, setControlGroupInput] = useState(
|
||||
() => (storage.current.get(storageKey) as ControlGroupInput) ?? undefined
|
||||
const [controlGroupState, setControlGroupState] = useState(
|
||||
() => (storage.current.get(storageKey) as ControlGroupRuntimeState) ?? undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldSync && controlGroupInput) {
|
||||
storage.current.set(storageKey, controlGroupInput);
|
||||
if (shouldSync && controlGroupState) {
|
||||
storage.current.set(storageKey, controlGroupState);
|
||||
}
|
||||
}, [shouldSync, controlGroupInput, storageKey]);
|
||||
}, [shouldSync, controlGroupState, storageKey]);
|
||||
|
||||
const getStoredControlGroupInput = () => {
|
||||
return (storage.current.get(storageKey) as ControlGroupInput) ?? undefined;
|
||||
const getStoredControlGroupState = () => {
|
||||
return (storage.current.get(storageKey) as ControlGroupRuntimeState) ?? undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
controlGroupInput,
|
||||
setControlGroupInput,
|
||||
getStoredControlGroupInput,
|
||||
controlGroupState,
|
||||
setControlGroupState,
|
||||
getStoredControlGroupState,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import type { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useControlGroupSyncToLocalStorage } from './use_control_group_sync_to_local_storage';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -15,11 +15,11 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
|
|||
const TEST_STORAGE_KEY = 'test_key';
|
||||
const DEFAULT_STORED_VALUE = {
|
||||
val: 'default_local_storage_value',
|
||||
} as unknown as ControlGroupInput;
|
||||
} as unknown as ControlGroupRuntimeState;
|
||||
|
||||
const ANOTHER_SAMPLE_VALUE = {
|
||||
val: 'another_local_storage_value',
|
||||
} as unknown as ControlGroupInput;
|
||||
} as unknown as ControlGroupRuntimeState;
|
||||
|
||||
let mockLocalStorage: Record<string, unknown> = {};
|
||||
describe('Filters Sync to Local Storage', () => {
|
||||
|
@ -47,7 +47,7 @@ describe('Filters Sync to Local Storage', () => {
|
|||
})
|
||||
);
|
||||
waitForNextUpdate();
|
||||
expect(result.current.controlGroupInput).toMatchObject(DEFAULT_STORED_VALUE);
|
||||
expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE);
|
||||
});
|
||||
it('should be undefined if localstorage as NO initial value', () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
|
@ -58,8 +58,8 @@ describe('Filters Sync to Local Storage', () => {
|
|||
})
|
||||
);
|
||||
waitForNextUpdate();
|
||||
expect(result.current.controlGroupInput).toBeUndefined();
|
||||
expect(result.current.setControlGroupInput).toBeTruthy();
|
||||
expect(result.current.controlGroupState).toBeUndefined();
|
||||
expect(result.current.setControlGroupState).toBeTruthy();
|
||||
});
|
||||
it('should be update values to local storage when sync is ON', () => {
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
|
@ -70,12 +70,12 @@ describe('Filters Sync to Local Storage', () => {
|
|||
})
|
||||
);
|
||||
waitFor(() => {
|
||||
expect(result.current.controlGroupInput).toBeUndefined();
|
||||
expect(result.current.setControlGroupInput).toBeTruthy();
|
||||
expect(result.current.controlGroupState).toBeUndefined();
|
||||
expect(result.current.setControlGroupState).toBeTruthy();
|
||||
});
|
||||
result.current.setControlGroupInput(DEFAULT_STORED_VALUE);
|
||||
result.current.setControlGroupState(DEFAULT_STORED_VALUE);
|
||||
waitFor(() => {
|
||||
expect(result.current.controlGroupInput).toMatchObject(DEFAULT_STORED_VALUE);
|
||||
expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE);
|
||||
expect(global.localStorage.getItem(TEST_STORAGE_KEY)).toBe(
|
||||
JSON.stringify(DEFAULT_STORED_VALUE)
|
||||
);
|
||||
|
@ -92,20 +92,20 @@ describe('Filters Sync to Local Storage', () => {
|
|||
|
||||
// Sync is ON
|
||||
waitFor(() => {
|
||||
expect(result.current.controlGroupInput).toBeUndefined();
|
||||
expect(result.current.setControlGroupInput).toBeTruthy();
|
||||
expect(result.current.controlGroupState).toBeUndefined();
|
||||
expect(result.current.setControlGroupState).toBeTruthy();
|
||||
});
|
||||
|
||||
result.current.setControlGroupInput(DEFAULT_STORED_VALUE);
|
||||
result.current.setControlGroupState(DEFAULT_STORED_VALUE);
|
||||
waitFor(() => {
|
||||
expect(result.current.controlGroupInput).toMatchObject(DEFAULT_STORED_VALUE);
|
||||
expect(result.current.controlGroupState).toMatchObject(DEFAULT_STORED_VALUE);
|
||||
});
|
||||
|
||||
// Sync is OFF
|
||||
rerender({ storageKey: TEST_STORAGE_KEY, shouldSync: false });
|
||||
result.current.setControlGroupInput(ANOTHER_SAMPLE_VALUE);
|
||||
result.current.setControlGroupState(ANOTHER_SAMPLE_VALUE);
|
||||
waitFor(() => {
|
||||
expect(result.current.controlGroupInput).toMatchObject(ANOTHER_SAMPLE_VALUE);
|
||||
expect(result.current.controlGroupState).toMatchObject(ANOTHER_SAMPLE_VALUE);
|
||||
// old value
|
||||
expect(global.localStorage.getItem(TEST_STORAGE_KEY)).toBe(
|
||||
JSON.stringify(DEFAULT_STORED_VALUE)
|
||||
|
|
|
@ -7,30 +7,19 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface UseViewEditModeArgs {
|
||||
controlGroup: ControlGroupContainer | undefined;
|
||||
initialMode?: ViewMode;
|
||||
}
|
||||
|
||||
export const useViewEditMode = ({
|
||||
controlGroup,
|
||||
initialMode = ViewMode.VIEW,
|
||||
}: UseViewEditModeArgs) => {
|
||||
export const useViewEditMode = ({ initialMode = ViewMode.VIEW }: UseViewEditModeArgs) => {
|
||||
const [filterGroupMode, setFilterGroupMode] = useState(initialMode);
|
||||
|
||||
const [hasPendingChanges, setHasPendingChanges] = useState(false);
|
||||
const [pendingChangesPopoverOpen, setPendingChangesPopoverOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (controlGroup && controlGroup.getInput().viewMode !== filterGroupMode) {
|
||||
controlGroup.updateInput({ viewMode: filterGroupMode });
|
||||
}
|
||||
}, [controlGroup, filterGroupMode]);
|
||||
|
||||
useEffect(() => {
|
||||
setPendingChangesPopoverOpen(hasPendingChanges);
|
||||
}, [hasPendingChanges]);
|
||||
|
@ -44,19 +33,18 @@ export const useViewEditMode = ({
|
|||
}, [hasPendingChanges]);
|
||||
|
||||
const switchToEditMode = useCallback(() => {
|
||||
controlGroup?.updateInput({ viewMode: ViewMode.EDIT });
|
||||
setFilterGroupMode(ViewMode.EDIT);
|
||||
}, [controlGroup]);
|
||||
}, []);
|
||||
|
||||
const switchToViewMode = useCallback(() => {
|
||||
controlGroup?.updateInput({ viewMode: ViewMode.VIEW });
|
||||
setHasPendingChanges(false);
|
||||
setFilterGroupMode(ViewMode.VIEW);
|
||||
}, [controlGroup]);
|
||||
}, []);
|
||||
|
||||
const isViewMode = filterGroupMode === ViewMode.VIEW;
|
||||
|
||||
return {
|
||||
filterGroupMode,
|
||||
isViewMode,
|
||||
hasPendingChanges,
|
||||
pendingChangesPopoverOpen,
|
||||
|
|
|
@ -7,27 +7,24 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupOutput, ControlGroupInput } from '@kbn/controls-plugin/public';
|
||||
import { Subject } from 'rxjs';
|
||||
import type { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export const controlGroupFilterOutputMock$ = new Subject<ControlGroupOutput>();
|
||||
|
||||
export const controlGroupFilterInputMock$ = new Subject<ControlGroupInput>();
|
||||
|
||||
export const getInput$Mock = jest.fn(() => controlGroupFilterInputMock$);
|
||||
|
||||
export const getOutput$Mock = jest.fn(() => controlGroupFilterOutputMock$);
|
||||
export const controlGroupFilterOutputMock$ = new BehaviorSubject<Filter[] | undefined>([]);
|
||||
export const controlGroupFilterStateMock$ = new BehaviorSubject<ControlGroupRuntimeState>({
|
||||
initialChildControlState: {},
|
||||
} as unknown as ControlGroupRuntimeState);
|
||||
export const getInput$Mock = jest.fn(() => controlGroupFilterStateMock$);
|
||||
|
||||
export const getControlGroupMock = () => {
|
||||
return {
|
||||
reload: jest.fn(),
|
||||
getInput: jest.fn().mockReturnValue({
|
||||
viewMode: 'VIEW',
|
||||
}),
|
||||
updateInput: jest.fn(),
|
||||
getOutput$: getOutput$Mock,
|
||||
getInput$: getInput$Mock,
|
||||
openAddDataControlFlyout: jest.fn(),
|
||||
addOptionsListControl: jest.fn(),
|
||||
filters$: controlGroupFilterOutputMock$,
|
||||
setChainingSystem: jest.fn(),
|
||||
snapshotRuntimeState: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,38 +7,51 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type {
|
||||
AwaitingControlGroupAPI,
|
||||
ControlGroupContainer,
|
||||
ControlGroupInputBuilder,
|
||||
ControlGroupRendererProps,
|
||||
import {
|
||||
type ControlGroupRendererApi,
|
||||
type ControlGroupRendererProps,
|
||||
type ControlGroupStateBuilder,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import React, { useState, forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TEST_IDS } from '../constants';
|
||||
import { getControlGroupMock } from './control_group';
|
||||
|
||||
export const getMockedControlGroupRenderer = (
|
||||
controlGroupContainerMock: ControlGroupContainer | undefined
|
||||
) => {
|
||||
const controlGroupMock = controlGroupContainerMock ?? getControlGroupMock();
|
||||
|
||||
const MockedControlGroupRenderer = forwardRef<AwaitingControlGroupAPI, ControlGroupRendererProps>(
|
||||
({ getCreationOptions }, ref) => {
|
||||
useImperativeHandle(ref, () => controlGroupMock as unknown as ControlGroupContainer, []);
|
||||
const [creationOptionsCalled, setCreationOptionsCalled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (creationOptionsCalled) return;
|
||||
setCreationOptionsCalled(true);
|
||||
if (getCreationOptions) {
|
||||
getCreationOptions({}, {
|
||||
addOptionsListControl: controlGroupMock.addOptionsListControl,
|
||||
} as unknown as ControlGroupInputBuilder);
|
||||
}
|
||||
}, [getCreationOptions, creationOptionsCalled]);
|
||||
return <div data-test-subj={TEST_IDS.MOCKED_CONTROL} />;
|
||||
export const addOptionsListControlMock = jest
|
||||
.fn()
|
||||
.mockImplementation((initialState, controlState, id) => {
|
||||
if (!initialState.initialChildControlState) {
|
||||
initialState.initialChildControlState = {};
|
||||
}
|
||||
);
|
||||
initialState.initialChildControlState[id] = controlState;
|
||||
});
|
||||
|
||||
export const getMockedControlGroupRenderer = (
|
||||
controlGroupApiMock: ControlGroupRendererApi | undefined
|
||||
) => {
|
||||
const controlGroupMock = controlGroupApiMock ?? getControlGroupMock();
|
||||
|
||||
const MockedControlGroupRenderer = ({
|
||||
onApiAvailable,
|
||||
getCreationOptions,
|
||||
}: ControlGroupRendererProps) => {
|
||||
const [creationOptionsCalled, setCreationOptionsCalled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (creationOptionsCalled) return;
|
||||
setCreationOptionsCalled(true);
|
||||
if (getCreationOptions) {
|
||||
getCreationOptions({}, {
|
||||
addOptionsListControl: addOptionsListControlMock,
|
||||
} as unknown as ControlGroupStateBuilder);
|
||||
}
|
||||
}, [getCreationOptions, creationOptionsCalled]);
|
||||
|
||||
useEffect(() => {
|
||||
onApiAvailable(controlGroupMock as unknown as ControlGroupRendererApi);
|
||||
}, [onApiAvailable]);
|
||||
|
||||
return <div data-test-subj={TEST_IDS.MOCKED_CONTROL} />;
|
||||
};
|
||||
|
||||
MockedControlGroupRenderer.displayName = 'MockedControlGroup';
|
||||
return MockedControlGroupRenderer;
|
||||
|
|
|
@ -7,19 +7,21 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ControlGroupRuntimeState, OptionsListControlState } from '@kbn/controls-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { ALERT_DURATION, ALERT_RULE_NAME, ALERT_START, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
|
||||
export const sampleOutputData = {
|
||||
export interface ControlGroupOutput {
|
||||
loading: boolean;
|
||||
rendered: boolean;
|
||||
dataViewIds: string[];
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export const sampleOutputData: ControlGroupOutput = {
|
||||
loading: false,
|
||||
rendered: true,
|
||||
dataViewIds: ['alert-filters-test-dv'],
|
||||
embeddableLoaded: {
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
|
@ -36,124 +38,86 @@ export const sampleOutputData = {
|
|||
],
|
||||
};
|
||||
|
||||
export const initialInputData = {
|
||||
viewMode: 'view',
|
||||
id: 'f9e81d5a-f6ab-4179-866d-c029554131be',
|
||||
panels: {
|
||||
export const initialInputData: ControlGroupRuntimeState<OptionsListControlState> = {
|
||||
initialChildControlState: {
|
||||
'0': {
|
||||
type: 'optionsListControl',
|
||||
order: 0,
|
||||
grow: true,
|
||||
width: 'small',
|
||||
explicitInput: {
|
||||
id: '0',
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_STATUS,
|
||||
title: 'Status',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
hidePanelTitles: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_STATUS,
|
||||
title: 'Status',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
'1': {
|
||||
type: 'optionsListControl',
|
||||
order: 1,
|
||||
grow: true,
|
||||
width: 'small',
|
||||
explicitInput: {
|
||||
id: '1',
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_RULE_NAME,
|
||||
title: 'Rule',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
hidePanelTitles: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_RULE_NAME,
|
||||
title: 'Rule',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
'2': {
|
||||
type: 'optionsListControl',
|
||||
order: 2,
|
||||
grow: true,
|
||||
width: 'small',
|
||||
explicitInput: {
|
||||
id: '2',
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_START,
|
||||
title: 'Started at',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
hidePanelTitles: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: true,
|
||||
exclude: true,
|
||||
},
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_START,
|
||||
title: 'Started at',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: true,
|
||||
exclude: true,
|
||||
},
|
||||
'3': {
|
||||
type: 'optionsListControl',
|
||||
order: 3,
|
||||
grow: true,
|
||||
width: 'small',
|
||||
explicitInput: {
|
||||
id: '3',
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_DURATION,
|
||||
title: 'Duration',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
hidePanelTitles: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: ALERT_DURATION,
|
||||
title: 'Duration',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
'4': {
|
||||
type: 'optionsListControl',
|
||||
order: 4,
|
||||
grow: true,
|
||||
width: 'small',
|
||||
explicitInput: {
|
||||
id: '4',
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: 'host.name',
|
||||
title: 'Host',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
hidePanelTitles: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
dataViewId: 'alert-filters-test-dv',
|
||||
fieldName: 'host.name',
|
||||
title: 'Host',
|
||||
hideExclude: true,
|
||||
hideSort: true,
|
||||
placeholder: '',
|
||||
selectedOptions: [],
|
||||
existsSelected: false,
|
||||
exclude: false,
|
||||
},
|
||||
},
|
||||
defaultControlWidth: 'small',
|
||||
defaultControlGrow: true,
|
||||
controlStyle: 'oneLine',
|
||||
labelPosition: 'oneLine',
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
autoApplySelections: true,
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: false,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
},
|
||||
timeRange: {
|
||||
from: '2007-04-20T14:00:52.236Z',
|
||||
to: '2023-04-20T21:59:59.999Z',
|
||||
mode: 'absolute',
|
||||
},
|
||||
filters: [],
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupContainer } from '@kbn/controls-plugin/public';
|
||||
import type { ControlGroupRendererApi } from '@kbn/controls-plugin/public';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { FC } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
@ -67,7 +67,7 @@ export const mockAlertFilterControls = (outputFilters?: Filter[]) => {
|
|||
const Component: FC<AlertFilterControlsProps> = ({ onInit, onFiltersChange }) => {
|
||||
useEffect(() => {
|
||||
if (onInit) {
|
||||
onInit(getControlGroupMock() as unknown as ControlGroupContainer);
|
||||
onInit(getControlGroupMock() as unknown as ControlGroupRendererApi);
|
||||
}
|
||||
|
||||
if (onFiltersChange) {
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ControlGroupInput, OptionsListEmbeddableInput } from '@kbn/controls-plugin/common';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type {
|
||||
AddOptionsListControlProps,
|
||||
ControlGroupContainer,
|
||||
ControlGroupRenderer,
|
||||
OptionsListControlState,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupRendererApi,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
export type FilterUrlFormat = Record<
|
||||
string,
|
||||
Pick<
|
||||
OptionsListEmbeddableInput,
|
||||
OptionsListControlState,
|
||||
'selectedOptions' | 'title' | 'fieldName' | 'existsSelected' | 'exclude'
|
||||
>
|
||||
>;
|
||||
|
@ -30,17 +30,20 @@ export interface FilterContextType {
|
|||
addControl: (controls: FilterControlConfig) => void;
|
||||
}
|
||||
|
||||
export type FilterControlConfig = Omit<AddOptionsListControlProps, 'controlId' | 'dataViewId'> & {
|
||||
export type FilterControlConfig = Omit<OptionsListControlState, 'dataViewId'> & {
|
||||
/*
|
||||
* Determines the presence and order of a control
|
||||
* */
|
||||
persist?: boolean;
|
||||
};
|
||||
|
||||
export type FilterGroupHandler = ControlGroupContainer;
|
||||
export type FilterGroupHandler = ControlGroupRendererApi;
|
||||
|
||||
export interface FilterGroupProps extends Pick<ControlGroupRuntimeState, 'chainingSystem'> {
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
|
||||
export interface FilterGroupProps
|
||||
extends Pick<ControlGroupInput, 'timeRange' | 'filters' | 'query' | 'chainingSystem'> {
|
||||
spaceId?: string;
|
||||
dataViewId: string | null;
|
||||
featureIds: AlertConsumers[];
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ControlGroupInput, OptionsListEmbeddableInput } from '@kbn/controls-plugin/common';
|
||||
import {
|
||||
getFilterItemObjListFromControlInput,
|
||||
getFilterItemObjListFromControlState,
|
||||
mergeControls,
|
||||
reorderControlsWithDefaultControls,
|
||||
getFilterControlsComparator,
|
||||
|
@ -69,14 +68,13 @@ const defaultControlsObj = defaultControls.reduce((prev, current) => {
|
|||
describe('utils', () => {
|
||||
describe('getFilterItemObjListFromControlOutput', () => {
|
||||
it('should return ordered filterItem where passed in order', () => {
|
||||
const filterItemObjList = getFilterItemObjListFromControlInput(
|
||||
initialInputData as ControlGroupInput
|
||||
);
|
||||
const filterItemObjList = getFilterItemObjListFromControlState(initialInputData);
|
||||
|
||||
filterItemObjList.forEach((item, idx) => {
|
||||
const panelObj =
|
||||
initialInputData.panels[String(idx) as keyof typeof initialInputData.panels]
|
||||
.explicitInput;
|
||||
initialInputData.initialChildControlState[
|
||||
String(idx) as keyof typeof initialInputData.initialChildControlState
|
||||
];
|
||||
expect(item).toMatchObject({
|
||||
fieldName: panelObj.fieldName,
|
||||
selectedOptions: panelObj.selectedOptions,
|
||||
|
@ -90,16 +88,14 @@ describe('utils', () => {
|
|||
it('should return ordered filterItem where NOT passed in order', () => {
|
||||
const newInputData = {
|
||||
...initialInputData,
|
||||
panels: {
|
||||
'0': initialInputData.panels['3'],
|
||||
'1': initialInputData.panels['0'],
|
||||
initialChildControlState: {
|
||||
'0': initialInputData.initialChildControlState['3'],
|
||||
'1': initialInputData.initialChildControlState['0'],
|
||||
},
|
||||
};
|
||||
const filterItemObjList = getFilterItemObjListFromControlInput(
|
||||
newInputData as ControlGroupInput
|
||||
);
|
||||
const filterItemObjList = getFilterItemObjListFromControlState(newInputData);
|
||||
|
||||
let panelObj = newInputData.panels['1'].explicitInput as OptionsListEmbeddableInput;
|
||||
let panelObj = newInputData.initialChildControlState['1'];
|
||||
expect(filterItemObjList[0]).toMatchObject({
|
||||
fieldName: panelObj.fieldName,
|
||||
selectedOptions: panelObj.selectedOptions,
|
||||
|
@ -108,7 +104,7 @@ describe('utils', () => {
|
|||
exclude: panelObj.exclude,
|
||||
});
|
||||
|
||||
panelObj = newInputData.panels['0'].explicitInput;
|
||||
panelObj = newInputData.initialChildControlState['0'];
|
||||
expect(filterItemObjList[1]).toMatchObject({
|
||||
fieldName: panelObj.fieldName,
|
||||
selectedOptions: panelObj.selectedOptions,
|
||||
|
|
|
@ -8,26 +8,24 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
ControlGroupInput,
|
||||
ControlGroupRuntimeState,
|
||||
OptionsListControlState,
|
||||
ControlPanelState,
|
||||
OptionsListEmbeddableInput,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
} from '@kbn/controls-plugin/public';
|
||||
|
||||
import { isEmpty, isEqual, pick } from 'lodash';
|
||||
import type { FilterControlConfig } from './types';
|
||||
|
||||
export const getPanelsInOrderFromControlsInput = (controlInput: ControlGroupInput) => {
|
||||
const panels = controlInput.panels;
|
||||
|
||||
export const getPanelsInOrderFromControlsState = (controlState: ControlGroupRuntimeState) => {
|
||||
const panels = controlState.initialChildControlState;
|
||||
return Object.values(panels).sort((a, b) => a.order - b.order);
|
||||
};
|
||||
|
||||
export const getFilterItemObjListFromControlInput = (controlInput: ControlGroupInput) => {
|
||||
const panels = getPanelsInOrderFromControlsInput(controlInput);
|
||||
export const getFilterItemObjListFromControlState = (controlState: ControlGroupRuntimeState) => {
|
||||
const panels = getPanelsInOrderFromControlsState(controlState);
|
||||
return panels.map((panel) => {
|
||||
const {
|
||||
explicitInput: { fieldName, selectedOptions, title, existsSelected, exclude, hideActionBar },
|
||||
} = panel as ControlPanelState<OptionsListEmbeddableInput>;
|
||||
const { fieldName, selectedOptions, title, existsSelected, exclude, hideActionBar } =
|
||||
panel as ControlPanelState<OptionsListControlState>;
|
||||
|
||||
return {
|
||||
fieldName: fieldName as string,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { PublishingSubject } from '../publishing_subject';
|
|||
|
||||
export interface PublishesDisabledActionIds {
|
||||
disabledActionIds: PublishingSubject<string[] | undefined>;
|
||||
setDisabledActionIds: (ids: string[] | undefined) => void;
|
||||
getAllTriggersDisabled?: () => boolean;
|
||||
}
|
||||
|
||||
|
@ -22,6 +23,8 @@ export const apiPublishesDisabledActionIds = (
|
|||
unknownApi: null | unknown
|
||||
): unknownApi is PublishesDisabledActionIds => {
|
||||
return Boolean(
|
||||
unknownApi && (unknownApi as PublishesDisabledActionIds)?.disabledActionIds !== undefined
|
||||
unknownApi &&
|
||||
(unknownApi as PublishesDisabledActionIds)?.disabledActionIds !== undefined &&
|
||||
typeof (unknownApi as PublishesDisabledActionIds)?.setDisabledActionIds === 'function'
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { ControlWidth, ControlInputTransform } from './types';
|
||||
export type { ControlWidth, ControlInputTransform, ParentIgnoreSettings } from './types';
|
||||
|
||||
// Control Group exports
|
||||
export {
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public';
|
||||
import { OptionsListComponentState } from '../../public/options_list/types';
|
||||
import { ControlFactory, ControlOutput } from '../../public/types';
|
||||
import { OptionsListEmbeddableInput } from './types';
|
||||
|
||||
import * as optionsListStateModule from '../../public/options_list/options_list_reducers';
|
||||
import { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public/options_list';
|
||||
|
||||
const mockOptionsListComponentState = {
|
||||
searchString: { value: '', valid: true },
|
||||
|
|
|
@ -8,14 +8,10 @@
|
|||
*/
|
||||
|
||||
import { RangeSliderEmbeddableInput } from '..';
|
||||
import {
|
||||
ControlFactory,
|
||||
ControlOutput,
|
||||
RangeSliderEmbeddable,
|
||||
RangeSliderEmbeddableFactory,
|
||||
} from '../../public';
|
||||
import { RangeSliderEmbeddable, RangeSliderEmbeddableFactory } from '../../public/range_slider';
|
||||
import * as rangeSliderStateModule from '../../public/range_slider/range_slider_reducers';
|
||||
import { RangeSliderComponentState } from '../../public/range_slider/types';
|
||||
import { ControlFactory, ControlOutput } from '../../public/types';
|
||||
|
||||
export const mockRangeSliderEmbeddableInput = {
|
||||
id: 'sample options list',
|
||||
|
|
|
@ -20,13 +20,7 @@ import {
|
|||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||
import {
|
||||
ControlGroupContainerFactory,
|
||||
OptionsListEmbeddableInput,
|
||||
RangeSliderEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
RANGE_SLIDER_CONTROL,
|
||||
} from '..';
|
||||
import { ControlGroupContainerFactory, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '..';
|
||||
|
||||
import { decorators } from './decorators';
|
||||
import { ControlsPanels } from '../control_group/types';
|
||||
|
@ -40,7 +34,9 @@ import {
|
|||
OptionsListResponse,
|
||||
OptionsListRequest,
|
||||
OptionsListSuggestions,
|
||||
OptionsListEmbeddableInput,
|
||||
} from '../../common/options_list/types';
|
||||
import { RangeSliderEmbeddableInput } from '../range_slider';
|
||||
|
||||
export default {
|
||||
title: 'Controls',
|
||||
|
|
|
@ -11,7 +11,7 @@ import { OptionsListEmbeddableFactory } from '../options_list';
|
|||
import { RangeSliderEmbeddableFactory } from '../range_slider';
|
||||
import { TimeSliderEmbeddableFactory } from '../time_slider';
|
||||
import { ControlsServiceType } from '../services/controls/types';
|
||||
import { ControlFactory } from '..';
|
||||
import { ControlFactory } from '../types';
|
||||
|
||||
export const populateStorybookControlFactories = (controlsServiceStub: ControlsServiceType) => {
|
||||
const optionsListFactoryStub = new OptionsListEmbeddableFactory();
|
||||
|
|
|
@ -16,7 +16,6 @@ import { act, render, waitFor } from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import { OptionsListEmbeddableFactory } from '../..';
|
||||
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '../../../common';
|
||||
import { mockControlGroupContainer, mockControlGroupInput } from '../../../common/mocks';
|
||||
import { RangeSliderEmbeddableFactory } from '../../range_slider';
|
||||
|
@ -24,6 +23,7 @@ import { pluginServices } from '../../services';
|
|||
import { ControlGroupContainerContext } from '../embeddable/control_group_container';
|
||||
import { ControlGroupComponentState, ControlGroupInput } from '../types';
|
||||
import { ControlGroup } from './control_group_component';
|
||||
import { OptionsListEmbeddableFactory } from '../../options_list';
|
||||
|
||||
jest.mock('@dnd-kit/core', () => ({
|
||||
/** DnD kit has a memory leak based on this layout measuring strategy on unmount; setting it to undefined prevents this */
|
||||
|
|
|
@ -15,7 +15,6 @@ import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub
|
|||
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { stubFieldSpecMap } from '@kbn/data-views-plugin/common/field.stub';
|
||||
import { OptionsListEmbeddableFactory } from '../..';
|
||||
import {
|
||||
OptionsListEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
|
@ -35,6 +34,7 @@ import { pluginServices } from '../../services';
|
|||
import { ControlGroupContainerContext } from '../embeddable/control_group_container';
|
||||
import { ControlGroupInput } from '../types';
|
||||
import { ControlEditor, EditControlProps } from './control_editor';
|
||||
import { OptionsListEmbeddableFactory } from '../../options_list';
|
||||
|
||||
describe('Data control editor', () => {
|
||||
interface MountOptions {
|
||||
|
|
|
@ -28,8 +28,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { ControlGroupInput } from '..';
|
||||
import { ParentIgnoreSettings } from '../..';
|
||||
import { getDefaultControlGroupInput } from '../../../common';
|
||||
import { getDefaultControlGroupInput, ParentIgnoreSettings } from '../../../common';
|
||||
import { ControlSettingTooltipLabel } from '../../components/control_setting_tooltip_label';
|
||||
import { ControlStyle } from '../../types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
|
|
|
@ -8,64 +8,70 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
ControlGroupRenderer,
|
||||
CONTROL_GROUP_TYPE,
|
||||
} from '..';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { PublishesUnifiedSearch, PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { act, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { ControlGroupRendererApi } from '.';
|
||||
import { getControlGroupEmbeddableFactory } from '../../react_controls/control_group/get_control_group_factory';
|
||||
import { CONTROL_GROUP_TYPE } from '../types';
|
||||
import { ControlGroupRenderer, ControlGroupRendererProps } from './control_group_renderer';
|
||||
|
||||
type ParentApiType = PublishesUnifiedSearch & {
|
||||
unifiedSearchFilters$?: PublishingSubject<Filter[] | undefined>;
|
||||
};
|
||||
|
||||
describe('control group renderer', () => {
|
||||
let mockControlGroupFactory: ControlGroupContainerFactory;
|
||||
let mockControlGroupContainer: ControlGroupContainer;
|
||||
const core = coreMock.createStart();
|
||||
const dataViews = dataViewPluginMocks.createStartContract();
|
||||
const factory = getControlGroupEmbeddableFactory({ core, dataViews });
|
||||
const buildControlGroupSpy = jest.spyOn(factory, 'buildEmbeddable');
|
||||
|
||||
const mountControlGroupRenderer = async (
|
||||
props: Omit<ControlGroupRendererProps, 'onApiAvailable'> = {}
|
||||
) => {
|
||||
let controlGroupApi: ControlGroupRendererApi | undefined;
|
||||
const component = render(
|
||||
<ControlGroupRenderer
|
||||
{...props}
|
||||
onApiAvailable={(newApi) => {
|
||||
controlGroupApi = newApi;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(controlGroupApi).toBeDefined();
|
||||
});
|
||||
return { component, api: controlGroupApi! as ControlGroupRendererApi };
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const embeddable = embeddablePluginMock.createSetupContract();
|
||||
embeddable.registerReactEmbeddableFactory(CONTROL_GROUP_TYPE, async () => {
|
||||
return factory;
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockControlGroupContainer = {
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
updateInput: jest.fn(),
|
||||
getInput: jest.fn().mockReturnValue({}),
|
||||
} as unknown as ControlGroupContainer;
|
||||
mockControlGroupFactory = {
|
||||
create: jest.fn().mockReturnValue(mockControlGroupContainer),
|
||||
} as unknown as ControlGroupContainerFactory;
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockControlGroupFactory);
|
||||
buildControlGroupSpy.mockClear();
|
||||
});
|
||||
|
||||
test('calls create method on the Control Group embeddable factory with returned initial input', async () => {
|
||||
await act(async () => {
|
||||
mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { controlStyle: 'twoLine' } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(pluginServices.getServices().embeddable.getEmbeddableFactory).toHaveBeenCalledWith(
|
||||
CONTROL_GROUP_TYPE
|
||||
);
|
||||
expect(mockControlGroupFactory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ controlStyle: 'twoLine' }),
|
||||
undefined,
|
||||
{ lastSavedInput: expect.objectContaining({ controlStyle: 'twoLine' }) },
|
||||
undefined
|
||||
);
|
||||
test('calls build method from the control group embeddable factory', async () => {
|
||||
await mountControlGroupRenderer();
|
||||
expect(buildControlGroupSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('destroys control group container on unmount', async () => {
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(<ControlGroupRenderer />);
|
||||
test('calling `updateInput` forces control group to be rebuilt', async () => {
|
||||
const { api } = await mountControlGroupRenderer();
|
||||
expect(buildControlGroupSpy).toBeCalledTimes(1);
|
||||
act(() => api.updateInput({ autoApplySelections: false }));
|
||||
await waitFor(() => {
|
||||
expect(buildControlGroupSpy).toBeCalledTimes(2);
|
||||
});
|
||||
wrapper!.unmount();
|
||||
expect(mockControlGroupContainer.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('filter changes are dispatched to control group if they are different', async () => {
|
||||
|
@ -75,57 +81,36 @@ describe('control group renderer', () => {
|
|||
const updatedFilters: Filter[] = [
|
||||
{ meta: { alias: 'test', disabled: false, negate: true, index: 'test' } },
|
||||
];
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { filters: initialFilters } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ filters: updatedFilters });
|
||||
});
|
||||
expect(mockControlGroupContainer.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filters: updatedFilters })
|
||||
|
||||
const { component, api } = await mountControlGroupRenderer({ filters: initialFilters });
|
||||
expect((api.parentApi as ParentApiType).unifiedSearchFilters$?.getValue()).toEqual(
|
||||
initialFilters
|
||||
);
|
||||
component.rerender(
|
||||
<ControlGroupRenderer onApiAvailable={jest.fn()} filters={updatedFilters} />
|
||||
);
|
||||
expect((api.parentApi as ParentApiType).unifiedSearchFilters$?.getValue()).toEqual(
|
||||
updatedFilters
|
||||
);
|
||||
});
|
||||
|
||||
test('query changes are dispatched to control group if they are different', async () => {
|
||||
const initialQuery = { language: 'kql', query: 'query' };
|
||||
const updatedQuery = { language: 'kql', query: 'super query' };
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { query: initialQuery } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ query: updatedQuery });
|
||||
});
|
||||
expect(mockControlGroupContainer.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ query: updatedQuery })
|
||||
);
|
||||
|
||||
const { component, api } = await mountControlGroupRenderer({ query: initialQuery });
|
||||
expect((api.parentApi as ParentApiType).query$.getValue()).toEqual(initialQuery);
|
||||
component.rerender(<ControlGroupRenderer onApiAvailable={jest.fn()} query={updatedQuery} />);
|
||||
expect((api.parentApi as ParentApiType).query$.getValue()).toEqual(updatedQuery);
|
||||
});
|
||||
|
||||
test('time range changes are dispatched to control group if they are different', async () => {
|
||||
const initialTime = { from: new Date().toISOString(), to: new Date().toISOString() };
|
||||
const updatedTime = { from: new Date().toISOString() + 10, to: new Date().toISOString() + 20 };
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithIntl(
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={() => Promise.resolve({ initialInput: { timeRange: initialTime } })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ timeRange: updatedTime });
|
||||
});
|
||||
expect(mockControlGroupContainer.updateInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeRange: updatedTime })
|
||||
);
|
||||
|
||||
const { component, api } = await mountControlGroupRenderer({ timeRange: initialTime });
|
||||
expect((api.parentApi as ParentApiType).timeRange$.getValue()).toEqual(initialTime);
|
||||
component.rerender(<ControlGroupRenderer onApiAvailable={jest.fn()} timeRange={updatedTime} />);
|
||||
expect((api.parentApi as ParentApiType).timeRange$.getValue()).toEqual(updatedTime);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,136 +7,186 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { isEqual, pick } from 'lodash';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { compareFilters } from '@kbn/es-query';
|
||||
import { useSearchApi, type ViewMode as ViewModeType } from '@kbn/presentation-publishing';
|
||||
|
||||
import { CONTROL_GROUP_TYPE } from '../../../common';
|
||||
import {
|
||||
getDefaultControlGroupInput,
|
||||
getDefaultControlGroupPersistableInput,
|
||||
persistableControlGroupInputKeys,
|
||||
} from '../../../common';
|
||||
import { ControlGroupContainer } from '../embeddable/control_group_container';
|
||||
import { ControlGroupContainerFactory } from '../embeddable/control_group_container_factory';
|
||||
ControlGroupApi,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupSerializedState,
|
||||
} from '../../react_controls/control_group/types';
|
||||
import {
|
||||
ControlGroupCreationOptions,
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
CONTROL_GROUP_TYPE,
|
||||
} from '../types';
|
||||
import {
|
||||
AwaitingControlGroupAPI,
|
||||
buildApiFromControlGroupContainer,
|
||||
ControlGroupAPI,
|
||||
} from './control_group_api';
|
||||
import { controlGroupInputBuilder, ControlGroupInputBuilder } from './control_group_input_builder';
|
||||
controlGroupStateBuilder,
|
||||
type ControlGroupStateBuilder,
|
||||
} from '../../react_controls/control_group/utils/control_group_state_builder';
|
||||
import { getDefaultControlGroupRuntimeState } from '../../react_controls/control_group/utils/initialization_utils';
|
||||
import { ControlGroupCreationOptions, ControlGroupRendererApi } from './types';
|
||||
|
||||
export interface ControlGroupRendererProps {
|
||||
filters?: Filter[];
|
||||
onApiAvailable: (api: ControlGroupRendererApi) => void;
|
||||
getCreationOptions?: (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
builder: ControlGroupInputBuilder
|
||||
) => Promise<ControlGroupCreationOptions>;
|
||||
initialState: Partial<ControlGroupRuntimeState>,
|
||||
builder: ControlGroupStateBuilder
|
||||
) => Promise<Partial<ControlGroupCreationOptions>>;
|
||||
viewMode?: ViewModeType;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
query?: Query;
|
||||
dataLoading?: boolean;
|
||||
}
|
||||
|
||||
export const ControlGroupRenderer = forwardRef<AwaitingControlGroupAPI, ControlGroupRendererProps>(
|
||||
({ getCreationOptions, filters, timeRange, query }, ref) => {
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupContainer>();
|
||||
export const ControlGroupRenderer = ({
|
||||
onApiAvailable,
|
||||
getCreationOptions,
|
||||
filters,
|
||||
timeRange,
|
||||
query,
|
||||
viewMode,
|
||||
dataLoading,
|
||||
}: ControlGroupRendererProps) => {
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
const [regenerateId, setRegenerateId] = useState(uuidv4());
|
||||
const [controlGroup, setControlGroup] = useState<ControlGroupRendererApi | undefined>();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => buildApiFromControlGroupContainer(controlGroup) as ControlGroupAPI,
|
||||
[controlGroup]
|
||||
);
|
||||
/**
|
||||
* Parent API set up
|
||||
*/
|
||||
const searchApi = useSearchApi({
|
||||
filters,
|
||||
query,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
const controlGroupDomRef = useRef(null);
|
||||
const id = useMemo(() => uuidv4(), []);
|
||||
const viewMode$ = useMemo(
|
||||
() => new BehaviorSubject<ViewModeType>(viewMode ?? ViewMode.VIEW),
|
||||
// viewMode only used as initial value - changes do not effect memoized value.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (viewMode) viewMode$.next(viewMode);
|
||||
}, [viewMode, viewMode$]);
|
||||
|
||||
// onMount
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
let destroyControlGroup: () => void;
|
||||
const dataLoading$ = useMemo(
|
||||
() => new BehaviorSubject<boolean>(Boolean(dataLoading)),
|
||||
// dataLoading only used as initial value - changes do not effect memoized value.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (dataLoading !== dataLoading$.getValue()) dataLoading$.next(Boolean(dataLoading));
|
||||
}, [dataLoading, dataLoading$]);
|
||||
|
||||
(async () => {
|
||||
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
|
||||
const { pluginServices } = await import('../../services/plugin_services');
|
||||
const { embeddable } = pluginServices.getServices();
|
||||
const reload$ = useMemo(() => new Subject<void>(), []);
|
||||
|
||||
const factory = embeddable.getEmbeddableFactory(CONTROL_GROUP_TYPE) as EmbeddableFactory<
|
||||
ControlGroupInput,
|
||||
ControlGroupOutput,
|
||||
ControlGroupContainer
|
||||
> & {
|
||||
create: ControlGroupContainerFactory['create'];
|
||||
};
|
||||
const { initialInput, settings, fieldFilterPredicate } =
|
||||
(await getCreationOptions?.(getDefaultControlGroupInput(), controlGroupInputBuilder)) ??
|
||||
{};
|
||||
const newControlGroup = (await factory?.create(
|
||||
{
|
||||
id,
|
||||
...getDefaultControlGroupInput(),
|
||||
...initialInput,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
...settings,
|
||||
lastSavedInput: {
|
||||
...getDefaultControlGroupPersistableInput(),
|
||||
...pick(initialInput, persistableControlGroupInputKeys),
|
||||
},
|
||||
},
|
||||
fieldFilterPredicate
|
||||
)) as ControlGroupContainer;
|
||||
/**
|
||||
* Control group API set up
|
||||
*/
|
||||
const runtimeState$ = useMemo(
|
||||
() => new BehaviorSubject<ControlGroupRuntimeState>(getDefaultControlGroupRuntimeState()),
|
||||
[]
|
||||
);
|
||||
const [serializedState, setSerializedState] = useState<ControlGroupSerializedState | undefined>();
|
||||
|
||||
if (canceled) {
|
||||
newControlGroup.destroy();
|
||||
controlGroup?.destroy();
|
||||
return;
|
||||
}
|
||||
const updateInput = useCallback(
|
||||
(newState: Partial<ControlGroupRuntimeState>) => {
|
||||
runtimeState$.next({
|
||||
...runtimeState$.getValue(),
|
||||
...newState,
|
||||
});
|
||||
},
|
||||
[runtimeState$]
|
||||
);
|
||||
|
||||
if (controlGroupDomRef.current) {
|
||||
newControlGroup.render(controlGroupDomRef.current);
|
||||
}
|
||||
setControlGroup(newControlGroup);
|
||||
destroyControlGroup = () => newControlGroup.destroy();
|
||||
})();
|
||||
return () => {
|
||||
canceled = true;
|
||||
destroyControlGroup?.();
|
||||
/**
|
||||
* To mimic `input$`, subscribe to unsaved changes and snapshot the runtime state whenever
|
||||
* something change
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!controlGroup) return;
|
||||
const stateChangeSubscription = controlGroup.unsavedChanges.subscribe((changes) => {
|
||||
runtimeState$.next({ ...runtimeState$.getValue(), ...changes });
|
||||
});
|
||||
return () => {
|
||||
stateChangeSubscription.unsubscribe();
|
||||
};
|
||||
}, [controlGroup, runtimeState$]);
|
||||
|
||||
/**
|
||||
* On mount
|
||||
*/
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const { initialState, editorConfig } =
|
||||
(await getCreationOptions?.(
|
||||
getDefaultControlGroupRuntimeState(),
|
||||
controlGroupStateBuilder
|
||||
)) ?? {};
|
||||
updateInput({
|
||||
...initialState,
|
||||
editorConfig,
|
||||
});
|
||||
const state = {
|
||||
...omit(initialState, ['initialChildControlState', 'ignoreParentSettings']),
|
||||
editorConfig,
|
||||
controlStyle: initialState?.labelPosition,
|
||||
panelsJSON: JSON.stringify(initialState?.initialChildControlState ?? {}),
|
||||
ignoreParentSettingsJSON: JSON.stringify(initialState?.ignoreParentSettings ?? {}),
|
||||
};
|
||||
// exhaustive deps disabled because we want the control group to be created only on first render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!controlGroup) return;
|
||||
if (
|
||||
(timeRange && !isEqual(controlGroup.getInput().timeRange, timeRange)) ||
|
||||
!compareFilters(controlGroup.getInput().filters ?? [], filters ?? []) ||
|
||||
!isEqual(controlGroup.getInput().query, query)
|
||||
) {
|
||||
controlGroup.updateInput({
|
||||
timeRange,
|
||||
query,
|
||||
filters,
|
||||
});
|
||||
if (!cancelled) {
|
||||
setSerializedState(state as ControlGroupSerializedState);
|
||||
}
|
||||
}, [query, filters, controlGroup, timeRange]);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// exhaustive deps disabled because we want the control group to be created only on first render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <div ref={controlGroupDomRef} />;
|
||||
}
|
||||
);
|
||||
return !serializedState ? null : (
|
||||
<ReactEmbeddableRenderer<ControlGroupSerializedState, ControlGroupRuntimeState, ControlGroupApi>
|
||||
key={regenerateId} // this key forces a re-mount when `updateInput` is called
|
||||
maybeId={id}
|
||||
type={CONTROL_GROUP_TYPE}
|
||||
getParentApi={() => ({
|
||||
reload$,
|
||||
dataLoading: dataLoading$,
|
||||
viewMode: viewMode$,
|
||||
query$: searchApi.query$,
|
||||
timeRange$: searchApi.timeRange$,
|
||||
unifiedSearchFilters$: searchApi.filters$,
|
||||
getSerializedStateForChild: () => ({
|
||||
rawState: serializedState,
|
||||
}),
|
||||
getRuntimeStateForChild: () => {
|
||||
return runtimeState$.getValue();
|
||||
},
|
||||
})}
|
||||
onApiAvailable={(controlGroupApi) => {
|
||||
const controlGroupRendererApi: ControlGroupRendererApi = {
|
||||
...controlGroupApi,
|
||||
reload: () => reload$.next(),
|
||||
updateInput: (newInput) => {
|
||||
updateInput(newInput);
|
||||
setRegenerateId(uuidv4()); // force remount
|
||||
},
|
||||
getInput$: () => runtimeState$,
|
||||
};
|
||||
setControlGroup(controlGroupRendererApi);
|
||||
onApiAvailable(controlGroupRendererApi);
|
||||
}}
|
||||
hidePanelChrome
|
||||
panelProps={{ hideLoader: true }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import type { ControlGroupRendererProps } from './control_group_renderer';
|
||||
|
||||
const Component = dynamic(async () => {
|
||||
const { ControlGroupRenderer } = await import('./control_group_renderer');
|
||||
return {
|
||||
default: ControlGroupRenderer,
|
||||
};
|
||||
});
|
||||
|
||||
export function LazyControlGroupRenderer(props: ControlGroupRendererProps) {
|
||||
return <Component {...props} />;
|
||||
}
|
|
@ -7,11 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ControlGroupContainer } from '..';
|
||||
|
||||
// TODO lock down ControlGroupAPI
|
||||
export type ControlGroupAPI = ControlGroupContainer;
|
||||
export type AwaitingControlGroupAPI = ControlGroupAPI | null;
|
||||
|
||||
export const buildApiFromControlGroupContainer = (container?: ControlGroupContainer) =>
|
||||
container ?? null;
|
||||
export type { ControlGroupRendererProps } from './control_group_renderer';
|
||||
export { LazyControlGroupRenderer as ControlGroupRenderer } from './control_group_renderer_lazy';
|
||||
export type { ControlGroupCreationOptions, ControlGroupRendererApi } from './types';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
ControlGroupApi,
|
||||
ControlGroupEditorConfig,
|
||||
ControlGroupRuntimeState,
|
||||
} from '../../react_controls/control_group/types';
|
||||
|
||||
export type ControlGroupRendererApi = ControlGroupApi & {
|
||||
reload: () => void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Calling `updateInput` will cause the entire control group to be re-initialized.
|
||||
*
|
||||
* Therefore, to update the runtime state without `updateInput`, you should add public setters to the
|
||||
* relavant API (`ControlGroupApi` or the individual control type APIs) for the state you wish to update
|
||||
* and call those setters instead.
|
||||
*/
|
||||
updateInput: (input: Partial<ControlGroupRuntimeState>) => void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Instead of subscribing to the whole runtime state, it is more efficient to subscribe to the individual
|
||||
* publishing subjects of the control group API.
|
||||
*/
|
||||
getInput$: () => BehaviorSubject<ControlGroupRuntimeState>;
|
||||
};
|
||||
|
||||
export interface ControlGroupCreationOptions {
|
||||
initialState?: Partial<ControlGroupRuntimeState>;
|
||||
editorConfig?: ControlGroupEditorConfig;
|
||||
}
|
|
@ -10,22 +10,16 @@
|
|||
export type { ControlGroupContainer } from './embeddable/control_group_container';
|
||||
export type { ControlGroupInput, ControlGroupOutput } from './types';
|
||||
|
||||
export { CONTROL_GROUP_TYPE } from './types';
|
||||
export { ControlGroupContainerFactory } from './embeddable/control_group_container_factory';
|
||||
export { CONTROL_GROUP_TYPE } from './types';
|
||||
|
||||
export { ACTION_EDIT_CONTROL, ACTION_DELETE_CONTROL } from './actions';
|
||||
export { ACTION_DELETE_CONTROL, ACTION_EDIT_CONTROL } from './actions';
|
||||
|
||||
export {
|
||||
type AddDataControlProps,
|
||||
type AddOptionsListControlProps,
|
||||
type ControlGroupInputBuilder,
|
||||
type AddRangeSliderControlProps,
|
||||
controlGroupInputBuilder,
|
||||
} from './external_api/control_group_input_builder';
|
||||
export { controlGroupInputBuilder } from './external_api/control_group_input_builder';
|
||||
|
||||
export type { ControlGroupAPI, AwaitingControlGroupAPI } from './external_api/control_group_api';
|
||||
|
||||
export {
|
||||
type ControlGroupRendererProps,
|
||||
ControlGroupRenderer,
|
||||
} from './external_api/control_group_renderer';
|
||||
export { ControlGroupRenderer } from './external_api';
|
||||
export type {
|
||||
ControlGroupRendererApi,
|
||||
ControlGroupRendererProps,
|
||||
ControlGroupCreationOptions,
|
||||
} from './external_api';
|
||||
|
|
|
@ -36,12 +36,6 @@ export type ControlGroupReduxState = ReduxEmbeddableState<
|
|||
|
||||
export type FieldFilterPredicate = (f: DataViewField) => boolean;
|
||||
|
||||
export interface ControlGroupCreationOptions {
|
||||
initialInput?: Partial<ControlGroupInput>;
|
||||
settings?: ControlGroupSettings;
|
||||
fieldFilterPredicate?: FieldFilterPredicate;
|
||||
}
|
||||
|
||||
export interface ControlGroupSettings {
|
||||
showAddButton?: boolean;
|
||||
staticDataViewId?: string;
|
||||
|
|
|
@ -13,37 +13,38 @@ export type {
|
|||
ControlGroupApi,
|
||||
ControlGroupRuntimeState,
|
||||
ControlGroupSerializedState,
|
||||
ControlStateTransform,
|
||||
ControlPanelState,
|
||||
} from './react_controls/control_group/types';
|
||||
export {
|
||||
controlGroupStateBuilder,
|
||||
type ControlGroupStateBuilder,
|
||||
} from './react_controls/control_group/utils/control_group_state_builder';
|
||||
|
||||
export type {
|
||||
DataControlApi,
|
||||
DefaultDataControlState,
|
||||
DataControlFactory,
|
||||
DataControlServices,
|
||||
} from './react_controls/controls/data_controls/types';
|
||||
export { controlGroupStateBuilder } from './react_controls/control_group/control_group_state_builder';
|
||||
export type { OptionsListControlState } from './react_controls/controls/data_controls/options_list_control/types';
|
||||
|
||||
export { ACTION_EDIT_CONTROL } from './react_controls/actions/edit_control_action/edit_control_action';
|
||||
|
||||
export {
|
||||
ACTION_DELETE_CONTROL,
|
||||
ControlGroupRenderer,
|
||||
type ControlGroupRendererProps,
|
||||
type ControlGroupRendererApi,
|
||||
type ControlGroupCreationOptions,
|
||||
} from './control_group';
|
||||
|
||||
export type { ControlWidth, ControlStyle } from '../common/types';
|
||||
|
||||
/**
|
||||
* TODO: remove all exports below this when control group embeddable is removed
|
||||
*/
|
||||
|
||||
export type {
|
||||
ControlOutput,
|
||||
ControlFactory,
|
||||
ControlEmbeddable,
|
||||
ControlEditorProps,
|
||||
CommonControlOutput,
|
||||
IEditableControlFactory,
|
||||
CanClearSelections,
|
||||
} from './types';
|
||||
|
||||
export type {
|
||||
ControlWidth,
|
||||
ControlStyle,
|
||||
ParentIgnoreSettings,
|
||||
ControlInput,
|
||||
DataControlInput,
|
||||
} from '../common/types';
|
||||
|
||||
export {
|
||||
CONTROL_GROUP_TYPE,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
|
@ -52,41 +53,13 @@ export {
|
|||
} from '../common';
|
||||
|
||||
export {
|
||||
type AddDataControlProps,
|
||||
type AddOptionsListControlProps,
|
||||
type AddRangeSliderControlProps,
|
||||
type ControlGroupContainer,
|
||||
ControlGroupContainerFactory,
|
||||
type ControlGroupInput,
|
||||
type ControlGroupInputBuilder,
|
||||
type ControlGroupAPI,
|
||||
type AwaitingControlGroupAPI,
|
||||
type ControlGroupOutput,
|
||||
controlGroupInputBuilder,
|
||||
} from './control_group';
|
||||
|
||||
export {
|
||||
OptionsListEmbeddableFactory,
|
||||
type OptionsListEmbeddable,
|
||||
type OptionsListEmbeddableInput,
|
||||
} from './options_list';
|
||||
|
||||
export {
|
||||
RangeSliderEmbeddableFactory,
|
||||
type RangeSliderEmbeddable,
|
||||
type RangeSliderEmbeddableInput,
|
||||
} from './range_slider';
|
||||
|
||||
export {
|
||||
ACTION_EDIT_CONTROL,
|
||||
ACTION_DELETE_CONTROL,
|
||||
ControlGroupRenderer,
|
||||
type ControlGroupRendererProps,
|
||||
} from './control_group';
|
||||
|
||||
/** TODO: Remove this once it is no longer needed in the examples plugin */
|
||||
export { CONTROL_WIDTH_OPTIONS } from './control_group/editor/editor_constants';
|
||||
|
||||
export function plugin() {
|
||||
return new ControlsPlugin();
|
||||
}
|
||||
|
|
|
@ -12,10 +12,11 @@ import React from 'react';
|
|||
import { render } from '@testing-library/react';
|
||||
import { OptionsListEmbeddableContext } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
||||
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||
import { OptionsListControl } from './options_list_control';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { OptionsListEmbeddableInput } from '..';
|
||||
import { ControlOutput } from '../../types';
|
||||
|
||||
describe('Options list control', () => {
|
||||
const defaultProps = {
|
||||
|
|
|
@ -12,7 +12,6 @@ import useAsync from 'react-use/lib/useAsync';
|
|||
|
||||
import { Direction, EuiFormRow, EuiLoadingSpinner, EuiRadioGroup, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import { ControlEditorProps, OptionsListEmbeddableInput } from '../..';
|
||||
import {
|
||||
getCompatibleSearchTechniques,
|
||||
OptionsListSearchTechnique,
|
||||
|
@ -25,6 +24,8 @@ import {
|
|||
import { pluginServices } from '../../services';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { ControlSettingTooltipLabel } from '../../components/control_setting_tooltip_label';
|
||||
import { OptionsListEmbeddableInput } from '..';
|
||||
import { ControlEditorProps } from '../../types';
|
||||
|
||||
const selectionOptions = [
|
||||
{
|
||||
|
|
|
@ -14,12 +14,13 @@ import { stubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
|||
import { render, RenderResult, within } from '@testing-library/react';
|
||||
import userEvent, { type UserEvent } from '@testing-library/user-event';
|
||||
|
||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
||||
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||
import { pluginServices } from '../../services';
|
||||
import { OptionsListEmbeddableContext } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||
import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover';
|
||||
import { OptionsListEmbeddableInput } from '..';
|
||||
import { ControlOutput } from '../../types';
|
||||
|
||||
describe('Options list popover', () => {
|
||||
let user: UserEvent;
|
||||
|
|
|
@ -38,22 +38,17 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
ControlInput,
|
||||
ControlOutput,
|
||||
OptionsListEmbeddableInput,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
} from '../..';
|
||||
import { ControlGroupContainer, OPTIONS_LIST_CONTROL } from '../..';
|
||||
import { OptionsListSelection } from '../../../common/options_list/options_list_selections';
|
||||
import { ControlFilterOutput } from '../../control_group/types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlsDataViewsService } from '../../services/data_views/types';
|
||||
import { ControlsOptionsListService } from '../../services/options_list/types';
|
||||
import { CanClearSelections } from '../../types';
|
||||
import { CanClearSelections, ControlInput, ControlOutput } from '../../types';
|
||||
import { OptionsListControl } from '../components/options_list_control';
|
||||
import { getDefaultComponentState, optionsListReducers } from '../options_list_reducers';
|
||||
import { MIN_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
|
||||
import { OptionsListEmbeddableInput } from '..';
|
||||
|
||||
const diffDataFetchProps = (
|
||||
last?: OptionsListDataFetchProps,
|
||||
|
|
|
@ -28,21 +28,16 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
|
||||
import {
|
||||
ControlGroupContainer,
|
||||
ControlInput,
|
||||
ControlOutput,
|
||||
RangeSliderEmbeddableInput,
|
||||
RANGE_SLIDER_CONTROL,
|
||||
} from '../..';
|
||||
import { ControlGroupContainer, RANGE_SLIDER_CONTROL } from '../..';
|
||||
import { ControlFilterOutput } from '../../control_group/types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { ControlsDataService } from '../../services/data/types';
|
||||
import { ControlsDataViewsService } from '../../services/data_views/types';
|
||||
import { CanClearSelections } from '../../types';
|
||||
import { CanClearSelections, ControlInput, ControlOutput } from '../../types';
|
||||
import { RangeSliderControl } from '../components/range_slider_control';
|
||||
import { getDefaultComponentState, rangeSliderReducers } from '../range_slider_reducers';
|
||||
import { RangeSliderReduxState } from '../types';
|
||||
import { RangeSliderEmbeddableInput } from '..';
|
||||
|
||||
const diffDataFetchProps = (
|
||||
current?: RangeSliderDataFetchProps,
|
||||
|
|
|
@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { EmbeddableApiContext, HasUniqueId } from '@kbn/presentation-publishing';
|
||||
import { type Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
const ACTION_EDIT_CONTROL = 'editDataControl';
|
||||
export const ACTION_EDIT_CONTROL = 'editDataControl';
|
||||
|
||||
export class EditControlAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_EDIT_CONTROL;
|
||||
|
|
|
@ -11,8 +11,12 @@ import React from 'react';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ControlGroupEditor } from './control_group_editor';
|
||||
import { ControlGroupApi, ControlStyle, ParentIgnoreSettings } from '../../..';
|
||||
import { ControlGroupChainingSystem, DEFAULT_CONTROL_STYLE } from '../../../../common';
|
||||
import { ControlGroupApi, ControlStyle } from '../../..';
|
||||
import {
|
||||
ControlGroupChainingSystem,
|
||||
DEFAULT_CONTROL_STYLE,
|
||||
ParentIgnoreSettings,
|
||||
} from '../../../../common';
|
||||
import { DefaultControlApi } from '../../controls/types';
|
||||
|
||||
describe('render', () => {
|
||||
|
|
|
@ -28,11 +28,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { ControlStyle, ParentIgnoreSettings } from '../../..';
|
||||
import { ControlStyle } from '../../..';
|
||||
|
||||
import { ControlStateManager } from '../../controls/types';
|
||||
import { ControlGroupStrings } from '../control_group_strings';
|
||||
import { ControlGroupApi, ControlGroupEditorState } from '../types';
|
||||
import { ParentIgnoreSettings } from '../../../../common';
|
||||
|
||||
const CONTROL_LAYOUT_OPTIONS = [
|
||||
{
|
||||
|
|
|
@ -101,6 +101,7 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
|
|||
grow,
|
||||
width,
|
||||
labelPosition,
|
||||
disabledActionIds,
|
||||
rawViewMode,
|
||||
] = useBatchedOptionalPublishingSubjects(
|
||||
api?.dataLoading,
|
||||
|
@ -110,6 +111,7 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
|
|||
api?.grow,
|
||||
api?.width,
|
||||
api?.parentApi?.labelPosition,
|
||||
api?.parentApi?.disabledActionIds,
|
||||
viewModeSubject
|
||||
);
|
||||
const usingTwoLineLayout = labelPosition === 'twoLine';
|
||||
|
@ -149,7 +151,7 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
|
|||
'controlFrameFloatingActions--oneLine': !usingTwoLineLayout,
|
||||
})}
|
||||
viewMode={viewMode}
|
||||
disabledActions={[]}
|
||||
disabledActions={disabledActionIds}
|
||||
isEnabled={true}
|
||||
>
|
||||
<EuiFormRow
|
||||
|
|
|
@ -11,7 +11,7 @@ import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
|||
import { PublishesUnifiedSearch, PublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { apiPublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';
|
||||
import { BehaviorSubject, debounceTime, map, merge, Observable, switchMap } from 'rxjs';
|
||||
import { ParentIgnoreSettings } from '../../..';
|
||||
import { ParentIgnoreSettings } from '../../../../common';
|
||||
|
||||
export interface ControlGroupFetchContext {
|
||||
unifiedSearchFilters?: Filter[] | undefined;
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import React, { useEffect } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
|
@ -20,31 +21,33 @@ import {
|
|||
combineCompatibleChildrenApis,
|
||||
} from '@kbn/presentation-containers';
|
||||
import {
|
||||
apiPublishesDataViews,
|
||||
PublishesDataViews,
|
||||
apiPublishesDataViews,
|
||||
useBatchedPublishingSubjects,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { apiPublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';
|
||||
import { ControlStyle, ParentIgnoreSettings } from '../..';
|
||||
|
||||
import { ControlStyle } from '../..';
|
||||
import {
|
||||
ControlGroupChainingSystem,
|
||||
CONTROL_GROUP_TYPE,
|
||||
ControlGroupChainingSystem,
|
||||
DEFAULT_CONTROL_STYLE,
|
||||
ParentIgnoreSettings,
|
||||
} from '../../../common';
|
||||
import { openDataControlEditor } from '../controls/data_controls/open_data_control_editor';
|
||||
import { ControlGroup } from './components/control_group';
|
||||
import { chaining$, controlFetch$, controlGroupFetch$ } from './control_fetch';
|
||||
import { initializeControlGroupUnsavedChanges } from './control_group_unsaved_changes_api';
|
||||
import { initControlsManager } from './init_controls_manager';
|
||||
import { openEditControlGroupFlyout } from './open_edit_control_group_flyout';
|
||||
import { deserializeControlGroup } from './serialization_utils';
|
||||
import { initSelectionsManager } from './selections_manager';
|
||||
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';
|
||||
import { deserializeControlGroup } from './utils/serialization_utils';
|
||||
|
||||
const DEFAULT_CHAINING_SYSTEM = 'HIERARCHICAL';
|
||||
|
||||
|
@ -98,6 +101,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
initialLabelPosition ?? DEFAULT_CONTROL_STYLE // TODO: Rename `DEFAULT_CONTROL_STYLE`
|
||||
);
|
||||
const allowExpensiveQueries$ = new BehaviorSubject<boolean>(true);
|
||||
const disabledActionIds$ = new BehaviorSubject<string[] | undefined>(undefined);
|
||||
|
||||
/** TODO: Handle loading; loading should be true if any child is loading */
|
||||
const dataLoading$ = new BehaviorSubject<boolean | undefined>(false);
|
||||
|
@ -131,9 +135,7 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
|
||||
const api = setApi({
|
||||
...controlsManager.api,
|
||||
getLastSavedControlState: (controlUuid: string) => {
|
||||
return lastSavedRuntimeState.initialChildControlState[controlUuid] ?? {};
|
||||
},
|
||||
disabledActionIds: disabledActionIds$,
|
||||
...unsavedChanges.api,
|
||||
...selectionsManager.api,
|
||||
controlFetch$: (controlUuid: string) =>
|
||||
|
@ -150,8 +152,13 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
autoApplySelections$,
|
||||
allowExpensiveQueries$,
|
||||
snapshotRuntimeState: () => {
|
||||
// TODO: Remove this if it ends up being unnecessary
|
||||
return {} as unknown as ControlGroupRuntimeState;
|
||||
return {
|
||||
chainingSystem: chainingSystem$.getValue(),
|
||||
labelPosition: labelPosition$.getValue(),
|
||||
autoApplySelections: autoApplySelections$.getValue(),
|
||||
ignoreParentSettings: ignoreParentSettings$.getValue(),
|
||||
initialChildControlState: controlsManager.snapshotControlsRuntimeState(),
|
||||
};
|
||||
},
|
||||
dataLoading: dataLoading$,
|
||||
onEdit: async () => {
|
||||
|
@ -167,15 +174,12 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
);
|
||||
},
|
||||
isEditingEnabled: () => true,
|
||||
getTypeDisplayName: () =>
|
||||
i18n.translate('controls.controlGroup.displayName', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
openAddDataControlFlyout: (options) => {
|
||||
openAddDataControlFlyout: (settings) => {
|
||||
const parentDataViewId = apiPublishesDataViews(parentApi)
|
||||
? parentApi.dataViews.value?.[0]?.id
|
||||
: undefined;
|
||||
const newControlState = controlsManager.getNewControlState();
|
||||
|
||||
openDataControlEditor({
|
||||
initialState: {
|
||||
...newControlState,
|
||||
|
@ -185,14 +189,11 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
onSave: ({ type: controlType, state: initialState }) => {
|
||||
controlsManager.api.addNewPanel({
|
||||
panelType: controlType,
|
||||
initialState: options?.controlInputTransform
|
||||
? options.controlInputTransform(
|
||||
initialState as Partial<ControlGroupSerializedState>,
|
||||
controlType
|
||||
)
|
||||
initialState: settings?.controlStateTransform
|
||||
? settings.controlStateTransform(initialState, controlType)
|
||||
: initialState,
|
||||
});
|
||||
options?.onSave?.();
|
||||
settings?.onSave?.();
|
||||
},
|
||||
controlGroupApi: api,
|
||||
services,
|
||||
|
@ -217,6 +218,20 @@ export const getControlGroupEmbeddableFactory = (services: {
|
|||
? parentApi.saveNotification$
|
||||
: undefined,
|
||||
reload$: apiPublishesReload(parentApi) ? parentApi.reload$ : undefined,
|
||||
|
||||
/** Public getters */
|
||||
getTypeDisplayName: () =>
|
||||
i18n.translate('controls.controlGroup.displayName', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
getEditorConfig: () => initialRuntimeState.editorConfig,
|
||||
getLastSavedControlState: (controlUuid: string) => {
|
||||
return lastSavedRuntimeState.initialChildControlState[controlUuid] ?? {};
|
||||
},
|
||||
|
||||
/** Public setters */
|
||||
setDisabledActionIds: (ids) => disabledActionIds$.next(ids),
|
||||
setChainingSystem: (newChainingSystem) => chainingSystem$.next(newChainingSystem),
|
||||
});
|
||||
|
||||
/** Subscribe to all children's output data views, combine them, and output them */
|
||||
|
|
|
@ -264,9 +264,7 @@ export function initControlsManager(
|
|||
|
||||
export function getLastUsedDataViewId(
|
||||
controlsInOrder: ControlsInOrder,
|
||||
initialControlPanelsState: ControlPanelsState<
|
||||
ControlPanelState & Partial<DefaultDataControlState>
|
||||
>
|
||||
initialControlPanelsState: ControlPanelsState<Partial<DefaultDataControlState>>
|
||||
) {
|
||||
let dataViewId: string | undefined;
|
||||
for (let i = controlsInOrder.length - 1; i >= 0; i--) {
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
|
@ -20,6 +18,7 @@ import {
|
|||
HasEditCapabilities,
|
||||
HasParentApi,
|
||||
PublishesDataLoading,
|
||||
PublishesDisabledActionIds,
|
||||
PublishesFilters,
|
||||
PublishesTimeslice,
|
||||
PublishesUnifiedSearch,
|
||||
|
@ -27,18 +26,26 @@ import {
|
|||
PublishingSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { PublishesDataViews } from '@kbn/presentation-publishing/interfaces/publishes_data_views';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { PublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload';
|
||||
import { ParentIgnoreSettings } from '../..';
|
||||
import { ControlInputTransform } from '../../../common';
|
||||
import { ControlGroupChainingSystem } from '../../../common/control_group/types';
|
||||
import { ControlStyle } from '../../types';
|
||||
import { DefaultControlState } from '../controls/types';
|
||||
import { ControlFetchContext } from './control_fetch/control_fetch';
|
||||
import { FieldFilterPredicate } from '../../control_group/types';
|
||||
import { ParentIgnoreSettings } from '../../../common';
|
||||
|
||||
export interface ControlPanelsState<ControlState extends ControlPanelState = ControlPanelState> {
|
||||
[panelId: string]: ControlState;
|
||||
}
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Control group API
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type ControlStateTransform<State extends DefaultControlState = DefaultControlState> = (
|
||||
newState: Partial<State>,
|
||||
controlType: string
|
||||
) => Partial<State>;
|
||||
|
||||
export type ControlGroupUnsavedChanges = Omit<
|
||||
ControlGroupRuntimeState,
|
||||
|
@ -47,8 +54,6 @@ export type ControlGroupUnsavedChanges = Omit<
|
|||
filters: Filter[] | undefined;
|
||||
};
|
||||
|
||||
export type ControlPanelState = DefaultControlState & { type: string; order: number };
|
||||
|
||||
export type ControlGroupApi = PresentationContainer &
|
||||
DefaultEmbeddableApi<ControlGroupSerializedState, ControlGroupRuntimeState> &
|
||||
PublishesFilters &
|
||||
|
@ -56,46 +61,62 @@ export type ControlGroupApi = PresentationContainer &
|
|||
HasSerializedChildState<ControlPanelState> &
|
||||
HasEditCapabilities &
|
||||
PublishesDataLoading &
|
||||
Pick<PublishesUnsavedChanges, 'unsavedChanges'> &
|
||||
Pick<PublishesUnsavedChanges<ControlGroupRuntimeState>, 'unsavedChanges'> &
|
||||
PublishesTimeslice &
|
||||
PublishesDisabledActionIds &
|
||||
Partial<HasParentApi<PublishesUnifiedSearch> & HasSaveNotification & PublishesReload> & {
|
||||
asyncResetUnsavedChanges: () => Promise<void>;
|
||||
autoApplySelections$: PublishingSubject<boolean>;
|
||||
controlFetch$: (controlUuid: string) => Observable<ControlFetchContext>;
|
||||
getLastSavedControlState: (controlUuid: string) => object;
|
||||
ignoreParentSettings$: PublishingSubject<ParentIgnoreSettings | undefined>;
|
||||
allowExpensiveQueries$: PublishingSubject<boolean>;
|
||||
untilInitialized: () => Promise<void>;
|
||||
autoApplySelections$: PublishingSubject<boolean>;
|
||||
ignoreParentSettings$: PublishingSubject<ParentIgnoreSettings | undefined>;
|
||||
labelPosition: PublishingSubject<ControlStyle>;
|
||||
|
||||
asyncResetUnsavedChanges: () => Promise<void>;
|
||||
controlFetch$: (controlUuid: string) => Observable<ControlFetchContext>;
|
||||
openAddDataControlFlyout: (options?: {
|
||||
controlInputTransform?: ControlInputTransform;
|
||||
controlStateTransform?: ControlStateTransform;
|
||||
onSave?: () => void;
|
||||
}) => void;
|
||||
labelPosition: PublishingSubject<ControlStyle>;
|
||||
untilInitialized: () => Promise<void>;
|
||||
|
||||
/** Public getters */
|
||||
getEditorConfig: () => ControlGroupEditorConfig | undefined;
|
||||
getLastSavedControlState: (controlUuid: string) => object;
|
||||
|
||||
/** Public setters */
|
||||
setChainingSystem: (chainingSystem: ControlGroupChainingSystem) => void;
|
||||
};
|
||||
|
||||
export interface ControlGroupRuntimeState {
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Control group state
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface ControlGroupEditorConfig {
|
||||
hideDataViewSelector?: boolean;
|
||||
hideWidthSettings?: boolean;
|
||||
hideAdditionalSettings?: boolean;
|
||||
fieldFilterPredicate?: FieldFilterPredicate;
|
||||
}
|
||||
|
||||
export interface ControlGroupRuntimeState<State extends DefaultControlState = DefaultControlState> {
|
||||
chainingSystem: ControlGroupChainingSystem;
|
||||
labelPosition: ControlStyle; // TODO: Rename this type to ControlLabelPosition
|
||||
autoApplySelections: boolean;
|
||||
ignoreParentSettings?: ParentIgnoreSettings;
|
||||
|
||||
initialChildControlState: ControlPanelsState<ControlPanelState>;
|
||||
/** TODO: Handle the editor config, which is used with the control group renderer component */
|
||||
editorConfig?: {
|
||||
hideDataViewSelector?: boolean;
|
||||
hideWidthSettings?: boolean;
|
||||
hideAdditionalSettings?: boolean;
|
||||
};
|
||||
initialChildControlState: ControlPanelsState<State>;
|
||||
|
||||
/*
|
||||
* Configuration settings that are never persisted
|
||||
* - remove after https://github.com/elastic/kibana/issues/189939 is resolved
|
||||
*/
|
||||
editorConfig?: ControlGroupEditorConfig;
|
||||
}
|
||||
|
||||
export type ControlGroupEditorState = Pick<
|
||||
ControlGroupRuntimeState,
|
||||
'chainingSystem' | 'labelPosition' | 'autoApplySelections' | 'ignoreParentSettings'
|
||||
>;
|
||||
|
||||
export interface ControlGroupSerializedState {
|
||||
chainingSystem: ControlGroupChainingSystem;
|
||||
panelsJSON: string;
|
||||
export interface ControlGroupSerializedState
|
||||
extends Pick<ControlGroupRuntimeState, 'chainingSystem' | 'editorConfig'> {
|
||||
panelsJSON: string; // stringified version of ControlSerializedState
|
||||
ignoreParentSettingsJSON: string;
|
||||
// In runtime state, we refer to this property as `labelPosition`;
|
||||
// to avoid migrations, we will continue to refer to this property as `controlStyle` in the serialized state
|
||||
|
@ -104,3 +125,23 @@ export interface ControlGroupSerializedState {
|
|||
// to avoid migrations, we will continue to refer to this property as `showApplySelections` in the serialized state
|
||||
showApplySelections: boolean | undefined;
|
||||
}
|
||||
|
||||
export type ControlGroupEditorState = Pick<
|
||||
ControlGroupRuntimeState,
|
||||
'chainingSystem' | 'labelPosition' | 'autoApplySelections' | 'ignoreParentSettings'
|
||||
>;
|
||||
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Control group panel state
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface ControlPanelsState<State extends DefaultControlState = DefaultControlState> {
|
||||
[panelId: string]: ControlPanelState<State>;
|
||||
}
|
||||
|
||||
export type ControlPanelState<State extends DefaultControlState = DefaultControlState> = State & {
|
||||
type: string;
|
||||
order: number;
|
||||
};
|
||||
|
|
|
@ -9,13 +9,17 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, TIME_SLIDER_CONTROL } from '../../../common';
|
||||
import { ControlGroupRuntimeState, ControlPanelsState } from './types';
|
||||
import { pluginServices } from '../../services';
|
||||
import { OptionsListControlState } from '../controls/data_controls/options_list_control/types';
|
||||
import { RangesliderControlState } from '../controls/data_controls/range_slider/types';
|
||||
import { DefaultDataControlState } from '../controls/data_controls/types';
|
||||
import { getDataControlFieldRegistry } from '../controls/data_controls/data_control_editor_utils';
|
||||
import {
|
||||
OPTIONS_LIST_CONTROL,
|
||||
RANGE_SLIDER_CONTROL,
|
||||
TIME_SLIDER_CONTROL,
|
||||
} from '../../../../common';
|
||||
import { ControlGroupRuntimeState, ControlPanelsState } from '../types';
|
||||
import { pluginServices } from '../../../services';
|
||||
import { OptionsListControlState } from '../../controls/data_controls/options_list_control/types';
|
||||
import { RangesliderControlState } from '../../controls/data_controls/range_slider/types';
|
||||
import { DefaultDataControlState } from '../../controls/data_controls/types';
|
||||
import { getDataControlFieldRegistry } from '../../controls/data_controls/data_control_editor_utils';
|
||||
|
||||
export type ControlGroupStateBuilder = typeof controlGroupStateBuilder;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { DEFAULT_CONTROL_STYLE } from '../../../../common';
|
||||
import { ControlGroupRuntimeState } from '../types';
|
||||
|
||||
export const getDefaultControlGroupRuntimeState = (): ControlGroupRuntimeState => ({
|
||||
initialChildControlState: {},
|
||||
labelPosition: DEFAULT_CONTROL_STYLE,
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
autoApplySelections: true,
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: false,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
},
|
||||
});
|
|
@ -7,10 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SerializedPanelState } from '@kbn/presentation-containers';
|
||||
import { omit } from 'lodash';
|
||||
import { ControlGroupRuntimeState, ControlGroupSerializedState } from './types';
|
||||
import { parseReferenceName } from '../controls/data_controls/reference_name_utils';
|
||||
|
||||
import { SerializedPanelState } from '@kbn/presentation-containers';
|
||||
|
||||
import { parseReferenceName } from '../../controls/data_controls/reference_name_utils';
|
||||
import { ControlGroupRuntimeState, ControlGroupSerializedState } from '../types';
|
||||
|
||||
export const deserializeControlGroup = (
|
||||
state: SerializedPanelState<ControlGroupSerializedState>
|
||||
|
@ -45,7 +47,7 @@ export const deserializeControlGroup = (
|
|||
autoApplySelections:
|
||||
typeof state.rawState.showApplySelections === 'boolean'
|
||||
? !state.rawState.showApplySelections
|
||||
: false, // Rename "showApplySelections" to "autoApplySelections"
|
||||
: true, // Rename "showApplySelections" to "autoApplySelections"
|
||||
labelPosition: state.rawState.controlStyle, // Rename "controlStyle" to "labelPosition"
|
||||
};
|
||||
};
|
|
@ -62,6 +62,7 @@ const controlGroupApi = {
|
|||
parentApi: dashboardApi,
|
||||
grow: new BehaviorSubject(DEFAULT_CONTROL_GROW),
|
||||
width: new BehaviorSubject(DEFAULT_CONTROL_WIDTH),
|
||||
getEditorConfig: () => undefined,
|
||||
} as unknown as ControlGroupApi;
|
||||
|
||||
describe('Data control editor', () => {
|
||||
|
@ -290,4 +291,54 @@ describe('Data control editor', () => {
|
|||
expect(getPressedAttribute(controlEditor, 'create__search')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('control editor config', () => {
|
||||
const getEditorConfig = jest.fn().mockImplementation(() => undefined);
|
||||
|
||||
beforeAll(() => {
|
||||
controlGroupApi.getEditorConfig = getEditorConfig;
|
||||
});
|
||||
|
||||
test('all elements are visible when no editor config', async () => {
|
||||
const controlEditor = await mountComponent({
|
||||
initialState: {
|
||||
fieldName: 'machine.os.raw',
|
||||
},
|
||||
controlType: 'optionsList',
|
||||
controlId: 'testId',
|
||||
initialDefaultPanelTitle: 'OS',
|
||||
});
|
||||
|
||||
const dataViewPicker = controlEditor.queryByTestId('control-editor-data-view-picker');
|
||||
expect(dataViewPicker).toBeInTheDocument();
|
||||
const widthSettings = controlEditor.queryByTestId('control-editor-width-settings');
|
||||
expect(widthSettings).toBeInTheDocument();
|
||||
const customSettings = controlEditor.queryByTestId('control-editor-custom-settings');
|
||||
expect(customSettings).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('can hide elements with the editor config', async () => {
|
||||
getEditorConfig.mockImplementationOnce(() => ({
|
||||
hideDataViewSelector: true,
|
||||
hideWidthSettings: true,
|
||||
hideAdditionalSettings: true,
|
||||
}));
|
||||
|
||||
const controlEditor = await mountComponent({
|
||||
initialState: {
|
||||
fieldName: 'machine.os.raw',
|
||||
},
|
||||
controlType: 'optionsList',
|
||||
controlId: 'testId',
|
||||
initialDefaultPanelTitle: 'OS',
|
||||
});
|
||||
|
||||
const dataViewPicker = controlEditor.queryByTestId('control-editor-data-view-picker');
|
||||
expect(dataViewPicker).not.toBeInTheDocument();
|
||||
const widthSettings = controlEditor.queryByTestId('control-editor-width-settings');
|
||||
expect(widthSettings).not.toBeInTheDocument();
|
||||
const customSettings = controlEditor.queryByTestId('control-editor-custom-settings');
|
||||
expect(customSettings).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,15 +39,15 @@ import {
|
|||
LazyFieldPicker,
|
||||
withSuspense,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { DataControlFieldRegistry } from '../../../types';
|
||||
import { CONTROL_WIDTH_OPTIONS } from '../../..';
|
||||
import { ControlWidth, DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '../../../../common';
|
||||
|
||||
import { DataControlFieldRegistry } from '../../../types';
|
||||
import { ControlWidth, DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '../../../../common';
|
||||
import { getAllControlTypes, getControlFactory } from '../../control_factory_registry';
|
||||
import { ControlGroupApi } from '../../control_group/types';
|
||||
import { DataControlEditorStrings } from './data_control_constants';
|
||||
import { getDataControlFieldRegistry } from './data_control_editor_utils';
|
||||
import { DataControlFactory, DefaultDataControlState, isDataControlFactory } from './types';
|
||||
import { CONTROL_WIDTH_OPTIONS } from '../../../control_group/editor/editor_constants';
|
||||
|
||||
export interface ControlEditorProps<
|
||||
State extends DefaultDataControlState = DefaultDataControlState
|
||||
|
@ -153,9 +153,7 @@ export const DataControlEditor = <State extends DefaultDataControlState = Defaul
|
|||
const [panelTitle, setPanelTitle] = useState<string>(initialState.title ?? defaultPanelTitle);
|
||||
const [selectedControlType, setSelectedControlType] = useState<string | undefined>(controlType);
|
||||
const [controlOptionsValid, setControlOptionsValid] = useState<boolean>(true);
|
||||
|
||||
/** TODO: Make `editorConfig` work when refactoring the `ControlGroupRenderer` */
|
||||
// const editorConfig = controlGroup.getEditorConfig();
|
||||
const editorConfig = useMemo(() => controlGroupApi.getEditorConfig(), [controlGroupApi]);
|
||||
|
||||
// TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709
|
||||
const {
|
||||
|
@ -238,36 +236,37 @@ export const DataControlEditor = <State extends DefaultDataControlState = Defaul
|
|||
title={<h2>{DataControlEditorStrings.manageControl.dataSource.getFormGroupTitle()}</h2>}
|
||||
description={DataControlEditorStrings.manageControl.dataSource.getFormGroupDescription()}
|
||||
>
|
||||
{/* {!editorConfig?.hideDataViewSelector && ( */}
|
||||
<EuiFormRow
|
||||
label={DataControlEditorStrings.manageControl.dataSource.getDataViewTitle()}
|
||||
>
|
||||
{dataViewListError ? (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={DataControlEditorStrings.manageControl.dataSource.getDataViewListErrorTitle()}
|
||||
>
|
||||
<p>{dataViewListError.message}</p>
|
||||
</EuiCallOut>
|
||||
) : (
|
||||
<DataViewPicker
|
||||
dataViews={dataViewListItems}
|
||||
selectedDataViewId={editorState.dataViewId}
|
||||
onChangeDataViewId={(newDataViewId) => {
|
||||
setEditorState({ ...editorState, dataViewId: newDataViewId });
|
||||
setSelectedControlType(undefined);
|
||||
}}
|
||||
trigger={{
|
||||
label:
|
||||
selectedDataView?.getName() ??
|
||||
DataControlEditorStrings.manageControl.dataSource.getSelectDataViewMessage(),
|
||||
}}
|
||||
selectableProps={{ isLoading: dataViewListLoading }}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
{/* )} */}
|
||||
{!editorConfig?.hideDataViewSelector && (
|
||||
<EuiFormRow
|
||||
data-test-subj="control-editor-data-view-picker"
|
||||
label={DataControlEditorStrings.manageControl.dataSource.getDataViewTitle()}
|
||||
>
|
||||
{dataViewListError ? (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={DataControlEditorStrings.manageControl.dataSource.getDataViewListErrorTitle()}
|
||||
>
|
||||
<p>{dataViewListError.message}</p>
|
||||
</EuiCallOut>
|
||||
) : (
|
||||
<DataViewPicker
|
||||
dataViews={dataViewListItems}
|
||||
selectedDataViewId={editorState.dataViewId}
|
||||
onChangeDataViewId={(newDataViewId) => {
|
||||
setEditorState({ ...editorState, dataViewId: newDataViewId });
|
||||
setSelectedControlType(undefined);
|
||||
}}
|
||||
trigger={{
|
||||
label:
|
||||
selectedDataView?.getName() ??
|
||||
DataControlEditorStrings.manageControl.dataSource.getSelectDataViewMessage(),
|
||||
}}
|
||||
selectableProps={{ isLoading: dataViewListLoading }}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
<EuiFormRow label={DataControlEditorStrings.manageControl.dataSource.getFieldTitle()}>
|
||||
{fieldListError ? (
|
||||
|
@ -281,9 +280,8 @@ export const DataControlEditor = <State extends DefaultDataControlState = Defaul
|
|||
) : (
|
||||
<FieldPicker
|
||||
filterPredicate={(field: DataViewField) => {
|
||||
/** TODO: Make `fieldFilterPredicate` work when refactoring the `ControlGroupRenderer` */
|
||||
// const customPredicate = controlGroup.fieldFilterPredicate?.(field) ?? true;
|
||||
return Boolean(fieldRegistry?.[field.name]);
|
||||
const customPredicate = editorConfig?.fieldFilterPredicate?.(field) ?? true;
|
||||
return Boolean(fieldRegistry?.[field.name]) && customPredicate;
|
||||
}}
|
||||
selectedFieldName={editorState.fieldName}
|
||||
dataView={selectedDataView}
|
||||
|
@ -356,33 +354,34 @@ export const DataControlEditor = <State extends DefaultDataControlState = Defaul
|
|||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{/* {!editorConfig?.hideWidthSettings && ( */}
|
||||
<EuiFormRow
|
||||
label={DataControlEditorStrings.manageControl.displaySettings.getWidthInputTitle()}
|
||||
>
|
||||
<div>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={DataControlEditorStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
idSelected={editorState.width ?? DEFAULT_CONTROL_WIDTH}
|
||||
onChange={(newWidth: string) =>
|
||||
setEditorState({ ...editorState, width: newWidth as ControlWidth })
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={DataControlEditorStrings.manageControl.displaySettings.getGrowSwitchTitle()}
|
||||
color="primary"
|
||||
checked={editorState.grow ?? DEFAULT_CONTROL_GROW}
|
||||
onChange={() => setEditorState({ ...editorState, grow: !editorState.grow })}
|
||||
data-test-subj="control-editor-grow-switch"
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
{/* )} */}
|
||||
{!editorConfig?.hideWidthSettings && (
|
||||
<EuiFormRow
|
||||
data-test-subj="control-editor-width-settings"
|
||||
label={DataControlEditorStrings.manageControl.displaySettings.getWidthInputTitle()}
|
||||
>
|
||||
<div>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={DataControlEditorStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
options={CONTROL_WIDTH_OPTIONS}
|
||||
idSelected={editorState.width ?? DEFAULT_CONTROL_WIDTH}
|
||||
onChange={(newWidth: string) =>
|
||||
setEditorState({ ...editorState, width: newWidth as ControlWidth })
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={DataControlEditorStrings.manageControl.displaySettings.getGrowSwitchTitle()}
|
||||
color="primary"
|
||||
checked={editorState.grow ?? DEFAULT_CONTROL_GROW}
|
||||
onChange={() => setEditorState({ ...editorState, grow: !editorState.grow })}
|
||||
data-test-subj="control-editor-grow-switch"
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiDescribedFormGroup>
|
||||
{CustomSettingsComponent}
|
||||
{!editorConfig?.hideAdditionalSettings && CustomSettingsComponent}
|
||||
{controlId && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
|
||||
import { ControlEmbeddable, ControlFactory, ControlInput, ControlOutput } from '../..';
|
||||
import { ControlsServiceType, ControlTypeRegistry } from './types';
|
||||
import { ControlEmbeddable, ControlFactory, ControlInput, ControlOutput } from '../../types';
|
||||
|
||||
export type ControlsServiceFactory = PluginServiceFactory<ControlsServiceType>;
|
||||
export const controlsServiceFactory = () => getStubControlsService();
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
import { EmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
|
||||
import { ControlEmbeddable, ControlFactory, ControlInput, ControlOutput } from '../..';
|
||||
import { ControlsServiceType, ControlTypeRegistry } from './types';
|
||||
import { ControlEmbeddable, ControlFactory, ControlInput, ControlOutput } from '../../types';
|
||||
|
||||
export type ControlsServiceFactory = PluginServiceFactory<ControlsServiceType>;
|
||||
export const controlsServiceFactory = () => controlsService;
|
||||
|
|
|
@ -44,7 +44,8 @@
|
|||
"@kbn/content-management-utils",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/presentation-panel-plugin"
|
||||
"@kbn/presentation-panel-plugin",
|
||||
"@kbn/shared-ux-utility"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -267,6 +267,7 @@ export const legacyEmbeddableToApi = (
|
|||
|
||||
dataViews,
|
||||
disabledActionIds,
|
||||
setDisabledActionIds: (ids) => disabledActionIds.next(ids),
|
||||
|
||||
panelTitle,
|
||||
setPanelTitle,
|
||||
|
|
|
@ -138,6 +138,7 @@ export abstract class Embeddable<
|
|||
defaultPanelDescription: this.defaultPanelDescription,
|
||||
canLinkToLibrary: this.canLinkToLibrary,
|
||||
disabledActionIds: this.disabledActionIds,
|
||||
setDisabledActionIds: this.setDisabledActionIds,
|
||||
unlinkFromLibrary: this.unlinkFromLibrary,
|
||||
setHidePanelTitle: this.setHidePanelTitle,
|
||||
defaultPanelTitle: this.defaultPanelTitle,
|
||||
|
@ -180,6 +181,7 @@ export abstract class Embeddable<
|
|||
public panelDescription: LegacyEmbeddableAPI['panelDescription'];
|
||||
public defaultPanelDescription: LegacyEmbeddableAPI['defaultPanelDescription'];
|
||||
public disabledActionIds: LegacyEmbeddableAPI['disabledActionIds'];
|
||||
public setDisabledActionIds: LegacyEmbeddableAPI['setDisabledActionIds'];
|
||||
public unlinkFromLibrary: LegacyEmbeddableAPI['unlinkFromLibrary'];
|
||||
public setTimeRange: LegacyEmbeddableAPI['setTimeRange'];
|
||||
public defaultPanelTitle: LegacyEmbeddableAPI['defaultPanelTitle'];
|
||||
|
|
|
@ -5,19 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Observable, switchMap, tap } from 'rxjs';
|
||||
|
||||
import {
|
||||
type ControlGroupInput,
|
||||
type ControlGroupInputBuilder,
|
||||
type AwaitingControlGroupAPI,
|
||||
ControlGroupRenderer,
|
||||
ControlGroupRendererApi,
|
||||
type ControlGroupRuntimeState,
|
||||
type ControlGroupStateBuilder,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { first } from 'rxjs';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
|
||||
import { Timeslice } from '../../../common/descriptor_types';
|
||||
|
||||
export interface Props {
|
||||
|
@ -26,54 +24,23 @@ export interface Props {
|
|||
waitForTimesliceToLoad$: Observable<void>;
|
||||
}
|
||||
|
||||
export class Timeslider extends Component<Props, {}> {
|
||||
private _isMounted: boolean = false;
|
||||
private readonly _subscriptions = new Subscription();
|
||||
export function Timeslider({ setTimeslice, timeRange, waitForTimesliceToLoad$ }: Props) {
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [api, setApi] = useState<ControlGroupRendererApi | undefined>();
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
_getCreationOptions = async (
|
||||
initialInput: Partial<ControlGroupInput>,
|
||||
builder: ControlGroupInputBuilder
|
||||
) => {
|
||||
builder.addTimeSliderControl(initialInput);
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.VIEW,
|
||||
timeRange: this.props.timeRange,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
_onLoadComplete = (controlGroup: AwaitingControlGroupAPI) => {
|
||||
if (!this._isMounted || !controlGroup) {
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscriptions.add(
|
||||
controlGroup
|
||||
.getOutput$()
|
||||
.pipe(
|
||||
distinctUntilChanged(({ timeslice: timesliceA }, { timeslice: timesliceB }) =>
|
||||
_.isEqual(timesliceA, timesliceB)
|
||||
)
|
||||
)
|
||||
.subscribe(({ timeslice }) => {
|
||||
// use waitForTimesliceToLoad$ observable to wait until next frame loaded
|
||||
// .pipe(first()) waits until the first value is emitted from an observable and then automatically unsubscribes
|
||||
this.props.waitForTimesliceToLoad$.pipe(first()).subscribe(() => {
|
||||
controlGroup.anyControlOutputConsumerLoading$.next(false);
|
||||
});
|
||||
|
||||
this.props.setTimeslice(
|
||||
let canceled = false;
|
||||
const subscription = api.timeslice$
|
||||
.pipe(
|
||||
tap(() => {
|
||||
if (!canceled) setDataLoading(true);
|
||||
}),
|
||||
switchMap((timeslice) => {
|
||||
setTimeslice(
|
||||
timeslice === undefined
|
||||
? undefined
|
||||
: {
|
||||
|
@ -81,19 +48,37 @@ export class Timeslider extends Component<Props, {}> {
|
|||
to: timeslice[1],
|
||||
}
|
||||
);
|
||||
return waitForTimesliceToLoad$;
|
||||
})
|
||||
);
|
||||
};
|
||||
)
|
||||
.subscribe(() => {
|
||||
if (!canceled) setDataLoading(false);
|
||||
});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mapTimeslider mapTimeslider--animation">
|
||||
<ControlGroupRenderer
|
||||
ref={this._onLoadComplete}
|
||||
getCreationOptions={this._getCreationOptions}
|
||||
timeRange={this.props.timeRange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
canceled = true;
|
||||
};
|
||||
}, [api, setTimeslice, waitForTimesliceToLoad$]);
|
||||
|
||||
return (
|
||||
<div className="mapTimeslider mapTimeslider--animation">
|
||||
<ControlGroupRenderer
|
||||
onApiAvailable={(nextApi: ControlGroupRendererApi) => {
|
||||
setApi(nextApi);
|
||||
}}
|
||||
dataLoading={dataLoading}
|
||||
getCreationOptions={async (
|
||||
initialState: Partial<ControlGroupRuntimeState>,
|
||||
builder: ControlGroupStateBuilder
|
||||
) => {
|
||||
builder.addTimeSliderControl(initialState);
|
||||
return {
|
||||
initialState,
|
||||
};
|
||||
}}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,14 +7,11 @@
|
|||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
type ControlEmbeddable,
|
||||
ControlGroupAPI,
|
||||
ControlGroupRenderer,
|
||||
type ControlInput,
|
||||
type ControlOutput,
|
||||
type ControlGroupInput,
|
||||
ControlGroupRendererApi,
|
||||
DataControlApi,
|
||||
ControlGroupRuntimeState,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
@ -41,46 +38,42 @@ export const ControlsContent: React.FC<Props> = ({
|
|||
const subscriptions = useRef<Subscription>(new Subscription());
|
||||
|
||||
const getInitialInput = useCallback(
|
||||
(loadedDataView: DataView) => async () => {
|
||||
const initialInput: Partial<ControlGroupInput> = {
|
||||
id: loadedDataView.id,
|
||||
viewMode: ViewMode.VIEW,
|
||||
() => async () => {
|
||||
const initialInput: Partial<ControlGroupRuntimeState> = {
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
controlStyle: 'oneLine',
|
||||
defaultControlWidth: 'small',
|
||||
panels: controlPanels,
|
||||
filters,
|
||||
query,
|
||||
timeRange,
|
||||
labelPosition: 'oneLine',
|
||||
initialChildControlState: controlPanels,
|
||||
};
|
||||
|
||||
return { initialInput };
|
||||
return { initialState: initialInput };
|
||||
},
|
||||
[controlPanels, filters, query, timeRange]
|
||||
[controlPanels]
|
||||
);
|
||||
|
||||
const loadCompleteHandler = useCallback(
|
||||
(controlGroup: ControlGroupAPI) => {
|
||||
(controlGroup: ControlGroupRendererApi) => {
|
||||
if (!controlGroup) return;
|
||||
|
||||
controlGroup.untilAllChildrenReady().then(() => {
|
||||
controlGroup.getChildIds().map((id) => {
|
||||
const embeddable =
|
||||
controlGroup.getChild<ControlEmbeddable<ControlInput, ControlOutput>>(id);
|
||||
embeddable.renderPrepend = () => (
|
||||
<ControlTitle title={embeddable.getTitle()} embeddableId={id} />
|
||||
controlGroup.untilInitialized().then(() => {
|
||||
const children = controlGroup.children$.getValue();
|
||||
Object.keys(children).map((childId) => {
|
||||
const child = children[childId] as DataControlApi;
|
||||
child.CustomPrependComponent = () => (
|
||||
<ControlTitle title={child.panelTitle.getValue()} embeddableId={childId} />
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
subscriptions.current.add(
|
||||
controlGroup.onFiltersPublished$.subscribe((newFilters) => {
|
||||
controlGroup.filters$.subscribe((newFilters = []) => {
|
||||
onFiltersChange(newFilters);
|
||||
})
|
||||
);
|
||||
|
||||
subscriptions.current.add(
|
||||
controlGroup.getInput$().subscribe(({ panels }) => setControlPanels(panels))
|
||||
controlGroup
|
||||
.getInput$()
|
||||
.subscribe(({ initialChildControlState }) => setControlPanels(initialChildControlState))
|
||||
);
|
||||
},
|
||||
[onFiltersChange, setControlPanels]
|
||||
|
@ -100,8 +93,8 @@ export const ControlsContent: React.FC<Props> = ({
|
|||
return (
|
||||
<ControlGroupContainer>
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={getInitialInput(dataView)}
|
||||
ref={loadCompleteHandler}
|
||||
getCreationOptions={getInitialInput()}
|
||||
onApiAvailable={loadCompleteHandler}
|
||||
timeRange={timeRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
|
|
|
@ -28,33 +28,24 @@ const controlPanelConfigs: ControlPanels = {
|
|||
width: 'medium',
|
||||
grow: false,
|
||||
type: 'optionsListControl',
|
||||
explicitInput: {
|
||||
id: availableControlsPanels.HOST_OS_NAME,
|
||||
fieldName: availableControlsPanels.HOST_OS_NAME,
|
||||
title: 'Operating System',
|
||||
},
|
||||
fieldName: availableControlsPanels.HOST_OS_NAME,
|
||||
title: 'Operating System',
|
||||
},
|
||||
[availableControlsPanels.CLOUD_PROVIDER]: {
|
||||
order: 1,
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
type: 'optionsListControl',
|
||||
explicitInput: {
|
||||
id: availableControlsPanels.CLOUD_PROVIDER,
|
||||
fieldName: availableControlsPanels.CLOUD_PROVIDER,
|
||||
title: 'Cloud Provider',
|
||||
},
|
||||
fieldName: availableControlsPanels.CLOUD_PROVIDER,
|
||||
title: 'Cloud Provider',
|
||||
},
|
||||
[availableControlsPanels.SERVICE_NAME]: {
|
||||
order: 2,
|
||||
width: 'medium',
|
||||
grow: false,
|
||||
type: 'optionsListControl',
|
||||
explicitInput: {
|
||||
id: availableControlsPanels.SERVICE_NAME,
|
||||
fieldName: availableControlsPanels.SERVICE_NAME,
|
||||
title: 'Service Name',
|
||||
},
|
||||
fieldName: availableControlsPanels.SERVICE_NAME,
|
||||
title: 'Service Name',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -108,7 +99,7 @@ const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId:
|
|||
...acc,
|
||||
[key]: {
|
||||
...controlPanelConfig,
|
||||
explicitInput: { ...controlPanelConfig.explicitInput, dataViewId },
|
||||
dataViewId,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
@ -116,11 +107,10 @@ const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId:
|
|||
|
||||
const cleanControlPanels = (controlPanels: ControlPanels) => {
|
||||
return Object.entries(controlPanels).reduce((acc, [key, controlPanelConfig]) => {
|
||||
const { explicitInput } = controlPanelConfig;
|
||||
const { dataViewId, ...rest } = explicitInput;
|
||||
const { dataViewId, ...rest } = controlPanelConfig;
|
||||
return {
|
||||
...acc,
|
||||
[key]: { ...controlPanelConfig, explicitInput: rest },
|
||||
[key]: rest,
|
||||
};
|
||||
}, {});
|
||||
};
|
||||
|
@ -140,21 +130,20 @@ const mergeDefaultPanelsWithUrlConfig = (dataView: DataView, urlPanels: ControlP
|
|||
);
|
||||
};
|
||||
|
||||
const PanelRT = rt.type({
|
||||
order: rt.number,
|
||||
width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]),
|
||||
grow: rt.boolean,
|
||||
type: rt.string,
|
||||
explicitInput: rt.intersection([
|
||||
rt.type({ id: rt.string }),
|
||||
rt.partial({
|
||||
dataViewId: rt.string,
|
||||
fieldName: rt.string,
|
||||
title: rt.union([rt.string, rt.undefined]),
|
||||
selectedOptions: rt.array(rt.string),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const PanelRT = rt.intersection([
|
||||
rt.type({
|
||||
order: rt.number,
|
||||
type: rt.string,
|
||||
}),
|
||||
rt.partial({
|
||||
width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]),
|
||||
grow: rt.boolean,
|
||||
dataViewId: rt.string,
|
||||
fieldName: rt.string,
|
||||
title: rt.union([rt.string, rt.undefined]),
|
||||
selectedOptions: rt.array(rt.string),
|
||||
}),
|
||||
]);
|
||||
|
||||
const ControlPanelRT = rt.record(rt.string, PanelRT);
|
||||
|
||||
|
|
|
@ -19,11 +19,8 @@ export const controlPanelConfigs: ControlPanels = {
|
|||
width: 'medium',
|
||||
grow: false,
|
||||
type: 'optionsListControl',
|
||||
explicitInput: {
|
||||
id: availableControlsPanels.NAMESPACE,
|
||||
fieldName: availableControlsPanels.NAMESPACE,
|
||||
title: 'Namespace',
|
||||
},
|
||||
fieldName: availableControlsPanels.NAMESPACE,
|
||||
title: 'Namespace',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -6,23 +6,22 @@
|
|||
*/
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
const PanelRT = rt.type({
|
||||
order: rt.number,
|
||||
width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]),
|
||||
grow: rt.boolean,
|
||||
type: rt.string,
|
||||
explicitInput: rt.intersection([
|
||||
rt.type({ id: rt.string }),
|
||||
rt.partial({
|
||||
dataViewId: rt.string,
|
||||
exclude: rt.boolean,
|
||||
existsSelected: rt.boolean,
|
||||
fieldName: rt.string,
|
||||
selectedOptions: rt.array(rt.string),
|
||||
title: rt.union([rt.string, rt.undefined]),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
const PanelRT = rt.intersection([
|
||||
rt.type({
|
||||
order: rt.number,
|
||||
type: rt.string,
|
||||
}),
|
||||
rt.partial({
|
||||
width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]),
|
||||
grow: rt.boolean,
|
||||
dataViewId: rt.string,
|
||||
fieldName: rt.string,
|
||||
exclude: rt.boolean,
|
||||
existsSelected: rt.boolean,
|
||||
title: rt.union([rt.string, rt.undefined]),
|
||||
selectedOptions: rt.array(rt.string),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const ControlPanelRT = rt.record(rt.string, PanelRT);
|
||||
|
||||
|
|
|
@ -7,10 +7,7 @@
|
|||
"id": "logsExplorer",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"logsExplorer"
|
||||
],
|
||||
"configPath": ["xpack", "logsExplorer"],
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"dataViews",
|
||||
|
@ -20,12 +17,10 @@
|
|||
"share",
|
||||
"unifiedSearch",
|
||||
"unifiedDocViewer",
|
||||
"discoverShared",
|
||||
"discoverShared"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["controls","embeddable","fleet", "kibanaReact", "kibanaUtils"],
|
||||
"extraPublicDirs": [
|
||||
"common",
|
||||
]
|
||||
"requiredBundles": ["controls", "fleet", "kibanaReact", "kibanaUtils"],
|
||||
"extraPublicDirs": ["common"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,12 +83,12 @@ const getPublicControlsStateFromControlPanels = (
|
|||
const getOptionsListPublicControlStateFromControlPanel = (
|
||||
optionsListControlPanel: ControlPanels[string]
|
||||
): OptionsListControl => ({
|
||||
mode: optionsListControlPanel.explicitInput.exclude ? 'exclude' : 'include',
|
||||
selection: optionsListControlPanel.explicitInput.existsSelected
|
||||
mode: optionsListControlPanel.exclude ? 'exclude' : 'include',
|
||||
selection: optionsListControlPanel.existsSelected
|
||||
? { type: 'exists' }
|
||||
: {
|
||||
type: 'options',
|
||||
selectedOptions: optionsListControlPanel.explicitInput.selectedOptions ?? [],
|
||||
selectedOptions: optionsListControlPanel.selectedOptions ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -121,16 +121,13 @@ const getControlPanelFromOptionsListPublicControlState = (
|
|||
|
||||
return {
|
||||
...defaultControlPanelConfig,
|
||||
explicitInput: {
|
||||
...defaultControlPanelConfig.explicitInput,
|
||||
exclude: publicControlState.mode === 'exclude',
|
||||
...(publicControlState.selection.type === 'exists'
|
||||
? {
|
||||
existsSelected: true,
|
||||
}
|
||||
: {
|
||||
selectedOptions: publicControlState.selection.selectedOptions,
|
||||
}),
|
||||
},
|
||||
exclude: publicControlState.mode === 'exclude',
|
||||
...(publicControlState.selection.type === 'exists'
|
||||
? {
|
||||
existsSelected: true,
|
||||
}
|
||||
: {
|
||||
selectedOptions: publicControlState.selection.selectedOptions,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ const CustomDataSourceFilters = ({
|
|||
logsExplorerControllerStateService,
|
||||
data,
|
||||
}: CustomDataSourceFiltersProps) => {
|
||||
const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels(
|
||||
const { getInitialState, setControlGroupAPI, query, filters, timeRange } = useControlPanels(
|
||||
logsExplorerControllerStateService,
|
||||
data
|
||||
);
|
||||
|
@ -30,8 +30,8 @@ const CustomDataSourceFilters = ({
|
|||
return (
|
||||
<div data-test-subj={DATA_SOURCE_FILTERS_CUSTOMIZATION_ID}>
|
||||
<ControlGroupRenderer
|
||||
ref={setControlGroupAPI}
|
||||
getCreationOptions={getInitialInput}
|
||||
onApiAvailable={setControlGroupAPI}
|
||||
getCreationOptions={getInitialState}
|
||||
query={query as Query}
|
||||
filters={filters ?? []}
|
||||
timeRange={timeRange}
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupRuntimeState, ControlGroupRendererApi } from '@kbn/controls-plugin/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { Query, TimeRange } from '@kbn/es-query';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { useCallback } from 'react';
|
||||
|
@ -27,31 +25,28 @@ export const useControlPanels = (
|
|||
return state.context.controlPanels;
|
||||
});
|
||||
|
||||
const getInitialInput = useCallback(
|
||||
async (initialInput: Partial<ControlGroupInput>) => {
|
||||
const input: Partial<ControlGroupInput> = {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.VIEW,
|
||||
panels: controlPanels ?? initialInput.panels,
|
||||
filters: filters ?? [],
|
||||
query: query as Query,
|
||||
timeRange: { from: fromDate!, to: toDate! },
|
||||
const getInitialState = useCallback(
|
||||
async (initialState: Partial<ControlGroupRuntimeState>) => {
|
||||
const state: Partial<ControlGroupRuntimeState> = {
|
||||
...initialState,
|
||||
initialChildControlState: controlPanels ?? initialState.initialChildControlState,
|
||||
};
|
||||
|
||||
return { initialInput: input };
|
||||
return { initialState: state };
|
||||
},
|
||||
[controlPanels, filters, fromDate, query, toDate]
|
||||
[controlPanels]
|
||||
);
|
||||
|
||||
const setControlGroupAPI = useCallback(
|
||||
(controlGroupAPI: ControlGroupAPI) => {
|
||||
logsExplorerControllerStateService.send({
|
||||
type: 'INITIALIZE_CONTROL_GROUP_API',
|
||||
controlGroupAPI,
|
||||
});
|
||||
(controlGroupAPI: ControlGroupRendererApi | undefined) => {
|
||||
if (controlGroupAPI)
|
||||
logsExplorerControllerStateService.send({
|
||||
type: 'INITIALIZE_CONTROL_GROUP_API',
|
||||
controlGroupAPI,
|
||||
});
|
||||
},
|
||||
[logsExplorerControllerStateService]
|
||||
);
|
||||
|
||||
return { getInitialInput, setControlGroupAPI, query, filters, timeRange };
|
||||
return { getInitialState, setControlGroupAPI, query, filters, timeRange };
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { mapValues, pick } from 'lodash';
|
||||
import { InvokeCreator } from 'xstate';
|
||||
import {
|
||||
|
@ -35,22 +34,13 @@ export const subscribeControlGroup =
|
|||
if (!('discoverStateContainer' in context)) return;
|
||||
const { discoverStateContainer } = context;
|
||||
|
||||
const filtersSubscription = context.controlGroupAPI.onFiltersPublished$.subscribe(
|
||||
(newFilters) => {
|
||||
discoverStateContainer.internalState.transitions.setCustomFilters(newFilters);
|
||||
discoverStateContainer.actions.fetchData();
|
||||
}
|
||||
);
|
||||
|
||||
const inputSubscription = context.controlGroupAPI.getInput$().subscribe(({ panels }) => {
|
||||
if (!deepEqual(panels, context.controlPanels)) {
|
||||
send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels });
|
||||
}
|
||||
const filtersSubscription = context.controlGroupAPI.filters$.subscribe((newFilters = []) => {
|
||||
discoverStateContainer.internalState.transitions.setCustomFilters(newFilters);
|
||||
discoverStateContainer.actions.fetchData();
|
||||
});
|
||||
|
||||
return () => {
|
||||
filtersSubscription.unsubscribe();
|
||||
inputSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -71,7 +61,7 @@ export const updateControlPanels =
|
|||
newControlPanels!
|
||||
);
|
||||
|
||||
context.controlGroupAPI.updateInput({ panels: controlPanelsWithId });
|
||||
context.controlGroupAPI.updateInput({ initialChildControlState: controlPanelsWithId });
|
||||
|
||||
return controlPanelsWithId;
|
||||
};
|
||||
|
@ -114,7 +104,7 @@ export const getVisibleControlPanelsConfig = (dataView?: DataView) => {
|
|||
const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId: string = '') => {
|
||||
return mapValues(controlPanels, (controlPanelConfig) => ({
|
||||
...controlPanelConfig,
|
||||
explicitInput: { ...controlPanelConfig.explicitInput, dataViewId },
|
||||
dataViewId,
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ControlGroupAPI } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupRendererApi } from '@kbn/controls-plugin/public';
|
||||
import { QueryState, RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
|
||||
import type {
|
||||
DiscoverAppState,
|
||||
|
@ -30,7 +30,7 @@ export interface WithAllSelection {
|
|||
allSelection: AllDatasetSelection;
|
||||
}
|
||||
export interface WithControlPanelGroupAPI {
|
||||
controlGroupAPI: ControlGroupAPI;
|
||||
controlGroupAPI: ControlGroupRendererApi;
|
||||
}
|
||||
|
||||
export interface WithControlPanels {
|
||||
|
@ -198,7 +198,7 @@ export type LogsExplorerControllerEvent =
|
|||
}
|
||||
| {
|
||||
type: 'INITIALIZE_CONTROL_GROUP_API';
|
||||
controlGroupAPI: ControlGroupAPI | undefined;
|
||||
controlGroupAPI: ControlGroupRendererApi | undefined;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_CONTROL_PANELS';
|
||||
|
@ -222,5 +222,5 @@ export type LogsExplorerControllerEvent =
|
|||
}
|
||||
| DoneInvokeEvent<DataSourceSelection>
|
||||
| DoneInvokeEvent<ControlPanels>
|
||||
| DoneInvokeEvent<ControlGroupAPI>
|
||||
| DoneInvokeEvent<ControlGroupRendererApi>
|
||||
| DoneInvokeEvent<Error>;
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
"@kbn/deeplinks-observability",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/fleet-plugin",
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { skip } from 'rxjs';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
import { ControlGroupRenderer, ControlGroupRendererApi } from '@kbn/controls-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import styled from 'styled-components';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
@ -27,13 +27,13 @@ export function QuickFilters({
|
|||
initialState: { tagsFilter, statusFilter },
|
||||
onStateChange,
|
||||
}: Props) {
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<AwaitingControlGroupAPI>();
|
||||
const [controlGroupAPI, setControlGroupAPI] = useState<ControlGroupRendererApi | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!controlGroupAPI) {
|
||||
return;
|
||||
}
|
||||
const subscription = controlGroupAPI.onFiltersPublished$.subscribe((newFilters) => {
|
||||
const subscription = controlGroupAPI.filters$.pipe(skip(1)).subscribe((newFilters = []) => {
|
||||
if (newFilters.length === 0) {
|
||||
onStateChange({ tagsFilter: undefined, statusFilter: undefined });
|
||||
} else {
|
||||
|
@ -55,39 +55,42 @@ export function QuickFilters({
|
|||
return (
|
||||
<Container>
|
||||
<ControlGroupRenderer
|
||||
getCreationOptions={async (initialInput, builder) => {
|
||||
await builder.addOptionsListControl(initialInput, {
|
||||
dataViewId: dataView.id!,
|
||||
fieldName: 'status',
|
||||
width: 'small',
|
||||
grow: true,
|
||||
title: STATUS_LABEL,
|
||||
controlId: 'slo-status-filter',
|
||||
exclude: statusFilter?.meta?.negate,
|
||||
selectedOptions: getSelectedOptions(statusFilter),
|
||||
existsSelected: Boolean(statusFilter?.query?.exists?.field === 'status'),
|
||||
placeholder: ALL_LABEL,
|
||||
});
|
||||
await builder.addOptionsListControl(initialInput, {
|
||||
dataViewId: dataView.id!,
|
||||
title: TAGS_LABEL,
|
||||
fieldName: 'slo.tags',
|
||||
width: 'small',
|
||||
grow: false,
|
||||
controlId: 'slo-tags-filter',
|
||||
selectedOptions: getSelectedOptions(tagsFilter),
|
||||
exclude: statusFilter?.meta?.negate,
|
||||
existsSelected: Boolean(tagsFilter?.query?.exists?.field === 'slo.tags'),
|
||||
placeholder: ALL_LABEL,
|
||||
});
|
||||
return {
|
||||
initialInput: {
|
||||
...initialInput,
|
||||
viewMode: ViewMode.VIEW,
|
||||
onApiAvailable={setControlGroupAPI}
|
||||
getCreationOptions={async (initialState, builder) => {
|
||||
builder.addOptionsListControl(
|
||||
initialState,
|
||||
{
|
||||
dataViewId: dataView.id!,
|
||||
fieldName: 'status',
|
||||
width: 'small',
|
||||
grow: true,
|
||||
title: STATUS_LABEL,
|
||||
exclude: statusFilter?.meta?.negate,
|
||||
selectedOptions: getSelectedOptions(statusFilter),
|
||||
existsSelected: Boolean(statusFilter?.query?.exists?.field === 'status'),
|
||||
placeholder: ALL_LABEL,
|
||||
},
|
||||
'slo-status-filter'
|
||||
);
|
||||
builder.addOptionsListControl(
|
||||
initialState,
|
||||
{
|
||||
dataViewId: dataView.id!,
|
||||
title: TAGS_LABEL,
|
||||
fieldName: 'slo.tags',
|
||||
width: 'small',
|
||||
grow: false,
|
||||
selectedOptions: getSelectedOptions(tagsFilter),
|
||||
exclude: statusFilter?.meta?.negate,
|
||||
existsSelected: Boolean(tagsFilter?.query?.exists?.field === 'slo.tags'),
|
||||
placeholder: ALL_LABEL,
|
||||
},
|
||||
'slo-tags-filter'
|
||||
);
|
||||
return {
|
||||
initialState,
|
||||
};
|
||||
}}
|
||||
ref={setControlGroupAPI}
|
||||
timeRange={{ from: 'now-24h', to: 'now' }}
|
||||
/>
|
||||
</Container>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue