From 9e04d2c5c7b2139b7e23743b133ea79bf93f5ba3 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 26 Aug 2021 09:53:28 +0200 Subject: [PATCH] [Reporting/Dashboard] Update integration to use v2 reports (#108553) * very wip, updating dashboard integration to use v2 reports. at the moment time filters are not working correctly * added missing dependency to hook * added tests and refined ForwadedAppState interface * remove unused import * updated test because generating a report from an unsaved report is possible * migrated locator to forward state on history only, reordered methods on react component * remove unused import * update locator test and use panel index number if panelIndex does not exist * ensure locator params are serializable * - moved getSerializableRecord to locator.ts to ensure that the values we get from it will never contain something that cannot be passed to history.push - updated types to remove some `& SerializableRecord` instances - fixed embeddable drilldown Jest tests given that we no longer expect state to be in the URL * update generated api docs * remove unused variable * - removed SerializedRecord extension from dashboard locator params interface - factored out state conversion logic from the locator getLocation * updated locator jest tests and SerializableRecord types * explicitly map values to dashboardlocatorparams and export serializable params type * use serializable params type in embeddable * factored out logic for converting panels to dashboard panels map * use "type =" instead of "interface" * big update to locator params: type fixes and added options key * added comment about why we are using "type" alias instead of "interface" declaration * simplify is v2 job param check Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../kibana-plugin-plugins-data-public.md | 2 +- ...gin-plugins-data-public.refreshinterval.md | 15 +- ...ugins-data-public.refreshinterval.pause.md | 11 - ...ugins-data-public.refreshinterval.value.md | 11 - src/plugins/dashboard/common/bwc/types.ts | 3 +- .../dashboard/common/embeddable/types.ts | 5 +- .../dashboard/common/migrate_to_730_panels.ts | 7 +- .../hooks/use_dashboard_app_state.ts | 9 + .../lib/convert_dashboard_state.ts | 14 +- .../lib/convert_saved_panels_to_panel_map.ts | 18 ++ .../dashboard/public/application/lib/index.ts | 1 + .../load_dashboard_history_location_state.ts | 29 +++ .../lib/load_dashboard_url_state.ts | 11 +- .../application/lib/migrate_app_state.ts | 3 +- .../application/top_nav/dashboard_top_nav.tsx | 4 +- .../application/top_nav/show_share_modal.tsx | 49 ++++- src/plugins/dashboard/public/locator.test.ts | 190 +++++++++++++++--- src/plugins/dashboard/public/locator.ts | 60 ++++-- src/plugins/dashboard/public/types.ts | 8 +- .../data/common/query/timefilter/types.ts | 9 +- src/plugins/data/public/public.api.md | 6 +- ...embeddable_to_dashboard_drilldown.test.tsx | 46 ++++- x-pack/plugins/reporting/common/job_utils.ts | 2 +- .../register_pdf_png_reporting.tsx | 5 +- .../reporting_panel_content.tsx | 8 +- .../apps/dashboard/reporting/screenshots.ts | 12 +- 26 files changed, 388 insertions(+), 150 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md create mode 100644 src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts create mode 100644 src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts 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 05743f40a7b68..d5a39e3108325 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1955,12 +1955,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);