[8.9] [controls] fix Dashboard getting stuck at loading in Kibana when Controls is used and mapping changed from integer to keyword (#163529) (#163750)

# Backport

This will backport the following commits from `main` to `8.9`:
- [[controls] fix Dashboard getting stuck at loading in Kibana when
Controls is used and mapping changed from integer to keyword
(#163529)](https://github.com/elastic/kibana/pull/163529)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Nathan
Reese","email":"reese.nathan@elastic.co"},"sourceCommit":{"committedDate":"2023-08-11T19:58:19Z","message":"[controls]
fix Dashboard getting stuck at loading in Kibana when Controls is used
and mapping changed from integer to keyword (#163529)\n\nCloses
https://github.com/elastic/kibana/issues/162474\r\n\r\n### Changes\r\n*
RangeSliderEmbeddable - call setInitializationFinished
when\r\nrunRangeSliderQuery throws. This fixes the issue\r\n*
Investigated if OptionsListEmbeddable is vulnerable to the same
issue.\r\nIt's not because it uses its own REST API that has a service
wrapper\r\n`OptionsListService`. `OptionsListService` handles REST API
errors.\r\n* Add unit test verifying
OptionsListService.runOptionsListRequest does\r\nnot throw when there
are REST API errors and always returns a response.\r\n* Add unit tests
ensuring setInitializationFinished is called for
both\r\nRangeSliderEmbeddable and OptionsListEmbeddable in all
cases\r\n* Other clean up\r\n* Fix uses of `dataViewsService.get`.
`dataViewsService.get` throws when\r\ndata view is not found. It does
not return undefined. PR updates\r\nOptionsListEmbeddable,
RangeSliderEmbeddable, and mocked data service\r\n* Fix uses of
`dataView.getFieldByName`. `dataView.getFieldByName`\r\nreturns
undefined when field is not found and never throws. PR
updates\r\nOptionsListEmbeddable and RangeSliderEmbeddable\r\n * Remove
`resp` wrapper around mocked `fetch` results.\r\n\r\n### Test
instructions\r\n1) In console run \r\n ```\r\n PUT test1\r\n\r\n PUT
test1/_mapping\r\n {\r\n \"properties\": {\r\n \"value\": {\r\n
\"type\": \"integer\"\r\n }\r\n }\r\n }\r\n\r\n PUT test1/_doc/1\r\n
{\r\n \"value\" : 1\r\n }\r\n\r\n PUT test1/_doc/2\r\n {\r\n \"value\" :
10\r\n }\r\n ```\r\n2) create data view `test*`\r\n3) create dashboard
with range slider control on test*.value.\r\n4) select a range in the
range slider\r\n5) save dashboard\r\n6) run the following in console\r\n
```\r\n PUT test2\r\n\r\n PUT test2/_mapping\r\n {\r\n \"properties\":
{\r\n \"value\": {\r\n \"type\": \"keyword\"\r\n }\r\n }\r\n }\r\n\r\n
PUT test2/_doc/1\r\n {\r\n \"value\" : \"foo\"\r\n }\r\n\r\n DELETE
test1\r\n ```\r\n7) Open dashboard saved above. Verify dashboard opens
and control\r\ndisplays an error message about being unable to run
aggregation on\r\nkeyword field.\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Devon Thomson
<devon.thomson@elastic.co>","sha":"0a74fa03a028e7002b5fdebdbfa7dc0c7283aa75","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Feature:Input
Control","Team:Presentation","v8.10.0","v8.9.2"],"number":163529,"url":"https://github.com/elastic/kibana/pull/163529","mergeCommit":{"message":"[controls]
fix Dashboard getting stuck at loading in Kibana when Controls is used
and mapping changed from integer to keyword (#163529)\n\nCloses
https://github.com/elastic/kibana/issues/162474\r\n\r\n### Changes\r\n*
RangeSliderEmbeddable - call setInitializationFinished
when\r\nrunRangeSliderQuery throws. This fixes the issue\r\n*
Investigated if OptionsListEmbeddable is vulnerable to the same
issue.\r\nIt's not because it uses its own REST API that has a service
wrapper\r\n`OptionsListService`. `OptionsListService` handles REST API
errors.\r\n* Add unit test verifying
OptionsListService.runOptionsListRequest does\r\nnot throw when there
are REST API errors and always returns a response.\r\n* Add unit tests
ensuring setInitializationFinished is called for
both\r\nRangeSliderEmbeddable and OptionsListEmbeddable in all
cases\r\n* Other clean up\r\n* Fix uses of `dataViewsService.get`.
`dataViewsService.get` throws when\r\ndata view is not found. It does
not return undefined. PR updates\r\nOptionsListEmbeddable,
RangeSliderEmbeddable, and mocked data service\r\n* Fix uses of
`dataView.getFieldByName`. `dataView.getFieldByName`\r\nreturns
undefined when field is not found and never throws. PR
updates\r\nOptionsListEmbeddable and RangeSliderEmbeddable\r\n * Remove
`resp` wrapper around mocked `fetch` results.\r\n\r\n### Test
instructions\r\n1) In console run \r\n ```\r\n PUT test1\r\n\r\n PUT
test1/_mapping\r\n {\r\n \"properties\": {\r\n \"value\": {\r\n
\"type\": \"integer\"\r\n }\r\n }\r\n }\r\n\r\n PUT test1/_doc/1\r\n
{\r\n \"value\" : 1\r\n }\r\n\r\n PUT test1/_doc/2\r\n {\r\n \"value\" :
10\r\n }\r\n ```\r\n2) create data view `test*`\r\n3) create dashboard
with range slider control on test*.value.\r\n4) select a range in the
range slider\r\n5) save dashboard\r\n6) run the following in console\r\n
```\r\n PUT test2\r\n\r\n PUT test2/_mapping\r\n {\r\n \"properties\":
{\r\n \"value\": {\r\n \"type\": \"keyword\"\r\n }\r\n }\r\n }\r\n\r\n
PUT test2/_doc/1\r\n {\r\n \"value\" : \"foo\"\r\n }\r\n\r\n DELETE
test1\r\n ```\r\n7) Open dashboard saved above. Verify dashboard opens
and control\r\ndisplays an error message about being unable to run
aggregation on\r\nkeyword field.\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Devon Thomson
<devon.thomson@elastic.co>","sha":"0a74fa03a028e7002b5fdebdbfa7dc0c7283aa75"}},"sourceBranch":"main","suggestedTargetBranches":["8.9"],"targetPullRequestStates":[{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/163529","number":163529,"mergeCommit":{"message":"[controls]
fix Dashboard getting stuck at loading in Kibana when Controls is used
and mapping changed from integer to keyword (#163529)\n\nCloses
https://github.com/elastic/kibana/issues/162474\r\n\r\n### Changes\r\n*
RangeSliderEmbeddable - call setInitializationFinished
when\r\nrunRangeSliderQuery throws. This fixes the issue\r\n*
Investigated if OptionsListEmbeddable is vulnerable to the same
issue.\r\nIt's not because it uses its own REST API that has a service
wrapper\r\n`OptionsListService`. `OptionsListService` handles REST API
errors.\r\n* Add unit test verifying
OptionsListService.runOptionsListRequest does\r\nnot throw when there
are REST API errors and always returns a response.\r\n* Add unit tests
ensuring setInitializationFinished is called for
both\r\nRangeSliderEmbeddable and OptionsListEmbeddable in all
cases\r\n* Other clean up\r\n* Fix uses of `dataViewsService.get`.
`dataViewsService.get` throws when\r\ndata view is not found. It does
not return undefined. PR updates\r\nOptionsListEmbeddable,
RangeSliderEmbeddable, and mocked data service\r\n* Fix uses of
`dataView.getFieldByName`. `dataView.getFieldByName`\r\nreturns
undefined when field is not found and never throws. PR
updates\r\nOptionsListEmbeddable and RangeSliderEmbeddable\r\n * Remove
`resp` wrapper around mocked `fetch` results.\r\n\r\n### Test
instructions\r\n1) In console run \r\n ```\r\n PUT test1\r\n\r\n PUT
test1/_mapping\r\n {\r\n \"properties\": {\r\n \"value\": {\r\n
\"type\": \"integer\"\r\n }\r\n }\r\n }\r\n\r\n PUT test1/_doc/1\r\n
{\r\n \"value\" : 1\r\n }\r\n\r\n PUT test1/_doc/2\r\n {\r\n \"value\" :
10\r\n }\r\n ```\r\n2) create data view `test*`\r\n3) create dashboard
with range slider control on test*.value.\r\n4) select a range in the
range slider\r\n5) save dashboard\r\n6) run the following in console\r\n
```\r\n PUT test2\r\n\r\n PUT test2/_mapping\r\n {\r\n \"properties\":
{\r\n \"value\": {\r\n \"type\": \"keyword\"\r\n }\r\n }\r\n }\r\n\r\n
PUT test2/_doc/1\r\n {\r\n \"value\" : \"foo\"\r\n }\r\n\r\n DELETE
test1\r\n ```\r\n7) Open dashboard saved above. Verify dashboard opens
and control\r\ndisplays an error message about being unable to run
aggregation on\r\nkeyword field.\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Devon Thomson
<devon.thomson@elastic.co>","sha":"0a74fa03a028e7002b5fdebdbfa7dc0c7283aa75"}},{"branch":"8.9","label":"v8.9.2","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
This commit is contained in:
Kibana Machine 2023-08-11 17:17:05 -04:00 committed by GitHub
parent b17767646a
commit 075824034c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 471 additions and 104 deletions

View file

@ -0,0 +1,108 @@
/*
* 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 { ControlGroupInput } from '../../../common';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { storybookFlightsDataView } from '@kbn/presentation-util-plugin/public/mocks';
import { OPTIONS_LIST_CONTROL } from '../../../common';
import { ControlGroupContainer } from '../../control_group/embeddable/control_group_container';
import { pluginServices } from '../../services';
import { injectStorybookDataView } from '../../services/data_views/data_views.story';
import { OptionsListEmbeddableFactory } from './options_list_embeddable_factory';
import { OptionsListEmbeddable } from './options_list_embeddable';
pluginServices.getServices().controls.getControlFactory = jest
.fn()
.mockImplementation((type: string) => {
if (type === OPTIONS_LIST_CONTROL) return new OptionsListEmbeddableFactory();
});
describe('initialize', () => {
describe('without selected options', () => {
test('should notify control group when initialization is finished', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
// data view not required for test case
// setInitializationFinished is called before fetching options when value is not provided
injectStorybookDataView(undefined);
const control = await container.addOptionsListControl({
dataViewId: 'demoDataFlights',
fieldName: 'OriginCityName',
});
expect(container.getInput().panels[control.getInput().id].type).toBe(OPTIONS_LIST_CONTROL);
expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true);
});
});
describe('with selected options', () => {
test('should set error message when data view can not be found', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(undefined);
const control = (await container.addOptionsListControl({
dataViewId: 'demoDataFlights',
fieldName: 'OriginCityName',
selectedOptions: ['Seoul', 'Tokyo'],
})) as OptionsListEmbeddable;
// await redux dispatch
await new Promise((resolve) => process.nextTick(resolve));
const reduxState = control.getState();
expect(reduxState.output.loading).toBe(false);
expect(reduxState.componentState.error).toBe(
'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set'
);
});
test('should set error message when field can not be found', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
const control = (await container.addOptionsListControl({
dataViewId: 'demoDataFlights',
fieldName: 'myField',
selectedOptions: ['Seoul', 'Tokyo'],
})) as OptionsListEmbeddable;
// await redux dispatch
await new Promise((resolve) => process.nextTick(resolve));
const reduxState = control.getState();
expect(reduxState.output.loading).toBe(false);
expect(reduxState.componentState.error).toBe('Could not locate field: myField');
});
test('should notify control group when initialization is finished', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
const control = await container.addOptionsListControl({
dataViewId: 'demoDataFlights',
fieldName: 'OriginCityName',
selectedOptions: ['Seoul', 'Tokyo'],
});
expect(container.getInput().panels[control.getInput().id].type).toBe(OPTIONS_LIST_CONTROL);
expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true);
});
});
});

View file

@ -245,13 +245,6 @@ export class OptionsListEmbeddable
if (!this.dataView || this.dataView.id !== dataViewId) {
try {
this.dataView = await this.dataViewsService.get(dataViewId);
if (!this.dataView)
throw new Error(
i18n.translate('controls.optionsList.errors.dataViewNotFound', {
defaultMessage: 'Could not locate data view: {dataViewId}',
values: { dataViewId },
})
);
} catch (e) {
this.dispatch.setErrorMessage(e.message);
}
@ -260,25 +253,21 @@ export class OptionsListEmbeddable
}
if (this.dataView && (!this.field || this.field.name !== fieldName)) {
try {
const originalField = this.dataView.getFieldByName(fieldName);
if (!originalField) {
throw new Error(
i18n.translate('controls.optionsList.errors.fieldNotFound', {
defaultMessage: 'Could not locate field: {fieldName}',
values: { fieldName },
})
);
}
this.field = originalField.toSpec();
} catch (e) {
this.dispatch.setErrorMessage(e.message);
const field = this.dataView.getFieldByName(fieldName);
if (field) {
this.field = field.toSpec();
this.dispatch.setField(this.field);
} else {
this.dispatch.setErrorMessage(
i18n.translate('controls.optionsList.errors.fieldNotFound', {
defaultMessage: 'Could not locate field: {fieldName}',
values: { fieldName },
})
);
}
this.dispatch.setField(this.field);
}
return { dataView: this.dataView, field: this.field! };
return { dataView: this.dataView, field: this.field };
};
private runOptionsListQuery = async (size: number = MIN_OPTIONS_LIST_REQUEST_SIZE) => {

View file

@ -0,0 +1,211 @@
/*
* 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 { of } from 'rxjs';
import { ControlGroupInput } from '../../../common';
import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
import { storybookFlightsDataView } from '@kbn/presentation-util-plugin/public/mocks';
import { RANGE_SLIDER_CONTROL } from '../../../common';
import { ControlGroupContainer } from '../../control_group/embeddable/control_group_container';
import { pluginServices } from '../../services';
import { injectStorybookDataView } from '../../services/data_views/data_views.story';
import { RangeSliderEmbeddableFactory } from './range_slider_embeddable_factory';
import { RangeSliderEmbeddable } from './range_slider_embeddable';
let totalResults = 20;
beforeEach(() => {
totalResults = 20;
pluginServices.getServices().controls.getControlFactory = jest
.fn()
.mockImplementation((type: string) => {
if (type === RANGE_SLIDER_CONTROL) return new RangeSliderEmbeddableFactory();
});
pluginServices.getServices().data.searchSource.create = jest.fn().mockImplementation(() => {
let isAggsRequest = false;
return {
setField: (key: string) => {
if (key === 'aggs') {
isAggsRequest = true;
}
},
fetch$: () => {
return isAggsRequest
? of({
rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } },
})
: of({
rawResponse: { hits: { total: { value: totalResults } } },
});
},
};
});
});
describe('initialize', () => {
describe('without selected range', () => {
test('should notify control group when initialization is finished', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
// data view not required for test case
// setInitializationFinished is called before fetching slider range when value is not provided
injectStorybookDataView(undefined);
const control = await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
});
expect(container.getInput().panels[control.getInput().id].type).toBe(RANGE_SLIDER_CONTROL);
expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true);
});
});
describe('with selected range', () => {
test('should set error message when data view can not be found', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(undefined);
const control = (await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['150', '300'],
})) as RangeSliderEmbeddable;
// await redux dispatch
await new Promise((resolve) => process.nextTick(resolve));
const reduxState = control.getState();
expect(reduxState.output.loading).toBe(false);
expect(reduxState.componentState.error).toBe(
'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set'
);
});
test('should set error message when field can not be found', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
const control = (await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'myField',
value: ['150', '300'],
})) as RangeSliderEmbeddable;
// await redux dispatch
await new Promise((resolve) => process.nextTick(resolve));
const reduxState = control.getState();
expect(reduxState.output.loading).toBe(false);
expect(reduxState.componentState.error).toBe('Could not locate field: myField');
});
test('should set invalid state when filter returns zero results', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
totalResults = 0;
const control = (await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['150', '300'],
})) as RangeSliderEmbeddable;
// await redux dispatch
await new Promise((resolve) => process.nextTick(resolve));
const reduxState = control.getState();
expect(reduxState.output.filters?.length).toBe(0);
expect(reduxState.componentState.isInvalid).toBe(true);
});
test('should set range and filter', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
const control = (await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['150', '300'],
})) as RangeSliderEmbeddable;
// await redux dispatch
await new Promise((resolve) => process.nextTick(resolve));
const reduxState = control.getState();
expect(reduxState.output.filters?.length).toBe(1);
expect(reduxState.output.filters?.[0].query).toEqual({
range: {
AvgTicketPrice: {
gte: 150,
lte: 300,
},
},
});
expect(reduxState.componentState.isInvalid).toBe(false);
expect(reduxState.componentState.min).toBe(0);
expect(reduxState.componentState.max).toBe(1000);
});
test('should notify control group when initialization is finished', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
const control = await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['150', '300'],
});
expect(container.getInput().panels[control.getInput().id].type).toBe(RANGE_SLIDER_CONTROL);
expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true);
});
test('should notify control group when initialization throws', async () => {
const reduxEmbeddablePackage = await lazyLoadReduxToolsPackage();
const controlGroupInput = { chainingSystem: 'NONE', panels: {} } as ControlGroupInput;
const container = new ControlGroupContainer(reduxEmbeddablePackage, controlGroupInput);
injectStorybookDataView(storybookFlightsDataView);
pluginServices.getServices().data.searchSource.create = jest.fn().mockImplementation(() => ({
setField: () => {},
fetch$: () => {
throw new Error('Simulated _search request error');
},
}));
const control = await container.addRangeSliderControl({
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['150', '300'],
});
expect(container.getInput().panels[control.getInput().id].type).toBe(RANGE_SLIDER_CONTROL);
expect(container.getOutput().embeddableLoaded[control.getInput().id]).toBe(true);
});
});
});

View file

@ -62,14 +62,6 @@ interface RangeSliderDataFetchProps {
validate?: boolean;
}
const fieldMissingError = (fieldName: string) =>
new Error(
i18n.translate('controls.rangeSlider.errors.fieldNotFound', {
defaultMessage: 'Could not locate field: {fieldName}',
values: { fieldName },
})
);
export const RangeSliderControlContext = createContext<RangeSliderEmbeddable | null>(null);
export const useRangeSlider = (): RangeSliderEmbeddable => {
const rangeSlider = useContext<RangeSliderEmbeddable | null>(RangeSliderControlContext);
@ -147,15 +139,14 @@ export class RangeSliderEmbeddable
try {
await this.runRangeSliderQuery();
await this.buildFilter();
if (initialValue) {
this.setInitializationFinished();
}
} catch (e) {
batch(() => {
this.dispatch.setLoading(false);
this.dispatch.setErrorMessage(e.message);
});
this.onLoadingError(e.message);
}
if (initialValue) {
this.setInitializationFinished();
}
this.setupSubscriptions();
};
@ -182,7 +173,7 @@ export class RangeSliderEmbeddable
await this.runRangeSliderQuery();
await this.buildFilter();
} catch (e) {
this.dispatch.setErrorMessage(e.message);
this.onLoadingError(e.message);
}
})
);
@ -209,34 +200,27 @@ export class RangeSliderEmbeddable
if (!this.dataView || this.dataView.id !== dataViewId) {
try {
this.dataView = await this.dataViewsService.get(dataViewId);
if (!this.dataView) {
throw new Error(
i18n.translate('controls.rangeSlider.errors.dataViewNotFound', {
defaultMessage: 'Could not locate data view: {dataViewId}',
values: { dataViewId },
})
);
}
this.dispatch.setDataViewId(this.dataView.id);
} catch (e) {
this.dispatch.setErrorMessage(e.message);
this.onLoadingError(e.message);
}
}
if (this.dataView && (!this.field || this.field.name !== fieldName)) {
try {
this.field = this.dataView.getFieldByName(fieldName);
if (this.field === undefined) {
throw fieldMissingError(fieldName);
}
this.field = this.dataView.getFieldByName(fieldName);
if (this.field) {
this.dispatch.setField(this.field?.toSpec());
} catch (e) {
this.dispatch.setErrorMessage(e.message);
} else {
this.onLoadingError(
i18n.translate('controls.rangeSlider.errors.fieldNotFound', {
defaultMessage: 'Could not locate field: {fieldName}',
values: { fieldName },
})
);
}
}
return { dataView: this.dataView, field: this.field! };
return { dataView: this.dataView, field: this.field };
};
private runRangeSliderQuery = async () => {
@ -245,16 +229,6 @@ export class RangeSliderEmbeddable
const { dataView, field } = await this.getCurrentDataViewAndField();
if (!dataView || !field) return;
const { fieldName } = this.getInput();
if (!field) {
batch(() => {
this.dispatch.setLoading(false);
this.dispatch.publishFilters([]);
});
throw fieldMissingError(fieldName);
}
const embeddableInput = this.getInput();
const { ignoreParentSettings, timeRange: globalTimeRange, timeslice } = embeddableInput;
let { filters = [] } = embeddableInput;
@ -278,8 +252,6 @@ export class RangeSliderEmbeddable
const { min, max } = await this.fetchMinMax({
dataView,
field,
}).catch((e) => {
throw e;
});
this.dispatch.setMinMax({
@ -332,9 +304,7 @@ export class RangeSliderEmbeddable
};
searchSource.setField('aggs', aggs);
const resp = await lastValueFrom(searchSource.fetch$()).catch((e) => {
throw e;
});
const resp = await lastValueFrom(searchSource.fetch$());
const min = get(resp, 'rawResponse.aggregations.minAgg.value');
const max = get(resp, 'rawResponse.aggregations.maxAgg.value');
@ -397,11 +367,8 @@ export class RangeSliderEmbeddable
searchSource.setField('query', query);
}
const {
rawResponse: {
hits: { total },
},
} = await lastValueFrom(searchSource.fetch$());
const resp = await lastValueFrom(searchSource.fetch$());
const total = resp?.rawResponse?.hits?.total;
const docCount = typeof total === 'number' ? total : total?.value;
if (!docCount) {
@ -425,6 +392,14 @@ export class RangeSliderEmbeddable
});
};
private onLoadingError(errorMessage: string) {
batch(() => {
this.dispatch.setLoading(false);
this.dispatch.publishFilters([]);
this.dispatch.setErrorMessage(errorMessage);
});
}
public clearSelections() {
this.dispatch.setSelectedRange(['', '']);
}
@ -434,7 +409,7 @@ export class RangeSliderEmbeddable
await this.runRangeSliderQuery();
await this.buildFilter();
} catch (e) {
this.dispatch.setErrorMessage(e.message);
this.onLoadingError(e.message);
}
};

View file

@ -19,9 +19,7 @@ export const dataServiceFactory: DataServiceFactory = () => ({
setField: () => {},
fetch$: () =>
of({
resp: {
rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } },
},
rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } },
}),
}),
} as unknown as DataPublicPluginStart['search']['searchSource'],

View file

@ -13,17 +13,39 @@ import { ControlsDataViewsService } from './types';
export type DataViewsServiceFactory = PluginServiceFactory<ControlsDataViewsService>;
let currentDataView: DataView;
export const injectStorybookDataView = (dataView: DataView) => (currentDataView = dataView);
let currentDataView: DataView | undefined;
export const injectStorybookDataView = (dataView?: DataView) => (currentDataView = dataView);
export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({
get: (() =>
new Promise((r) =>
setTimeout(() => r(currentDataView), 100)
get: ((dataViewId) =>
new Promise((resolve, reject) =>
setTimeout(() => {
if (!currentDataView) {
reject(
new Error(
'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set'
)
);
} else if (currentDataView.id === dataViewId) {
resolve(currentDataView);
} else {
reject(
new Error(
`mock DataViews service currentDataView.id: ${currentDataView.id} does not match requested dataViewId: ${dataViewId}`
)
);
}
}, 100)
) as unknown) as DataViewsPublicPluginStart['get'],
getIdsWithTitle: (() =>
new Promise((r) =>
setTimeout(() => r([{ id: currentDataView.id, title: currentDataView.title }]), 100)
new Promise((resolve) =>
setTimeout(() => {
const idsWithTitle: Array<{ id: string | undefined; title: string }> = [];
if (currentDataView) {
idsWithTitle.push({ id: currentDataView.id, title: currentDataView.title });
}
resolve(idsWithTitle);
}, 100)
) as unknown) as DataViewsPublicPluginStart['getIdsWithTitle'],
getDefaultId: () => Promise.resolve(currentDataView?.id ?? null),
});

View file

@ -0,0 +1,62 @@
/*
* 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 { DataView, FieldSpec } from '@kbn/data-views-plugin/common';
import { KibanaPluginServiceParams } from '@kbn/presentation-util-plugin/public';
import type { OptionsListRequest } from '../../../common/options_list/types';
import type { ControlsPluginStartDeps } from '../../types';
import type { ControlsHTTPService } from '../http/types';
import type { ControlsDataService } from '../data/types';
import { optionsListServiceFactory } from './options_list_service';
describe('runOptionsListRequest', () => {
test('should return OptionsListFailureResponse when fetch throws', async () => {
const mockCore = {
coreStart: {
uiSettings: {
get: () => {
return undefined;
},
},
},
} as unknown as KibanaPluginServiceParams<ControlsPluginStartDeps>;
const mockData = {
query: {
timefilter: {
timefilter: {},
},
},
} as unknown as ControlsDataService;
const mockHttp = {
fetch: () => {
throw new Error('Simulated network error');
},
} as unknown as ControlsHTTPService;
const optionsListService = optionsListServiceFactory(mockCore, {
data: mockData,
http: mockHttp,
});
const response = (await optionsListService.runOptionsListRequest(
{
dataView: {
toSpec: () => {
return {};
},
title: 'myDataView',
} as unknown as DataView,
field: {
name: 'myField',
} as unknown as FieldSpec,
} as unknown as OptionsListRequest,
{} as unknown as AbortSignal
)) as any;
expect(response.error.message).toBe('Simulated network error');
});
});

View file

@ -151,6 +151,9 @@ export type OptionsListServiceFactory = KibanaPluginServiceFactory<
OptionsListServiceRequiredServices
>;
export const optionsListServiceFactory: OptionsListServiceFactory = (core, requiredServices) => {
return new OptionsListService(core.coreStart, requiredServices);
export const optionsListServiceFactory: OptionsListServiceFactory = (
startParams,
requiredServices
) => {
return new OptionsListService(startParams.coreStart, requiredServices);
};

View file

@ -61,14 +61,19 @@ const numberFields = [
const getConfig = (() => {}) as FieldFormatsGetConfigFn;
export const flightFieldByName: { [key: string]: DataViewField } = {};
flightFieldNames.forEach(
(flightFieldName) =>
(flightFieldByName[flightFieldName] = {
name: flightFieldName,
type: numberFields.includes(flightFieldName) ? 'number' : 'string',
aggregatable: true,
} as unknown as DataViewField)
);
flightFieldNames.forEach((flightFieldName) => {
const fieldBase = {
name: flightFieldName,
type: numberFields.includes(flightFieldName) ? 'number' : 'string',
aggregatable: true,
};
flightFieldByName[flightFieldName] = {
...fieldBase,
toSpec: () => {
return fieldBase;
},
} as unknown as DataViewField;
});
flightFieldByName.Cancelled = { name: 'Cancelled', type: 'boolean' } as DataViewField;
flightFieldByName.timestamp = { name: 'timestamp', type: 'date' } as DataViewField;

View file

@ -399,7 +399,6 @@
"controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "Paramètres personnalisés pour votre contrôle {controlType}.",
"controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "Paramètres de {controlType}",
"controls.optionsList.controlAndPopover.exists": "{negate, plural, one {Existe} many {Existent} other {Existent}}",
"controls.optionsList.errors.dataViewNotFound": "Impossible de localiser la vue de données : {dataViewId}",
"controls.optionsList.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}",
"controls.optionsList.popover.ariaLabel": "Fenêtre contextuelle pour le contrôle {fieldName}",
"controls.optionsList.popover.cardinalityLabel": "{totalOptions, number} {totalOptions, plural, one {option} many {options disponibles} other {options}}",
@ -409,7 +408,6 @@
"controls.optionsList.popover.invalidSelectionsLabel": "{selectedOptions} {selectedOptions, plural, one {sélection ignorée} many {Sélections ignorées} other {sélections ignorées}}",
"controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, one {Sélection ignorée} many {Sélections ignorées} other {Sélections ignorées}}",
"controls.optionsList.popover.suggestionsAriaLabel": "{optionCount, plural, one {option disponible} many {options disponibles} other {options disponibles}} pour {fieldName}",
"controls.rangeSlider.errors.dataViewNotFound": "Impossible de localiser la vue de données : {dataViewId}",
"controls.rangeSlider.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}",
"controls.controlGroup.emptyState.addControlButtonTitle": "Ajouter un contrôle",
"controls.controlGroup.emptyState.badgeText": "Nouveauté",

View file

@ -399,7 +399,6 @@
"controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "{controlType}コントロールのカスタム設定",
"controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "{controlType}設定",
"controls.optionsList.controlAndPopover.exists": "{negate, plural, other {存在します}}",
"controls.optionsList.errors.dataViewNotFound": "データビューが見つかりませんでした:{dataViewId}",
"controls.optionsList.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}",
"controls.optionsList.popover.ariaLabel": "{fieldName}コントロールのポップオーバー",
"controls.optionsList.popover.cardinalityLabel": "{totalOptions, number}{totalOptions, plural, other {オプション}}",
@ -409,7 +408,6 @@
"controls.optionsList.popover.invalidSelectionsLabel": "{selectedOptions}{selectedOptions, plural, other {選択項目}}が無視されました",
"controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, other {選択項目}}が無視されました",
"controls.optionsList.popover.suggestionsAriaLabel": "{fieldName}の{optionCount, plural, other {オプション}}があります",
"controls.rangeSlider.errors.dataViewNotFound": "データビューが見つかりませんでした:{dataViewId}",
"controls.rangeSlider.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}",
"controls.controlGroup.emptyState.addControlButtonTitle": "コントロールを追加",
"controls.controlGroup.emptyState.badgeText": "新規",

View file

@ -399,7 +399,6 @@
"controls.controlGroup.manageControl.controlTypeSettings.formGroupDescription": "{controlType} 控件的定制设置。",
"controls.controlGroup.manageControl.controlTypeSettings.formGroupTitle": "{controlType} 设置",
"controls.optionsList.controlAndPopover.exists": "{negate, plural, other {存在}}",
"controls.optionsList.errors.dataViewNotFound": "找不到数据视图:{dataViewId}",
"controls.optionsList.errors.fieldNotFound": "找不到字段:{fieldName}",
"controls.optionsList.popover.ariaLabel": "{fieldName} 控件的弹出框",
"controls.optionsList.popover.cardinalityLabel": "{totalOptions, number} 个{totalOptions, plural, other {选项}}",
@ -409,7 +408,6 @@
"controls.optionsList.popover.invalidSelectionsLabel": "已忽略 {selectedOptions} 个{selectedOptions, plural, other {选择的内容}}",
"controls.optionsList.popover.invalidSelectionsSectionTitle": "已忽略{invalidSelectionCount, plural, other {选择的内容}}",
"controls.optionsList.popover.suggestionsAriaLabel": "{fieldName} 的可用{optionCount, plural, other {选项}}",
"controls.rangeSlider.errors.dataViewNotFound": "找不到数据视图:{dataViewId}",
"controls.rangeSlider.errors.fieldNotFound": "找不到字段:{fieldName}",
"controls.controlGroup.emptyState.addControlButtonTitle": "添加控件",
"controls.controlGroup.emptyState.badgeText": "新建",