diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index 185dd771c4ace..7548aa62eb313 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -80,7 +80,6 @@
| [QuerySuggestionField](./kibana-plugin-plugins-data-public.querysuggestionfield.md) | \* |
| [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) | \* |
| [Reason](./kibana-plugin-plugins-data-public.reason.md) | |
-| [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | |
| [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | |
| [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | |
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object |
@@ -176,6 +175,7 @@
| [RangeFilter](./kibana-plugin-plugins-data-public.rangefilter.md) | |
| [RangeFilterMeta](./kibana-plugin-plugins-data-public.rangefiltermeta.md) | |
| [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) | |
+| [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | |
| [SavedQueryTimeFilter](./kibana-plugin-plugins-data-public.savedquerytimefilter.md) | |
| [SearchBarProps](./kibana-plugin-plugins-data-public.searchbarprops.md) | |
| [StatefulSearchBarProps](./kibana-plugin-plugins-data-public.statefulsearchbarprops.md) | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md
index 6a6350d8ba4f6..b6067e081b943 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md
@@ -2,18 +2,13 @@
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md)
-## RefreshInterval interface
+## RefreshInterval type
Signature:
```typescript
-export interface RefreshInterval
+export declare type RefreshInterval = {
+ pause: boolean;
+ value: number;
+};
```
-
-## Properties
-
-| Property | Type | Description |
-| --- | --- | --- |
-| [pause](./kibana-plugin-plugins-data-public.refreshinterval.pause.md) | boolean
| |
-| [value](./kibana-plugin-plugins-data-public.refreshinterval.value.md) | number
| |
-
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md
deleted file mode 100644
index fb854fcbbc277..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) > [pause](./kibana-plugin-plugins-data-public.refreshinterval.pause.md)
-
-## RefreshInterval.pause property
-
-Signature:
-
-```typescript
-pause: boolean;
-```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md
deleted file mode 100644
index 021a01391b71e..0000000000000
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) > [value](./kibana-plugin-plugins-data-public.refreshinterval.value.md)
-
-## RefreshInterval.value property
-
-Signature:
-
-```typescript
-value: number;
-```
diff --git a/src/plugins/dashboard/common/bwc/types.ts b/src/plugins/dashboard/common/bwc/types.ts
index f3c384a76c391..ba479210cb009 100644
--- a/src/plugins/dashboard/common/bwc/types.ts
+++ b/src/plugins/dashboard/common/bwc/types.ts
@@ -7,6 +7,7 @@
*/
import { SavedObjectReference } from 'kibana/public';
+import type { Serializable } from '@kbn/utility-types';
import { GridData } from '../';
@@ -110,7 +111,7 @@ export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620;
// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property.
// Source: https://github.com/elastic/kibana/pull/14949
export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & {
- embeddableConfig: { [key: string]: unknown };
+ embeddableConfig: { [key: string]: Serializable };
version: string;
};
diff --git a/src/plugins/dashboard/common/embeddable/types.ts b/src/plugins/dashboard/common/embeddable/types.ts
index 83507d21665d9..d786078766f78 100644
--- a/src/plugins/dashboard/common/embeddable/types.ts
+++ b/src/plugins/dashboard/common/embeddable/types.ts
@@ -6,10 +6,11 @@
* Side Public License, v 1.
*/
-export interface GridData {
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type GridData = {
w: number;
h: number;
x: number;
y: number;
i: string;
-}
+};
diff --git a/src/plugins/dashboard/common/migrate_to_730_panels.ts b/src/plugins/dashboard/common/migrate_to_730_panels.ts
index 48c3ddb463ed8..ad0fa7658b6fa 100644
--- a/src/plugins/dashboard/common/migrate_to_730_panels.ts
+++ b/src/plugins/dashboard/common/migrate_to_730_panels.ts
@@ -7,6 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
+import type { SerializableRecord } from '@kbn/utility-types';
import semverSatisfies from 'semver/functions/satisfies';
import uuid from 'uuid';
import {
@@ -80,7 +81,7 @@ function migratePre61PanelToLatest(
panel: RawSavedDashboardPanelTo60,
version: string,
useMargins: boolean,
- uiState?: { [key: string]: { [key: string]: unknown } }
+ uiState?: { [key: string]: SerializableRecord }
): RawSavedDashboardPanel730ToLatest {
if (panel.col === undefined || panel.row === undefined) {
throw new Error(
@@ -138,7 +139,7 @@ function migrate610PanelToLatest(
panel: RawSavedDashboardPanel610,
version: string,
useMargins: boolean,
- uiState?: { [key: string]: { [key: string]: unknown } }
+ uiState?: { [key: string]: SerializableRecord }
): RawSavedDashboardPanel730ToLatest {
(['w', 'x', 'h', 'y'] as Array).forEach((key) => {
if (panel.gridData[key] === undefined) {
@@ -273,7 +274,7 @@ export function migratePanelsTo730(
>,
version: string,
useMargins: boolean,
- uiState?: { [key: string]: { [key: string]: unknown } }
+ uiState?: { [key: string]: SerializableRecord }
): RawSavedDashboardPanel730ToLatest[] {
return panels.map((panel) => {
if (isPre61Panel(panel)) {
diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts
index 9df486c677dda..c01cd43b1f1e3 100644
--- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts
+++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts
@@ -25,7 +25,9 @@ import {
DashboardRedirect,
DashboardState,
} from '../../types';
+import { DashboardAppLocatorParams } from '../../locator';
import {
+ loadDashboardHistoryLocationState,
tryDestroyDashboardContainer,
syncDashboardContainerInput,
savedObjectToDashboardState,
@@ -88,6 +90,7 @@ export const useDashboardAppState = ({
savedObjectsTagging,
dashboardCapabilities,
dashboardSessionStorage,
+ scopedHistory,
} = services;
const { docTitle } = chrome;
const { notifications } = core;
@@ -149,10 +152,15 @@ export const useDashboardAppState = ({
*/
const dashboardSessionStorageState = dashboardSessionStorage.getState(savedDashboardId) || {};
const dashboardURLState = loadDashboardUrlState(dashboardBuildContext);
+ const forwardedAppState = loadDashboardHistoryLocationState(
+ scopedHistory()?.location?.state as undefined | DashboardAppLocatorParams
+ );
+
const initialDashboardState = {
...savedDashboardState,
...dashboardSessionStorageState,
...dashboardURLState,
+ ...forwardedAppState,
// if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it.
...(incomingEmbeddable ? { viewMode: ViewMode.EDIT } : {}),
@@ -312,6 +320,7 @@ export const useDashboardAppState = ({
getStateTransfer,
savedDashboards,
usageCollection,
+ scopedHistory,
notifications,
indexPatterns,
kibanaVersion,
diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts
index d17f8405d734f..ad84b794a2379 100644
--- a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts
+++ b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts
@@ -11,19 +11,15 @@ import type { KibanaExecutionContext } from 'src/core/public';
import { DashboardSavedObject } from '../../saved_dashboards';
import { getTagsFromSavedDashboard, migrateAppState } from '.';
import { EmbeddablePackageState, ViewMode } from '../../services/embeddable';
-import {
- convertPanelStateToSavedDashboardPanel,
- convertSavedDashboardPanelToPanelState,
-} from '../../../common/embeddable/embeddable_saved_object_converters';
+import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters';
import {
DashboardState,
RawDashboardState,
- DashboardPanelMap,
- SavedDashboardPanel,
DashboardAppServices,
DashboardContainerInput,
DashboardBuildContext,
} from '../../types';
+import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map';
interface SavedObjectToDashboardStateProps {
version: string;
@@ -77,11 +73,7 @@ export const savedObjectToDashboardState = ({
usageCollection
);
- const panels: DashboardPanelMap = {};
- rawState.panels?.forEach((panel: SavedDashboardPanel) => {
- panels[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel);
- });
- return { ...rawState, panels };
+ return { ...rawState, panels: convertSavedPanelsToPanelMap(rawState.panels) };
};
/**
diff --git a/src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts b/src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts
new file mode 100644
index 0000000000000..b91940f45081b
--- /dev/null
+++ b/src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { convertSavedDashboardPanelToPanelState } from '../../../common/embeddable/embeddable_saved_object_converters';
+import type { SavedDashboardPanel, DashboardPanelMap } from '../../types';
+
+export const convertSavedPanelsToPanelMap = (panels?: SavedDashboardPanel[]): DashboardPanelMap => {
+ const panelsMap: DashboardPanelMap = {};
+ panels?.forEach((panel, idx) => {
+ panelsMap![panel.panelIndex ?? String(idx)] = convertSavedDashboardPanelToPanelState(panel);
+ });
+ return panelsMap;
+};
diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts
index 937c1d2a77c06..9aba481c3fb86 100644
--- a/src/plugins/dashboard/public/application/lib/index.ts
+++ b/src/plugins/dashboard/public/application/lib/index.ts
@@ -20,6 +20,7 @@ export { syncDashboardFilterState } from './sync_dashboard_filter_state';
export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns';
export { syncDashboardContainerInput } from './sync_dashboard_container_input';
export { diffDashboardContainerInput, diffDashboardState } from './diff_dashboard_state';
+export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state';
export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container';
export {
stateToDashboardContainerInput,
diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts b/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts
new file mode 100644
index 0000000000000..d20e14cea74b5
--- /dev/null
+++ b/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { ForwardedDashboardState } from '../../locator';
+import { DashboardState } from '../../types';
+import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map';
+
+export const loadDashboardHistoryLocationState = (
+ state?: ForwardedDashboardState
+): Partial => {
+ if (!state) {
+ return {};
+ }
+
+ const { panels, ...restOfState } = state;
+ if (!panels?.length) {
+ return restOfState;
+ }
+
+ return {
+ ...restOfState,
+ ...{ panels: convertSavedPanelsToPanelMap(panels) },
+ };
+};
diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts b/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts
index efff2ba6bc087..f76382c1fbbdd 100644
--- a/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts
+++ b/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts
@@ -11,15 +11,14 @@ import _ from 'lodash';
import { migrateAppState } from '.';
import { replaceUrlHashQuery } from '../../../../kibana_utils/public';
import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants';
-import { convertSavedDashboardPanelToPanelState } from '../../../common/embeddable/embeddable_saved_object_converters';
-import {
+import type {
DashboardBuildContext,
DashboardPanelMap,
DashboardState,
RawDashboardState,
- SavedDashboardPanel,
} from '../../types';
import { migrateLegacyQuery } from './migrate_legacy_query';
+import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map';
/**
* Loads any dashboard state from the URL, and removes the state from the URL.
@@ -32,12 +31,10 @@ export const loadDashboardUrlState = ({
const rawAppStateInUrl = kbnUrlStateStorage.get(DASHBOARD_STATE_STORAGE_KEY);
if (!rawAppStateInUrl) return {};
- const panelsMap: DashboardPanelMap = {};
+ let panelsMap: DashboardPanelMap = {};
if (rawAppStateInUrl.panels && rawAppStateInUrl.panels.length > 0) {
const rawState = migrateAppState(rawAppStateInUrl, kibanaVersion, usageCollection);
- rawState.panels?.forEach((panel: SavedDashboardPanel) => {
- panelsMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel);
- });
+ panelsMap = convertSavedPanelsToPanelMap(rawState.panels);
}
const migratedQuery = rawAppStateInUrl.query
diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts
index fb8ef1b9ba2da..06290205d65df 100644
--- a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts
+++ b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts
@@ -7,6 +7,7 @@
*/
import semverSatisfies from 'semver/functions/satisfies';
+import type { SerializableRecord } from '@kbn/utility-types';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
@@ -75,7 +76,7 @@ export function migrateAppState(
>,
kibanaVersion,
appState.useMargins as boolean,
- appState.uiState as Record>
+ appState.uiState as { [key: string]: SerializableRecord }
) as SavedDashboardPanel[];
delete appState.uiState;
}
diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx
index 80368d52cb110..e6a2c41fd4ecb 100644
--- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx
+++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx
@@ -404,6 +404,7 @@ export function DashboardTopNav({
(anchorElement: HTMLElement) => {
if (!share) return;
const currentState = dashboardAppState.getLatestDashboardState();
+ const timeRange = timefilter.getTime();
ShowShareModal({
share,
kibanaVersion,
@@ -412,9 +413,10 @@ export function DashboardTopNav({
currentDashboardState: currentState,
savedDashboard: dashboardAppState.savedDashboard,
isDirty: Boolean(dashboardAppState.hasUnsavedChanges),
+ timeRange,
});
},
- [dashboardAppState, dashboardCapabilities, share, kibanaVersion]
+ [dashboardAppState, dashboardCapabilities, share, kibanaVersion, timefilter]
);
const dashboardTopNavActions = useMemo(() => {
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 239d2bf72b9c1..b9c77dec87b66 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,16 +6,20 @@
* Side Public License, v 1.
*/
-import { Capabilities } from 'src/core/public';
import { EuiCheckboxGroup } from '@elastic/eui';
-import React from 'react';
-import { ReactElement, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import moment from 'moment';
+import React, { ReactElement, useState } from 'react';
+import type { Capabilities } from 'src/core/public';
import { DashboardSavedObject } from '../..';
+import { shareModalStrings } from '../../dashboard_strings';
+import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator';
+import { TimeRange } from '../../services/data';
+import { ViewMode } from '../../services/embeddable';
import { setStateToKbnUrl, unhashUrl } from '../../services/kibana_utils';
import { SharePluginStart } from '../../services/share';
-import { dashboardUrlParams } from '../dashboard_router';
-import { shareModalStrings } from '../../dashboard_strings';
import { DashboardAppCapabilities, DashboardState } from '../../types';
+import { dashboardUrlParams } from '../dashboard_router';
import { stateToRawDashboardState } from '../lib/convert_dashboard_state';
const showFilterBarId = 'showFilterBar';
@@ -28,6 +32,7 @@ interface ShowShareModalProps {
savedDashboard: DashboardSavedObject;
currentDashboardState: DashboardState;
dashboardCapabilities: DashboardAppCapabilities;
+ timeRange: TimeRange;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
@@ -46,6 +51,7 @@ export function ShowShareModal({
savedDashboard,
dashboardCapabilities,
currentDashboardState,
+ timeRange,
}: ShowShareModalProps) {
const EmbedUrlParamExtension = ({
setParamValue,
@@ -104,6 +110,25 @@ export function ShowShareModal({
);
};
+ const rawDashboardState = stateToRawDashboardState({
+ state: currentDashboardState,
+ version: kibanaVersion,
+ });
+
+ const locatorParams: DashboardAppLocatorParams = {
+ dashboardId: savedDashboard.id,
+ filters: rawDashboardState.filters,
+ preserveSavedFilters: true,
+ query: rawDashboardState.query,
+ savedQuery: rawDashboardState.savedQuery,
+ useHash: false,
+ panels: rawDashboardState.panels,
+ timeRange,
+ viewMode: ViewMode.VIEW, // For share locators we always load the dashboard in view mode
+ refreshInterval: undefined, // We don't share refresh interval externally
+ options: rawDashboardState.options,
+ };
+
share.toggleShareContextMenu({
isDirty,
anchorElement,
@@ -111,14 +136,24 @@ export function ShowShareModal({
allowShortUrl: dashboardCapabilities.createShortUrl,
shareableUrl: setStateToKbnUrl(
'_a',
- stateToRawDashboardState({ state: currentDashboardState, version: kibanaVersion }),
+ rawDashboardState,
{ useHash: false, storeInHashQuery: true },
unhashUrl(window.location.href)
),
objectId: savedDashboard.id,
objectType: 'dashboard',
sharingData: {
- title: savedDashboard.title,
+ title:
+ savedDashboard.title ||
+ i18n.translate('dashboard.share.defaultDashboardTitle', {
+ defaultMessage: 'Dashboard [{date}]',
+ values: { date: moment().toISOString(true) },
+ }),
+ locatorParams: {
+ id: DASHBOARD_APP_LOCATOR,
+ version: kibanaVersion,
+ params: locatorParams,
+ },
},
embedUrlParamExtensions: [
{
diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts
index 0b647ac00ce31..f3f5aec9f478c 100644
--- a/src/plugins/dashboard/public/locator.test.ts
+++ b/src/plugins/dashboard/public/locator.test.ts
@@ -17,7 +17,7 @@ describe('dashboard locator', () => {
hashedItemStore.storage = mockStorage;
});
- test('creates a link to a saved dashboard', async () => {
+ test('creates a link to an unsaved dashboard', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
@@ -26,7 +26,7 @@ describe('dashboard locator', () => {
expect(location).toMatchObject({
app: 'dashboards',
- path: '#/create?_a=()&_g=()',
+ path: '#/create?_g=()',
state: {},
});
});
@@ -42,8 +42,14 @@ describe('dashboard locator', () => {
expect(location).toMatchObject({
app: 'dashboards',
- path: '#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))',
- state: {},
+ path: '#/create?_g=(time:(from:now-15m,mode:relative,to:now))',
+ state: {
+ timeRange: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ },
});
});
@@ -82,8 +88,47 @@ describe('dashboard locator', () => {
expect(location).toMatchObject({
app: 'dashboards',
- path: `#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`,
- state: {},
+ path: `#/view/123?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`,
+ state: {
+ filters: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'hi',
+ },
+ },
+ {
+ $state: {
+ store: 'globalState',
+ },
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'hi',
+ },
+ },
+ ],
+ query: {
+ language: 'kuery',
+ query: 'bye',
+ },
+ refreshInterval: {
+ pause: false,
+ value: 300,
+ },
+ timeRange: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ },
});
});
@@ -103,8 +148,23 @@ describe('dashboard locator', () => {
expect(location).toMatchObject({
app: 'dashboards',
- path: `#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`,
- state: {},
+ path: `#/view/123?_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`,
+ state: {
+ filters: [],
+ query: {
+ language: 'kuery',
+ query: 'bye',
+ },
+ refreshInterval: {
+ pause: false,
+ value: 300,
+ },
+ timeRange: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ },
});
});
@@ -119,10 +179,11 @@ describe('dashboard locator', () => {
expect(location).toMatchObject({
app: 'dashboards',
- path: `#/create?_a=(savedQuery:__savedQueryId__)&_g=()`,
- state: {},
+ path: `#/create?_g=()`,
+ state: {
+ savedQuery: '__savedQueryId__',
+ },
});
- expect(location.path).toContain('__savedQueryId__');
});
test('panels', async () => {
@@ -136,8 +197,10 @@ describe('dashboard locator', () => {
expect(location).toMatchObject({
app: 'dashboards',
- path: `#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()`,
- state: {},
+ path: `#/create?_g=()`,
+ state: {
+ panels: [{ fakePanelContent: 'fakePanelContent' }],
+ },
});
});
@@ -224,16 +287,62 @@ describe('dashboard locator', () => {
filters: [appliedFilter],
});
- expect(location1.path).toEqual(expect.stringContaining('query:savedfilter1'));
- expect(location1.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ expect(location1.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`);
+ expect(location1.state).toMatchObject({
+ filters: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'savedfilter1',
+ },
+ },
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'appliedfilter',
+ },
+ },
+ ],
+ });
const location2 = await definition.getLocation({
dashboardId: 'dashboard2',
filters: [appliedFilter],
});
- expect(location2.path).toEqual(expect.stringContaining('query:savedfilter2'));
- expect(location2.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ expect(location2.path).toMatchInlineSnapshot(`"#/view/dashboard2?_g=(filters:!())"`);
+ expect(location2.state).toMatchObject({
+ filters: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'savedfilter2',
+ },
+ },
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'appliedfilter',
+ },
+ },
+ ],
+ });
});
test("doesn't fail if can't retrieve filters from destination dashboard", async () => {
@@ -252,8 +361,21 @@ describe('dashboard locator', () => {
filters: [appliedFilter],
});
- expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
- expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`);
+ expect(location.state).toMatchObject({
+ filters: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'appliedfilter',
+ },
+ },
+ ],
+ });
});
test('can enforce empty filters', async () => {
@@ -273,11 +395,10 @@ describe('dashboard locator', () => {
preserveSavedFilters: false,
});
- expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
- expect(location.path).not.toEqual(expect.stringContaining('query:appliedfilter'));
- expect(location.path).toMatchInlineSnapshot(
- `"#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"`
- );
+ expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`);
+ expect(location.state).toMatchObject({
+ filters: [],
+ });
});
test('no filters in result url if no filters applied', async () => {
@@ -295,8 +416,8 @@ describe('dashboard locator', () => {
dashboardId: 'dashboard1',
});
- expect(location.path).not.toEqual(expect.stringContaining('filters'));
- expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_a=()&_g=()"`);
+ expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=()"`);
+ expect(location.state).toMatchObject({});
});
test('can turn off preserving filters', async () => {
@@ -316,8 +437,21 @@ describe('dashboard locator', () => {
preserveSavedFilters: false,
});
- expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1'));
- expect(location.path).toEqual(expect.stringContaining('query:appliedfilter'));
+ expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`);
+ expect(location.state).toMatchObject({
+ filters: [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: {
+ query: 'appliedfilter',
+ },
+ },
+ ],
+ });
});
});
});
diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts
index ed4e7a5dd4d4c..a256a65a5d7f4 100644
--- a/src/plugins/dashboard/public/locator.ts
+++ b/src/plugins/dashboard/public/locator.ts
@@ -7,14 +7,21 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
+import { flow } from 'lodash';
import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public';
import type { LocatorDefinition, LocatorPublic } from '../../share/public';
import type { SavedDashboardPanel } from '../common/types';
+import type { RawDashboardState } from './types';
import { esFilters } from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
import { ViewMode } from '../../embeddable/public';
import { DashboardConstants } from './dashboard_constants';
+/**
+ * Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions).
+ */
+const getSerializableRecord: (o: O) => O & SerializableRecord = flow(JSON.stringify, JSON.parse);
+
const cleanEmptyKeys = (stateObj: Record) => {
Object.keys(stateObj).forEach((key) => {
if (stateObj[key] === undefined) {
@@ -26,7 +33,12 @@ const cleanEmptyKeys = (stateObj: Record) => {
export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR';
-export interface DashboardAppLocatorParams extends SerializableRecord {
+/**
+ * We use `type` instead of `interface` to avoid having to extend this type with
+ * `SerializableRecord`. See https://github.com/microsoft/TypeScript/issues/15300.
+ */
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type DashboardAppLocatorParams = {
/**
* If given, the dashboard saved object with this id will be loaded. If not given,
* a new, unsaved dashboard will be loaded up.
@@ -40,7 +52,7 @@ export interface DashboardAppLocatorParams extends SerializableRecord {
/**
* Optionally set the refresh interval.
*/
- refreshInterval?: RefreshInterval & SerializableRecord;
+ refreshInterval?: RefreshInterval;
/**
* Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the
@@ -80,13 +92,15 @@ export interface DashboardAppLocatorParams extends SerializableRecord {
/**
* List of dashboard panels
*/
- panels?: SavedDashboardPanel[] & SerializableRecord;
+ panels?: SavedDashboardPanel[];
/**
* Saved query ID
*/
savedQuery?: string;
-}
+
+ options?: RawDashboardState['options'];
+};
export type DashboardAppLocator = LocatorPublic;
@@ -95,17 +109,29 @@ export interface DashboardAppLocatorDependencies {
getDashboardFilterFields: (dashboardId: string) => Promise;
}
+export type ForwardedDashboardState = Omit<
+ DashboardAppLocatorParams,
+ 'dashboardId' | 'preserveSavedFilters' | 'useHash' | 'searchSessionId'
+>;
+
export class DashboardAppLocatorDefinition implements LocatorDefinition {
public readonly id = DASHBOARD_APP_LOCATOR;
constructor(protected readonly deps: DashboardAppLocatorDependencies) {}
public readonly getLocation = async (params: DashboardAppLocatorParams) => {
- const useHash = params.useHash ?? this.deps.useHashedUrl;
- const hash = params.dashboardId ? `view/${params.dashboardId}` : `create`;
+ const {
+ filters,
+ useHash: paramsUseHash,
+ preserveSavedFilters,
+ dashboardId,
+ ...restParams
+ } = params;
+ const useHash = paramsUseHash ?? this.deps.useHashedUrl;
+ const hash = dashboardId ? `view/${dashboardId}` : `create`;
const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => {
- if (params.preserveSavedFilters === false) return [];
+ if (preserveSavedFilters === false) return [];
if (!params.dashboardId) return [];
try {
return await this.deps.getDashboardFilterFields(params.dashboardId);
@@ -116,26 +142,16 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition !esFilters.isFilterPinned(f)),
- viewMode: params.viewMode,
- panels: params.panels,
- savedQuery: params.savedQuery,
- }),
- { useHash },
- `#/${hash}`
- );
-
+ let path = `#/${hash}`;
path = setStateToKbnUrl(
'_g',
cleanEmptyKeys({
@@ -154,7 +170,7 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition void;
export type RedirectToProps =
diff --git a/src/plugins/data/common/query/timefilter/types.ts b/src/plugins/data/common/query/timefilter/types.ts
index 0f468a8f6b586..51558183c3db3 100644
--- a/src/plugins/data/common/query/timefilter/types.ts
+++ b/src/plugins/data/common/query/timefilter/types.ts
@@ -6,14 +6,15 @@
* Side Public License, v 1.
*/
-import { Moment } from 'moment';
+import type { Moment } from 'moment';
-export interface RefreshInterval {
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type RefreshInterval = {
pause: boolean;
value: number;
-}
+};
-// eslint-disable-next-line
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type TimeRange = {
from: string;
to: string;
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 90e63c2e4d86a..0644c7447f5d6 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -1957,12 +1957,10 @@ export interface Reason {
// Warning: (ae-missing-release-tag) "RefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export interface RefreshInterval {
- // (undocumented)
+export type RefreshInterval = {
pause: boolean;
- // (undocumented)
value: number;
-}
+};
// Warning: (ae-missing-release-tag) "SavedQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx
index a817d9f65c916..3272a6e27de23 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx
@@ -88,6 +88,7 @@ describe('.execute() & getHref', () => {
useHashedUrl: false,
getDashboardFilterFields: async () => [],
});
+ const getLocationSpy = jest.spyOn(definition, 'getLocation');
const drilldown = new EmbeddableToDashboardDrilldown({
start: ((() => ({
core: {
@@ -147,9 +148,14 @@ describe('.execute() & getHref', () => {
return {
href,
+ getLocationSpy,
};
}
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
test('navigates to correct dashboard', async () => {
const testDashboardId = 'dashboardId';
const { href } = await setupTestBed(
@@ -183,7 +189,7 @@ describe('.execute() & getHref', () => {
test('navigates with query if filters are enabled', async () => {
const queryString = 'querystring';
const queryLanguage = 'kuery';
- const { href } = await setupTestBed(
+ const { getLocationSpy } = await setupTestBed(
{
useCurrentFilters: true,
},
@@ -193,8 +199,12 @@ describe('.execute() & getHref', () => {
[]
);
- expect(href).toEqual(expect.stringContaining(queryString));
- expect(href).toEqual(expect.stringContaining(queryLanguage));
+ const {
+ state: { query },
+ } = await getLocationSpy.mock.results[0].value;
+
+ expect(query.query).toBe(queryString);
+ expect(query.language).toBe(queryLanguage);
});
test('when user chooses to keep current filters, current filters are set on destination dashboard', async () => {
@@ -202,7 +212,7 @@ describe('.execute() & getHref', () => {
const existingGlobalFilterKey = 'existingGlobalFilter';
const newAppliedFilterKey = 'newAppliedFilter';
- const { href } = await setupTestBed(
+ const { getLocationSpy } = await setupTestBed(
{
useCurrentFilters: true,
},
@@ -212,9 +222,16 @@ describe('.execute() & getHref', () => {
[getFilter(false, newAppliedFilterKey)]
);
- expect(href).toEqual(expect.stringContaining(existingAppFilterKey));
- expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey));
- expect(href).toEqual(expect.stringContaining(newAppliedFilterKey));
+ const {
+ state: { filters },
+ } = await getLocationSpy.mock.results[0].value;
+
+ expect(filters.length).toBe(3);
+
+ const filtersString = JSON.stringify(filters);
+ expect(filtersString).toEqual(expect.stringContaining(existingAppFilterKey));
+ expect(filtersString).toEqual(expect.stringContaining(existingGlobalFilterKey));
+ expect(filtersString).toEqual(expect.stringContaining(newAppliedFilterKey));
});
test('when user chooses to remove current filters, current app filters are remove on destination dashboard', async () => {
@@ -222,7 +239,7 @@ describe('.execute() & getHref', () => {
const existingGlobalFilterKey = 'existingGlobalFilter';
const newAppliedFilterKey = 'newAppliedFilter';
- const { href } = await setupTestBed(
+ const { getLocationSpy } = await setupTestBed(
{
useCurrentFilters: false,
},
@@ -232,9 +249,16 @@ describe('.execute() & getHref', () => {
[getFilter(false, newAppliedFilterKey)]
);
- expect(href).not.toEqual(expect.stringContaining(existingAppFilterKey));
- expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey));
- expect(href).toEqual(expect.stringContaining(newAppliedFilterKey));
+ const {
+ state: { filters },
+ } = await getLocationSpy.mock.results[0].value;
+
+ expect(filters.length).toBe(2);
+
+ const filtersString = JSON.stringify(filters);
+ expect(filtersString).not.toEqual(expect.stringContaining(existingAppFilterKey));
+ expect(filtersString).toEqual(expect.stringContaining(existingGlobalFilterKey));
+ expect(filtersString).toEqual(expect.stringContaining(newAppliedFilterKey));
});
test('when user chooses to keep current time range, current time range is passed in url', async () => {
diff --git a/x-pack/plugins/reporting/common/job_utils.ts b/x-pack/plugins/reporting/common/job_utils.ts
index 1a8699eeca025..d8b4503cfefba 100644
--- a/x-pack/plugins/reporting/common/job_utils.ts
+++ b/x-pack/plugins/reporting/common/job_utils.ts
@@ -8,4 +8,4 @@
// TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy
// export type entirely
export const isJobV2Params = ({ sharingData }: { sharingData: Record }): boolean =>
- Array.isArray(sharingData.locatorParams);
+ sharingData.locatorParams != null;
diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx
index 811d5803895db..0b31083a0fe8d 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx
@@ -127,6 +127,7 @@ export const reportingScreenshotShareProvider = ({
};
const isV2Job = isJobV2Params(jobProviderOptions);
+ const requiresSavedState = !isV2Job;
const pngReportType = isV2Job ? 'pngV2' : 'png';
@@ -149,7 +150,7 @@ export const reportingScreenshotShareProvider = ({
uiSettings={uiSettings}
reportType={pngReportType}
objectId={objectId}
- requiresSavedState={true}
+ requiresSavedState={requiresSavedState}
getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)}
isDirty={isDirty}
onClose={onClose}
@@ -183,7 +184,7 @@ export const reportingScreenshotShareProvider = ({
uiSettings={uiSettings}
reportType={pdfReportType}
objectId={objectId}
- requiresSavedState={true}
+ requiresSavedState={requiresSavedState}
layoutOption={objectType === 'dashboard' ? 'print' : undefined}
getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)}
isDirty={isDirty}
diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx
index 11169dd2d2fb7..64f1ecddcbb41 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx
@@ -104,6 +104,10 @@ class ReportingPanelContentUi extends Component {
window.addEventListener('resize', this.setAbsoluteReportGenerationUrl);
}
+ private isNotSaved = () => {
+ return this.props.objectId === undefined || this.props.objectId === '';
+ };
+
public render() {
if (
this.props.requiresSavedState &&
@@ -226,10 +230,6 @@ class ReportingPanelContentUi extends Component {
this.setState({ isStale: true });
};
- private isNotSaved = () => {
- return this.props.objectId === undefined || this.props.objectId === '';
- };
-
private setAbsoluteReportGenerationUrl = () => {
if (!this.mounted) {
return;
diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts
index 881b847f1180b..312eba7bd6380 100644
--- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts
+++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts
@@ -74,15 +74,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('Print PDF button', () => {
- it('is not available if new', async () => {
+ it('is available if new', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.reporting.openPdfReportingPanel();
- expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true');
+ expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null);
await (await testSubjects.find('kibanaChrome')).clickMouseButton(); // close popover
});
- it('becomes available when saved', async () => {
+ it('is available when saved', async () => {
await PageObjects.dashboard.saveDashboard('My PDF Dashboard');
await PageObjects.reporting.openPdfReportingPanel();
expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null);
@@ -109,15 +109,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
describe('Print PNG button', () => {
- it('is not available if new', async () => {
+ it('is available if new', async () => {
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.reporting.openPngReportingPanel();
- expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true');
+ expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null);
await (await testSubjects.find('kibanaChrome')).clickMouseButton(); // close popover
});
- it('becomes available when saved', async () => {
+ it('is available when saved', async () => {
await PageObjects.dashboard.saveDashboard('My PNG Dash');
await PageObjects.reporting.openPngReportingPanel();
expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null);