[Dashboards as code] consolidate URL BWC and add functional tests (#221989)

Closes https://github.com/elastic/kibana/issues/221683

PR does the following
* Consolidates `SharedDashboardState` and `DashboardLocatorParams` types
into a single type `DashboardLocatorParams`.
* Combine URL parsing and locator parsing into single function,
`extractDashboardState`. Previously this was done in 2 seperate code
paths: `loadDashboardHistoryLocationState` for locator state and
`loadAndRemoveDashboardState` for URL state.
* `extractDashboardState` takes `unknown` instead of
`DashboardLocatorParams`. Input value could contain legacy versions of
`DashboardLocatorParams` or really anything since its provided in the
URL by users. `unknown` better reflects that the input could be a lot of
different things.
* `extractDashboardState` uses duck typing to try its best to convert
from legacy versions of `DashboardLocatorParams` to current
`DashboardState` type
* Replaced `bwc_shared_urls` functional test with unit tests
* Added function test `bwc_short_urls` functional tests to ensure stored
URLs continue to work. Added tests for 8.14, 8.18, and 8.19 URL state.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Devon Thomson <devon.thomson@elastic.co>
This commit is contained in:
Nathan Reese 2025-06-04 18:30:11 -06:00 committed by GitHub
parent fca224701f
commit 5e594b4a82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 958 additions and 381 deletions

View file

@ -20,7 +20,7 @@ pageLoadAssetSize:
console: 61298
contentConnectors: 65000
contentManagement: 16254
controls: 12000
controls: 13000
core: 564663
crossClusterReplication: 65408
customIntegrations: 22034

View file

@ -38,7 +38,17 @@ export function serializeRuntimeState(
...defaultRuntimeState,
...omit(runtimeState, ['initialChildControlState']),
controls: Object.entries(runtimeState?.initialChildControlState ?? {}).map(
([controlId, value]) => ({ ...value, id: controlId })
([controlId, value]) => {
const { grow, order, type, width, ...controlConfig } = value;
return {
id: controlId,
grow,
order,
type,
width,
controlConfig,
};
}
),
},
};

View file

@ -1,34 +0,0 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ScopedHistory } from '@kbn/core-application-browser';
import { ForwardedDashboardState } from './locator';
import type { DashboardState } from '../types';
import { convertPanelsArrayToPanelSectionMaps } from '../lib/dashboard_panel_converters';
export const loadDashboardHistoryLocationState = (
getScopedHistory: () => ScopedHistory
): Partial<DashboardState> => {
const state = getScopedHistory().location.state as undefined | ForwardedDashboardState;
if (!state) {
return {};
}
const { panels, ...restOfState } = state;
if (!panels?.length) {
return restOfState;
}
return {
...restOfState,
...convertPanelsArrayToPanelSectionMaps(panels),
};
};

View file

