mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[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:
parent
fca224701f
commit
5e594b4a82
24 changed files with 958 additions and 381 deletions
|
@ -20,7 +20,7 @@ pageLoadAssetSize:
|
|||
console: 61298
|
||||
contentConnectors: 65000
|
||||
contentManagement: 16254
|
||||
controls: 12000
|
||||
controls: 13000
|
||||
core: 564663
|
||||
crossClusterReplication: 65408
|
||||
customIntegrations: 22034
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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 = ({
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue