@@ -85,16 +100,6 @@ export function DocViewTable({
const isCollapsible = value.length > COLLAPSE_LINE_LENGTH;
const isCollapsed = isCollapsible && !fieldRowOpen[field];
- const toggleColumn =
- onRemoveColumn && onAddColumn && Array.isArray(columns)
- ? () => {
- if (columns.includes(field)) {
- onRemoveColumn(field);
- } else {
- onAddColumn(field);
- }
- }
- : undefined;
const displayUnderscoreWarning = !mapping(field) && field.indexOf('_') === 0;
const fieldType = isNestedFieldParent(field, indexPattern)
@@ -109,10 +114,10 @@ export function DocViewTable({
displayUnderscoreWarning={displayUnderscoreWarning}
isCollapsed={isCollapsed}
isCollapsible={isCollapsible}
- isColumnActive={Array.isArray(columns) && columns.includes(field)}
+ isColumnActive={!!columns?.includes(field)}
onFilter={filter}
onToggleCollapse={() => toggleValueCollapse(field)}
- onToggleColumn={toggleColumn}
+ onToggleColumn={() => toggleColumn(field)}
value={value}
valueRaw={valueRaw}
/>
@@ -123,7 +128,7 @@ export function DocViewTable({
data-test-subj={`tableDocViewRow-multifieldsTitle-${field}`}
>
|
-
+ |
{i18n.translate('discover.fieldChooser.discoverField.multiFields', {
defaultMessage: 'Multi fields',
@@ -142,10 +147,12 @@ export function DocViewTable({
displayUnderscoreWarning={displayUnderscoreWarning}
isCollapsed={isCollapsed}
isCollapsible={isCollapsible}
- isColumnActive={Array.isArray(columns) && columns.includes(field)}
+ isColumnActive={Array.isArray(columns) && columns.includes(multiField)}
onFilter={filter}
- onToggleCollapse={() => toggleValueCollapse(field)}
- onToggleColumn={toggleColumn}
+ onToggleCollapse={() => {
+ toggleValueCollapse(multiField);
+ }}
+ onToggleColumn={() => toggleColumn(multiField)}
value={value}
valueRaw={valueRaw}
/>
diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts
index 89cb60700074d1..30edb102c420aa 100644
--- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts
+++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { ISearchSource } from 'src/plugins/data/public';
import { getTopNavLinks } from './get_top_nav_links';
import { inspectorPluginMock } from '../../../../../inspector/public/mocks';
import { indexPatternMock } from '../../../__mocks__/index_pattern';
@@ -33,6 +34,7 @@ test('getTopNavLinks result', () => {
savedSearch: savedSearchMock,
services,
state,
+ searchSource: {} as ISearchSource,
});
expect(topNavLinks).toMatchInlineSnapshot(`
Array [
diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts
index 513508c478aa95..a1215836f9c5fc 100644
--- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts
+++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts
@@ -15,7 +15,7 @@ import { Adapters } from '../../../../../inspector/common/adapters';
import { SavedSearch } from '../../../saved_searches';
import { onSaveSearch } from './on_save_search';
import { GetStateReturn } from '../../angular/discover_state';
-import { IndexPattern } from '../../../kibana_services';
+import { IndexPattern, ISearchSource } from '../../../kibana_services';
/**
* Helper function to build the top nav links
@@ -29,6 +29,7 @@ export const getTopNavLinks = ({
services,
state,
onOpenInspector,
+ searchSource,
}: {
getFieldCounts: () => Promise>;
indexPattern: IndexPattern;
@@ -38,6 +39,7 @@ export const getTopNavLinks = ({
services: DiscoverServices;
state: GetStateReturn;
onOpenInspector: () => void;
+ searchSource: ISearchSource;
}) => {
const newSearch = {
id: 'new',
@@ -93,7 +95,7 @@ export const getTopNavLinks = ({
return;
}
const sharingData = await getSharingData(
- savedSearch.searchSource,
+ searchSource,
state.appStateContainer.getState(),
services.uiSettings,
getFieldCounts
diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts
index 85acd575138f71..31de1f2f6ed66a 100644
--- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts
+++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts
@@ -9,7 +9,7 @@
import { Capabilities, IUiSettingsClient } from 'kibana/public';
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
import { getSortForSearchSource } from '../angular/doc_table';
-import { SearchSource } from '../../../../data/common';
+import { ISearchSource } from '../../../../data/common';
import { AppState } from '../angular/discover_state';
import { SortOrder } from '../../saved_searches/types';
@@ -39,7 +39,7 @@ const getSharingDataFields = async (
* Preparing data to share the current state as link or CSV/Report
*/
export async function getSharingData(
- currentSearchSource: SearchSource,
+ currentSearchSource: ISearchSource,
state: AppState,
config: IUiSettingsClient,
getFieldCounts: () => Promise>
diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts
index 06e90c93bc77c9..2e4ab90ee58e54 100644
--- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts
+++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts
@@ -35,7 +35,8 @@ export async function persistSavedSearch(
state: AppState;
}
) {
- updateSearchSource(savedSearch.searchSource, {
+ updateSearchSource({
+ persistentSearchSource: savedSearch.searchSource,
indexPattern,
services,
sort: state.sort as SortOrder[],
diff --git a/src/plugins/discover/public/application/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/helpers/update_search_source.test.ts
index 51586a6bccc234..97e2de3541d354 100644
--- a/src/plugins/discover/public/application/helpers/update_search_source.test.ts
+++ b/src/plugins/discover/public/application/helpers/update_search_source.test.ts
@@ -17,9 +17,12 @@ import { SortOrder } from '../../saved_searches/types';
describe('updateSearchSource', () => {
test('updates a given search source', async () => {
- const searchSourceMock = createSearchSourceMock({});
+ const persistentSearchSourceMock = createSearchSourceMock({});
+ const volatileSearchSourceMock = createSearchSourceMock({});
const sampleSize = 250;
- const result = updateSearchSource(searchSourceMock, {
+ updateSearchSource({
+ persistentSearchSource: persistentSearchSourceMock,
+ volatileSearchSource: volatileSearchSourceMock,
indexPattern: indexPatternMock,
services: ({
data: dataPluginMock.createStartContract(),
@@ -36,15 +39,18 @@ describe('updateSearchSource', () => {
columns: [],
useNewFieldsApi: false,
});
- expect(result.getField('index')).toEqual(indexPatternMock);
- expect(result.getField('size')).toEqual(sampleSize);
- expect(result.getField('fields')).toBe(undefined);
+ expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
+ expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
+ expect(volatileSearchSourceMock.getField('fields')).toBe(undefined);
});
test('updates a given search source with the usage of the new fields api', async () => {
- const searchSourceMock = createSearchSourceMock({});
+ const persistentSearchSourceMock = createSearchSourceMock({});
+ const volatileSearchSourceMock = createSearchSourceMock({});
const sampleSize = 250;
- const result = updateSearchSource(searchSourceMock, {
+ updateSearchSource({
+ persistentSearchSource: persistentSearchSourceMock,
+ volatileSearchSource: volatileSearchSourceMock,
indexPattern: indexPatternMock,
services: ({
data: dataPluginMock.createStartContract(),
@@ -61,16 +67,19 @@ describe('updateSearchSource', () => {
columns: [],
useNewFieldsApi: true,
});
- expect(result.getField('index')).toEqual(indexPatternMock);
- expect(result.getField('size')).toEqual(sampleSize);
- expect(result.getField('fields')).toEqual([{ field: '*' }]);
- expect(result.getField('fieldsFromSource')).toBe(undefined);
+ expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
+ expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
+ expect(volatileSearchSourceMock.getField('fields')).toEqual([{ field: '*' }]);
+ expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
});
test('requests unmapped fields when the flag is provided, using the new fields api', async () => {
- const searchSourceMock = createSearchSourceMock({});
+ const persistentSearchSourceMock = createSearchSourceMock({});
+ const volatileSearchSourceMock = createSearchSourceMock({});
const sampleSize = 250;
- const result = updateSearchSource(searchSourceMock, {
+ updateSearchSource({
+ persistentSearchSource: persistentSearchSourceMock,
+ volatileSearchSource: volatileSearchSourceMock,
indexPattern: indexPatternMock,
services: ({
data: dataPluginMock.createStartContract(),
@@ -88,16 +97,21 @@ describe('updateSearchSource', () => {
useNewFieldsApi: true,
showUnmappedFields: true,
});
- expect(result.getField('index')).toEqual(indexPatternMock);
- expect(result.getField('size')).toEqual(sampleSize);
- expect(result.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
- expect(result.getField('fieldsFromSource')).toBe(undefined);
+ expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
+ expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
+ expect(volatileSearchSourceMock.getField('fields')).toEqual([
+ { field: '*', include_unmapped: 'true' },
+ ]);
+ expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
});
test('updates a given search source when showUnmappedFields option is set to true', async () => {
- const searchSourceMock = createSearchSourceMock({});
+ const persistentSearchSourceMock = createSearchSourceMock({});
+ const volatileSearchSourceMock = createSearchSourceMock({});
const sampleSize = 250;
- const result = updateSearchSource(searchSourceMock, {
+ updateSearchSource({
+ persistentSearchSource: persistentSearchSourceMock,
+ volatileSearchSource: volatileSearchSourceMock,
indexPattern: indexPatternMock,
services: ({
data: dataPluginMock.createStartContract(),
@@ -115,9 +129,11 @@ describe('updateSearchSource', () => {
useNewFieldsApi: true,
showUnmappedFields: true,
});
- expect(result.getField('index')).toEqual(indexPatternMock);
- expect(result.getField('size')).toEqual(sampleSize);
- expect(result.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]);
- expect(result.getField('fieldsFromSource')).toBe(undefined);
+ expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock);
+ expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize);
+ expect(volatileSearchSourceMock.getField('fields')).toEqual([
+ { field: '*', include_unmapped: 'true' },
+ ]);
+ expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined);
});
});
diff --git a/src/plugins/discover/public/application/helpers/update_search_source.ts b/src/plugins/discover/public/application/helpers/update_search_source.ts
index 55d2b05a29b690..ba5ac0e8227965 100644
--- a/src/plugins/discover/public/application/helpers/update_search_source.ts
+++ b/src/plugins/discover/public/application/helpers/update_search_source.ts
@@ -15,24 +15,25 @@ import { DiscoverServices } from '../../build_services';
/**
* Helper function to update the given searchSource before fetching/sharing/persisting
*/
-export function updateSearchSource(
- searchSource: ISearchSource,
- {
- indexPattern,
- services,
- sort,
- columns,
- useNewFieldsApi,
- showUnmappedFields,
- }: {
- indexPattern: IndexPattern;
- services: DiscoverServices;
- sort: SortOrder[];
- columns: string[];
- useNewFieldsApi: boolean;
- showUnmappedFields?: boolean;
- }
-) {
+export function updateSearchSource({
+ indexPattern,
+ services,
+ sort,
+ columns,
+ useNewFieldsApi,
+ showUnmappedFields,
+ persistentSearchSource,
+ volatileSearchSource,
+}: {
+ indexPattern: IndexPattern;
+ services: DiscoverServices;
+ sort: SortOrder[];
+ columns: string[];
+ useNewFieldsApi: boolean;
+ showUnmappedFields?: boolean;
+ persistentSearchSource: ISearchSource;
+ volatileSearchSource?: ISearchSource;
+}) {
const { uiSettings, data } = services;
const usedSort = getSortForSearchSource(
sort,
@@ -40,23 +41,32 @@ export function updateSearchSource(
uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
);
- searchSource
+ persistentSearchSource
.setField('index', indexPattern)
- .setField('size', uiSettings.get(SAMPLE_SIZE_SETTING))
- .setField('sort', usedSort)
.setField('query', data.query.queryString.getQuery() || null)
.setField('filter', data.query.filterManager.getFilters());
- if (useNewFieldsApi) {
- searchSource.removeField('fieldsFromSource');
- const fields: Record = { field: '*' };
- if (showUnmappedFields) {
- fields.include_unmapped = 'true';
+
+ if (volatileSearchSource) {
+ volatileSearchSource
+ .setField('size', uiSettings.get(SAMPLE_SIZE_SETTING))
+ .setField('sort', usedSort)
+ .setField('highlightAll', true)
+ .setField('version', true)
+ // Even when searching rollups, we want to use the default strategy so that we get back a
+ // document-like response.
+ .setPreferredSearchStrategyId('default');
+
+ if (useNewFieldsApi) {
+ volatileSearchSource.removeField('fieldsFromSource');
+ const fields: Record = { field: '*' };
+ if (showUnmappedFields) {
+ fields.include_unmapped = 'true';
+ }
+ volatileSearchSource.setField('fields', [fields]);
+ } else {
+ volatileSearchSource.removeField('fields');
+ const fieldNames = indexPattern.fields.map((field) => field.name);
+ volatileSearchSource.setField('fieldsFromSource', fieldNames);
}
- searchSource.setField('fields', [fields]);
- } else {
- searchSource.removeField('fields');
- const fieldNames = indexPattern.fields.map((field) => field.name);
- searchSource.setField('fieldsFromSource', fieldNames);
}
- return searchSource;
}
diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts
index c53dfaff24112f..47161c2b8298e1 100644
--- a/src/plugins/discover/public/plugin.ts
+++ b/src/plugins/discover/public/plugin.ts
@@ -36,7 +36,7 @@ import { UrlGeneratorState } from '../../share/public';
import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types';
import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
import { DocViewTable } from './application/components/table/table';
-import { JsonCodeBlock } from './application/components/json_code_block/json_code_block';
+import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor';
import {
setDocViewsRegistry,
setUrlTracker,
@@ -187,7 +187,7 @@ export class DiscoverPlugin
defaultMessage: 'JSON',
}),
order: 20,
- component: JsonCodeBlock,
+ component: JsonCodeEditor,
});
const {
diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts
index a8ecb384f782b4..2dda0df1a85c5c 100644
--- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts
+++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts
@@ -44,8 +44,6 @@ describe('embeddable state transfer', () => {
const testAppId = 'testApp';
- const buildKey = (appId: string, key: string) => `${appId}-${key}`;
-
beforeEach(() => {
currentAppId$ = new Subject();
currentAppId$.next(originatingApp);
@@ -86,8 +84,10 @@ describe('embeddable state transfer', () => {
it('can send an outgoing editor state', async () => {
await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } });
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'superUltraTestDashboard',
+ [EMBEDDABLE_EDITOR_STATE_KEY]: {
+ [destinationApp]: {
+ originatingApp: 'superUltraTestDashboard',
+ },
},
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
@@ -104,8 +104,10 @@ describe('embeddable state transfer', () => {
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
- [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'superUltraTestDashboard',
+ [EMBEDDABLE_EDITOR_STATE_KEY]: {
+ [destinationApp]: {
+ originatingApp: 'superUltraTestDashboard',
+ },
},
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
@@ -125,9 +127,11 @@ describe('embeddable state transfer', () => {
state: { type: 'coolestType', input: { savedObjectId: '150' } },
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: {
- type: 'coolestType',
- input: { savedObjectId: '150' },
+ [EMBEDDABLE_PACKAGE_STATE_KEY]: {
+ [destinationApp]: {
+ type: 'coolestType',
+ input: { savedObjectId: '150' },
+ },
},
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
@@ -144,9 +148,11 @@ describe('embeddable state transfer', () => {
});
expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
kibanaIsNowForSports: 'extremeSportsKibana',
- [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: {
- type: 'coolestType',
- input: { savedObjectId: '150' },
+ [EMBEDDABLE_PACKAGE_STATE_KEY]: {
+ [destinationApp]: {
+ type: 'coolestType',
+ input: { savedObjectId: '150' },
+ },
},
});
expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', {
@@ -165,8 +171,10 @@ describe('embeddable state transfer', () => {
it('can fetch an incoming editor state', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'superUltraTestDashboard',
+ [EMBEDDABLE_EDITOR_STATE_KEY]: {
+ [testAppId]: {
+ originatingApp: 'superUltraTestDashboard',
+ },
},
});
const fetchedState = stateTransfer.getIncomingEditorState(testAppId);
@@ -175,14 +183,16 @@ describe('embeddable state transfer', () => {
it('can fetch an incoming editor state and ignore state for other apps', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'whoops not me',
- },
- [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'otherTestDashboard',
- },
- [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'superUltraTestDashboard',
+ [EMBEDDABLE_EDITOR_STATE_KEY]: {
+ otherApp1: {
+ originatingApp: 'whoops not me',
+ },
+ otherApp2: {
+ originatingApp: 'otherTestDashboard',
+ },
+ [testAppId]: {
+ originatingApp: 'superUltraTestDashboard',
+ },
},
});
const fetchedState = stateTransfer.getIncomingEditorState(testAppId);
@@ -194,8 +204,10 @@ describe('embeddable state transfer', () => {
it('incoming editor state returns undefined when state is not in the right shape', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: {
- helloSportsKibana: 'superUltraTestDashboard',
+ [EMBEDDABLE_EDITOR_STATE_KEY]: {
+ [testAppId]: {
+ helloSportsKibana: 'superUltraTestDashboard',
+ },
},
});
const fetchedState = stateTransfer.getIncomingEditorState(testAppId);
@@ -204,9 +216,11 @@ describe('embeddable state transfer', () => {
it('can fetch an incoming embeddable package state', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: {
- type: 'skisEmbeddable',
- input: { savedObjectId: '123' },
+ [EMBEDDABLE_PACKAGE_STATE_KEY]: {
+ [testAppId]: {
+ type: 'skisEmbeddable',
+ input: { savedObjectId: '123' },
+ },
},
});
const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId);
@@ -215,13 +229,15 @@ describe('embeddable state transfer', () => {
it('can fetch an incoming embeddable package state and ignore state for other apps', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: {
- type: 'skisEmbeddable',
- input: { savedObjectId: '123' },
- },
- [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: {
- type: 'crossCountryEmbeddable',
- input: { savedObjectId: '456' },
+ [EMBEDDABLE_PACKAGE_STATE_KEY]: {
+ [testAppId]: {
+ type: 'skisEmbeddable',
+ input: { savedObjectId: '123' },
+ },
+ testApp2: {
+ type: 'crossCountryEmbeddable',
+ input: { savedObjectId: '456' },
+ },
},
});
const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId);
@@ -236,7 +252,11 @@ describe('embeddable state transfer', () => {
it('embeddable package state returns undefined when state is not in the right shape', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' },
+ [EMBEDDABLE_PACKAGE_STATE_KEY]: {
+ [testAppId]: {
+ kibanaIsFor: 'sports',
+ },
+ },
});
const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId);
expect(fetchedState).toBeUndefined();
@@ -244,9 +264,11 @@ describe('embeddable state transfer', () => {
it('removes embeddable package key when removeAfterFetch is true', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: {
- type: 'coolestType',
- input: { savedObjectId: '150' },
+ [EMBEDDABLE_PACKAGE_STATE_KEY]: {
+ [testAppId]: {
+ type: 'coolestType',
+ input: { savedObjectId: '150' },
+ },
},
iSHouldStillbeHere: 'doing the sports thing',
});
@@ -258,8 +280,10 @@ describe('embeddable state transfer', () => {
it('removes editor state key when removeAfterFetch is true', async () => {
store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, {
- [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: {
- originatingApp: 'superCoolFootballDashboard',
+ [EMBEDDABLE_EDITOR_STATE_KEY]: {
+ [testAppId]: {
+ originatingApp: 'superCoolFootballDashboard',
+ },
},
iSHouldStillbeHere: 'doing the sports thing',
});
diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts
index 8664a5aae7345f..52a5eccac99105 100644
--- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts
+++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts
@@ -75,10 +75,14 @@ export class EmbeddableStateTransfer {
* @param appId - The app to fetch incomingEditorState for
* @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates.
*/
- public clearEditorState(appId: string) {
+ public clearEditorState(appId?: string) {
const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY);
if (currentState) {
- delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)];
+ if (appId) {
+ delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]?.[appId];
+ } else {
+ delete currentState[EMBEDDABLE_EDITOR_STATE_KEY];
+ }
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState);
}
}
@@ -117,7 +121,6 @@ export class EmbeddableStateTransfer {
this.isTransferInProgress = true;
await this.navigateToWithState(appId, EMBEDDABLE_EDITOR_STATE_KEY, {
...options,
- appendToExistingState: true,
});
}
@@ -132,14 +135,9 @@ export class EmbeddableStateTransfer {
this.isTransferInProgress = true;
await this.navigateToWithState(appId, EMBEDDABLE_PACKAGE_STATE_KEY, {
...options,
- appendToExistingState: true,
});
}
- private buildKey(appId: string, key: string) {
- return `${appId}-${key}`;
- }
-
private getIncomingState(
guard: (state: unknown) => state is IncomingStateType,
appId: string,
@@ -148,15 +146,13 @@ export class EmbeddableStateTransfer {
keysToRemoveAfterFetch?: string[];
}
): IncomingStateType | undefined {
- const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[
- this.buildKey(appId, key)
- ];
+ const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]?.[appId];
const castState =
!guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined;
if (castState && options?.keysToRemoveAfterFetch) {
const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) };
options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => {
- delete stateReplace[this.buildKey(appId, keyToRemove)];
+ delete stateReplace[keyToRemove];
});
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace);
}
@@ -166,14 +162,16 @@ export class EmbeddableStateTransfer {
private async navigateToWithState(
appId: string,
key: string,
- options?: { path?: string; state?: OutgoingStateType; appendToExistingState?: boolean }
+ options?: { path?: string; state?: OutgoingStateType }
): Promise {
- const stateObject = options?.appendToExistingState
- ? {
- ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY),
- [this.buildKey(appId, key)]: options.state,
- }
- : { [this.buildKey(appId, key)]: options?.state };
+ const existingAppState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key] || {};
+ const stateObject = {
+ ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY),
+ [key]: {
+ ...existingAppState,
+ [appId]: options?.state,
+ },
+ };
this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject);
await this.navigateToApp(appId, { path: options?.path });
}
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index 3e7014d54958de..189f71b85206bc 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -590,7 +590,7 @@ export class EmbeddableStateTransfer {
// Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts
constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage);
- clearEditorState(appId: string): void;
+ clearEditorState(appId?: string): void;
getAppNameFromId: (appId: string) => string | undefined;
getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined;
getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined;
diff --git a/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts b/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts
index 5244a6c1e8bf17..3ef33b651f4d26 100644
--- a/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts
+++ b/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts
@@ -41,20 +41,26 @@ export const createSendRequestHelpers = (): SendRequestHelpers => {
// Set up successful request helpers.
sendRequestSpy
- .withArgs(successRequest.path, {
- body: JSON.stringify(successRequest.body),
- query: undefined,
- })
+ .withArgs(
+ successRequest.path,
+ sinon.match({
+ body: JSON.stringify(successRequest.body),
+ query: undefined,
+ })
+ )
.resolves(successResponse);
const sendSuccessRequest = () => sendRequest({ ...successRequest });
const getSuccessResponse = () => ({ data: successResponse.data, error: null });
// Set up failed request helpers.
sendRequestSpy
- .withArgs(errorRequest.path, {
- body: JSON.stringify(errorRequest.body),
- query: undefined,
- })
+ .withArgs(
+ errorRequest.path,
+ sinon.match({
+ body: JSON.stringify(errorRequest.body),
+ query: undefined,
+ })
+ )
.rejects(errorResponse);
const sendErrorRequest = () => sendRequest({ ...errorRequest });
const getErrorResponse = () => ({
diff --git a/src/plugins/es_ui_shared/public/request/send_request.ts b/src/plugins/es_ui_shared/public/request/send_request.ts
index 32703f21a4668a..11ab99cfb69786 100644
--- a/src/plugins/es_ui_shared/public/request/send_request.ts
+++ b/src/plugins/es_ui_shared/public/request/send_request.ts
@@ -13,6 +13,11 @@ export interface SendRequestConfig {
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head';
query?: HttpFetchQuery;
body?: any;
+ /**
+ * If set, flags this as a "system request" to indicate that this is not a user-initiated request. For more information, see
+ * HttpFetchOptions#asSystemRequest.
+ */
+ asSystemRequest?: boolean;
}
export interface SendRequestResponse {
@@ -22,11 +27,15 @@ export interface SendRequestResponse {
export const sendRequest = async (
httpClient: HttpSetup,
- { path, method, body, query }: SendRequestConfig
+ { path, method, body, query, asSystemRequest }: SendRequestConfig
): Promise> => {
try {
const stringifiedBody = typeof body === 'string' ? body : JSON.stringify(body);
- const response = await httpClient[method](path, { body: stringifiedBody, query });
+ const response = await httpClient[method](path, {
+ body: stringifiedBody,
+ query,
+ asSystemRequest,
+ });
return {
data: response.data ? response.data : response,
diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
index 9f41d13112bc88..82d3764dbf72ab 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
+++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
@@ -123,10 +123,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => {
// Set up successful request helpers.
sendRequestSpy
- .withArgs(successRequest.path, {
- body: JSON.stringify(successRequest.body),
- query: undefined,
- })
+ .withArgs(
+ successRequest.path,
+ sinon.match({
+ body: JSON.stringify(successRequest.body),
+ query: undefined,
+ })
+ )
.resolves(successResponse);
const setupSuccessRequest = (overrides = {}, requestTimings?: number[]) =>
setupUseRequest({ ...successRequest, ...overrides }, requestTimings);
@@ -134,10 +137,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => {
// Set up failed request helpers.
sendRequestSpy
- .withArgs(errorRequest.path, {
- body: JSON.stringify(errorRequest.body),
- query: undefined,
- })
+ .withArgs(
+ errorRequest.path,
+ sinon.match({
+ body: JSON.stringify(errorRequest.body),
+ query: undefined,
+ })
+ )
.rejects(errorResponse);
const setupErrorRequest = (overrides = {}, requestTimings?: number[]) =>
setupUseRequest({ ...errorRequest, ...overrides }, requestTimings);
@@ -152,10 +158,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => {
// Set up failed request helpers with the alternative error shape.
sendRequestSpy
- .withArgs(errorWithBodyRequest.path, {
- body: JSON.stringify(errorWithBodyRequest.body),
- query: undefined,
- })
+ .withArgs(
+ errorWithBodyRequest.path,
+ sinon.match({
+ body: JSON.stringify(errorWithBodyRequest.body),
+ query: undefined,
+ })
+ )
.rejects(errorWithBodyResponse);
const setupErrorWithBodyRequest = (overrides = {}) =>
setupUseRequest({ ...errorWithBodyRequest, ...overrides });
diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts
index 99eb38ff6023fa..33085bdbf4478d 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.ts
+++ b/src/plugins/es_ui_shared/public/request/use_request.ts
@@ -65,49 +65,59 @@ export const useRequest = (
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [path, method, queryStringified, bodyStringified]);
- const resendRequest = useCallback(async () => {
- // If we're on an interval, this allows us to reset it if the user has manually requested the
- // data, to avoid doubled-up requests.
- clearPollInterval();
-
- const requestId = ++requestCountRef.current;
-
- // We don't clear error or data, so it's up to the consumer to decide whether to display the
- // "old" error/data or loading state when a new request is in-flight.
- setIsLoading(true);
-
- const response = await sendRequest(httpClient, requestBody);
- const { data: serializedResponseData, error: responseError } = response;
-
- const isOutdatedRequest = requestId !== requestCountRef.current;
- const isUnmounted = isMounted.current === false;
-
- // Ignore outdated or irrelevant data.
- if (isOutdatedRequest || isUnmounted) {
- return;
- }
-
- // Surface to consumers that at least one request has resolved.
- isInitialRequestRef.current = false;
+ const resendRequest = useCallback(
+ async (asSystemRequest?: boolean) => {
+ // If we're on an interval, this allows us to reset it if the user has manually requested the
+ // data, to avoid doubled-up requests.
+ clearPollInterval();
- setError(responseError);
- // If there's an error, keep the data from the last request in case it's still useful to the user.
- if (!responseError) {
- const responseData = deserializer
- ? deserializer(serializedResponseData)
- : serializedResponseData;
- setData(responseData);
- }
- // Setting isLoading to false also acts as a signal for scheduling the next poll request.
- setIsLoading(false);
- }, [requestBody, httpClient, deserializer, clearPollInterval]);
+ const requestId = ++requestCountRef.current;
+
+ // We don't clear error or data, so it's up to the consumer to decide whether to display the
+ // "old" error/data or loading state when a new request is in-flight.
+ setIsLoading(true);
+
+ // Any requests that are sent in the background (without user interaction) should be flagged as "system requests". This should not be
+ // confused with any terminology in Elasticsearch. This is a Kibana-specific construct that allows the server to differentiate between
+ // user-initiated and requests "system"-initiated requests, for purposes like security features.
+ const requestPayload = { ...requestBody, asSystemRequest };
+ const response = await sendRequest(httpClient, requestPayload);
+ const { data: serializedResponseData, error: responseError } = response;
+
+ const isOutdatedRequest = requestId !== requestCountRef.current;
+ const isUnmounted = isMounted.current === false;
+
+ // Ignore outdated or irrelevant data.
+ if (isOutdatedRequest || isUnmounted) {
+ return;
+ }
+
+ // Surface to consumers that at least one request has resolved.
+ isInitialRequestRef.current = false;
+
+ setError(responseError);
+ // If there's an error, keep the data from the last request in case it's still useful to the user.
+ if (!responseError) {
+ const responseData = deserializer
+ ? deserializer(serializedResponseData)
+ : serializedResponseData;
+ setData(responseData);
+ }
+ // Setting isLoading to false also acts as a signal for scheduling the next poll request.
+ setIsLoading(false);
+ },
+ [requestBody, httpClient, deserializer, clearPollInterval]
+ );
const scheduleRequest = useCallback(() => {
// If there's a scheduled poll request, this new one will supersede it.
clearPollInterval();
if (pollIntervalMs) {
- pollIntervalIdRef.current = setTimeout(resendRequest, pollIntervalMs);
+ pollIntervalIdRef.current = setTimeout(
+ () => resendRequest(true), // This is happening on an interval in the background, so we flag it as a "system request".
+ pollIntervalMs
+ );
}
}, [pollIntervalMs, resendRequest, clearPollInterval]);
@@ -137,11 +147,15 @@ export const useRequest = (
};
}, [clearPollInterval]);
+ const resendRequestForConsumer = useCallback(() => {
+ return resendRequest();
+ }, [resendRequest]);
+
return {
isInitialRequest: isInitialRequestRef.current,
isLoading,
error,
data,
- resendRequest, // Gives the user the ability to manually request data
+ resendRequest: resendRequestForConsumer, // Gives the user the ability to manually request data
};
};
diff --git a/src/plugins/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts
index 7f6141b60e9a78..33bb7826917473 100644
--- a/src/plugins/expressions/common/execution/execution.abortion.test.ts
+++ b/src/plugins/expressions/common/execution/execution.abortion.test.ts
@@ -6,9 +6,11 @@
* Side Public License, v 1.
*/
+import { waitFor } from '@testing-library/react';
import { Execution } from './execution';
import { parseExpression } from '../ast';
import { createUnitTestExecutor } from '../test_helpers';
+import { ExpressionFunctionDefinition } from '../expression_functions';
jest.useFakeTimers();
@@ -81,4 +83,73 @@ describe('Execution abortion tests', () => {
jest.useFakeTimers();
});
+
+ test('nested expressions are aborted when parent aborted', async () => {
+ jest.useRealTimers();
+ const started = jest.fn();
+ const completed = jest.fn();
+ const aborted = jest.fn();
+
+ const defer: ExpressionFunctionDefinition<'defer', any, { time: number }, any> = {
+ name: 'defer',
+ args: {
+ time: {
+ aliases: ['_'],
+ help: 'Calls function from a context after delay unless aborted',
+ types: ['number'],
+ },
+ },
+ help: '',
+ fn: async (input, args, { abortSignal }) => {
+ started();
+ await new Promise((r) => {
+ const timeout = setTimeout(() => {
+ if (!abortSignal.aborted) {
+ completed();
+ }
+ r(undefined);
+ }, args.time);
+
+ abortSignal.addEventListener('abort', () => {
+ aborted();
+ clearTimeout(timeout);
+ r(undefined);
+ });
+ });
+
+ return args.time;
+ },
+ };
+
+ const expression = 'defer time={defer time={defer time=300}}';
+ const executor = createUnitTestExecutor();
+ executor.registerFunction(defer);
+ const execution = new Execution({
+ executor,
+ ast: parseExpression(expression),
+ params: {},
+ });
+
+ execution.start();
+
+ await waitFor(() => expect(started).toHaveBeenCalledTimes(1));
+
+ execution.cancel();
+ const result = await execution.result;
+ expect(result).toMatchObject({
+ type: 'error',
+ error: {
+ message: 'The expression was aborted.',
+ name: 'AbortError',
+ },
+ });
+
+ await waitFor(() => expect(aborted).toHaveBeenCalledTimes(1));
+
+ expect(started).toHaveBeenCalledTimes(1);
+ expect(aborted).toHaveBeenCalledTimes(1);
+ expect(completed).toHaveBeenCalledTimes(0);
+
+ jest.useFakeTimers();
+ });
});
diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts
index e555258a0cf998..bf545a0075bed7 100644
--- a/src/plugins/expressions/common/execution/execution.ts
+++ b/src/plugins/expressions/common/execution/execution.ts
@@ -120,6 +120,13 @@ export class Execution<
*/
private readonly firstResultFuture = new Defer |