mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[controls][embeddable rebuild] Update initializeDataControl to handle error cases (#186407)
PR updates initializeDataControl to handle error cases like DataView not found and field not found. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4b15e4db1b
commit
f4f927b3d0
5 changed files with 271 additions and 83 deletions
13
examples/controls_example/jest.config.js
Normal file
13
examples/controls_example/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/examples/controls_example'],
|
||||
};
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { coreMock } from '@kbn/core/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { first, skip } from 'rxjs';
|
||||
import { ControlGroupApi } from '../control_group/types';
|
||||
import { initializeDataControl } from './initialize_data_control';
|
||||
|
||||
describe('initializeDataControl', () => {
|
||||
const dataControlState = {
|
||||
dataViewId: 'myDataViewId',
|
||||
fieldName: 'myFieldName',
|
||||
};
|
||||
const editorStateManager = {};
|
||||
const controlGroupApi = {} as unknown as ControlGroupApi;
|
||||
const mockDataViews = dataViewPluginMocks.createStartContract();
|
||||
// @ts-ignore
|
||||
mockDataViews.get = async (id: string): Promise<DataView> => {
|
||||
if (id !== 'myDataViewId') {
|
||||
throw new Error(`Simulated error: no data view found for id ${id}`);
|
||||
}
|
||||
return {
|
||||
id,
|
||||
getFieldByName: (fieldName: string) => {
|
||||
return [
|
||||
{
|
||||
displayName: 'My field name',
|
||||
name: 'myFieldName',
|
||||
type: 'string',
|
||||
},
|
||||
].find((field) => fieldName === field.name);
|
||||
},
|
||||
} as unknown as DataView;
|
||||
};
|
||||
const services = {
|
||||
core: coreMock.createStart(),
|
||||
dataViews: mockDataViews,
|
||||
};
|
||||
|
||||
describe('dataViewId subscription', () => {
|
||||
describe('no blocking errors', () => {
|
||||
let dataControl: undefined | ReturnType<typeof initializeDataControl>;
|
||||
beforeAll((done) => {
|
||||
dataControl = initializeDataControl(
|
||||
'myControlId',
|
||||
'myControlType',
|
||||
dataControlState,
|
||||
editorStateManager,
|
||||
controlGroupApi,
|
||||
services
|
||||
);
|
||||
|
||||
dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set data view', () => {
|
||||
const dataViews = dataControl!.api.dataViews.value;
|
||||
expect(dataViews).not.toBeUndefined();
|
||||
expect(dataViews!.length).toBe(1);
|
||||
expect(dataViews![0].id).toBe('myDataViewId');
|
||||
});
|
||||
|
||||
test('should set default panel title', () => {
|
||||
const defaultPanelTitle = dataControl!.api.defaultPanelTitle!.value;
|
||||
expect(defaultPanelTitle).not.toBeUndefined();
|
||||
expect(defaultPanelTitle).toBe('My field name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data view does not exist', () => {
|
||||
let dataControl: undefined | ReturnType<typeof initializeDataControl>;
|
||||
beforeAll((done) => {
|
||||
dataControl = initializeDataControl(
|
||||
'myControlId',
|
||||
'myControlType',
|
||||
{
|
||||
...dataControlState,
|
||||
dataViewId: 'notGonnaFindMeDataViewId',
|
||||
},
|
||||
editorStateManager,
|
||||
controlGroupApi,
|
||||
services
|
||||
);
|
||||
|
||||
dataControl.api.dataViews.pipe(skip(1), first()).subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set blocking error', () => {
|
||||
const error = dataControl!.api.blockingError.value;
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error!.message).toBe(
|
||||
'Simulated error: no data view found for id notGonnaFindMeDataViewId'
|
||||
);
|
||||
});
|
||||
|
||||
test('should clear blocking error when valid data view id provided', (done) => {
|
||||
dataControl!.api.dataViews.pipe(skip(1), first()).subscribe((dataView) => {
|
||||
expect(dataView).not.toBeUndefined();
|
||||
expect(dataControl!.api.blockingError.value).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
dataControl!.stateManager.dataViewId.next('myDataViewId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('field does not exist', () => {
|
||||
let dataControl: undefined | ReturnType<typeof initializeDataControl>;
|
||||
beforeAll((done) => {
|
||||
dataControl = initializeDataControl(
|
||||
'myControlId',
|
||||
'myControlType',
|
||||
{
|
||||
...dataControlState,
|
||||
fieldName: 'notGonnaFindMeFieldName',
|
||||
},
|
||||
editorStateManager,
|
||||
controlGroupApi,
|
||||
services
|
||||
);
|
||||
|
||||
dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set blocking error', () => {
|
||||
const error = dataControl!.api.blockingError.value;
|
||||
expect(error).not.toBeUndefined();
|
||||
expect(error!.message).toBe('Could not locate field: notGonnaFindMeFieldName');
|
||||
});
|
||||
|
||||
test('should clear blocking error when valid field name provided', (done) => {
|
||||
dataControl!.api
|
||||
.defaultPanelTitle!.pipe(skip(1), first())
|
||||
.subscribe((defaultPanelTitle) => {
|
||||
expect(defaultPanelTitle).toBe('My field name');
|
||||
expect(dataControl!.api.blockingError.value).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
dataControl!.stateManager.fieldName.next('myFieldName');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, combineLatestWith, switchMap } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, switchMap } from 'rxjs';
|
||||
|
||||
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { DataView, DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
|
@ -15,6 +15,7 @@ import { Filter } from '@kbn/es-query';
|
|||
import { SerializedPanelState } from '@kbn/presentation-containers';
|
||||
import { StateComparators } from '@kbn/presentation-publishing';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ControlGroupApi } from '../control_group/types';
|
||||
import { initializeDefaultControlApi } from '../initialize_default_control_api';
|
||||
import { ControlApiInitialization, ControlStateManager, DefaultControlState } from '../types';
|
||||
|
@ -32,17 +33,13 @@ export const initializeDataControl = <EditorState extends object = {}>(
|
|||
dataViews: DataViewsPublicPluginStart;
|
||||
}
|
||||
): {
|
||||
dataControlApi: ControlApiInitialization<DataControlApi>;
|
||||
dataControlComparators: StateComparators<DefaultDataControlState>;
|
||||
dataControlStateManager: ControlStateManager<DefaultDataControlState>;
|
||||
serializeDataControl: () => SerializedPanelState<DefaultControlState>;
|
||||
api: ControlApiInitialization<DataControlApi>;
|
||||
cleanup: () => void;
|
||||
comparators: StateComparators<DefaultDataControlState>;
|
||||
stateManager: ControlStateManager<DefaultDataControlState>;
|
||||
serialize: () => SerializedPanelState<DefaultControlState>;
|
||||
} => {
|
||||
const {
|
||||
defaultControlApi,
|
||||
defaultControlComparators,
|
||||
defaultControlStateManager,
|
||||
serializeDefaultControl,
|
||||
} = initializeDefaultControlApi(state);
|
||||
const defaultControl = initializeDefaultControlApi(state);
|
||||
|
||||
const panelTitle = new BehaviorSubject<string | undefined>(state.title);
|
||||
const defaultPanelTitle = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
@ -51,42 +48,66 @@ export const initializeDataControl = <EditorState extends object = {}>(
|
|||
const dataViews = new BehaviorSubject<DataView[] | undefined>(undefined);
|
||||
const filters = new BehaviorSubject<Filter[] | undefined>(undefined);
|
||||
|
||||
const dataControlComparators: StateComparators<DefaultDataControlState> = {
|
||||
...defaultControlComparators,
|
||||
title: [panelTitle, (value: string | undefined) => panelTitle.next(value)],
|
||||
dataViewId: [dataViewId, (value: string) => dataViewId.next(value)],
|
||||
fieldName: [fieldName, (value: string) => fieldName.next(value)],
|
||||
};
|
||||
|
||||
const stateManager: ControlStateManager<DefaultDataControlState> = {
|
||||
...defaultControlStateManager,
|
||||
...defaultControl.stateManager,
|
||||
dataViewId,
|
||||
fieldName,
|
||||
title: panelTitle,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the data view + field whenever the selected data view ID or field name changes; use the
|
||||
* fetched field spec to set the default panel title, which is always equal to either the field
|
||||
* name or the field's display name.
|
||||
*/
|
||||
dataViewId
|
||||
function clearBlockingError() {
|
||||
if (defaultControl.api.blockingError.value) {
|
||||
defaultControl.api.setBlockingError(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const dataViewIdSubscription = dataViewId
|
||||
.pipe(
|
||||
combineLatestWith(fieldName),
|
||||
switchMap(async ([currentDataViewId, currentFieldName]) => {
|
||||
defaultControlApi.setDataLoading(true);
|
||||
const dataView = await services.dataViews.get(currentDataViewId);
|
||||
const field = dataView.getFieldByName(currentFieldName);
|
||||
defaultControlApi.setDataLoading(false);
|
||||
return { dataView, field };
|
||||
switchMap(async (currentDataViewId) => {
|
||||
let dataView: DataView | undefined;
|
||||
try {
|
||||
dataView = await services.dataViews.get(currentDataViewId);
|
||||
return { dataView };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe(async ({ dataView, field }) => {
|
||||
if (!dataView || !field) return;
|
||||
dataViews.next([dataView]);
|
||||
defaultPanelTitle.next(field.displayName || field.name);
|
||||
.subscribe(({ dataView, error }) => {
|
||||
if (error) {
|
||||
defaultControl.api.setBlockingError(error);
|
||||
} else {
|
||||
clearBlockingError();
|
||||
}
|
||||
dataViews.next(dataView ? [dataView] : undefined);
|
||||
});
|
||||
|
||||
const fieldNameSubscription = combineLatest([dataViews, fieldName]).subscribe(
|
||||
([nextDataViews, nextFieldName]) => {
|
||||
const dataView = nextDataViews
|
||||
? nextDataViews.find(({ id }) => dataViewId.value === id)
|
||||
: undefined;
|
||||
if (!dataView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = dataView.getFieldByName(nextFieldName);
|
||||
if (!field) {
|
||||
defaultControl.api.setBlockingError(
|
||||
new Error(
|
||||
i18n.translate('controlsExamples.errors.fieldNotFound', {
|
||||
defaultMessage: 'Could not locate field: {fieldName}',
|
||||
values: { fieldName: nextFieldName },
|
||||
})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
clearBlockingError();
|
||||
}
|
||||
defaultPanelTitle.next(field ? field.displayName || field.name : nextFieldName);
|
||||
}
|
||||
);
|
||||
|
||||
const onEdit = async () => {
|
||||
openDataControlEditor<DefaultDataControlState & EditorState>(
|
||||
{ ...stateManager, ...editorStateManager } as ControlStateManager<
|
||||
|
@ -99,8 +120,8 @@ export const initializeDataControl = <EditorState extends object = {}>(
|
|||
);
|
||||
};
|
||||
|
||||
const dataControlApi: ControlApiInitialization<DataControlApi> = {
|
||||
...defaultControlApi,
|
||||
const api: ControlApiInitialization<DataControlApi> = {
|
||||
...defaultControl.api,
|
||||
panelTitle,
|
||||
defaultPanelTitle,
|
||||
dataViews,
|
||||
|
@ -113,13 +134,22 @@ export const initializeDataControl = <EditorState extends object = {}>(
|
|||
};
|
||||
|
||||
return {
|
||||
dataControlApi,
|
||||
dataControlComparators,
|
||||
dataControlStateManager: stateManager,
|
||||
serializeDataControl: () => {
|
||||
api,
|
||||
cleanup: () => {
|
||||
dataViewIdSubscription.unsubscribe();
|
||||
fieldNameSubscription.unsubscribe();
|
||||
},
|
||||
comparators: {
|
||||
...defaultControl.comparators,
|
||||
title: [panelTitle, (value: string | undefined) => panelTitle.next(value)],
|
||||
dataViewId: [dataViewId, (value: string) => dataViewId.next(value)],
|
||||
fieldName: [fieldName, (value: string) => fieldName.next(value)],
|
||||
},
|
||||
stateManager,
|
||||
serialize: () => {
|
||||
return {
|
||||
rawState: {
|
||||
...serializeDefaultControl().rawState,
|
||||
...defaultControl.serialize().rawState,
|
||||
dataViewId: dataViewId.getValue(),
|
||||
fieldName: fieldName.getValue(),
|
||||
title: panelTitle.getValue(),
|
||||
|
|
|
@ -86,12 +86,7 @@ export const getSearchControlFactory = ({
|
|||
);
|
||||
const editorStateManager = { searchTechnique };
|
||||
|
||||
const {
|
||||
dataControlApi,
|
||||
dataControlComparators,
|
||||
dataControlStateManager,
|
||||
serializeDataControl,
|
||||
} = initializeDataControl<Pick<SearchControlState, 'searchTechnique'>>(
|
||||
const dataControl = initializeDataControl<Pick<SearchControlState, 'searchTechnique'>>(
|
||||
uuid,
|
||||
SEARCH_CONTROL_TYPE,
|
||||
initialState,
|
||||
|
@ -105,13 +100,13 @@ export const getSearchControlFactory = ({
|
|||
|
||||
const api = buildApi(
|
||||
{
|
||||
...dataControlApi,
|
||||
...dataControl.api,
|
||||
getTypeDisplayName: () =>
|
||||
i18n.translate('controlsExamples.searchControl.displayName', {
|
||||
defaultMessage: 'Search',
|
||||
}),
|
||||
serializeState: () => {
|
||||
const { rawState: dataControlState, references } = serializeDataControl();
|
||||
const { rawState: dataControlState, references } = dataControl.serialize();
|
||||
return {
|
||||
rawState: {
|
||||
...dataControlState,
|
||||
|
@ -126,7 +121,7 @@ export const getSearchControlFactory = ({
|
|||
},
|
||||
},
|
||||
{
|
||||
...dataControlComparators,
|
||||
...dataControl.comparators,
|
||||
searchTechnique: [
|
||||
searchTechnique,
|
||||
(newTechnique: SearchControlTechniques | undefined) =>
|
||||
|
@ -146,8 +141,8 @@ export const getSearchControlFactory = ({
|
|||
const onSearchStringChanged = combineLatest([searchString, searchTechnique])
|
||||
.pipe(debounceTime(200), distinctUntilChanged(deepEqual))
|
||||
.subscribe(([newSearchString, currentSearchTechnnique]) => {
|
||||
const currentDataView = dataControlApi.dataViews.getValue()?.[0];
|
||||
const currentField = dataControlStateManager.fieldName.getValue();
|
||||
const currentDataView = dataControl.api.dataViews.getValue()?.[0];
|
||||
const currentField = dataControl.stateManager.fieldName.getValue();
|
||||
|
||||
if (currentDataView && currentField) {
|
||||
if (newSearchString) {
|
||||
|
@ -179,8 +174,8 @@ export const getSearchControlFactory = ({
|
|||
* clear the previous search string.
|
||||
*/
|
||||
const onFieldChanged = combineLatest([
|
||||
dataControlStateManager.fieldName,
|
||||
dataControlStateManager.dataViewId,
|
||||
dataControl.stateManager.fieldName,
|
||||
dataControl.stateManager.dataViewId,
|
||||
])
|
||||
.pipe(distinctUntilChanged(deepEqual))
|
||||
.subscribe(() => {
|
||||
|
@ -199,6 +194,7 @@ export const getSearchControlFactory = ({
|
|||
useEffect(() => {
|
||||
return () => {
|
||||
// cleanup on unmount
|
||||
dataControl.cleanup();
|
||||
onSearchStringChanged.unsubscribe();
|
||||
onFieldChanged.unsubscribe();
|
||||
};
|
||||
|
|
|
@ -24,40 +24,34 @@ export type ControlApi = ControlApiInitialization<DefaultControlApi>;
|
|||
export const initializeDefaultControlApi = (
|
||||
state: DefaultControlState
|
||||
): {
|
||||
defaultControlApi: ControlApi;
|
||||
defaultControlStateManager: ControlStateManager<DefaultControlState>;
|
||||
defaultControlComparators: StateComparators<DefaultControlState>;
|
||||
serializeDefaultControl: () => SerializedPanelState<DefaultControlState>;
|
||||
api: ControlApi;
|
||||
stateManager: ControlStateManager<DefaultControlState>;
|
||||
comparators: StateComparators<DefaultControlState>;
|
||||
serialize: () => SerializedPanelState<DefaultControlState>;
|
||||
} => {
|
||||
const dataLoading = new BehaviorSubject<boolean | undefined>(false);
|
||||
const blockingError = new BehaviorSubject<Error | undefined>(undefined);
|
||||
const grow = new BehaviorSubject<boolean | undefined>(state.grow);
|
||||
const width = new BehaviorSubject<ControlWidth | undefined>(state.width);
|
||||
|
||||
const defaultControlApi: ControlApi = {
|
||||
grow,
|
||||
width,
|
||||
dataLoading,
|
||||
blockingError,
|
||||
setBlockingError: (error) => blockingError.next(error),
|
||||
setDataLoading: (loading) => dataLoading.next(loading),
|
||||
};
|
||||
|
||||
const defaultControlStateManager: ControlStateManager<DefaultControlState> = {
|
||||
grow,
|
||||
width,
|
||||
};
|
||||
|
||||
const defaultControlComparators: StateComparators<DefaultControlState> = {
|
||||
grow: [grow, (newGrow: boolean | undefined) => grow.next(newGrow)],
|
||||
width: [width, (newWidth: ControlWidth | undefined) => width.next(newWidth)],
|
||||
};
|
||||
|
||||
return {
|
||||
defaultControlApi,
|
||||
defaultControlComparators,
|
||||
defaultControlStateManager,
|
||||
serializeDefaultControl: () => {
|
||||
api: {
|
||||
grow,
|
||||
width,
|
||||
dataLoading,
|
||||
blockingError,
|
||||
setBlockingError: (error) => blockingError.next(error),
|
||||
setDataLoading: (loading) => dataLoading.next(loading),
|
||||
},
|
||||
comparators: {
|
||||
grow: [grow, (newGrow: boolean | undefined) => grow.next(newGrow)],
|
||||
width: [width, (newWidth: ControlWidth | undefined) => width.next(newWidth)],
|
||||
},
|
||||
stateManager: {
|
||||
grow,
|
||||
width,
|
||||
},
|
||||
serialize: () => {
|
||||
return { rawState: { grow: grow.getValue(), width: width.getValue() }, references: [] };
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue