Skip to content

Commit d60b8ab

Browse files
committed
Initial profile state PoC implementation
1 parent d07e1de commit d60b8ab

12 files changed

Lines changed: 269 additions & 19 deletions

File tree

src/platform/plugins/shared/discover/public/application/main/state_management/redux/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const DEFAULT_TAB_STATE: Omit<TabState, keyof TabItem> = {
4343
fieldsToReset: 'none',
4444
snapshotsByProfileId: {},
4545
},
46+
profileState: {},
4647
expandedDoc: undefined,
4748
expandedDocOwner: undefined,
4849
renderDocumentViewMeta: undefined,

src/platform/plugins/shared/discover/public/application/main/state_management/redux/context_awareness_toolkit.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,28 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10+
import { distinctUntilChanged, from, map } from 'rxjs';
1011
import type { ContextAwarenessToolkit } from '../../../../context_awareness/toolkit';
1112
import type { InternalStateStore } from './internal_state';
1213
import { internalStateActions } from '.';
14+
import { selectTab } from './selectors';
15+
import type {
16+
ProfileStateAdapter,
17+
ProfileStateKey,
18+
ProfileStateRegistry,
19+
} from '../../../../context_awareness';
1320

1421
export const createContextAwarenessToolkit = ({
1522
internalState,
23+
stateRegistry,
1624
tabId,
1725
}: {
1826
internalState: InternalStateStore;
27+
stateRegistry: ProfileStateRegistry;
1928
tabId: string;
2029
}): ContextAwarenessToolkit => {
30+
const stateAdapters = new Map<string, ProfileStateAdapter<object>>();
31+
2132
return {
2233
actions: {
2334
openInNewTab: async (params) => {
@@ -45,5 +56,42 @@ export const createContextAwarenessToolkit = ({
4556
);
4657
},
4758
},
59+
getStateAdapter: <TState extends object>(key: ProfileStateKey<TState>) => {
60+
if (stateAdapters.has(key.rawKey)) {
61+
return stateAdapters.get(key.rawKey) as unknown as ProfileStateAdapter<TState>;
62+
}
63+
64+
stateRegistry.getDefinition(key);
65+
66+
const getState = () => {
67+
const tabState = selectTab(internalState.getState(), tabId);
68+
return (tabState?.profileState[key.rawKey] ?? {}) as TState;
69+
};
70+
71+
const state$ = from(internalState).pipe(map(getState), distinctUntilChanged());
72+
73+
const adapter: ProfileStateAdapter<TState> = {
74+
getState,
75+
getState$: () => state$,
76+
setState: (profileState, _options) => {
77+
internalState.dispatch(
78+
internalStateActions.setProfileState({ tabId, key: key.rawKey, profileState })
79+
);
80+
},
81+
updateState: (stateUpdate, _options) => {
82+
internalState.dispatch(
83+
internalStateActions.setProfileState({
84+
tabId,
85+
key: key.rawKey,
86+
profileState: { ...getState(), ...stateUpdate },
87+
})
88+
);
89+
},
90+
};
91+
92+
stateAdapters.set(key.rawKey, adapter as unknown as ProfileStateAdapter<object>);
93+
94+
return adapter;
95+
},
4896
};
4997
};

src/platform/plugins/shared/discover/public/application/main/state_management/redux/internal_state.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ export const internalStateSlice = createSlice({
429429
};
430430
}),
431431

