([]);
const savedDashboard = useSavedDashboard(savedDashboardId, history);
@@ -68,9 +68,13 @@ export function DashboardApp({
history
);
const dashboardContainer = useDashboardContainer(dashboardStateManager, history, false);
+ const searchSessionIdQuery$ = useMemo(
+ () => createQueryParamObservable(history, DashboardConstants.SEARCH_SESSION_ID),
+ [history]
+ );
const refreshDashboardContainer = useCallback(
- (lastReloadRequestTime?: number) => {
+ (force?: boolean) => {
if (!dashboardContainer || !dashboardStateManager) {
return;
}
@@ -80,7 +84,7 @@ export function DashboardApp({
appStateDashboardInput: getDashboardContainerInput({
isEmbeddedExternally: Boolean(embedSettings),
dashboardStateManager,
- lastReloadRequestTime,
+ lastReloadRequestTime: force ? Date.now() : undefined,
dashboardCapabilities,
query: data.query,
}),
@@ -100,10 +104,35 @@ export function DashboardApp({
const shouldRefetch = Object.keys(changes).some(
(changeKey) => !noRefetchKeys.includes(changeKey as keyof DashboardContainerInput)
);
- if (getSearchSessionIdFromURL(history)) {
- // going away from a background search results
- removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true);
- }
+
+ const newSearchSessionId: string | undefined = (() => {
+ // do not update session id if this is irrelevant state change to prevent excessive searches
+ if (!shouldRefetch) return;
+
+ let searchSessionIdFromURL = getSearchSessionIdFromURL(history);
+ if (searchSessionIdFromURL) {
+ if (
+ data.search.session.isRestore() &&
+ data.search.session.isCurrentSession(searchSessionIdFromURL)
+ ) {
+ // navigating away from a restored session
+ dashboardStateManager.kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => {
+ if (nextUrl.includes(DashboardConstants.SEARCH_SESSION_ID)) {
+ return replaceUrlHashQuery(nextUrl, (query) => {
+ delete query[DashboardConstants.SEARCH_SESSION_ID];
+ return query;
+ });
+ }
+ return nextUrl;
+ });
+ searchSessionIdFromURL = undefined;
+ } else {
+ data.search.session.restore(searchSessionIdFromURL);
+ }
+ }
+
+ return searchSessionIdFromURL ?? data.search.session.start();
+ })();
if (changes.viewMode) {
setViewMode(changes.viewMode);
@@ -111,8 +140,7 @@ export function DashboardApp({
dashboardContainer.updateInput({
...changes,
- // do not start a new session if this is irrelevant state change to prevent excessive searches
- ...(shouldRefetch && { searchSessionId: data.search.session.start() }),
+ ...(newSearchSessionId && { searchSessionId: newSearchSessionId }),
});
}
},
@@ -159,23 +187,42 @@ export function DashboardApp({
subscriptions.add(
merge(
...[timeFilter.getRefreshIntervalUpdate$(), timeFilter.getTimeUpdate$()]
- ).subscribe(() => refreshDashboardContainer())
+ ).subscribe(() => triggerRefresh$.next())
);
+
subscriptions.add(
merge(
data.search.session.onRefresh$,
- data.query.timefilter.timefilter.getAutoRefreshFetch$()
+ data.query.timefilter.timefilter.getAutoRefreshFetch$(),
+ searchSessionIdQuery$
).subscribe(() => {
- setLastReloadTime(() => new Date().getTime());
+ triggerRefresh$.next({ force: true });
})
);
dashboardStateManager.registerChangeListener(() => {
// we aren't checking dirty state because there are changes the container needs to know about
// that won't make the dashboard "dirty" - like a view mode change.
- refreshDashboardContainer();
+ triggerRefresh$.next();
});
+ // debounce `refreshDashboardContainer()`
+ // use `forceRefresh=true` in case at least one debounced trigger asked for it
+ let forceRefresh: boolean = false;
+ subscriptions.add(
+ triggerRefresh$
+ .pipe(
+ tap((trigger) => {
+ forceRefresh = forceRefresh || (trigger?.force ?? false);
+ }),
+ debounceTime(50)
+ )
+ .subscribe(() => {
+ refreshDashboardContainer(forceRefresh);
+ forceRefresh = false;
+ })
+ );
+
return () => {
subscriptions.unsubscribe();
};
@@ -187,6 +234,8 @@ export function DashboardApp({
data.search.session,
indexPatternService,
dashboardStateManager,
+ searchSessionIdQuery$,
+ triggerRefresh$,
refreshDashboardContainer,
]);
@@ -216,11 +265,6 @@ export function DashboardApp({
};
}, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]);
- // Refresh the dashboard container when lastReloadTime changes
- useEffect(() => {
- refreshDashboardContainer(lastReloadTime);
- }, [lastReloadTime, refreshDashboardContainer]);
-
return (
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (
@@ -242,7 +286,7 @@ export function DashboardApp({
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
- setLastReloadTime(() => new Date().getTime());
+ triggerRefresh$.next({ force: true });
}
}}
/>
diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx
index 9141f2e592fd7..5206c76f50be2 100644
--- a/src/plugins/dashboard/public/application/dashboard_router.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_router.tsx
@@ -104,6 +104,7 @@ export async function mountApp({
mapsCapabilities: { save: Boolean(coreStart.application.capabilities.maps?.save) },
createShortUrl: Boolean(coreStart.application.capabilities.dashboard.createShortUrl),
visualizeCapabilities: { save: Boolean(coreStart.application.capabilities.visualize?.save) },
+ storeSearchSession: Boolean(coreStart.application.capabilities.dashboard.storeSearchSession),
},
};
diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts
index 90706a11b8ce2..c52bd1b4d47b8 100644
--- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts
+++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts
@@ -72,7 +72,7 @@ export class DashboardStateManager {
>;
private readonly stateContainerChangeSub: Subscription;
private readonly STATE_STORAGE_KEY = '_a';
- private readonly kbnUrlStateStorage: IKbnUrlStateStorage;
+ public readonly kbnUrlStateStorage: IKbnUrlStateStorage;
private readonly stateSyncRef: ISyncStateRef;
private readonly history: History;
private readonly usageCollection: UsageCollectionSetup | undefined;
@@ -596,7 +596,7 @@ export class DashboardStateManager {
this.toUrlState(this.stateContainer.get())
);
// immediately forces scheduled updates and changes location
- return this.kbnUrlStateStorage.flush({ replace });
+ return !!this.kbnUrlStateStorage.kbnUrlControls.flush(replace);
}
// TODO: find nicer solution for this
diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
index fa520ec22497b..780eb1bad8c2b 100644
--- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
@@ -89,7 +89,7 @@ export interface InheritedChildInput extends IndexSignature {
export type DashboardReactContextValue = KibanaReactContextValue;
export type DashboardReactContext = KibanaReactContext;
-const defaultCapabilities = {
+const defaultCapabilities: DashboardCapabilities = {
show: false,
createNew: false,
saveQuery: false,
@@ -97,6 +97,7 @@ const defaultCapabilities = {
hideWriteControls: true,
mapsCapabilities: { save: false },
visualizeCapabilities: { save: false },
+ storeSearchSession: true,
};
export class DashboardContainer extends Container {
diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts
index a4044e8668e59..93fbb50950850 100644
--- a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts
+++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts
@@ -16,6 +16,7 @@ import { useKibana } from '../../services/kibana_react';
import {
connectToQueryState,
esFilters,
+ noSearchSessionStorageCapabilityMessage,
QueryState,
syncQueryStateWithUrl,
} from '../../services/data';
@@ -159,13 +160,22 @@ export const useDashboardStateManager = (
stateManager.isNew()
);
- searchSession.setSearchSessionInfoProvider(
+ searchSession.enableStorage(
createSessionRestorationDataProvider({
data: dataPlugin,
getDashboardTitle: () => dashboardTitle,
getDashboardId: () => savedDashboard?.id || '',
getAppState: () => stateManager.getAppState(),
- })
+ }),
+ {
+ isDisabled: () =>
+ dashboardCapabilities.storeSearchSession
+ ? { disabled: false }
+ : {
+ disabled: true,
+ reasonText: noSearchSessionStorageCapabilityMessage,
+ },
+ }
);
setDashboardStateManager(stateManager);
@@ -192,6 +202,7 @@ export const useDashboardStateManager = (
toasts,
uiSettings,
usageCollection,
+ dashboardCapabilities.storeSearchSession,
]);
return { dashboardStateManager, viewMode, setViewMode };
diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
index bce8a661634f6..faec6b4f6f24b 100644
--- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
@@ -4,10 +4,16 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
- getTableColumns((id) => redirectTo({ destination: 'dashboard', id }), savedObjectsTagging),
- [savedObjectsTagging, redirectTo]
+ getTableColumns(
+ core.application,
+ kbnUrlStateStorage,
+ core.uiSettings.get('state:storeInSessionStorage'),
+ savedObjectsTagging
+ ),
+ [core.application, core.uiSettings, kbnUrlStateStorage, savedObjectsTagging]
);
const noItemsFragment = useMemo(
@@ -99,7 +103,6 @@ export const DashboardListing = ({
(filter: string) => {
let searchTerm = filter;
let references: SavedObjectsFindOptionsReference[] | undefined;
-
if (savedObjectsTagging) {
const parsed = savedObjectsTagging.ui.parseSearchQuery(filter, {
useName: true,
@@ -164,7 +167,9 @@ export const DashboardListing = ({
};
const getTableColumns = (
- redirectTo: (id?: string) => void,
+ application: ApplicationStart,
+ kbnUrlStateStorage: IKbnUrlStateStorage,
+ useHash: boolean,
savedObjectsTagging?: SavedObjectsTaggingApi
) => {
return [
@@ -172,9 +177,15 @@ const getTableColumns = (
field: 'title',
name: dashboardListingTable.getTitleColumnName(),
sortable: true,
- render: (field: string, record: { id: string; title: string }) => (
+ render: (field: string, record: { id: string; title: string; timeRestore: boolean }) => (
redirectTo(record.id)}
+ href={getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ useHash,
+ record.id,
+ record.timeRestore
+ )}
data-test-subj={`dashboardListingTitleLink-${record.title.split(' ').join('-')}`}
>
{field}
diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts
new file mode 100644
index 0000000000000..6dbc76803af90
--- /dev/null
+++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { getDashboardListItemLink } from './get_dashboard_list_item_link';
+import { ApplicationStart } from 'kibana/public';
+import { esFilters } from '../../../../data/public';
+import { createHashHistory } from 'history';
+import { createKbnUrlStateStorage } from '../../../../kibana_utils/public';
+import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator';
+
+const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647';
+
+const application = ({
+ getUrlForApp: jest.fn((appId: string, options?: { path?: string; absolute?: boolean }) => {
+ return `/app/${appId}${options?.path}`;
+ }),
+} as unknown) as ApplicationStart;
+
+const history = createHashHistory();
+const kbnUrlStateStorage = createKbnUrlStateStorage({
+ history,
+ useHash: false,
+});
+kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } });
+
+describe('listing dashboard link', () => {
+ test('creates a link to a dashboard without the timerange query if time is saved on the dashboard', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ true
+ );
+ expect(url).toMatchInlineSnapshot(`"/app/dashboards#/view/${DASHBOARD_ID}?_g=()"`);
+ });
+
+ test('creates a link to a dashboard with the timerange query if time is not saved on the dashboard', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:now-7d,to:now))"`
+ );
+ });
+});
+
+describe('when global time changes', () => {
+ beforeEach(() => {
+ kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
+ time: {
+ from: '2021-01-05T11:45:53.375Z',
+ to: '2021-01-21T11:46:00.990Z',
+ },
+ });
+ });
+
+ test('propagates the correct time on the query', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"`
+ );
+ });
+});
+
+describe('when global refreshInterval changes', () => {
+ beforeEach(() => {
+ kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
+ refreshInterval: { pause: false, value: 300 },
+ });
+ });
+
+ test('propagates the refreshInterval on the query', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(refreshInterval:(pause:!f,value:300))"`
+ );
+ });
+});
+
+describe('when global filters change', () => {
+ beforeEach(() => {
+ const filters = [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'q1' },
+ },
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'q1' },
+ $state: {
+ store: esFilters.FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ];
+ kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
+ filters,
+ });
+ });
+
+ test('propagates the filters on the query', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"`
+ );
+ });
+});
diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts
new file mode 100644
index 0000000000000..d14638b9e231f
--- /dev/null
+++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+import { ApplicationStart } from 'kibana/public';
+import { QueryState } from '../../../../data/public';
+import { setStateToKbnUrl } from '../../../../kibana_utils/public';
+import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants';
+import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator';
+import { IKbnUrlStateStorage } from '../../services/kibana_utils';
+
+export const getDashboardListItemLink = (
+ application: ApplicationStart,
+ kbnUrlStateStorage: IKbnUrlStateStorage,
+ useHash: boolean,
+ id: string,
+ timeRestore: boolean
+) => {
+ let url = application.getUrlForApp(DashboardConstants.DASHBOARDS_ID, {
+ path: `#${createDashboardEditUrl(id)}`,
+ });
+ const globalStateInUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {};
+
+ if (timeRestore) {
+ delete globalStateInUrl.time;
+ delete globalStateInUrl.refreshInterval;
+ }
+ url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url);
+ return url;
+};
diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx
new file mode 100644
index 0000000000000..d4703d14627a4
--- /dev/null
+++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Capabilities } from 'src/core/public';
+import { showPublicUrlSwitch } from './show_share_modal';
+
+describe('showPublicUrlSwitch', () => {
+ test('returns false if "dashboard" app is not available', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(false);
+ });
+
+ test('returns false if "dashboard" app is not accessible', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ dashboard: {
+ show: false,
+ },
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(false);
+ });
+
+ test('returns true if "dashboard" app is not available an accessible', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ dashboard: {
+ show: true,
+ },
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(true);
+ });
+});
diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx
index 660e7635eb99d..fe4f8ea411289 100644
--- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx
+++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx
@@ -6,6 +6,7 @@
* Public License, v 1.
*/
+import { Capabilities } from 'src/core/public';
import { EuiCheckboxGroup } from '@elastic/eui';
import React from 'react';
import { ReactElement, useState } from 'react';
@@ -27,6 +28,14 @@ interface ShowShareModalProps {
dashboardStateManager: DashboardStateManager;
}
+export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
+ if (!anonymousUserCapabilities.dashboard) return false;
+
+ const dashboard = (anonymousUserCapabilities.dashboard as unknown) as DashboardCapabilities;
+
+ return !!dashboard.show;
+};
+
export function ShowShareModal({
share,
anchorElement,
@@ -94,7 +103,7 @@ export function ShowShareModal({
share.toggleShareContextMenu({
anchorElement,
allowEmbed: true,
- allowShortUrl: !dashboardCapabilities.hideWriteControls || dashboardCapabilities.createShortUrl,
+ allowShortUrl: dashboardCapabilities.createShortUrl,
shareableUrl: setStateToKbnUrl(
'_a',
dashboardStateManager.getAppState(),
@@ -113,5 +122,6 @@ export function ShowShareModal({
component: EmbedUrlParamExtension,
},
],
+ showPublicUrlSwitch,
});
}
diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts
index 61e16beed61f4..e4f9388a919d1 100644
--- a/src/plugins/dashboard/public/application/types.ts
+++ b/src/plugins/dashboard/public/application/types.ts
@@ -55,6 +55,7 @@ export interface DashboardCapabilities {
saveQuery: boolean;
createNew: boolean;
show: boolean;
+ storeSearchSession: boolean;
}
export interface DashboardAppServices {
diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts
index cc784f5f81c9e..4bd43d1cd64a9 100644
--- a/src/plugins/dashboard/server/index.ts
+++ b/src/plugins/dashboard/server/index.ts
@@ -25,3 +25,4 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export { DashboardPluginSetup, DashboardPluginStart } from './types';
+export { findByValueEmbeddables } from './usage/find_by_value_embeddables';
diff --git a/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts b/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts
new file mode 100644
index 0000000000000..3da6a8050f14c
--- /dev/null
+++ b/src/plugins/dashboard/server/usage/find_by_value_embeddables.test.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { SavedDashboardPanel730ToLatest } from '../../common';
+import { findByValueEmbeddables } from './find_by_value_embeddables';
+
+const visualizationByValue = ({
+ embeddableConfig: {
+ value: 'visualization-by-value',
+ },
+ type: 'visualization',
+} as unknown) as SavedDashboardPanel730ToLatest;
+
+const mapByValue = ({
+ embeddableConfig: {
+ value: 'map-by-value',
+ },
+ type: 'map',
+} as unknown) as SavedDashboardPanel730ToLatest;
+
+const embeddableByRef = ({
+ panelRefName: 'panel_ref_1',
+} as unknown) as SavedDashboardPanel730ToLatest;
+
+describe('findByValueEmbeddables', () => {
+ it('finds the by value embeddables for the given type', async () => {
+ const savedObjectsResult = {
+ saved_objects: [
+ {
+ attributes: {
+ panelsJSON: JSON.stringify([visualizationByValue, mapByValue, embeddableByRef]),
+ },
+ },
+ {
+ attributes: {
+ panelsJSON: JSON.stringify([embeddableByRef, mapByValue, visualizationByValue]),
+ },
+ },
+ ],
+ };
+ const savedObjectClient = { find: jest.fn().mockResolvedValue(savedObjectsResult) };
+
+ const maps = await findByValueEmbeddables(savedObjectClient, 'map');
+
+ expect(maps.length).toBe(2);
+ expect(maps[0]).toEqual(mapByValue.embeddableConfig);
+ expect(maps[1]).toEqual(mapByValue.embeddableConfig);
+
+ const visualizations = await findByValueEmbeddables(savedObjectClient, 'visualization');
+
+ expect(visualizations.length).toBe(2);
+ expect(visualizations[0]).toEqual(visualizationByValue.embeddableConfig);
+ expect(visualizations[1]).toEqual(visualizationByValue.embeddableConfig);
+ });
+});
diff --git a/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts b/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts
new file mode 100644
index 0000000000000..0ae14cdcf7197
--- /dev/null
+++ b/src/plugins/dashboard/server/usage/find_by_value_embeddables.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { ISavedObjectsRepository, SavedObjectAttributes } from 'kibana/server';
+import { SavedDashboardPanel730ToLatest } from '../../common';
+
+export const findByValueEmbeddables = async (
+ savedObjectClient: Pick,
+ embeddableType: string
+) => {
+ const dashboards = await savedObjectClient.find({
+ type: 'dashboard',
+ });
+
+ return dashboards.saved_objects
+ .map((dashboard) => {
+ try {
+ return (JSON.parse(
+ dashboard.attributes.panelsJSON as string
+ ) as unknown) as SavedDashboardPanel730ToLatest[];
+ } catch (exception) {
+ return [];
+ }
+ })
+ .flat()
+ .filter((panel) => (panel as Record).panelRefName === undefined)
+ .filter((panel) => panel.type === embeddableType)
+ .map((panel) => panel.embeddableConfig);
+};
diff --git a/src/plugins/data/common/search/search_source/mocks.ts b/src/plugins/data/common/search/search_source/mocks.ts
index 328f05fac8594..08fe2b07096bb 100644
--- a/src/plugins/data/common/search/search_source/mocks.ts
+++ b/src/plugins/data/common/search/search_source/mocks.ts
@@ -6,7 +6,7 @@
* Public License, v 1.
*/
-import { BehaviorSubject } from 'rxjs';
+import { BehaviorSubject, of } from 'rxjs';
import type { MockedKeys } from '@kbn/utility-types/jest';
import { uiSettingsServiceMock } from '../../../../../core/public/mocks';
@@ -27,6 +27,7 @@ export const searchSourceInstanceMock: MockedKeys = {
createChild: jest.fn().mockReturnThis(),
setParent: jest.fn(),
getParent: jest.fn().mockReturnThis(),
+ fetch$: jest.fn().mockReturnValue(of({})),
fetch: jest.fn().mockResolvedValue({}),
onRequestStart: jest.fn(),
getSearchRequestBody: jest.fn(),
diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts
index 6d7654c6659f2..c2a4beb9b61a5 100644
--- a/src/plugins/data/common/search/search_source/search_source.test.ts
+++ b/src/plugins/data/common/search/search_source/search_source.test.ts
@@ -51,7 +51,14 @@ describe('SearchSource', () => {
let searchSource: SearchSource;
beforeEach(() => {
- mockSearchMethod = jest.fn().mockReturnValue(of({ rawResponse: '' }));
+ mockSearchMethod = jest
+ .fn()
+ .mockReturnValue(
+ of(
+ { rawResponse: { isPartial: true, isRunning: true } },
+ { rawResponse: { isPartial: false, isRunning: false } }
+ )
+ );
searchSourceDependencies = {
getConfig: jest.fn(),
@@ -564,6 +571,34 @@ describe('SearchSource', () => {
await searchSource.fetch(options);
expect(mockSearchMethod).toBeCalledTimes(1);
});
+
+ test('should return partial results', (done) => {
+ searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies);
+ const options = {};
+
+ const next = jest.fn();
+ const complete = () => {
+ expect(next).toBeCalledTimes(2);
+ expect(next.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "isPartial": true,
+ "isRunning": true,
+ },
+ ]
+ `);
+ expect(next.mock.calls[1]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "isPartial": false,
+ "isRunning": false,
+ },
+ ]
+ `);
+ done();
+ };
+ searchSource.fetch$(options).subscribe({ next, complete });
+ });
});
describe('#serialize', () => {
diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts
index 554e8385881f2..bb60f0d7b4ad4 100644
--- a/src/plugins/data/common/search/search_source/search_source.ts
+++ b/src/plugins/data/common/search/search_source/search_source.ts
@@ -60,7 +60,8 @@
import { setWith } from '@elastic/safer-lodash-set';
import { uniqueId, keyBy, pick, difference, omit, isObject, isFunction } from 'lodash';
-import { map } from 'rxjs/operators';
+import { map, switchMap, tap } from 'rxjs/operators';
+import { defer, from } from 'rxjs';
import { normalizeSortRequest } from './normalize_sort_request';
import { fieldWildcardFilter } from '../../../../kibana_utils/common';
import { IIndexPattern } from '../../index_patterns';
@@ -244,30 +245,35 @@ export class SearchSource {
}
/**
- * Fetch this source and reject the returned Promise on error
- *
- * @async
+ * Fetch this source from Elasticsearch, returning an observable over the response(s)
+ * @param options
*/
- async fetch(options: ISearchOptions = {}) {
+ fetch$(options: ISearchOptions = {}) {
const { getConfig } = this.dependencies;
- await this.requestIsStarting(options);
-
- const searchRequest = await this.flatten();
- this.history = [searchRequest];
-
- let response;
- if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) {
- response = await this.legacyFetch(searchRequest, options);
- } else {
- response = await this.fetchSearch(searchRequest, options);
- }
-
- // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved
- if ((response as any).error) {
- throw new RequestFailure(null, response);
- }
+ return defer(() => this.requestIsStarting(options)).pipe(
+ switchMap(() => {
+ const searchRequest = this.flatten();
+ this.history = [searchRequest];
+
+ return getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)
+ ? from(this.legacyFetch(searchRequest, options))
+ : this.fetchSearch$(searchRequest, options);
+ }),
+ tap((response) => {
+ // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved
+ if ((response as any).error) {
+ throw new RequestFailure(null, response);
+ }
+ })
+ );
+ }
- return response;
+ /**
+ * Fetch this source and reject the returned Promise on error
+ * @deprecated Use fetch$ instead
+ */
+ fetch(options: ISearchOptions = {}) {
+ return this.fetch$(options).toPromise();
}
/**
@@ -305,16 +311,16 @@ export class SearchSource {
* Run a search using the search service
* @return {Promise