Skip to content

Commit

Permalink
Control group state diffing (elastic#189128)
Browse files Browse the repository at this point in the history
<img width="800" alt="Screenshot 2024-07-29 at 3 48 24 PM"
src="https://github.com/user-attachments/assets/d1196ed3-f590-4415-8c32-8f39cc64a2a8">

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 2, 2024
1 parent 718f6c3 commit cf1222f
Show file tree
Hide file tree
Showing 27 changed files with 935 additions and 582 deletions.
3 changes: 1 addition & 2 deletions examples/controls_example/public/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ import {
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';

import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { ControlsExampleStartDeps } from '../plugin';
import { ControlGroupRendererExamples } from './control_group_renderer_examples';
import { ReactControlExample } from './react_control_example';
import { ReactControlExample } from './react_control_example/react_control_example';

const CONTROLS_AS_A_BUILDING_BLOCK = 'controls_as_a_building_block';
const CONTROLS_REFACTOR_TEST = 'controls_refactor_test';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@
*/

import React, { useEffect, useMemo, useState } from 'react';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';

import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonGroup,
EuiCallOut,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiSuperDatePicker,
EuiToolTip,
OnTimeChangeProps,
} from '@elastic/eui';
import {
Expand All @@ -39,12 +42,19 @@ import {
} from '@kbn/presentation-publishing';
import { toMountPoint } from '@kbn/react-kibana-mount';

import { ControlGroupApi } from '../react_controls/control_group/types';
import { OPTIONS_LIST_CONTROL_TYPE } from '../react_controls/data_controls/options_list_control/constants';
import { RANGE_SLIDER_CONTROL_TYPE } from '../react_controls/data_controls/range_slider/types';
import { SEARCH_CONTROL_TYPE } from '../react_controls/data_controls/search_control/types';
import { TIMESLIDER_CONTROL_TYPE } from '../react_controls/timeslider_control/types';
import { openDataControlEditor } from '../react_controls/data_controls/open_data_control_editor';
import {
clearControlGroupSerializedState,
getControlGroupSerializedState,
setControlGroupSerializedState,
WEB_LOGS_DATA_VIEW_ID,
} from './serialized_control_group_state';
import {
clearControlGroupRuntimeState,
getControlGroupRuntimeState,
setControlGroupRuntimeState,
} from './runtime_control_group_state';
import { ControlGroupApi } from '../../react_controls/control_group/types';
import { openDataControlEditor } from '../../react_controls/data_controls/open_data_control_editor';

const toggleViewButtons = [
{
Expand All @@ -59,67 +69,6 @@ const toggleViewButtons = [
},
];

const optionsListId = 'optionsList1';
const searchControlId = 'searchControl1';
const rangeSliderControlId = 'rangeSliderControl1';
const timesliderControlId = 'timesliderControl1';
const controlGroupPanels = {
[searchControlId]: {
type: SEARCH_CONTROL_TYPE,
order: 3,
grow: true,
width: 'medium',
explicitInput: {
id: searchControlId,
fieldName: 'message',
title: 'Message',
grow: true,
width: 'medium',
enhancements: {},
},
},
[rangeSliderControlId]: {
type: RANGE_SLIDER_CONTROL_TYPE,
order: 1,
grow: true,
width: 'medium',
explicitInput: {
id: rangeSliderControlId,
fieldName: 'bytes',
title: 'Bytes',
grow: true,
width: 'medium',
enhancements: {},
},
},
[timesliderControlId]: {
type: TIMESLIDER_CONTROL_TYPE,
order: 4,
grow: true,
width: 'medium',
explicitInput: {
id: timesliderControlId,
enhancements: {},
},
},
[optionsListId]: {
type: OPTIONS_LIST_CONTROL_TYPE,
order: 2,
grow: true,
width: 'medium',
explicitInput: {
id: searchControlId,
fieldName: 'agent.keyword',
title: 'Agent',
grow: true,
width: 'medium',
enhancements: {},
},
},
};

const WEB_LOGS_DATA_VIEW_ID = '90943e30-9a47-11e8-b64d-95841ca0b247';

export const ReactControlExample = ({
core,
dataViews: dataViewsService,
Expand Down Expand Up @@ -151,6 +100,9 @@ export const ReactControlExample = ({
const viewMode$ = useMemo(() => {
return new BehaviorSubject<ViewModeType>(ViewMode.EDIT as ViewModeType);
}, []);
const saveNotification$ = useMemo(() => {
return new Subject<void>();
}, []);
const [dataLoading, timeRange, viewMode] = useBatchedPublishingSubjects(
dataLoading$,
timeRange$,
Expand Down Expand Up @@ -188,6 +140,7 @@ export const ReactControlExample = ({
return Promise.resolve(undefined);
},
lastUsedDataViewId: new BehaviorSubject<string>(WEB_LOGS_DATA_VIEW_ID),
saveNotification$,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down Expand Up @@ -277,16 +230,57 @@ export const ReactControlExample = ({
};
}, [controlGroupFilters$, filters$, unifiedSearchFilters$]);

const [unsavedChanges, setUnsavedChanges] = useState<string | undefined>(undefined);
useEffect(() => {
if (!controlGroupApi) {
return;
}
const subscription = controlGroupApi.unsavedChanges.subscribe((nextUnsavedChanges) => {
if (!nextUnsavedChanges) {
clearControlGroupRuntimeState();
setUnsavedChanges(undefined);
return;
}

setControlGroupRuntimeState(nextUnsavedChanges);

// JSON.stringify removes keys where value is `undefined`
// switch `undefined` to `null` to see when value has been cleared
const replacer = (key: unknown, value: unknown) =>
typeof value === 'undefined' ? null : value;
setUnsavedChanges(JSON.stringify(nextUnsavedChanges, replacer, ' '));
});

return () => {
subscription.unsubscribe();
};
}, [controlGroupApi]);

return (
<>
{dataViewNotFound && (
<>
<EuiCallOut color="warning" iconType="warning">
<p>{`Install "Sample web logs" to run example`}</p>
</EuiCallOut>
<EuiSpacer size="m" />
</>
<EuiCallOut color="warning" iconType="warning">
<p>{`Install "Sample web logs" to run example`}</p>
</EuiCallOut>
)}
{!dataViewNotFound && (
<EuiCallOut title="This example uses session storage to persist saved state and unsaved changes">
<EuiButton
color="accent"
size="s"
onClick={() => {
clearControlGroupSerializedState();
clearControlGroupRuntimeState();
window.location.reload();
}}
>
Reset example
</EuiButton>
</EuiCallOut>
)}

<EuiSpacer size="m" />

<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
Expand Down Expand Up @@ -358,6 +352,37 @@ export const ReactControlExample = ({
}}
/>
</EuiFlexItem>
{unsavedChanges !== undefined && viewMode === 'edit' && (
<>
<EuiFlexItem grow={false}>
<EuiToolTip content={<pre>{unsavedChanges}</pre>}>
<EuiBadge color="warning">Unsaved changes</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
isDisabled={!controlGroupApi}
onClick={() => {
controlGroupApi?.resetUnsavedChanges();
}}
>
Reset
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={async () => {
if (controlGroupApi) {
saveNotification$.next();
setControlGroupSerializedState(await controlGroupApi.serializeState());
}
}}
>
Save
</EuiButton>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiSuperDatePicker
Expand All @@ -381,33 +406,8 @@ export const ReactControlExample = ({
type={CONTROL_GROUP_TYPE}
getParentApi={() => ({
...dashboardApi,
getSerializedStateForChild: () => ({
rawState: {
controlStyle: 'oneLine',
chainingSystem: 'HIERARCHICAL',
showApplySelections: false,
panelsJSON: JSON.stringify(controlGroupPanels),
ignoreParentSettingsJSON:
'{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
} as object,
references: [
{
name: `controlGroup_${searchControlId}:${SEARCH_CONTROL_TYPE}DataView`,
type: 'index-pattern',
id: WEB_LOGS_DATA_VIEW_ID,
},
{
name: `controlGroup_${rangeSliderControlId}:${RANGE_SLIDER_CONTROL_TYPE}DataView`,
type: 'index-pattern',
id: WEB_LOGS_DATA_VIEW_ID,
},
{
name: `controlGroup_${optionsListId}:optionsListControlDataView`,
type: 'index-pattern',
id: WEB_LOGS_DATA_VIEW_ID,
},
],
}),
getSerializedStateForChild: getControlGroupSerializedState,
getRuntimeStateForChild: getControlGroupRuntimeState,
})}
key={`control_group`}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ControlGroupRuntimeState } from '../../react_controls/control_group/types';

const RUNTIME_STATE_SESSION_STORAGE_KEY =
'kibana.examples.controls.reactControlExample.controlGroupRuntimeState';

export function clearControlGroupRuntimeState() {
sessionStorage.removeItem(RUNTIME_STATE_SESSION_STORAGE_KEY);
}

export function getControlGroupRuntimeState(): Partial<ControlGroupRuntimeState> {
const runtimeStateJSON = sessionStorage.getItem(RUNTIME_STATE_SESSION_STORAGE_KEY);
return runtimeStateJSON ? JSON.parse(runtimeStateJSON) : {};
}

export function setControlGroupRuntimeState(runtimeState: Partial<ControlGroupRuntimeState>) {
sessionStorage.setItem(RUNTIME_STATE_SESSION_STORAGE_KEY, JSON.stringify(runtimeState));
}
Loading

0 comments on commit cf1222f

Please sign in to comment.