432+
setProfileState: (state, action: TabAction<{ key: string; profileState: object }>) =>
433+
withTab(state, { tabId: state.tabs.unsafeCurrentId }, (tab) => {
434+
tab.profileState[action.payload.key] = action.payload.profileState;
435+
}),
436+
432437
resetOnSavedSearchChange: (state, action: TabAction) =>
433438
withTab(state, action.payload, (tab) => {
434439
tab.overriddenVisContextAfterInvalidation = undefined;
@@ -758,6 +763,7 @@ export const createInternalStateStore = (
758763
getContextAwarenessToolkit: (tabId: string) => {
759764
return createContextAwarenessToolkit({
760765
internalState,
766+
stateRegistry: options.services.profileStateRegistry,
761767
tabId,
762768
});
763769
},

src/platform/plugins/shared/discover/public/application/main/state_management/redux/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export interface TabState extends TabItem {
204204
dataRequestParams: InternalStateDataRequestParams;
205205
overriddenVisContextAfterInvalidation: UnifiedHistogramVisContext | {} | undefined; // it will be used during saving of the Discover Session
206206
defaultProfileState: DefaultProfileState;
207+
profileState: Record<string, object | undefined>;
207208
uiState: {
208209
esqlEditor?: Partial<ESQLEditorRestorableState>;
209210
dataGrid?: Partial<UnifiedDataTableRestorableState>;

src/platform/plugins/shared/discover/public/build_services.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import type { DiscoverStartPlugins } from './types';
7070
import type { DiscoverContextAppLocator } from './application/context/services/locator';
7171
import type { DiscoverSingleDocLocator } from './application/doc/locator';
7272
import type { DiscoverAppLocator } from '../common';
73-
import type { ProfilesManager } from './context_awareness';
73+
import { type ProfilesManager, ProfileStateRegistry } from './context_awareness';
7474
import type { DiscoverEBTManager } from './ebt_manager';
7575
import {
7676
CASCADE_LAYOUT_ENABLED_FEATURE_FLAG_KEY,
@@ -157,6 +157,7 @@ export interface DiscoverServices {
157157
noDataPage?: NoDataPagePluginStart;
158158
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
159159
profilesManager: ProfilesManager;
160+
profileStateRegistry: ProfileStateRegistry;
160161
ebtManager: DiscoverEBTManager;
161162
fieldsMetadata?: FieldsMetadataPublicStart;
162163
logsDataAccess?: LogsDataAccessPluginStart;
@@ -263,6 +264,7 @@ export const buildServices = ({
263264
noDataPage: plugins.noDataPage,
264265
observabilityAIAssistant: plugins.observabilityAIAssistant,
265266
profilesManager,
267+
profileStateRegistry: new ProfileStateRegistry(),
266268
ebtManager,
267269
fieldsMetadata: plugins.fieldsMetadata,
268270
logsDataAccess: plugins.logsDataAccess,

src/platform/plugins/shared/discover/public/context_awareness/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export {
1919
type GetProfilesOptions,
2020
} from './profiles_manager';
2121
export { type ProfileProviderSharedServices } from './profile_providers';
22+
export {
23+
type ProfileStateAdapter,
24+
type ProfileStateDefinition,
25+
type ProfileStateDescriptor,
26+
ProfileStateKey,
27+
ProfileStateType,
28+
ProfileStateRegistry,
29+
} from './profile_state';
2230
export {
2331
useProfileAccessor,
2432
useRootProfile,

src/platform/plugins/shared/discover/public/context_awareness/profile_providers/example/example_data_source_profile/profile.tsx

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,29 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import { EuiBadge, EuiFlyout } from '@elastic/eui';
10+
import {
11+
EuiBadge,
12+
EuiFlexGroup,
13+
EuiFlexItem,
14+
EuiFlyout,
15+
EuiFormRow,
16+
EuiSelect,
17+
EuiSpacer,
18+
EuiTitle,
19+
} from '@elastic/eui';
1120
import type { DataViewField } from '@kbn/data-views-plugin/common';
1221
import type { RowControlColumn } from '@kbn/discover-utils';
1322
import { AppMenuActionId, getFieldValue } from '@kbn/discover-utils';
1423
import { capitalize } from 'lodash';
1524
import React from 'react';
25+
import { useObservable } from '@kbn/use-observable';
1626
import type { DataSourceProfileProvider } from '../../../profiles';
1727
import { DataSourceCategory } from '../../../profiles';
1828
import { extractIndexPatternFrom } from '../../extract_index_pattern_from';
1929
import { ChartWithCustomButtons, CustomDocViewerFooter, CustomDocViewerHeader } from './components';
2030
import { CustomDocView } from './components/custom_doc_view';
2131
import { RestorableStateDocView } from './components/restorable_state_doc_view';
32+
import { COLOR_STATE_KEY } from '../../../profile_state';
2233

2334
export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvider<{
2435
formatRecord: (flattenedRecord: Record<string, unknown>) => string;
@@ -61,9 +72,11 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi
6172
);
6273
},
6374
}),
64-
getDocViewer:
65-
(prev, { context, toolkit }) =>
66-
(params) => {
75+
getDocViewer: (prev, { context, toolkit }) => {
76+
const stateAdapter = toolkit.getStateAdapter(COLOR_STATE_KEY);
77+
const colorState$ = stateAdapter.getState$();
78+
79+
return (params) => {
6780
const { openInNewTab, updateESQLQuery } = toolkit.actions;
6881
const recordId = params.record.id;
6982
const prevValue = prev(params);
@@ -91,12 +104,51 @@ export const createExampleDataSourceProfileProvider = (): DataSourceProfileProvi
91104
render: (props) => <RestorableStateDocView {...props} />,
92105
});
93106

107+
registry.add({
108+
id: 'doc_view_extensible_state_example',
109+
title: 'Extensible State Example',
110+
order: 2,
111+
render: function Render() {
112+
const colorState = useObservable(colorState$, stateAdapter.getState());
113+
const options = [
114+
{ value: 'default', text: 'None' },
115+
{ value: 'blue', text: 'Blue' },
116+
{ value: 'pink', text: 'Pink' },
117+
{ value: 'green', text: 'Green' },
118+
];
119+
120+
return (
121+
<>
122+
<EuiSpacer size="s" />
123+
<EuiFlexGroup direction="column" gutterSize="m" responsive={false}>
124+
<EuiTitle size="xs">
125+
<h3>Extensible State Example</h3>
126+
</EuiTitle>
127+
<EuiFlexItem grow={false}>
128+
<EuiFormRow label="Favourite color">
129+
<EuiSelect
130+
options={options}
131+
value={colorState.favouriteColor}
132+
onChange={(e) => {
133+
stateAdapter.updateState({ favouriteColor: e.target.value });
134+
}}
135+
aria-label="Select favourite color"
136+
/>
137+
</EuiFormRow>
138+
</EuiFlexItem>
139+
</EuiFlexGroup>
140+
</>
141+
);
142+
},
143+
});
144+
94145
return prevValue.docViewsRegistry(registry);
95146
},
96147
renderHeader: (props) => <CustomDocViewerHeader {...props} />,
97148
renderFooter: (props) => <CustomDocViewerFooter {...props} />,
98149
};
99-
},
150+
};
151+
},
100152
/**
101153
* The `getAppMenu` extension point gives access to AppMenuRegistry with methods `registerCustomItem` and
102154
* `registerCustomPopoverItem`.

src/platform/plugins/shared/discover/public/context_awareness/profile_providers/example/example_root_profile/profile.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,39 @@
1010
import { EuiBadge, EuiFlyout } from '@elastic/eui';
1111
import { getFieldValue } from '@kbn/discover-utils';
1212
import React from 'react';
13+
import { useObservable } from '@kbn/use-observable';
1314
import type { RootProfileProvider } from '../../../profiles';
1415
import { SolutionType } from '../../../profiles';
16+
import { COLOR_STATE_KEY } from '../../../profile_state';
1517

1618
export const createExampleRootProfileProvider = (): RootProfileProvider => ({
1719
profileId: 'example-root-profile',
1820
isExperimental: true,
1921
profile: {
2022
getDefaultAdHocDataViews,
2123
getDefaultEsqlQuery,
22-
getCellRenderers: (prev) => (params) => ({
23-
...prev(params),
24-
'@timestamp': (props) => {
25-
const timestamp = getFieldValue(props.row, '@timestamp') as string;
24+
getCellRenderers: (prev, { toolkit }) => {
25+
const stateAdapter = toolkit.getStateAdapter(COLOR_STATE_KEY);
26+
const colorState$ = stateAdapter.getState$();
2627

27-
return (
28-
<EuiBadge color="hollow" title={timestamp} data-test-subj="exampleRootProfileTimestamp">
29-
{timestamp}
30-
</EuiBadge>
31-
);
32-
},
33-
}),
28+
return (params) => ({
29+
...prev(params),
30+
'@timestamp': function Timestamp(props) {
31+
const timestamp = getFieldValue(props.row, '@timestamp') as string;
32+
const colorState = useObservable(colorState$, stateAdapter.getState());
33+
34+
return (
35+
<EuiBadge
36+
color={colorState.favouriteColor}
37+
title={timestamp}
38+
data-test-subj="exampleRootProfileTimestamp"
39+
>
40+
{timestamp}
41+
</EuiBadge>
42+
);
43+
},
44+
});
45+
},
3446
/**
3547
* The `getAppMenu` extension point gives access to AppMenuRegistry with methods `registerCustomItem` and
3648
* `registerCustomPopoverItem`.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/* eslint-disable max-classes-per-file */
11+
12+
import type { Observable } from 'rxjs';
13+
14+
export class ProfileStateKey<_TState extends object> {
15+
private constructor(public readonly rawKey: string) {}
16+
17+
static create<TState extends object>(rawKey: string): ProfileStateKey<TState> {
18+
return new ProfileStateKey<TState>(rawKey);
19+
}
20+
}
21+
22+
export enum ProfileStateHistoryMethod {
23+
Push = 'push',
24+
Replace = 'replace',
25+
}
26+
27+
export interface ProfileStateMutationOptions {
28+
historyMethod?: ProfileStateHistoryMethod;
29+
}
30+
31+
export interface ProfileStateAdapter<TState extends object> {
32+
getState: () => TState;
33+
getState$: () => Observable<TState>;
34+
setState: (state: TState, options?: ProfileStateMutationOptions) => void;
35+
updateState: (stateUpdate: Partial<TState>, options?: ProfileStateMutationOptions) => void;
36+
}
37+
38+
export enum ProfileStateType {
39+
Ui = 'ui',
40+
Url = 'url',
41+
Persistent = 'persistent',
42+
}
43+
44+
export type ProfileStateDescriptor<TState extends object> = {
45+
[key in keyof TState]: {
46+
type: ProfileStateType;
47+
};
48+
};
49+
50+
export interface ProfileStateDefinition<TState extends object> {
51+
key: ProfileStateKey<TState>;
52+
descriptor: ProfileStateDescriptor<TState>;
53+
}
54+
55+
export class ProfileStateRegistry {
56+
private readonly stateDefinitions = new Map<string, ProfileStateDefinition<object>>();
57+
58+
public registerDefinition<TState extends object>(definition: ProfileStateDefinition<TState>) {
59+
if (this.stateDefinitions.has(definition.key.rawKey)) {
60+
throw new Error(`State with key ${definition.key.rawKey} is already registered.`);
61+
}
62+
this.stateDefinitions.set(definition.key.rawKey, definition);
63+
}
64+
65+
public getDefinition<TState extends object>(
66+
key: ProfileStateKey<TState>
67+
): ProfileStateDefinition<TState> {
68+
const definition = this.stateDefinitions.get(key.rawKey);
69+
if (!definition) {
70+
throw new Error(`State with key ${key.rawKey} is not registered.`);
71+
}
72+
return definition as ProfileStateDefinition<TState>;
73+
}
74+
}
75+
76+
export const registerProfileStateDefinitions = (registry: ProfileStateRegistry) => {
77+
registry.registerDefinition(colorStateDefinition);
78+
};
79+
80+
interface ColorState {
81+
favouriteColor: string;
82+
leastFavouriteColor: string;
83+
}
84+
85+
export const COLOR_STATE_KEY = ProfileStateKey.create<ColorState>('color_state');
86+
87+
const colorStateDefinition: ProfileStateDefinition<ColorState> = {
88+
key: COLOR_STATE_KEY,
89+
descriptor: {
90+
favouriteColor: { type: ProfileStateType.Ui },
91+
leastFavouriteColor: { type: ProfileStateType.Ui },
92+
},
93+
};

0 commit comments

Comments
 (0)