@ -187,27 +187,6 @@ describe('dashboard locator', () => {
});
});
test('Control Group Input', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: false,
getDashboardFilterFields: async (dashboardId: string) => [],
});
const controlGroupState = {
autoApplySelections: false,
};
const location = await definition.getLocation({
controlGroupState,
});
expect(location).toMatchObject({
app: 'dashboards',
path: `#/create?_g=()`,
state: {
controlGroupState,
},
});
});
test('if no useHash setting is given, uses the one was start services', async () => {
const definition = new DashboardAppLocatorDefinition({
useHashedUrl: true,

View file

@ -12,10 +12,7 @@ import type { SerializableRecord, Writable } from '@kbn/utility-types';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import type { ViewMode } from '@kbn/presentation-publishing';
import type { RefreshInterval } from '@kbn/data-plugin/public';
import type {
ControlGroupRuntimeState,
ControlGroupSerializedState,
} from '@kbn/controls-plugin/common';
import type { ControlGroupSerializedState } from '@kbn/controls-plugin/common';
import type { DashboardPanelMap, DashboardSectionMap } from './dashboard_container/types';
import type {
@ -76,50 +73,34 @@ export interface DashboardState extends DashboardSettings {
controlGroupInput?: ControlGroupSerializedState;
}
/**
* Dashboard state stored in dashboard URLs
* Do not change type without considering BWC of stored URLs
*/
export type SharedDashboardState = Partial<
export type DashboardLocatorParams = Partial<
Omit<DashboardState, 'panels' | 'sections'> & {
controlGroupInput?: DashboardState['controlGroupInput'] & SerializableRecord;
/**
* Runtime control group state.
* @deprecated use controlGroupInput
*/
controlGroupState?: Partial<ControlGroupRuntimeState> & SerializableRecord;
panels: Array<DashboardPanel | DashboardSection>;
references?: DashboardState['references'] & SerializableRecord;
/**
* If provided, the dashboard with this id will be loaded. If not given, new, unsaved dashboard will be loaded.
*/
dashboardId?: string;
/**
* Determines whether to hash the contents of the url to avoid url length issues. Defaults to the uiSettings configuration for `storeInSessionStorage`.
*/
useHash?: boolean;
/**
* Denotes whether to merge provided filters from the locator state with the filters saved into the Dashboard.
* When false, the saved filters will be overwritten. Defaults to true.
*/
preserveSavedFilters?: boolean;
/**
* Search search session ID to restore.
* (Background search)
*/
searchSessionId?: string;
}
>;
export type DashboardLocatorParams = Partial<SharedDashboardState> & {
/**
* If given, the dashboard saved object with this id will be loaded. If not given,
* a new, unsaved dashboard will be loaded up.
*/
dashboardId?: string;
/**
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
* whether to hash the data in the url to avoid url length issues.
*/
useHash?: boolean;
/**
* When `true` filters from saved filters from destination dashboard as merged with applied filters
* When `false` applied filters take precedence and override saved filters
*
* true is default
*/
preserveSavedFilters?: boolean;
/**
* Search search session ID to restore.
* (Background search)
*/
searchSessionId?: string;
};

View file

@ -0,0 +1,117 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DEFAULT_DASHBOARD_STATE } from './default_dashboard_state';
import { loadDashboardApi } from './load_dashboard_api';
jest.mock('./performance/query_performance_tracking', () => {
return {
startQueryPerformanceTracking: () => {},
};
});
jest.mock('@kbn/content-management-content-insights-public', () => {
class ContentInsightsClientMock {
track() {}
}
return {
ContentInsightsClient: ContentInsightsClientMock,
};
});
const lastSavedQuery = { query: 'memory:>220000', language: 'kuery' };
describe('loadDashboardApi', () => {
const getDashboardApiMock = jest.fn();
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('./get_dashboard_api').getDashboardApi = getDashboardApiMock;
getDashboardApiMock.mockReturnValue({
api: {},
cleanUp: jest.fn(),
internalApi: {},
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('../services/dashboard_content_management_service').getDashboardContentManagementService =
() => ({
loadDashboardState: () => ({
dashboardFound: true,
dashboardInput: DEFAULT_DASHBOARD_STATE,
references: [],
}),
});
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('../services/dashboard_backup_service').getDashboardBackupService = () => ({
getState: () => ({
query: lastSavedQuery,
}),
});
});
afterEach(() => {
jest.resetAllMocks();
});
describe('initialState', () => {
test('should get initialState from saved object', async () => {
await loadDashboardApi({
getCreationOptions: async () => ({
useSessionStorageIntegration: false,
}),
savedObjectId: '12345',
});
expect(getDashboardApiMock).toHaveBeenCalled();
// @ts-ignore
expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual({
...DEFAULT_DASHBOARD_STATE,
references: [],
});
});
test('should overwrite saved object state with unsaved state', async () => {
await loadDashboardApi({
getCreationOptions: async () => ({
useSessionStorageIntegration: true,
}),
savedObjectId: '12345',
});
expect(getDashboardApiMock).toHaveBeenCalled();
// @ts-ignore
expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual({
...DEFAULT_DASHBOARD_STATE,
references: [],
query: lastSavedQuery,
});
});
// dashboard app passes URL state as override state
test('should overwrite saved object state and unsaved state with override state', async () => {
const queryFromUrl = { query: 'memory:>5000', language: 'kuery' };
await loadDashboardApi({
getCreationOptions: async () => ({
useSessionStorageIntegration: true,
getInitialInput: () => ({
query: queryFromUrl,
}),
}),
savedObjectId: '12345',
});
expect(getDashboardApiMock).toHaveBeenCalled();
// @ts-ignore
expect(getDashboardApiMock.mock.calls[0][0].initialState).toEqual({
...DEFAULT_DASHBOARD_STATE,
references: [],
query: queryFromUrl,
});
});
});
});

View file

@ -16,10 +16,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { debounceTime } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { SharedDashboardState } from '../../common/types';
import { DashboardState } from '../../common/types';
import { DashboardApi, DashboardCreationOptions } from '..';
import { DASHBOARD_APP_ID } from '../../common/constants';
import { loadDashboardHistoryLocationState } from '../../common/locator/load_dashboard_history_location_state';
import { DashboardRenderer } from '../dashboard_renderer/dashboard_renderer';
import { DashboardTopNav } from '../dashboard_top_nav';
import {
@ -45,7 +44,11 @@ import {
getSessionURLObservable,
removeSearchSessionIdFromURL,
} from './url/search_sessions_integration';
import { loadAndRemoveDashboardState, startSyncingExpandedPanelState } from './url/url_utils';
import {
extractDashboardState,
loadAndRemoveDashboardState,
startSyncingExpandedPanelState,
} from './url';
export interface DashboardAppProps {
history: History;
@ -137,8 +140,21 @@ export function DashboardApp({
const getCreationOptions = useCallback((): Promise<DashboardCreationOptions> => {
const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
const getInitialInput = () => {
const stateFromLocator = loadDashboardHistoryLocationState(getScopedHistory);
const initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
let stateFromLocator: Partial<DashboardState> = {};
try {
stateFromLocator = extractDashboardState(getScopedHistory().location.state);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Unable to extract dashboard state from locator. Error: ', e);
}
let initialUrlState: Partial<DashboardState> = {};
try {
initialUrlState = loadAndRemoveDashboardState(kbnUrlStateStorage);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Unable to extract dashboard state from URL. Error: ', e);
}
// Override all state with URL + Locator input
return {
@ -206,9 +222,7 @@ export function DashboardApp({
.change$(DASHBOARD_STATE_STORAGE_KEY)
.pipe(debounceTime(10)) // debounce URL updates so react has time to unsubscribe when changing URLs
.subscribe(() => {
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
DASHBOARD_STATE_STORAGE_KEY
);
const rawAppStateInUrl = kbnUrlStateStorage.get<unknown>(DASHBOARD_STATE_STORAGE_KEY);
if (rawAppStateInUrl) setRegenerateId(uuidv4());
});
return () => appStateSubscription.unsubscribe();

View file

@ -22,7 +22,6 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
import { DashboardLocatorParams } from '../../../../common';
import { convertPanelSectionMapsToPanelsArray } from '../../../../common/lib/dashboard_panel_converters';
import { SharedDashboardState } from '../../../../common/types';
import { getDashboardBackupService } from '../../../services/dashboard_backup_service';
import { dataService, shareService } from '../../../services/kibana_services';
import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities';
@ -122,11 +121,11 @@ export function ShowShareModal({
const hasPanelChanges = allUnsavedPanelsMap !== undefined;
const unsavedDashboardStateForLocator: SharedDashboardState = {
const unsavedDashboardStateForLocator: DashboardLocatorParams = {
...unsavedDashboardState,
controlGroupInput:
unsavedDashboardState.controlGroupInput as SharedDashboardState['controlGroupInput'],
references: unsavedDashboardState.references as SharedDashboardState['references'],
unsavedDashboardState.controlGroupInput as DashboardLocatorParams['controlGroupInput'],
references: unsavedDashboardState.references as DashboardLocatorParams['references'],
};
if (allUnsavedPanelsMap || allUnsavedSectionsMap) {
unsavedDashboardStateForLocator.panels = convertPanelSectionMapsToPanelsArray(

View file

@ -0,0 +1,150 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { extractDashboardState } from './extract_dashboard_state';
describe('extractDashboardState', () => {
describe('>= 8.19 state', () => {
test('should extract labelPosition', () => {
const dashboardState = extractDashboardState({
controlGroupInput: {
labelPosition: 'twoLine',
},
});
expect(dashboardState.controlGroupInput?.labelPosition).toBe('twoLine');
});
test('should extract autoApplySelections', () => {
const dashboardState = extractDashboardState({
controlGroupInput: {
autoApplySelections: false,
},
});
expect(dashboardState.controlGroupInput?.autoApplySelections).toBe(false);
});
test('should extract controls', () => {
const dashboardState = extractDashboardState({
controlGroupInput: {
controls: [
{
controlConfig: {
dataViewId: '90943e30-9a47-11e8-b64d-95841ca0b247',
fieldName: 'machine.os.keyword',
selectedOptions: ['win 7'],
},
grow: true,
order: 0,
type: 'optionsListControl',
width: 'small',
},
],
},
});
expect(dashboardState.controlGroupInput?.controls).toEqual([
{
controlConfig: {
dataViewId: '90943e30-9a47-11e8-b64d-95841ca0b247',
fieldName: 'machine.os.keyword',
selectedOptions: ['win 7'],
},
grow: true,
order: 0,
type: 'optionsListControl',
width: 'small',
},
]);
});
});
describe('>= 8.16 to < 8.19 state', () => {
test('should convert controlGroupState to controlGroupInput', () => {
const dashboardState = extractDashboardState({
controlGroupState: {
autoApplySelections: false,
initialChildControlState: {
['6c4f5ff4-92ff-4b40-bcc7-9aea6b06d693']: {
dataViewId: '90943e30-9a47-11e8-b64d-95841ca0b247',
fieldName: 'machine.os.keyword',
grow: false,
selectedOptions: ['win 7'],
},
},
labelPosition: 'twoLine',
},
});
expect(dashboardState.controlGroupInput?.autoApplySelections).toBe(false);
expect(dashboardState.controlGroupInput?.labelPosition).toBe('twoLine');
expect(dashboardState.controlGroupInput?.controls).toEqual([
{
controlConfig: {
dataViewId: '90943e30-9a47-11e8-b64d-95841ca0b247',
fieldName: 'machine.os.keyword',
selectedOptions: ['win 7'],
},
grow: false,
id: '6c4f5ff4-92ff-4b40-bcc7-9aea6b06d693',
},
]);
});
});
describe('< 8.16 state', () => {
test('should convert controlStyle to labelPosition', () => {
const dashboardState = extractDashboardState({
controlGroupInput: {
controlStyle: 'twoLine',
},
});
expect(dashboardState.controlGroupInput?.labelPosition).toBe('twoLine');
});
test('should convert showApplySelections to autoApplySelections', () => {
const dashboardState = extractDashboardState({
controlGroupInput: {
showApplySelections: true,
},
});
expect(dashboardState.controlGroupInput?.autoApplySelections).toBe(false);
});
test('should convert panels to controls', () => {
const dashboardState = extractDashboardState({
controlGroupInput: {
panels: {
['8311639d-92e5-4aa5-99a4-9502b10eead5']: {
explicitInput: {
dataViewId: '90943e30-9a47-11e8-b64d-95841ca0b247',
fieldName: 'machine.os.keyword',
selectedOptions: ['win 7'],
},
grow: true,
order: 0,
type: 'optionsListControl',
width: 'small',
},
},
},
});
expect(dashboardState.controlGroupInput?.controls).toEqual([
{
controlConfig: {
dataViewId: '90943e30-9a47-11e8-b64d-95841ca0b247',
fieldName: 'machine.os.keyword',
selectedOptions: ['win 7'],
},
grow: true,
order: 0,
type: 'optionsListControl',
width: 'small',
},
]);
});
});
});

View file

@ -0,0 +1,78 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
ControlGroupSerializedState,
DEFAULT_AUTO_APPLY_SELECTIONS,
DEFAULT_CONTROL_LABEL_POSITION,
} from '@kbn/controls-plugin/common';
import { serializeRuntimeState } from '@kbn/controls-plugin/public';
import { DashboardState } from '../../../../common';
export function extractControlGroupState(state: {
[key: string]: unknown;
}): DashboardState['controlGroupInput'] {
if (state.controlGroupState && typeof state.controlGroupState === 'object') {
// URL state created in 8.16 through 8.18 passed control group runtime state in with controlGroupState key
return serializeRuntimeState(state.controlGroupState).rawState;
}
if (!state.controlGroupInput || typeof state.controlGroupInput !== 'object') return;
const controlGroupInput = state.controlGroupInput as { [key: string]: unknown };
let autoApplySelections: ControlGroupSerializedState['autoApplySelections'] =
DEFAULT_AUTO_APPLY_SELECTIONS;
if (typeof controlGroupInput.autoApplySelections === 'boolean') {
autoApplySelections = controlGroupInput.autoApplySelections;
} else if (typeof controlGroupInput.showApplySelections === 'boolean') {
// <8.16 autoApplySelections exported as !showApplySelections
autoApplySelections = !controlGroupInput.showApplySelections;
}
let controls: ControlGroupSerializedState['controls'] = [];
if (Array.isArray(controlGroupInput.controls)) {
controls = controlGroupInput.controls;
} else if (controlGroupInput.panels && typeof controlGroupInput.panels === 'object') {
// <8.16 controls exported as panels
const panels = controlGroupInput.panels as {
[key: string]: { [key: string]: unknown } | undefined;
};
controls = Object.keys(controlGroupInput.panels).map((controlId) => {
const { explicitInput, ...restOfControlState } = panels[controlId] ?? {};
return {
...restOfControlState,
controlConfig: explicitInput,
};
}) as ControlGroupSerializedState['controls'];
}
let labelPosition: ControlGroupSerializedState['labelPosition'] = DEFAULT_CONTROL_LABEL_POSITION;
if (typeof controlGroupInput.labelPosition === 'string') {
labelPosition = controlGroupInput.labelPosition as ControlGroupSerializedState['labelPosition'];
} else if (typeof controlGroupInput.controlStyle === 'string') {
// <8.16 labelPosition exported as controlStyle
labelPosition = controlGroupInput.controlStyle as ControlGroupSerializedState['labelPosition'];
}
return {
autoApplySelections,
controls,
chainingSystem:
controlGroupInput.chainingSystem as ControlGroupSerializedState['chainingSystem'],
labelPosition,
...(controlGroupInput.ignoreParentSettings &&
typeof controlGroupInput.ignoreParentSettings === 'object'
? {
ignoreParentSettings:
controlGroupInput.ignoreParentSettings as ControlGroupSerializedState['ignoreParentSettings'],
}
: {}),
};
}

View file

@ -0,0 +1,52 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DashboardSettings } from '../../../../common';
export function extractSettings(state: { [key: string]: unknown }): Partial<DashboardSettings> {
const settings: Partial<DashboardSettings> = {};
if (typeof state.hidePanelTitles === 'boolean') {
settings.hidePanelTitles = state.hidePanelTitles;
}
if (typeof state.useMargins === 'boolean') {
settings.useMargins = state.useMargins;
}
if (typeof state.syncColors === 'boolean') {
settings.syncColors = state.syncColors;
}
if (typeof state.syncTooltips === 'boolean') {
settings.syncTooltips = state.syncTooltips;
}
if (typeof state.syncCursor === 'boolean') {
settings.syncCursor = state.syncCursor;
}
if (typeof state.description === 'string') {
settings.description = state.description;
}
if (Array.isArray(state.tags)) {
settings.tags = state.tags;
}
if (typeof state.timeRestore === 'boolean') {
settings.timeRestore = state.timeRestore;
}
if (typeof state.title === 'string') {
settings.title = state.title;
}
return settings;
}

View file

@ -0,0 +1,39 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { omit } from 'lodash';
import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state';
import { extractDashboardState } from './extract_dashboard_state';
const DASHBOARD_STATE = omit(DEFAULT_DASHBOARD_STATE, ['panels', 'sections']);
describe('extractDashboardState', () => {
test('should extract all DashboardState fields', () => {
const optionalState = {
timeRange: {
from: 'now-15m',
to: 'now',
},
references: [],
refreshInterval: {
pause: false,
value: 5,
},
};
expect(
extractDashboardState({
...DASHBOARD_STATE,
...optionalState,
})
).toEqual({
...DASHBOARD_STATE,
...optionalState,
});
});
});

View file

@ -0,0 +1,38 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DashboardState } from '../../../../common';
import { extractControlGroupState } from './extract_control_group_state';
import { extractSettings } from './extract_dashboard_settings';
import { extractPanelsState } from './extract_panels_state';
import { extractSearchState } from './extract_search_state';
export function extractDashboardState(state?: unknown): Partial<DashboardState> {
let dashboardState: Partial<DashboardState> = {};
if (state && typeof state === 'object') {
const stateAsObject = state as { [key: string]: unknown };
const controlGroupState = extractControlGroupState(stateAsObject);
if (controlGroupState) dashboardState.controlGroupInput = controlGroupState;
if (Array.isArray(stateAsObject.references))
dashboardState.references = stateAsObject.references;
if (typeof stateAsObject.viewMode === 'string')
dashboardState.viewMode = stateAsObject.viewMode as DashboardState['viewMode'];
dashboardState = {
...dashboardState,
...extractPanelsState(stateAsObject),
...extractSearchState(stateAsObject),
...extractSettings(stateAsObject),
};
}
return dashboardState;
}

View file

@ -0,0 +1,118 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { coreServices } from '../../../services/kibana_services';
import { extractPanelsState } from './extract_panels_state';
describe('extractPanelsState', () => {
describe('>= 8.18 panels state', () => {
test('should convert embeddableConfig to panelConfig', () => {
const dashboardState = extractPanelsState({
panels: [
{
panelConfig: {
timeRange: {
from: 'now-7d/d',
to: 'now',
},
},
gridData: {},
id: 'de71f4f0-1902-11e9-919b-ffe5949a18d2',
panelIndex: 'c505cc42-fbde-451d-8720-302dc78d7e0d',
title: 'Custom title',
type: 'map',
},
],
});
expect(dashboardState.panels).toEqual({
['c505cc42-fbde-451d-8720-302dc78d7e0d']: {
explicitInput: {
savedObjectId: 'de71f4f0-1902-11e9-919b-ffe5949a18d2',
timeRange: {
from: 'now-7d/d',
to: 'now',
},
title: 'Custom title',
},
gridData: {},
type: 'map',
panelRefName: undefined,
version: undefined,
},
});
});
});
describe('< 8.17 panels state', () => {
test('should convert embeddableConfig to panelConfig', () => {
const dashboardState = extractPanelsState({
panels: [
{
embeddableConfig: {
timeRange: {
from: 'now-7d/d',
to: 'now',
},
},
gridData: {},
id: 'de71f4f0-1902-11e9-919b-ffe5949a18d2',
panelIndex: 'c505cc42-fbde-451d-8720-302dc78d7e0d',
title: 'Custom title',
type: 'map',
},
],
});
expect(dashboardState.panels).toEqual({
['c505cc42-fbde-451d-8720-302dc78d7e0d']: {
explicitInput: {
savedObjectId: 'de71f4f0-1902-11e9-919b-ffe5949a18d2',
timeRange: {
from: 'now-7d/d',
to: 'now',
},
title: 'Custom title',
},
gridData: {},
type: 'map',
panelRefName: undefined,
version: undefined,
},
});
});
});
describe('< 7.3 panels state', () => {
test('should ignore state and notify user', () => {
const dashboardState = extractPanelsState({
panels: [
{
col: 1,
id: 'Visualization-MetricChart',
panelIndex: 1,
row: 1,
size_x: 6,
size_y: 3,
type: 'visualization',
},
{
col: 7,
id: 'Visualization-PieChart',
panelIndex: 2,
row: 1,
size_x: 6,
size_y: 3,
type: 'visualization',
},
],
});
expect(dashboardState).toEqual({});
expect(coreServices.notifications.toasts.addWarning).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,65 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import semverSatisfies from 'semver/functions/satisfies';
import { convertPanelsArrayToPanelSectionMaps } from '../../../../common/lib/dashboard_panel_converters';
import { DashboardState } from '../../../../common';
import { coreServices } from '../../../services/kibana_services';
import { getPanelTooOldErrorString } from '../../_dashboard_app_strings';
type PanelState = Pick<DashboardState, 'panels' | 'sections'>;
/**
* We no longer support loading panels from a version older than 7.3 in the URL.
* @returns whether or not there is a panel in the URL state saved with a version before 7.3
*/
const isPanelVersionTooOld = (panels: unknown[]) => {
for (const panel of panels) {
if (!panel || typeof panel !== 'object' || 'panels' in panel) {
continue;
}
const panelAsObject = panel as { [key: string]: unknown };
if (
!panelAsObject.gridData ||
!(panelAsObject.panelConfig || panelAsObject.embeddableConfig) ||
(panelAsObject.version && semverSatisfies(panelAsObject.version as string, '<7.3'))
)
return true;
}
return false;
};
export function extractPanelsState(state: { [key: string]: unknown }): Partial<PanelState> {
const panels = Array.isArray(state.panels) ? state.panels : [];
if (panels.length === 0) {
return {};
}
if (isPanelVersionTooOld(panels)) {
coreServices.notifications.toasts.addWarning(getPanelTooOldErrorString());
return {};
}
// < 8.17 panels state stored panelConfig as embeddableConfig
const standardizedPanels = panels.map((panel) => {
if (typeof panel === 'object' && panel?.embeddableConfig) {
const { embeddableConfig, ...rest } = panel;
return {
...rest,
panelConfig: embeddableConfig,
};
}
return panel;
});
return convertPanelsArrayToPanelSectionMaps(standardizedPanels);
}

View file

@ -0,0 +1,40 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DashboardState } from '../../../../common';
import { migrateLegacyQuery } from '../../../services/dashboard_content_management_service/lib/load_dashboard_state';
type DashboardSearchState = Pick<
DashboardState,
'filters' | 'query' | 'refreshInterval' | 'timeRange'
>;
export function extractSearchState(state: {
[key: string]: unknown;
}): Partial<DashboardSearchState> {
const searchState: Partial<DashboardSearchState> = {};
if (Array.isArray(state.filters)) {
searchState.filters = state.filters;
}
if (state.query && typeof state.query === 'object') {
searchState.query = migrateLegacyQuery(state.query);
}
if (state.refreshInterval && typeof state.refreshInterval === 'object') {
searchState.refreshInterval = state.refreshInterval as DashboardState['refreshInterval'];
}
if (state.timeRange && typeof state.timeRange === 'object') {
searchState.timeRange = state.timeRange as DashboardState['timeRange'];
}
return searchState;
}

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { extractDashboardState } from './extract_dashboard_state';

View file

@ -0,0 +1,11 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { extractDashboardState } from './bwc';
export { loadAndRemoveDashboardState, startSyncingExpandedPanelState } from './url_utils';

View file

@ -7,85 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { History } from 'history';
import _ from 'lodash';
import { skip } from 'rxjs';
import semverSatisfies from 'semver/functions/satisfies';
import { serializeRuntimeState } from '@kbn/controls-plugin/public';
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import type {
DashboardPanelMap,
DashboardSectionMap,
} from '../../../common/dashboard_container/types';
import { convertPanelsArrayToPanelSectionMaps } from '../../../common/lib/dashboard_panel_converters';
import type { DashboardState, SharedDashboardState } from '../../../common/types';
import type { DashboardPanel, DashboardSection } from '../../../server/content_management';
import type { SavedDashboardPanel } from '../../../server/dashboard_saved_object';
import { History } from 'history';
import { skip } from 'rxjs';
import type { DashboardState } from '../../../common/types';
import { DashboardApi } from '../../dashboard_api/types';
import { migrateLegacyQuery } from '../../services/dashboard_content_management_service/lib/load_dashboard_state';
import { coreServices } from '../../services/kibana_services';
import { DASHBOARD_STATE_STORAGE_KEY, createDashboardEditUrl } from '../../utils/urls';
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
const panelIsLegacy = (panel: unknown): panel is SavedDashboardPanel => {
return (panel as SavedDashboardPanel).embeddableConfig !== undefined;
};
/**
* We no longer support loading panels from a version older than 7.3 in the URL.
* @returns whether or not there is a panel in the URL state saved with a version before 7.3
*/
export const isPanelVersionTooOld = (
panels: Array<DashboardPanel | DashboardSection> | SavedDashboardPanel[]
) => {
for (const panel of panels) {
if ('panels' in panel) {
// can't use isDashboardSection type guard because of SavedDashboardPanel type
continue; // ignore sections
}
if (
!panel.gridData ||
!((panel as DashboardPanel).panelConfig || (panel as SavedDashboardPanel).embeddableConfig) ||
(panel.version && semverSatisfies(panel.version, '<7.3'))
)
return true;
}
return false;
};
function getPanelSectionMaps(
panels?: Array<DashboardPanel | DashboardSection>
): { panels: DashboardPanelMap; sections: DashboardSectionMap } | undefined {
if (!panels) {
return undefined;
}
if (panels.length === 0) {
return { panels: {}, sections: {} };
}
if (isPanelVersionTooOld(panels)) {
coreServices.notifications.toasts.addWarning(getPanelTooOldErrorString());
return undefined;
}
// convert legacy embeddableConfig keys to panelConfig
const standardizedPanels = panels.map((panel) => {
if (panelIsLegacy(panel)) {
const { embeddableConfig, ...rest } = panel;
return {
...rest,
panelConfig: embeddableConfig,
};
}
return panel;
});
return convertPanelsArrayToPanelSectionMaps(standardizedPanels);
}
import { extractDashboardState } from './bwc/extract_dashboard_state';
/**
* Loads any dashboard state from the URL, and removes the state from the URL.
@ -93,31 +22,18 @@ function getPanelSectionMaps(
export const loadAndRemoveDashboardState = (
kbnUrlStateStorage: IKbnUrlStateStorage
): Partial<DashboardState> => {
const rawAppStateInUrl = kbnUrlStateStorage.get<SharedDashboardState>(
DASHBOARD_STATE_STORAGE_KEY
);
const rawAppStateInUrl = kbnUrlStateStorage.get<unknown>(DASHBOARD_STATE_STORAGE_KEY);
if (!rawAppStateInUrl) return {};
const converted = getPanelSectionMaps(rawAppStateInUrl.panels);
// clear application state from URL
const nextUrl = replaceUrlHashQuery(window.location.href, (hashQuery) => {
delete hashQuery[DASHBOARD_STATE_STORAGE_KEY];
return hashQuery;
});
kbnUrlStateStorage.kbnUrlControls.update(nextUrl, true);
const partialState: Partial<DashboardState> = {
..._.omit(rawAppStateInUrl, ['controlGroupState', 'panels', 'query']),
...(rawAppStateInUrl.controlGroupState
? {
controlGroupInput: serializeRuntimeState(rawAppStateInUrl.controlGroupState).rawState,
}
: {}),
...(converted?.panels ? { panels: converted.panels } : {}),
...(converted?.sections ? { sections: converted.sections } : {}),
...(rawAppStateInUrl.query ? { query: migrateLegacyQuery(rawAppStateInUrl.query) } : {}),
};
return partialState;
return extractDashboardState(rawAppStateInUrl);
};
export const startSyncingExpandedPanelState = ({

View file

@ -1,168 +0,0 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const { dashboard, header } = getPageObjects(['dashboard', 'header']);
const toasts = getService('toasts');
const browser = getService('browser');
const log = getService('log');
const queryBar = getService('queryBar');
let kibanaLegacyBaseUrl: string;
const urlQuery =
`` +
`_g=(refreshInterval:(pause:!t,value:0),` +
`time:(from:'2012-11-17T00:00:00.000Z',mode:absolute,to:'2015-11-17T18:01:36.621Z'))&` +
`_a=(description:'',filters:!(),` +
`fullScreenMode:!f,` +
`options:(),` +
`panels:!((col:1,id:Visualization-MetricChart,panelIndex:1,row:1,size_x:6,size_y:3,type:visualization),` +
`(col:7,id:Visualization-PieChart,panelIndex:2,row:1,size_x:6,size_y:3,type:visualization)),` +
`query:(language:lucene,query:'memory:%3E220000'),` +
`timeRestore:!f,` +
`title:'New+Dashboard',` +
`uiState:(P-1:(vis:(defaultColors:('0+-+100':'rgb(0,104,55)'))),` +
`P-2:(vis:(colors:('200,000':%23F9D9F9,` +
`'240,000':%23F9D9F9,` +
`'280,000':%23F9D9F9,` +
`'320,000':%23F9D9F9,` +
`'360,000':%23F9D9F9),` +
`legendOpen:!t))),` +
`viewMode:edit)`;
describe('bwc shared urls', function describeIndexTests() {
before(async function () {
await dashboard.initTests();
await dashboard.preserveCrossAppState();
const currentUrl = await browser.getCurrentUrl();
kibanaLegacyBaseUrl =
currentUrl.substring(0, currentUrl.indexOf('/app/dashboards')) + '/app/kibana';
});
describe('5.6 urls', () => {
it('url with filters and query', async () => {
const url56 =
`` +
`_g=(refreshInterval:(display:Off,pause:!f,value:0),` +
`time:(from:'2012-11-17T00:00:00.000Z',mode:absolute,to:'2015-11-17T18:01:36.621Z'))&` +
`_a=(` +
`description:'',` +
`filters:!(('$state':(store:appState),` +
`meta:(alias:!n,disabled:!f,index:'logstash-*',key:bytes,negate:!f,type:phrase,value:'12345'),` +
`query:(match:(bytes:(query:12345,type:phrase))))),` +
`fullScreenMode:!f,` +
`options:(),` +
`panels:!((col:1,id:Visualization-MetricChart,panelIndex:1,row:1,size_x:6,size_y:3,type:visualization),` +
`(col:7,id:Visualization-PieChart,panelIndex:2,row:1,size_x:6,size_y:3,type:visualization)),` +
`query:(query_string:(analyze_wildcard:!t,query:'memory:>220000')),` +
`timeRestore:!f,` +
`title:'New+Dashboard',` +
`uiState:(),` +
`viewMode:edit)`;
const url = `${kibanaLegacyBaseUrl}#/dashboard?${url56}`;
log.debug(`Navigating to ${url}`);
await browser.get(url, true);
await header.waitUntilLoadingHasFinished();
const query = await queryBar.getQueryString();
expect(query).to.equal('memory:>220000');
const warningToast = await toasts.getElementByIndex(1);
expect(await warningToast.getVisibleText()).to.contain('Cannot load panels');
await dashboard.waitForRenderComplete();
});
});
describe('6.0 urls', () => {
let savedDashboardId: string;
it('loads an unsaved dashboard', async function () {
const url = `${kibanaLegacyBaseUrl}#/dashboard?${urlQuery}`;
log.debug(`Navigating to ${url}`);
await browser.get(url, true);
await header.waitUntilLoadingHasFinished();
const query = await queryBar.getQueryString();
expect(query).to.equal('memory:>220000');
const warningToast = await toasts.getElementByIndex(1);
expect(await warningToast.getVisibleText()).to.contain('Cannot load panels');
await dashboard.waitForRenderComplete();
});
it('loads a saved dashboard', async function () {
await dashboard.saveDashboard('saved with colors', {
saveAsNew: true,
storeTimeWithDashboard: true,
});
savedDashboardId = await dashboard.getDashboardIdFromCurrentUrl();
const url = `${kibanaLegacyBaseUrl}#/dashboard/${savedDashboardId}`;
log.debug(`Navigating to ${url}`);
await browser.get(url, true);
await header.waitUntilLoadingHasFinished();
const query = await queryBar.getQueryString();
expect(query).to.equal('memory:>220000');
await dashboard.waitForRenderComplete();
});
it('loads a saved dashboard with query via dashboard_no_match', async function () {
await dashboard.gotoDashboardLandingPage();
const currentUrl = await browser.getCurrentUrl();
const dashboardBaseUrl = currentUrl.substring(0, currentUrl.indexOf('/app/dashboards'));
const url = `${dashboardBaseUrl}/app/dashboards#/dashboard/${savedDashboardId}?_a=(query:(language:kuery,query:'boop'))`;
log.debug(`Navigating to ${url}`);
await browser.get(url);
await header.waitUntilLoadingHasFinished();
const query = await queryBar.getQueryString();
expect(query).to.equal('boop');
await dashboard.waitForRenderComplete();
});
it('uiState in url takes precedence over saved dashboard state', async function () {
const id = await dashboard.getDashboardIdFromCurrentUrl();
const updatedQuery = urlQuery.replace(/F9D9F9/g, '000000');
const url = `${kibanaLegacyBaseUrl}#/dashboard/${id}?${updatedQuery}`;
log.debug(`Navigating to ${url}`);
await browser.get(url, true);
await header.waitUntilLoadingHasFinished();
});
it('back button works for old dashboards after state migrations', async () => {
await dashboard.preserveCrossAppState();
const oldId = await dashboard.getDashboardIdFromCurrentUrl();
await dashboard.waitForRenderComplete();
const url = `${kibanaLegacyBaseUrl}#/dashboard?${urlQuery}`;
log.debug(`Navigating to ${url}`);
await browser.get(url);
await header.waitUntilLoadingHasFinished();
await dashboard.waitForRenderComplete();
await browser.goBack();
await header.waitUntilLoadingHasFinished();
const newId = await dashboard.getDashboardIdFromCurrentUrl();
expect(newId).to.be.equal(oldId);
await dashboard.waitForRenderComplete();
await queryBar.submitQuery();
});
});
});
}

View file

@ -0,0 +1,94 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const { common, dashboard, dashboardControls, header, home } = getPageObjects([
'common',
'dashboard',
'dashboardControls',
'header',
'home',
]);
const kibanaServer = getService('kibanaServer');
const browser = getService('browser');
const deployment = getService('deployment');
const find = getService('find');
async function assertDashboardRendered() {
await header.waitUntilLoadingHasFinished();
await dashboard.waitForRenderComplete();
const controlIds = await dashboardControls.getAllControlIds();
expect(controlIds.length).to.be(1);
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlIds[0]);
expect(selectionString).to.be('win 7');
const panels = await dashboard.getDashboardPanels();
expect(panels.length).to.be(1);
const titles = await dashboard.getPanelTitles();
expect(titles.length).to.be(1);
expect(titles[0]).to.equal('Custom map');
// can not use maps page object because its in x-pack
// TODO - move map page object to src/test page objects
expect((await find.allByCssSelector('.mapTocEntry')).length).to.be(4);
}
describe('bwc short urls', () => {
let baseUrl: string;
before(async () => {
await common.navigateToUrl('home', '/tutorial_directory/sampleData', {
useActualUrl: true,
});
await header.waitUntilLoadingHasFinished();
await home.addSampleDataSet('logs');
await kibanaServer.importExport.load(
'src/platform/test/functional/fixtures/kbn_archiver/dashboard/current/bwc_short_urls'
);
baseUrl = deployment.getHostPort();
});
after(async () => {
await common.navigateToUrl('home', '/tutorial_directory/sampleData', {
useActualUrl: true,
});
await header.waitUntilLoadingHasFinished();
await home.removeSampleDataSet('logs');
await kibanaServer.importExport.unload(
'src/platform/test/functional/fixtures/kbn_archiver/dashboard/current/bwc_short_urls'
);
});
// 8.14 before the Embeddable refactor
it('should load dashboard with 8.14 state', async () => {
await browser.navigateTo(baseUrl + '/goto/url_to_8_14_dashboard');
await assertDashboardRendered();
});
// 8.18 after the embeddable refactor and before Serialized state only
it('should load dashboard with 8.18 state', async () => {
await browser.navigateTo(baseUrl + '/goto/url_to_8_18_dashboard');
await assertDashboardRendered();
});
// 8.18 after the embeddable refactor and after Serialized state only
it('should load dashboard with 8.19 state', async () => {
await browser.navigateTo(baseUrl + '/goto/url_to_8_19_dashboard');
await assertDashboardRendered();
});
});
}

View file

@ -11,7 +11,7 @@ import expect from '@kbn/expect';
import chroma from 'chroma-js';
import rison from '@kbn/rison';
import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/common/content_management/constants';
import { SharedDashboardState } from '@kbn/dashboard-plugin/common/types';
import { DashboardLocatorParams } from '@kbn/dashboard-plugin/common';
import { DashboardPanel } from '@kbn/dashboard-plugin/server';
import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../../page_objects/dashboard_page';
import { FtrProviderContext } from '../../../ftr_provider_context';
@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const updateAppStateQueryParam = (
url: string,
setAppState: (appState: Partial<SharedDashboardState>) => Partial<SharedDashboardState>
setAppState: (appState: Partial<DashboardLocatorParams>) => Partial<DashboardLocatorParams>
) => {
log.debug(`updateAppStateQueryParam, before url: ${url}`);
@ -53,8 +53,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}
const urlBeforeClientQueryParams = url.substring(0, clientQueryParamsStartIndex);
const urlParams = new URLSearchParams(url.substring(clientQueryParamsStartIndex + 1));
const appState: Partial<SharedDashboardState> = urlParams.has('_a')
? (rison.decode(urlParams.get('_a')!) as Partial<SharedDashboardState>)
const appState: Partial<DashboardLocatorParams> = urlParams.has('_a')
? (rison.decode(urlParams.get('_a')!) as Partial<DashboardLocatorParams>)
: {};
const newAppState = {
...appState,
@ -200,7 +200,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const currentUrl = await getUrlFromShare();
const newUrl = updateAppStateQueryParam(
currentUrl,
(appState: Partial<SharedDashboardState>) => {
(appState: Partial<DashboardLocatorParams>) => {
return {
query: {
language: 'kuery',
@ -229,7 +229,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const currentPanelDimensions = await dashboard.getPanelDimensions();
const newUrl = updateAppStateQueryParam(
currentUrl,
(appState: Partial<SharedDashboardState>) => {
(appState: Partial<DashboardLocatorParams>) => {
log.debug(JSON.stringify(appState, null, ' '));
return {
panels: (appState.panels ?? []).map((widget) => {
@ -273,7 +273,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const currentUrl = (await getUrlFromShare()) ?? '';
const newUrl = updateAppStateQueryParam(
currentUrl,
(appState: Partial<SharedDashboardState>) => {
(appState: Partial<DashboardLocatorParams>) => {
return {
panels: [],
};
@ -306,7 +306,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const currentUrl = await getUrlFromShare();
const newUrl = updateAppStateQueryParam(
currentUrl,
(appState: Partial<SharedDashboardState>) => {
(appState: Partial<DashboardLocatorParams>) => {
return {
panels: (appState.panels ?? []).map((widget) => {
const panel = widget as DashboardPanel;
@ -351,7 +351,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const currentUrl = await getUrlFromShare();
const newUrl = updateAppStateQueryParam(
currentUrl,
(appState: Partial<SharedDashboardState>) => {
(appState: Partial<DashboardLocatorParams>) => {
return {
panels: (appState.panels ?? []).map((widget) => {
const panel = widget as DashboardPanel;

View file

@ -35,7 +35,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_time_picker'));
} else {
loadTestFile(require.resolve('./dashboard_time_picker'));
loadTestFile(require.resolve('./bwc_shared_urls'));
loadTestFile(require.resolve('./bwc_short_urls'));
loadTestFile(require.resolve('./panel_cloning'));
loadTestFile(require.resolve('./copy_panel_to'));
loadTestFile(require.resolve('./panel_context_menu'));

View file

@ -0,0 +1,68 @@
{
"id": "url_to_8_14_dashboard",
"type": "url",
"namespaces": [
"default"
],
"updated_at": "2025-06-02T21:07:10.533Z",
"created_at": "2025-06-02T21:07:10.533Z",
"created_by": "u_2113432222_cloud",
"version": "WzMxMTEsMV0=",
"attributes": {
"accessCount": 0,
"accessDate": 1748898430533,
"createDate": 1748898430533,
"slug": "nlpkp",
"locatorJSON": "{\"id\":\"LEGACY_SHORT_URL_LOCATOR\",\"version\":\"8.14.3\",\"state\":{\"url\":\"/app/dashboards#/create?_g=(refreshInterval:(pause:!t,value:60000),time:(from:now-24h%2Fh,to:now))&_a=(controlGroupInput:(chainingSystem:HIERARCHICAL,controlStyle:oneLine,id:control_group_0bfa1e05-fb6a-45b6-be34-a4c21dd70c43,ignoreParentSettings:(ignoreFilters:!f,ignoreQuery:!f,ignoreTimerange:!f,ignoreValidations:!f),panels:(c86a8845-49b3-453a-bc94-ef345c1cd65a:(explicitInput:(dataViewId:%2790943e30-9a47-11e8-b64d-95841ca0b247%27,fieldName:machine.os.keyword,grow:!t,id:c86a8845-49b3-453a-bc94-ef345c1cd65a,searchTechnique:prefix,selectedOptions:!(%27win%207%27),title:machine.os.keyword,width:medium),grow:!t,order:0,type:optionsListControl,width:medium)),showApplySelections:!f),panels:!((embeddableConfig:(description:%27%27,hiddenLayers:!(),isLayerTOCOpen:!t,mapBuffer:(maxLat:55.77657,maxLon:-45,minLat:21.94305,minLon:-135),mapCenter:(lat:42.16337,lon:-88.92107,zoom:3.64),openTOCDetails:!()),gridData:(h:15,i:d894350c-9f04-4ebf-a8c4-ed5c83ae08bf,w:24,x:0,y:0),id:de71f4f0-1902-11e9-919b-ffe5949a18d2,panelIndex:d894350c-9f04-4ebf-a8c4-ed5c83ae08bf,title:%27Custom%20map%27,type:map)))\"}}",
"url": ""
},
"references": [],
"managed": false,
"coreMigrationVersion": "8.8.0"
}
{
"id": "url_to_8_18_dashboard",
"type": "url",
"namespaces": [
"default"
],
"updated_at": "2025-06-02T21:05:35.714Z",
"created_at": "2025-06-02T21:05:35.714Z",
"created_by": "u_2113432222_cloud",
"updated_by": "u_2113432222_cloud",
"version": "WzE0OCwxXQ==",
"attributes": {
"accessCount": 0,
"accessDate": 1748898335714,
"createDate": 1748898335714,
"slug": "DmzZk",
"locatorJSON": "{\"id\":\"LEGACY_SHORT_URL_LOCATOR\",\"version\":\"8.18.2-SNAPSHOT\",\"state\":{\"url\":\"/app/dashboards#/create?_g=(refreshInterval:(pause:!t,value:1000),time:(from:now-24h%2Fh,to:now))&_a=(controlGroupState:(initialChildControlState:(%271ad147ca-fb8e-4c56-9172-2bdb2916222e%27:(dataViewId:%2790943e30-9a47-11e8-b64d-95841ca0b247%27,exclude:!n,existsSelected:!n,fieldName:machine.os.keyword,grow:!f,hideActionBar:!n,hideExclude:!n,hideExists:!n,hideSort:!n,order:0,placeholder:!n,runPastTimeout:!n,searchTechnique:prefix,selectedOptions:!(%27win%207%27),singleSelect:!n,sort:(by:_count,direction:desc),title:!n,type:optionsListControl,width:medium))),panels:!((gridData:(h:15,i:%279593d985-352b-45a3-873b-28001453427a%27,w:24,x:0,y:0),id:de71f4f0-1902-11e9-919b-ffe5949a18d2,panelConfig:(description:%27%27,hiddenLayers:!(),isLayerTOCOpen:!t,mapCenter:(lat:42.16337,lon:-88.92107,zoom:3.64),openTOCDetails:!()),panelIndex:%279593d985-352b-45a3-873b-28001453427a%27,title:%27Custom%20map%27,type:map)))\"}}",
"url": ""
},
"references": [],
"managed": false,
"coreMigrationVersion": "8.8.0"
}
{
"id": "url_to_8_19_dashboard",
"type": "url",
"namespaces": [
"default"
],
"updated_at": "2025-06-02T21:22:52.723Z",
"created_at": "2025-06-02T21:22:52.723Z",
"version": "WzEyNTcsMV0=",
"attributes": {
"accessCount": 0,
"accessDate": 1748899372723,
"createDate": 1748899372723,
"slug": "8mVkz",
"locatorJSON": "{\"id\":\"DASHBOARD_APP_LOCATOR\",\"version\":\"9.1.0\",\"state\":{\"preserveSavedFilters\":true,\"viewMode\":\"edit\",\"useHash\":false,\"timeRange\":{\"from\":\"2025-06-01T21:00:00.000Z\",\"to\":\"2025-06-02T21:22:52.704Z\"},\"references\":[{\"name\":\"controlGroup_eae4dc4e-63b0-4506-9566-5745d5bd50af:optionsListDataView\",\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\"}],\"controlGroupInput\":{\"autoApplySelections\":true,\"chainingSystem\":\"HIERARCHICAL\",\"ignoreParentSettings\":{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false},\"labelPosition\":\"oneLine\",\"controls\":[{\"id\":\"eae4dc4e-63b0-4506-9566-5745d5bd50af\",\"grow\":false,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"controlConfig\":{\"dataViewId\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"fieldName\":\"machine.os.keyword\",\"exclude\":false,\"existsSelected\":false,\"selectedOptions\":[\"win 7\"],\"searchTechnique\":\"prefix\",\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"}}}]},\"panels\":[{\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"7696a9aa-a648-4474-97fb-a6b3326636f2\"},\"panelIndex\":\"7696a9aa-a648-4474-97fb-a6b3326636f2\",\"panelConfig\":{\"description\":\"\",\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"hiddenLayers\":[],\"isLayerTOCOpen\":true,\"mapBuffer\":{\"minLon\":-112.5,\"minLat\":21.94305,\"maxLon\":-67.5,\"maxLat\":55.77657},\"mapCenter\":{\"lon\":-88.92107,\"lat\":42.16337,\"zoom\":3.64},\"openTOCDetails\":[]},\"title\":\"Custom map\",\"id\":\"de71f4f0-1902-11e9-919b-ffe5949a18d2\"}]}}",
"url": ""
},
"references": [],
"managed": false,
"coreMigrationVersion": "8.8.0"
}