[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>
This commit is contained in:
Jean-Louis Leysens 2021-08-26 09:53:28 +02:00 committed by GitHub
parent de7ae4138d
commit 9e04d2c5c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 388 additions and 150 deletions

View file

@ -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) | |

View file

@ -2,18 +2,13 @@
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md)
## RefreshInterval interface
## RefreshInterval type
<b>Signature:</b>
```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) | <code>boolean</code> | |
| [value](./kibana-plugin-plugins-data-public.refreshinterval.value.md) | <code>number</code> | |

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) &gt; [pause](./kibana-plugin-plugins-data-public.refreshinterval.pause.md)
## RefreshInterval.pause property
<b>Signature:</b>
```typescript
pause: boolean;
```

View file

@ -1,11 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) &gt; [value](./kibana-plugin-plugins-data-public.refreshinterval.value.md)
## RefreshInterval.value property
<b>Signature:</b>
```typescript
value: number;
```

View file

@ -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;
};

View file

@ -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;
}
};

View file

@ -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<keyof GridData>).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)) {

View file

@ -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,

View file

@ -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) };
};
/**

View file

@ -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;
};

View file

@ -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,

View file

@ -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<DashboardState> => {
if (!state) {
return {};
}
const { panels, ...restOfState } = state;
if (!panels?.length) {
return restOfState;
}
return {
...restOfState,
...{ panels: convertSavedPanelsToPanelMap(panels) },
};
};

View file

@ -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<RawDashboardState>(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

View file

@ -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<string, Record<string, unknown>>
appState.uiState as { [key: string]: SerializableRecord }
) as SavedDashboardPanel[];
delete appState.uiState;
}

View file

@ -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(() => {

View file

@ -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: [
{

View file

@ -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',
},
},
],
});
});
});
});

View file

@ -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) => O & SerializableRecord = flow(JSON.stringify, JSON.parse);
const cleanEmptyKeys = (stateObj: Record<string, unknown>) => {
Object.keys(stateObj).forEach((key) => {
if (stateObj[key] === undefined) {
@ -26,7 +33,12 @@ const cleanEmptyKeys = (stateObj: Record<string, unknown>) => {
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<DashboardAppLocatorParams>;
@ -95,17 +109,29 @@ export interface DashboardAppLocatorDependencies {
getDashboardFilterFields: (dashboardId: string) => Promise<Filter[]>;
}
export type ForwardedDashboardState = Omit<
DashboardAppLocatorParams,
'dashboardId' | 'preserveSavedFilters' | 'useHash' | 'searchSessionId'
>;
export class DashboardAppLocatorDefinition implements LocatorDefinition<DashboardAppLocatorParams> {
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<Filter[]> => {
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<Dashboar
}
};
const state: ForwardedDashboardState = restParams;
// leave filters `undefined` if no filters was applied
// in this case dashboard will restore saved filters on its own
const filters = params.filters && [
state.filters = params.filters && [
...(await getSavedFiltersFromDestinationDashboardIfNeeded()),
...params.filters,
];
let path = setStateToKbnUrl(
'_a',
cleanEmptyKeys({
query: params.query,
filters: filters?.filter((f) => !esFilters.isFilterPinned(f)),
viewMode: params.viewMode,
panels: params.panels,
savedQuery: params.savedQuery,
}),
{ useHash },
`#/${hash}`
);
let path = `#/${hash}`;
path = setStateToKbnUrl<QueryState>(
'_g',
cleanEmptyKeys({
@ -154,7 +170,7 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition<Dashboar
return {
app: DashboardConstants.DASHBOARDS_ID,
path,
state: {},
state: getSerializableRecord(cleanEmptyKeys(state)),
};
};
}

View file

@ -34,6 +34,7 @@ import { SavedObjectLoader, SavedObjectsStart } from './services/saved_objects';
import { IKbnUrlStateStorage } from './services/kibana_utils';
import { DashboardContainer, DashboardSavedObject } from '.';
import { VisualizationsStart } from '../../visualizations/public';
import { DashboardAppLocatorParams } from './locator';
export { SavedDashboardPanel };
@ -123,6 +124,8 @@ export type DashboardBuildContext = Pick<
search: DashboardAppServices['data']['search'];
notifications: DashboardAppServices['core']['notifications'];
locatorState?: DashboardAppLocatorParams;
history: History;
kibanaVersion: string;
isEmbeddedExternally: boolean;
@ -135,11 +138,12 @@ export type DashboardBuildContext = Pick<
executionContext?: KibanaExecutionContext;
};
export interface DashboardOptions {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type DashboardOptions = {
hidePanelTitles: boolean;
useMargins: boolean;
syncColors: boolean;
}
};
export type DashboardRedirect = (props: RedirectToProps) => void;
export type RedirectToProps =

View file

@ -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;

View file

@ -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)
//

View file

@ -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 () => {

View file

@ -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<string, unknown> }): boolean =>
Array.isArray(sharingData.locatorParams);
sharingData.locatorParams != null;

View file

@ -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}

View file

@ -104,6 +104,10 @@ class ReportingPanelContentUi extends Component<Props, State> {
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<Props, State> {
this.setState({ isStale: true });
};
private isNotSaved = () => {
return this.props.objectId === undefined || this.props.objectId === '';
};
private setAbsoluteReportGenerationUrl = () => {
if (!this.mounted) {
return;

View file

@ -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);