mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
More dashboard migrations (#39387)
* More dashboard migrations * address review comments * remove unused translations * use logger instead of console * remove need for lodash * clean up translations * Add ui metric tracking so we have a better idea whether being stricter with migrations will cause issues * undo trackUiMetric... not available when migrations run
This commit is contained in:
parent
b519f36333
commit
698efe7566
27 changed files with 1226 additions and 502 deletions
|
@ -137,9 +137,6 @@
|
|||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ export function embeddableStateChanged(changeData: {
|
|||
const originalPanelState = getPanel(getState(), panelId);
|
||||
const newPanelState: SavedDashboardPanel = {
|
||||
...originalPanelState,
|
||||
embeddableConfig: _.cloneDeep(embeddableState.customization),
|
||||
embeddableConfig: _.cloneDeep(embeddableState.customization) || {},
|
||||
};
|
||||
dispatch(updatePanel(newPanelState));
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ import {
|
|||
SavedDashboardPanelMap,
|
||||
DashboardAppStateParameters,
|
||||
AddFilterFn,
|
||||
DashboardAppStateDefaults,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
|
@ -96,7 +97,7 @@ export class DashboardStateManager {
|
|||
private hideWriteControls: boolean;
|
||||
public isDirty: boolean;
|
||||
private changeListeners: Array<(status: { dirty: boolean }) => void>;
|
||||
private stateMonitor: StateMonitor<DashboardAppStateParameters>;
|
||||
private stateMonitor: StateMonitor<DashboardAppStateDefaults>;
|
||||
private panelIndexPatternMapping: { [key: string]: StaticIndexPattern[] } = {};
|
||||
private addFilter: AddFilterFn;
|
||||
private unsubscribe: () => void;
|
||||
|
@ -149,7 +150,7 @@ export class DashboardStateManager {
|
|||
/**
|
||||
* Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState.
|
||||
*/
|
||||
this.stateMonitor = stateMonitorFactory.create<DashboardAppStateParameters>(
|
||||
this.stateMonitor = stateMonitorFactory.create<DashboardAppStateDefaults>(
|
||||
this.appState,
|
||||
this.stateDefaults
|
||||
);
|
||||
|
|
|
@ -36,13 +36,7 @@ import {
|
|||
} from '../dashboard_constants';
|
||||
import { DashboardViewMode } from '../dashboard_view_mode';
|
||||
import { DashboardPanel } from '../panel';
|
||||
import { PanelUtils } from '../panel/panel_utils';
|
||||
import {
|
||||
GridData,
|
||||
SavedDashboardPanel,
|
||||
Pre61SavedDashboardPanel,
|
||||
SavedDashboardPanelMap,
|
||||
} from '../types';
|
||||
import { GridData, SavedDashboardPanel, SavedDashboardPanelMap } from '../types';
|
||||
|
||||
let lastValidGridSize = 0;
|
||||
|
||||
|
@ -179,20 +173,6 @@ class DashboardGridUi extends React.Component<Props, State> {
|
|||
|
||||
public buildLayoutFromPanels(): GridData[] {
|
||||
return _.map(this.props.panels, panel => {
|
||||
// panel version numbers added in 6.1. Any panel without version number is assumed to be 6.0.0
|
||||
const panelVersion =
|
||||
'version' in panel
|
||||
? PanelUtils.parseVersion(panel.version)
|
||||
: PanelUtils.parseVersion('6.0.0');
|
||||
|
||||
if (panelVersion.major < 6 || (panelVersion.major === 6 && panelVersion.minor < 1)) {
|
||||
panel = PanelUtils.convertPanelDataPre_6_1((panel as unknown) as Pre61SavedDashboardPanel);
|
||||
}
|
||||
|
||||
if (panelVersion.major < 6 || (panelVersion.major === 6 && panelVersion.minor < 3)) {
|
||||
PanelUtils.convertPanelDataPre_6_3(panel as SavedDashboardPanel, this.props.useMargins);
|
||||
}
|
||||
|
||||
return (panel as SavedDashboardPanel).gridData;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Provider } from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
import sizeMe from 'react-sizeme';
|
||||
|
||||
import { getEmbeddableFactoryMock } from '../__tests__';
|
||||
import { store } from '../../store';
|
||||
import { DashboardGridContainer } from './dashboard_grid_container';
|
||||
import { updatePanels, updateTimeRange, updateUseMargins } from '../actions';
|
||||
|
||||
jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.3.0' }), { virtual: true });
|
||||
|
||||
jest.mock('ui/notify',
|
||||
() => ({
|
||||
toastNotifications: {
|
||||
addDanger: () => {},
|
||||
}
|
||||
}), { virtual: true });
|
||||
|
||||
function getProps(props = {}) {
|
||||
const defaultTestProps = {
|
||||
hidden: false,
|
||||
getEmbeddableFactory: () => getEmbeddableFactoryMock(),
|
||||
};
|
||||
return Object.assign(defaultTestProps, props);
|
||||
}
|
||||
|
||||
function createOldPanelData(col, id, row, sizeX, sizeY, panelIndex) {
|
||||
return { col, id, row, size_x: sizeX, size_y: sizeY, type: 'visualization', panelIndex };
|
||||
}
|
||||
|
||||
const getSelection = window.getSelection;
|
||||
beforeAll(() => {
|
||||
// sizeme detects the width to be 0 in our test environment. noPlaceholder will mean that the grid contents will
|
||||
// get rendered even when width is 0, which will improve our tests.
|
||||
sizeMe.noPlaceholders = true;
|
||||
|
||||
// react-grid-layout calls getSelection which isn't support by jsdom
|
||||
// it's called regardless of whether we need to remove selection,
|
||||
// and in this case we don't need to remove selection
|
||||
window.getSelection = () => {
|
||||
return {
|
||||
removeAllRanges: () => {}
|
||||
};
|
||||
};
|
||||
store.dispatch(updateTimeRange({ to: 'now', from: 'now-15m' }));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
sizeMe.noPlaceholders = false;
|
||||
window.getSelection = getSelection;
|
||||
});
|
||||
|
||||
test('loads old panel data in the right order', () => {
|
||||
const panelData = [
|
||||
createOldPanelData(3, 'foo1', 1, 2, 2, 1),
|
||||
createOldPanelData(5, 'foo2', 1, 2, 2, 2),
|
||||
createOldPanelData(9, 'foo3', 1, 2, 2, 3),
|
||||
createOldPanelData(11, 'foo4', 1, 2, 2, 4),
|
||||
createOldPanelData(1, 'foo5', 1, 2, 2, 5),
|
||||
createOldPanelData(7, 'foo6', 1, 2, 2, 6),
|
||||
createOldPanelData(4, 'foo7', 6, 3, 2, 7),
|
||||
createOldPanelData(1, 'foo8', 8, 3, 2, 8),
|
||||
createOldPanelData(10, 'foo9', 8, 3, 2, 9),
|
||||
createOldPanelData(10, 'foo10', 6, 3, 2, 10),
|
||||
createOldPanelData(4, 'foo11', 8, 3, 2, 11),
|
||||
createOldPanelData(7, 'foo12', 8, 3, 2, 12),
|
||||
createOldPanelData(1, 'foo13', 6, 3, 2, 13),
|
||||
createOldPanelData(7, 'foo14', 6, 3, 2, 14),
|
||||
createOldPanelData(5, 'foo15', 3, 6, 3, 15),
|
||||
createOldPanelData(1, 'foo17', 3, 4, 3, 16)
|
||||
];
|
||||
|
||||
store.dispatch(updatePanels(panelData));
|
||||
store.dispatch(updateUseMargins(false));
|
||||
|
||||
const grid = mountWithIntl(<Provider store={store}><DashboardGridContainer {...getProps()} /></Provider>);
|
||||
|
||||
const panels = store.getState().dashboard.panels;
|
||||
expect(Object.keys(panels).length).toBe(16);
|
||||
|
||||
const foo8Panel = _.find(panels, panel => panel.id === 'foo8');
|
||||
expect(foo8Panel.row).toBe(undefined);
|
||||
expect(foo8Panel.gridData.y).toBe(35);
|
||||
expect(foo8Panel.gridData.x).toBe(0);
|
||||
|
||||
grid.unmount();
|
||||
});
|
||||
|
||||
test('loads old panel data in the right order with margins', () => {
|
||||
const panelData = [
|
||||
createOldPanelData(3, 'foo1', 1, 2, 2, 1),
|
||||
createOldPanelData(5, 'foo2', 1, 2, 2, 2),
|
||||
createOldPanelData(9, 'foo3', 1, 2, 2, 3),
|
||||
createOldPanelData(11, 'foo4', 1, 2, 2, 4),
|
||||
createOldPanelData(1, 'foo5', 1, 2, 2, 5),
|
||||
createOldPanelData(7, 'foo6', 1, 2, 2, 6),
|
||||
createOldPanelData(4, 'foo7', 6, 3, 2, 7),
|
||||
createOldPanelData(1, 'foo8', 8, 3, 2, 8),
|
||||
createOldPanelData(10, 'foo9', 8, 3, 2, 9),
|
||||
createOldPanelData(10, 'foo10', 6, 3, 2, 10),
|
||||
createOldPanelData(4, 'foo11', 8, 3, 2, 11),
|
||||
createOldPanelData(7, 'foo12', 8, 3, 2, 12),
|
||||
createOldPanelData(1, 'foo13', 6, 3, 2, 13),
|
||||
createOldPanelData(7, 'foo14', 6, 3, 2, 14),
|
||||
createOldPanelData(5, 'foo15', 3, 6, 3, 15),
|
||||
createOldPanelData(1, 'foo17', 3, 4, 3, 16)
|
||||
];
|
||||
|
||||
store.dispatch(updatePanels(panelData));
|
||||
store.dispatch(updateUseMargins(true));
|
||||
|
||||
const grid = mountWithIntl(<Provider store={store}><DashboardGridContainer {...getProps()} /></Provider>);
|
||||
|
||||
const panels = store.getState().dashboard.panels;
|
||||
expect(Object.keys(panels).length).toBe(16);
|
||||
|
||||
const foo8Panel = _.find(panels, panel => panel.id === 'foo8');
|
||||
expect(foo8Panel.row).toBe(undefined);
|
||||
expect(foo8Panel.gridData.y).toBe(28);
|
||||
expect(foo8Panel.gridData.x).toBe(0);
|
||||
|
||||
grid.unmount();
|
||||
});
|
|
@ -19,17 +19,13 @@
|
|||
|
||||
import { DashboardViewMode } from '../dashboard_view_mode';
|
||||
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
|
||||
import {
|
||||
Pre61SavedDashboardPanel,
|
||||
Pre64SavedDashboardPanel,
|
||||
DashboardAppStateParameters,
|
||||
} from '../types';
|
||||
import { DashboardAppStateDefaults } from '../types';
|
||||
|
||||
export function getAppStateDefaults(
|
||||
savedDashboard: SavedObjectDashboard,
|
||||
hideWriteControls: boolean
|
||||
): DashboardAppStateParameters {
|
||||
const appState = {
|
||||
): DashboardAppStateDefaults {
|
||||
return {
|
||||
fullScreenMode: false,
|
||||
title: savedDashboard.title,
|
||||
description: savedDashboard.description || '',
|
||||
|
@ -41,30 +37,4 @@ export function getAppStateDefaults(
|
|||
viewMode:
|
||||
savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
|
||||
};
|
||||
|
||||
// For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level.
|
||||
// TODO: introduce a migration for this
|
||||
if (savedDashboard.uiStateJSON) {
|
||||
const uiState = JSON.parse(savedDashboard.uiStateJSON);
|
||||
appState.panels.forEach((panel: Pre61SavedDashboardPanel) => {
|
||||
panel.embeddableConfig = uiState[`P-${panel.panelIndex}`];
|
||||
});
|
||||
delete savedDashboard.uiStateJSON;
|
||||
}
|
||||
|
||||
// For BWC of pre 6.4 where search embeddables stored state directly on the panel and not under embeddableConfig.
|
||||
// TODO: introduce a migration for this
|
||||
appState.panels.forEach((panel: Pre64SavedDashboardPanel) => {
|
||||
if (panel.columns || panel.sort) {
|
||||
panel.embeddableConfig = {
|
||||
...panel.embeddableConfig,
|
||||
columns: panel.columns,
|
||||
sort: panel.sort,
|
||||
};
|
||||
delete panel.columns;
|
||||
delete panel.sort;
|
||||
}
|
||||
});
|
||||
|
||||
return appState;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
jest.mock(
|
||||
'ui/chrome',
|
||||
() => ({
|
||||
getKibanaVersion: () => '6.3.0',
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'ui/notify',
|
||||
() => ({
|
||||
toastNotifications: {
|
||||
addDanger: () => {},
|
||||
},
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
import { SavedDashboardPanel } from '../types';
|
||||
import { migrateAppState } from './migrate_app_state';
|
||||
|
||||
test('migrate app state from 6.0', async () => {
|
||||
const mockSave = jest.fn();
|
||||
const appState = {
|
||||
uiState: {
|
||||
'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } },
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
col: 1,
|
||||
id: 'Visualization-MetricChart',
|
||||
panelIndex: 1,
|
||||
row: 1,
|
||||
size_x: 6,
|
||||
size_y: 3,
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
translateHashToRison: () => 'a',
|
||||
getQueryParamName: () => 'a',
|
||||
save: mockSave,
|
||||
};
|
||||
migrateAppState(appState);
|
||||
expect(appState.uiState).toBeUndefined();
|
||||
|
||||
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
|
||||
|
||||
expect(newPanel.gridData.w).toBe(24);
|
||||
expect(newPanel.gridData.h).toBe(15);
|
||||
expect(newPanel.gridData.x).toBe(0);
|
||||
expect(newPanel.gridData.y).toBe(0);
|
||||
|
||||
expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)');
|
||||
expect(mockSave).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('migrate sort from 6.1', async () => {
|
||||
const mockSave = jest.fn();
|
||||
const appState = {
|
||||
uiState: {
|
||||
'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } },
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
col: 1,
|
||||
id: 'Visualization-MetricChart',
|
||||
panelIndex: 1,
|
||||
row: 1,
|
||||
size_x: 6,
|
||||
size_y: 3,
|
||||
type: 'visualization',
|
||||
sort: 'sort',
|
||||
},
|
||||
],
|
||||
translateHashToRison: () => 'a',
|
||||
getQueryParamName: () => 'a',
|
||||
save: mockSave,
|
||||
useMargins: false,
|
||||
};
|
||||
migrateAppState(appState);
|
||||
expect(appState.uiState).toBeUndefined();
|
||||
|
||||
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
|
||||
expect(newPanel.gridData.w).toBe(24);
|
||||
expect(newPanel.gridData.h).toBe(15);
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)');
|
||||
expect(mockSave).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('migrates 6.0 even when uiState does not exist', async () => {
|
||||
const mockSave = jest.fn();
|
||||
const appState = {
|
||||
panels: [
|
||||
{
|
||||
col: 1,
|
||||
id: 'Visualization-MetricChart',
|
||||
panelIndex: 1,
|
||||
row: 1,
|
||||
size_x: 6,
|
||||
size_y: 3,
|
||||
type: 'visualization',
|
||||
sort: 'sort',
|
||||
},
|
||||
],
|
||||
translateHashToRison: () => 'a',
|
||||
getQueryParamName: () => 'a',
|
||||
save: mockSave,
|
||||
};
|
||||
migrateAppState(appState);
|
||||
expect((appState as any).uiState).toBeUndefined();
|
||||
|
||||
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
|
||||
expect(newPanel.gridData.w).toBe(24);
|
||||
expect(newPanel.gridData.h).toBe(15);
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect(mockSave).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('6.2 migration adjusts w & h without margins', async () => {
|
||||
const mockSave = jest.fn();
|
||||
const appState = {
|
||||
panels: [
|
||||
{
|
||||
id: 'Visualization-MetricChart',
|
||||
panelIndex: 1,
|
||||
gridData: {
|
||||
h: 3,
|
||||
w: 7,
|
||||
x: 2,
|
||||
y: 5,
|
||||
},
|
||||
type: 'visualization',
|
||||
sort: 'sort',
|
||||
version: '6.2.0',
|
||||
},
|
||||
],
|
||||
translateHashToRison: () => 'a',
|
||||
getQueryParamName: () => 'a',
|
||||
save: mockSave,
|
||||
useMargins: false,
|
||||
};
|
||||
migrateAppState(appState);
|
||||
expect((appState as any).uiState).toBeUndefined();
|
||||
|
||||
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
|
||||
expect(newPanel.gridData.w).toBe(28);
|
||||
expect(newPanel.gridData.h).toBe(15);
|
||||
expect(newPanel.gridData.x).toBe(8);
|
||||
expect(newPanel.gridData.y).toBe(25);
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect(mockSave).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('6.2 migration adjusts w & h with margins', async () => {
|
||||
const mockSave = jest.fn();
|
||||
const appState = {
|
||||
panels: [
|
||||
{
|
||||
id: 'Visualization-MetricChart',
|
||||
panelIndex: 1,
|
||||
gridData: {
|
||||
h: 3,
|
||||
w: 7,
|
||||
x: 2,
|
||||
y: 5,
|
||||
},
|
||||
type: 'visualization',
|
||||
sort: 'sort',
|
||||
version: '6.2.0',
|
||||
},
|
||||
],
|
||||
translateHashToRison: () => 'a',
|
||||
getQueryParamName: () => 'a',
|
||||
save: mockSave,
|
||||
useMargins: true,
|
||||
};
|
||||
migrateAppState(appState);
|
||||
expect((appState as any).uiState).toBeUndefined();
|
||||
|
||||
const newPanel = (appState.panels[0] as unknown) as SavedDashboardPanel;
|
||||
expect(newPanel.gridData.w).toBe(28);
|
||||
expect(newPanel.gridData.h).toBe(12);
|
||||
expect(newPanel.gridData.x).toBe(8);
|
||||
expect(newPanel.gridData.y).toBe(20);
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect(mockSave).toBeCalledTimes(1);
|
||||
});
|
|
@ -17,20 +17,68 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedDashboardPanel, DashboardAppState } from '../types';
|
||||
import semver from 'semver';
|
||||
import chrome from 'ui/chrome';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { trackUiMetric } from '../../../../ui_metric/public';
|
||||
import {
|
||||
DashboardAppState,
|
||||
SavedDashboardPanelTo60,
|
||||
SavedDashboardPanel730ToLatest,
|
||||
SavedDashboardPanel610,
|
||||
SavedDashboardPanel630,
|
||||
SavedDashboardPanel640To720,
|
||||
SavedDashboardPanel620,
|
||||
} from '../types';
|
||||
import { migratePanelsTo730 } from '../migrations/migrate_to_730_panels';
|
||||
|
||||
/**
|
||||
* Creates a new instance of AppState based of the saved dashboard.
|
||||
* Attempts to migrate the state stored in the URL into the latest version of it.
|
||||
*
|
||||
* @param appState {AppState} AppState class to instantiate
|
||||
* Once we hit a major version, we can remove support for older style URLs and get rid of this logic.
|
||||
*/
|
||||
export function migrateAppState(appState: DashboardAppState) {
|
||||
// For BWC in pre 6.1 versions where uiState was stored at the dashboard level, not at the panel level.
|
||||
if (appState.uiState) {
|
||||
appState.panels.forEach((panel: SavedDashboardPanel) => {
|
||||
panel.embeddableConfig = appState.uiState[`P-${panel.panelIndex}`];
|
||||
});
|
||||
export function migrateAppState(appState: { [key: string]: unknown } | DashboardAppState) {
|
||||
if (!appState.panels) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.panel.invalidData', {
|
||||
defaultMessage: 'Invalid data in url',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const panelNeedsMigration = (appState.panels as Array<
|
||||
| SavedDashboardPanelTo60
|
||||
| SavedDashboardPanel610
|
||||
| SavedDashboardPanel620
|
||||
| SavedDashboardPanel630
|
||||
| SavedDashboardPanel640To720
|
||||
| SavedDashboardPanel730ToLatest
|
||||
>).some(panel => {
|
||||
if ((panel as { version?: string }).version === undefined) return true;
|
||||
|
||||
const version = (panel as SavedDashboardPanel730ToLatest).version;
|
||||
|
||||
// This will help us figure out when to remove support for older style URLs.
|
||||
trackUiMetric('DashboardPanelVersionInUrl', `${version}`);
|
||||
|
||||
return semver.satisfies(version, '<7.3');
|
||||
});
|
||||
|
||||
if (panelNeedsMigration) {
|
||||
appState.panels = migratePanelsTo730(
|
||||
appState.panels as Array<
|
||||
| SavedDashboardPanelTo60
|
||||
| SavedDashboardPanel610
|
||||
| SavedDashboardPanel620
|
||||
| SavedDashboardPanel630
|
||||
| SavedDashboardPanel640To720
|
||||
>,
|
||||
chrome.getKibanaVersion(),
|
||||
appState.useMargins,
|
||||
appState.uiState
|
||||
);
|
||||
delete appState.uiState;
|
||||
|
||||
appState.save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,17 +17,17 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { DashboardDoc } from './types';
|
||||
import { DashboardDoc730ToLatest } from './types';
|
||||
import { isDoc } from '../../../migrations/is_doc';
|
||||
|
||||
export function isDashboardDoc(
|
||||
doc: { [key: string]: unknown } | DashboardDoc
|
||||
): doc is DashboardDoc {
|
||||
doc: { [key: string]: unknown } | DashboardDoc730ToLatest
|
||||
): doc is DashboardDoc730ToLatest {
|
||||
if (!isDoc(doc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof (doc as DashboardDoc).attributes.panelsJSON !== 'string') {
|
||||
if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
jest.mock(
|
||||
'ui/chrome',
|
||||
() => ({
|
||||
getKibanaVersion: () => '6.3.0',
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'ui/notify',
|
||||
() => ({
|
||||
toastNotifications: {
|
||||
addDanger: () => {},
|
||||
},
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
import { migratePanelsTo730 } from './migrate_to_730_panels';
|
||||
import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from '../types';
|
||||
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants';
|
||||
import {
|
||||
RawSavedDashboardPanelTo60,
|
||||
RawSavedDashboardPanel610,
|
||||
RawSavedDashboardPanel620,
|
||||
RawSavedDashboardPanel630,
|
||||
RawSavedDashboardPanel640To720,
|
||||
} from './types';
|
||||
|
||||
test('6.0 migrates uiState, sort, scales, and gridData', async () => {
|
||||
const uiState = {
|
||||
'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } },
|
||||
};
|
||||
const panels: RawSavedDashboardPanelTo60[] = [
|
||||
{
|
||||
col: 1,
|
||||
panelIndex: 1,
|
||||
row: 1,
|
||||
size_x: 6,
|
||||
size_y: 3,
|
||||
name: 'panel-123',
|
||||
sort: 'sort',
|
||||
columns: ['bye'],
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true, uiState);
|
||||
|
||||
const newPanel = newPanels[0];
|
||||
expect(newPanel.gridData.w).toBe(24);
|
||||
expect(newPanel.gridData.h).toBe(12);
|
||||
expect(newPanel.version).toBe('8.0.0');
|
||||
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
expect((newPanel as any).columns).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect((newPanel.embeddableConfig as any).columns).toEqual(['bye']);
|
||||
expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)');
|
||||
});
|
||||
|
||||
test('6.0 migrates even when uiState does not exist', async () => {
|
||||
const panels: RawSavedDashboardPanelTo60[] = [
|
||||
{
|
||||
col: 3,
|
||||
panelIndex: 1,
|
||||
row: 4,
|
||||
size_x: 7,
|
||||
size_y: 5,
|
||||
sort: 'sort',
|
||||
name: 'panel-123',
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true);
|
||||
|
||||
const newPanel = newPanels[0];
|
||||
expect(newPanel.gridData.w).toBe(28);
|
||||
expect(newPanel.gridData.h).toBe(20);
|
||||
expect(newPanel.gridData.x).toBe(8);
|
||||
expect(newPanel.gridData.y).toBe(12);
|
||||
expect(newPanel.version).toBe('8.0.0');
|
||||
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
});
|
||||
|
||||
test('6.0 migration gives default width and height when missing', () => {
|
||||
const panels: RawSavedDashboardPanelTo60[] = [
|
||||
{
|
||||
col: 3,
|
||||
row: 1,
|
||||
panelIndex: 1,
|
||||
name: 'panel-123',
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true);
|
||||
expect(newPanels[0].gridData.w).toBe(DEFAULT_PANEL_WIDTH);
|
||||
expect(newPanels[0].gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
|
||||
expect(newPanels[0].version).toBe('8.0.0');
|
||||
});
|
||||
|
||||
test('6.0 migrates old panel data in the right order', () => {
|
||||
const createOldPanelData = (
|
||||
col: number,
|
||||
id: string,
|
||||
row: number,
|
||||
sizeX: number,
|
||||
sizeY: number,
|
||||
panelIndex: number
|
||||
) => {
|
||||
return { col, id, row, size_x: sizeX, size_y: sizeY, type: 'visualization', panelIndex };
|
||||
};
|
||||
const panelData = [
|
||||
createOldPanelData(3, 'foo1', 1, 2, 2, 1),
|
||||
createOldPanelData(5, 'foo2', 1, 2, 2, 2),
|
||||
createOldPanelData(9, 'foo3', 1, 2, 2, 3),
|
||||
createOldPanelData(11, 'foo4', 1, 2, 2, 4),
|
||||
createOldPanelData(1, 'foo5', 1, 2, 2, 5),
|
||||
createOldPanelData(7, 'foo6', 1, 2, 2, 6),
|
||||
createOldPanelData(4, 'foo7', 6, 3, 2, 7),
|
||||
createOldPanelData(1, 'foo8', 8, 3, 2, 8),
|
||||
createOldPanelData(10, 'foo9', 8, 3, 2, 9),
|
||||
createOldPanelData(10, 'foo10', 6, 3, 2, 10),
|
||||
createOldPanelData(4, 'foo11', 8, 3, 2, 11),
|
||||
createOldPanelData(7, 'foo12', 8, 3, 2, 12),
|
||||
createOldPanelData(1, 'foo13', 6, 3, 2, 13),
|
||||
createOldPanelData(7, 'foo14', 6, 3, 2, 14),
|
||||
createOldPanelData(5, 'foo15', 3, 6, 3, 15),
|
||||
createOldPanelData(1, 'foo17', 3, 4, 3, 16),
|
||||
];
|
||||
|
||||
const newPanels = migratePanelsTo730(
|
||||
panelData,
|
||||
'8.0.0',
|
||||
false,
|
||||
{}
|
||||
) as SavedDashboardPanel730ToLatest[];
|
||||
const foo8Panel = newPanels.find(panel => panel.id === 'foo8');
|
||||
|
||||
expect(foo8Panel).toBeDefined();
|
||||
expect((foo8Panel! as any).row).toBe(undefined);
|
||||
expect(foo8Panel!.gridData.y).toBe(35);
|
||||
expect(foo8Panel!.gridData.x).toBe(0);
|
||||
});
|
||||
|
||||
// We want to run these same panel migrations on URLs, when panels are not in Raw form.
|
||||
test('6.0 migrations keep id and type properties if they exist', () => {
|
||||
const panels: SavedDashboardPanelTo60[] = [
|
||||
{
|
||||
id: '1',
|
||||
panelIndex: '1',
|
||||
col: 3,
|
||||
row: 1,
|
||||
type: 'visualization',
|
||||
sort: 'sort',
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', false, {});
|
||||
expect((newPanels[0] as SavedDashboardPanel730ToLatest).type).toBe('visualization');
|
||||
expect((newPanels[0] as SavedDashboardPanel730ToLatest).id).toBe('1');
|
||||
});
|
||||
|
||||
test('6.0 migrates old panel data in the right order without margins', () => {
|
||||
const createOldPanelData = (
|
||||
col: number,
|
||||
id: string,
|
||||
row: number,
|
||||
sizeX: number,
|
||||
sizeY: number,
|
||||
panelIndex: number
|
||||
) => {
|
||||
return { col, id, row, size_x: sizeX, size_y: sizeY, type: 'visualization', panelIndex };
|
||||
};
|
||||
const panelData = [
|
||||
createOldPanelData(3, 'foo1', 1, 2, 2, 1),
|
||||
createOldPanelData(5, 'foo2', 1, 2, 2, 2),
|
||||
createOldPanelData(9, 'foo3', 1, 2, 2, 3),
|
||||
createOldPanelData(11, 'foo4', 1, 2, 2, 4),
|
||||
createOldPanelData(1, 'foo5', 1, 2, 2, 5),
|
||||
createOldPanelData(7, 'foo6', 1, 2, 2, 6),
|
||||
createOldPanelData(4, 'foo7', 6, 3, 2, 7),
|
||||
createOldPanelData(1, 'foo8', 8, 3, 2, 8),
|
||||
createOldPanelData(10, 'foo9', 8, 3, 2, 9),
|
||||
createOldPanelData(10, 'foo10', 6, 3, 2, 10),
|
||||
createOldPanelData(4, 'foo11', 8, 3, 2, 11),
|
||||
createOldPanelData(7, 'foo12', 8, 3, 2, 12),
|
||||
createOldPanelData(1, 'foo13', 6, 3, 2, 13),
|
||||
createOldPanelData(7, 'foo14', 6, 3, 2, 14),
|
||||
createOldPanelData(5, 'foo15', 3, 6, 3, 15),
|
||||
createOldPanelData(1, 'foo17', 3, 4, 3, 16),
|
||||
];
|
||||
|
||||
const newPanels = migratePanelsTo730(
|
||||
panelData,
|
||||
'8.0.0',
|
||||
false,
|
||||
{}
|
||||
) as SavedDashboardPanel730ToLatest[];
|
||||
const foo8Panel = newPanels.find(panel => panel.id === 'foo8');
|
||||
|
||||
expect(foo8Panel).toBeDefined();
|
||||
expect((foo8Panel! as any).row).toBe(undefined);
|
||||
expect(foo8Panel!.gridData.y).toBe(35);
|
||||
expect(foo8Panel!.gridData.x).toBe(0);
|
||||
});
|
||||
|
||||
test('6.0 migrates old panel data in the right order with margins', () => {
|
||||
const createOldPanelData = (
|
||||
col: number,
|
||||
id: string,
|
||||
row: number,
|
||||
sizeX: number,
|
||||
sizeY: number,
|
||||
panelIndex: number
|
||||
): SavedDashboardPanelTo60 => {
|
||||
return { col, id, row, size_x: sizeX, size_y: sizeY, type: 'visualization', panelIndex };
|
||||
};
|
||||
const panelData: SavedDashboardPanelTo60[] = [
|
||||
createOldPanelData(3, 'foo1', 1, 2, 2, 1),
|
||||
createOldPanelData(5, 'foo2', 1, 2, 2, 2),
|
||||
createOldPanelData(9, 'foo3', 1, 2, 2, 3),
|
||||
createOldPanelData(11, 'foo4', 1, 2, 2, 4),
|
||||
createOldPanelData(1, 'foo5', 1, 2, 2, 5),
|
||||
createOldPanelData(7, 'foo6', 1, 2, 2, 6),
|
||||
createOldPanelData(4, 'foo7', 6, 3, 2, 7),
|
||||
createOldPanelData(1, 'foo8', 8, 3, 2, 8),
|
||||
createOldPanelData(10, 'foo9', 8, 3, 2, 9),
|
||||
createOldPanelData(10, 'foo10', 6, 3, 2, 10),
|
||||
createOldPanelData(4, 'foo11', 8, 3, 2, 11),
|
||||
createOldPanelData(7, 'foo12', 8, 3, 2, 12),
|
||||
createOldPanelData(1, 'foo13', 6, 3, 2, 13),
|
||||
createOldPanelData(7, 'foo14', 6, 3, 2, 14),
|
||||
createOldPanelData(5, 'foo15', 3, 6, 3, 15),
|
||||
createOldPanelData(1, 'foo17', 3, 4, 3, 16),
|
||||
];
|
||||
|
||||
const newPanels = migratePanelsTo730(
|
||||
panelData,
|
||||
'8.0.0',
|
||||
true,
|
||||
{}
|
||||
) as SavedDashboardPanel730ToLatest[];
|
||||
const foo8Panel = newPanels.find(panel => panel.id === 'foo8');
|
||||
|
||||
expect(foo8Panel).toBeDefined();
|
||||
expect((foo8Panel! as any).row).toBe(undefined);
|
||||
expect(foo8Panel!.gridData.y).toBe(28);
|
||||
expect(foo8Panel!.gridData.x).toBe(0);
|
||||
});
|
||||
|
||||
test('6.1 migrates uiState, sort, and scales', async () => {
|
||||
const uiState = {
|
||||
'P-1': { vis: { defaultColors: { '0+-+100': 'rgb(0,104,55)' } } },
|
||||
};
|
||||
const panels: RawSavedDashboardPanel610[] = [
|
||||
{
|
||||
panelIndex: 1,
|
||||
sort: 'sort',
|
||||
version: '6.1.0',
|
||||
name: 'panel-123',
|
||||
gridData: {
|
||||
h: 3,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
i: '123',
|
||||
},
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true, uiState);
|
||||
|
||||
const newPanel = newPanels[0];
|
||||
expect(newPanel.gridData.w).toBe(24);
|
||||
expect(newPanel.gridData.h).toBe(12);
|
||||
expect(newPanel.version).toBe('8.0.0');
|
||||
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect((newPanel.embeddableConfig as any).vis.defaultColors['0+-+100']).toBe('rgb(0,104,55)');
|
||||
});
|
||||
|
||||
test('6.2 migrates sort and scales', async () => {
|
||||
const panels: RawSavedDashboardPanel620[] = [
|
||||
{
|
||||
panelIndex: '1',
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 3,
|
||||
i: '1',
|
||||
},
|
||||
sort: 'sort',
|
||||
version: '6.2.0',
|
||||
name: 'panel-123',
|
||||
embeddableConfig: { hi: 'bye' },
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true);
|
||||
|
||||
const newPanel = newPanels[0];
|
||||
expect(newPanel.gridData.w).toBe(24);
|
||||
expect(newPanel.gridData.h).toBe(12);
|
||||
expect(newPanel.version).toBe('8.0.0');
|
||||
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect((newPanel.embeddableConfig as any).hi).toBe('bye');
|
||||
});
|
||||
|
||||
test('6.3 migrates sort, does not scale', async () => {
|
||||
const panels: RawSavedDashboardPanel630[] = [
|
||||
{
|
||||
name: 'panel-1',
|
||||
panelIndex: '1',
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 3,
|
||||
i: '1',
|
||||
},
|
||||
sort: 'sort',
|
||||
columns: ['hi'],
|
||||
version: '6.3.0',
|
||||
embeddableConfig: { hi: 'bye' },
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true);
|
||||
|
||||
const newPanel = newPanels[0];
|
||||
expect(newPanel.gridData.w).toBe(6);
|
||||
expect(newPanel.gridData.h).toBe(3);
|
||||
expect(newPanel.version).toBe('8.0.0');
|
||||
expect((newPanel as any).sort).toBeUndefined();
|
||||
expect((newPanel.embeddableConfig as any).hi).toBe('bye');
|
||||
expect((newPanel.embeddableConfig as any).sort).toBe('sort');
|
||||
expect((newPanel.embeddableConfig as any).columns).toEqual(['hi']);
|
||||
});
|
||||
|
||||
test('6.4 migration converts panel index to string', async () => {
|
||||
const panels: RawSavedDashboardPanel640To720[] = [
|
||||
{
|
||||
panelIndex: 1,
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 3,
|
||||
i: '1',
|
||||
},
|
||||
version: '6.4.0',
|
||||
embeddableConfig: { hi: 'bye' },
|
||||
name: 'panel-123',
|
||||
},
|
||||
];
|
||||
const newPanels = migratePanelsTo730(panels, '8.0.0', true);
|
||||
|
||||
const newPanel = newPanels[0];
|
||||
expect(newPanel.gridData.w).toBe(6);
|
||||
expect(newPanel.gridData.h).toBe(3);
|
||||
expect(newPanel.version).toBe('8.0.0');
|
||||
expect((newPanel.embeddableConfig as any).hi).toBe('bye');
|
||||
expect(newPanel.panelIndex).toBe('1');
|
||||
});
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import semver from 'semver';
|
||||
import { GridData } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/types';
|
||||
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants';
|
||||
import {
|
||||
RawSavedDashboardPanelTo60,
|
||||
RawSavedDashboardPanel630,
|
||||
RawSavedDashboardPanel640To720,
|
||||
RawSavedDashboardPanel730ToLatest,
|
||||
RawSavedDashboardPanel610,
|
||||
RawSavedDashboardPanel620,
|
||||
} from './types';
|
||||
import {
|
||||
SavedDashboardPanelTo60,
|
||||
SavedDashboardPanel620,
|
||||
SavedDashboardPanel630,
|
||||
SavedDashboardPanel610,
|
||||
} from '../types';
|
||||
|
||||
const PANEL_HEIGHT_SCALE_FACTOR = 5;
|
||||
const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4;
|
||||
const PANEL_WIDTH_SCALE_FACTOR = 4;
|
||||
|
||||
/**
|
||||
* Note!
|
||||
*
|
||||
* The 7.3.0 migrations reference versions that are quite old because for a long time all of this
|
||||
* migration logic was done ad hoc in the code itself, not on the indexed data (migrations didn't even
|
||||
* exist at the point most of that logic was put in place).
|
||||
*
|
||||
* So you could have a dashboard in 7.2.0 that was created in 6.3 and it will have data of a different
|
||||
* shape than some other dashboards that were created more recently.
|
||||
*
|
||||
* Moving forward migrations should be simpler since all 7.3.0+ dashboards should finally have the
|
||||
* same data.
|
||||
*/
|
||||
|
||||
function isPre61Panel(
|
||||
panel: unknown | RawSavedDashboardPanelTo60
|
||||
): panel is RawSavedDashboardPanelTo60 {
|
||||
return (panel as RawSavedDashboardPanelTo60).row !== undefined;
|
||||
}
|
||||
|
||||
function is61Panel(panel: unknown | RawSavedDashboardPanel610): panel is RawSavedDashboardPanel610 {
|
||||
return semver.satisfies((panel as RawSavedDashboardPanel610).version, '6.1.x');
|
||||
}
|
||||
|
||||
function is62Panel(panel: unknown | RawSavedDashboardPanel620): panel is RawSavedDashboardPanel620 {
|
||||
return semver.satisfies((panel as RawSavedDashboardPanel620).version, '6.2.x');
|
||||
}
|
||||
|
||||
function is63Panel(panel: unknown | RawSavedDashboardPanel630): panel is RawSavedDashboardPanel630 {
|
||||
return semver.satisfies((panel as RawSavedDashboardPanel630).version, '6.3.x');
|
||||
}
|
||||
|
||||
function is640To720Panel(
|
||||
panel: unknown | RawSavedDashboardPanel640To720
|
||||
): panel is RawSavedDashboardPanel640To720 {
|
||||
return (
|
||||
semver.satisfies((panel as RawSavedDashboardPanel630).version, '>6.3') &&
|
||||
semver.satisfies((panel as RawSavedDashboardPanel630).version, '<7.3')
|
||||
);
|
||||
}
|
||||
|
||||
// Migrations required for 6.0 and prior:
|
||||
// 1. (6.1) migrate size_x/y/row/col into gridData
|
||||
// 2. (6.2) migrate uiState into embeddableConfig
|
||||
// 3. (6.3) scale grid dimensions
|
||||
// 4. (6.4) remove columns, sort properties
|
||||
// 5. (7.3) make sure panelIndex is a string
|
||||
function migratePre61PanelToLatest(
|
||||
panel: RawSavedDashboardPanelTo60,
|
||||
version: string,
|
||||
useMargins: boolean,
|
||||
uiState?: { [key: string]: { [key: string]: unknown } }
|
||||
): RawSavedDashboardPanel730ToLatest {
|
||||
if (panel.col === undefined || panel.row === undefined) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', {
|
||||
defaultMessage:
|
||||
'Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected col and/or row fields',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const embeddableConfig = uiState ? uiState[`P-${panel.panelIndex}`] || {} : {};
|
||||
|
||||
if (panel.columns || panel.sort) {
|
||||
embeddableConfig.columns = panel.columns;
|
||||
embeddableConfig.sort = panel.sort;
|
||||
}
|
||||
|
||||
const heightScaleFactor = useMargins
|
||||
? PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS
|
||||
: PANEL_HEIGHT_SCALE_FACTOR;
|
||||
|
||||
const { columns, sort, row, col, size_x: sizeX, size_y: sizeY, ...rest } = panel;
|
||||
return {
|
||||
...rest,
|
||||
version,
|
||||
panelIndex: panel.panelIndex.toString(),
|
||||
gridData: {
|
||||
x: (col - 1) * PANEL_WIDTH_SCALE_FACTOR,
|
||||
y: (row - 1) * heightScaleFactor,
|
||||
w: sizeX ? sizeX * PANEL_WIDTH_SCALE_FACTOR : DEFAULT_PANEL_WIDTH,
|
||||
h: sizeY ? sizeY * heightScaleFactor : DEFAULT_PANEL_HEIGHT,
|
||||
i: panel.panelIndex.toString(),
|
||||
},
|
||||
embeddableConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrations required for 6.1 panels:
|
||||
// 1. (6.2) migrate uiState into embeddableConfig
|
||||
// 2. (6.3) scale grid dimensions
|
||||
// 3. (6.4) remove columns, sort properties
|
||||
// 4. (7.3) make sure panel index is a string
|
||||
function migrate610PanelToLatest(
|
||||
panel: RawSavedDashboardPanel610,
|
||||
version: string,
|
||||
useMargins: boolean,
|
||||
uiState?: { [key: string]: { [key: string]: unknown } }
|
||||
): RawSavedDashboardPanel730ToLatest {
|
||||
(['w', 'x', 'h', 'y'] as Array<keyof GridData>).forEach(key => {
|
||||
if (panel.gridData[key] === undefined) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', {
|
||||
defaultMessage:
|
||||
'Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: {key}',
|
||||
values: { key },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const embeddableConfig = uiState ? uiState[`P-${panel.panelIndex}`] : {};
|
||||
|
||||
// 2. (6.4) remove columns, sort properties
|
||||
if (panel.columns || panel.sort) {
|
||||
embeddableConfig.columns = panel.columns;
|
||||
embeddableConfig.sort = panel.sort;
|
||||
}
|
||||
|
||||
// 1. (6.3) scale grid dimensions
|
||||
const heightScaleFactor = useMargins
|
||||
? PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS
|
||||
: PANEL_HEIGHT_SCALE_FACTOR;
|
||||
const { columns, sort, ...rest } = panel;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
version,
|
||||
panelIndex: panel.panelIndex.toString(),
|
||||
gridData: {
|
||||
w: panel.gridData.w * PANEL_WIDTH_SCALE_FACTOR,
|
||||
h: panel.gridData.h * heightScaleFactor,
|
||||
x: panel.gridData.x * PANEL_WIDTH_SCALE_FACTOR,
|
||||
y: panel.gridData.y * heightScaleFactor,
|
||||
i: panel.gridData.i,
|
||||
},
|
||||
embeddableConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrations required for 6.2 panels:
|
||||
// 1. (6.3) scale grid dimensions
|
||||
// 2. (6.4) remove columns, sort properties
|
||||
// 3. (7.3) make sure panel index is a string
|
||||
function migrate620PanelToLatest(
|
||||
panel: RawSavedDashboardPanel620,
|
||||
version: string,
|
||||
useMargins: boolean
|
||||
): RawSavedDashboardPanel730ToLatest {
|
||||
// Migrate column, sort
|
||||
const embeddableConfig = panel.embeddableConfig || {};
|
||||
if (panel.columns || panel.sort) {
|
||||
embeddableConfig.columns = panel.columns;
|
||||
embeddableConfig.sort = panel.sort;
|
||||
}
|
||||
|
||||
// Scale grid dimensions
|
||||
const heightScaleFactor = useMargins
|
||||
? PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS
|
||||
: PANEL_HEIGHT_SCALE_FACTOR;
|
||||
const { columns, sort, ...rest } = panel;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
version,
|
||||
panelIndex: panel.panelIndex.toString(),
|
||||
gridData: {
|
||||
w: panel.gridData.w * PANEL_WIDTH_SCALE_FACTOR,
|
||||
h: panel.gridData.h * heightScaleFactor,
|
||||
x: panel.gridData.x * PANEL_WIDTH_SCALE_FACTOR,
|
||||
y: panel.gridData.y * heightScaleFactor,
|
||||
i: panel.gridData.i,
|
||||
},
|
||||
embeddableConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrations required for 6.3 panels:
|
||||
// 1. (6.4) remove columns, sort properties
|
||||
// 2. (7.3) make sure panel index is a string
|
||||
function migrate630PanelToLatest(
|
||||
panel: RawSavedDashboardPanel630,
|
||||
version: string
|
||||
): RawSavedDashboardPanel730ToLatest {
|
||||
// Migrate column, sort
|
||||
const embeddableConfig = panel.embeddableConfig || {};
|
||||
if (panel.columns || panel.sort) {
|
||||
embeddableConfig.columns = panel.columns;
|
||||
embeddableConfig.sort = panel.sort;
|
||||
}
|
||||
|
||||
const { columns, sort, ...rest } = panel;
|
||||
return {
|
||||
...rest,
|
||||
version,
|
||||
panelIndex: panel.panelIndex.toString(),
|
||||
embeddableConfig,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrations required for 6.4 to 7.20 panels:
|
||||
// 1. (7.3) make sure panel index is a string
|
||||
function migrate640To720PanelsToLatest(
|
||||
panel: RawSavedDashboardPanel630,
|
||||
version: string
|
||||
): RawSavedDashboardPanel730ToLatest {
|
||||
return {
|
||||
...panel,
|
||||
version,
|
||||
panelIndex: panel.panelIndex.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function migratePanelsTo730(
|
||||
panels: Array<
|
||||
| RawSavedDashboardPanelTo60
|
||||
| RawSavedDashboardPanel610
|
||||
| RawSavedDashboardPanel620
|
||||
| RawSavedDashboardPanel630
|
||||
| RawSavedDashboardPanel640To720
|
||||
// We run these on post processed panels too for url BWC
|
||||
| SavedDashboardPanelTo60
|
||||
| SavedDashboardPanel610
|
||||
| SavedDashboardPanel620
|
||||
| SavedDashboardPanel630
|
||||
>,
|
||||
version: string,
|
||||
useMargins: boolean,
|
||||
uiState?: { [key: string]: { [key: string]: unknown } }
|
||||
): RawSavedDashboardPanel730ToLatest[] {
|
||||
return panels.map(panel => {
|
||||
if (isPre61Panel(panel)) {
|
||||
return migratePre61PanelToLatest(panel, version, useMargins, uiState);
|
||||
}
|
||||
|
||||
if (is61Panel(panel)) {
|
||||
return migrate610PanelToLatest(panel, version, useMargins, uiState);
|
||||
}
|
||||
|
||||
if (is62Panel(panel)) {
|
||||
return migrate620PanelToLatest(panel, version, useMargins);
|
||||
}
|
||||
|
||||
if (is63Panel(panel)) {
|
||||
return migrate630PanelToLatest(panel, version);
|
||||
}
|
||||
|
||||
if (is640To720Panel(panel)) {
|
||||
return migrate640To720PanelsToLatest(panel, version);
|
||||
}
|
||||
|
||||
return panel as RawSavedDashboardPanel730ToLatest;
|
||||
});
|
||||
}
|
|
@ -18,14 +18,25 @@
|
|||
*/
|
||||
|
||||
import { migrations730 } from './migrations_730';
|
||||
import { DashboardDoc } from './types';
|
||||
import {
|
||||
DashboardDoc700To720,
|
||||
DashboardDoc730ToLatest,
|
||||
RawSavedDashboardPanel730ToLatest,
|
||||
} from './types';
|
||||
|
||||
const mockLogger = {
|
||||
warning: () => {},
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
};
|
||||
|
||||
test('dashboard migration 7.3.0 migrates filters to query on search source', () => {
|
||||
const doc: DashboardDoc = {
|
||||
const doc: DashboardDoc700To720 = {
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
references: [],
|
||||
attributes: {
|
||||
useMargins: true,
|
||||
description: '',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
|
@ -38,7 +49,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source', ()
|
|||
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
|
||||
},
|
||||
};
|
||||
const newDoc = migrations730(doc);
|
||||
const newDoc = migrations730(doc, mockLogger);
|
||||
|
||||
expect(newDoc).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -49,7 +60,7 @@ Object {
|
|||
},
|
||||
"panelsJSON": "[{\\"id\\":\\"1\\",\\"type\\":\\"visualization\\",\\"foo\\":true},{\\"id\\":\\"2\\",\\"type\\":\\"visualization\\",\\"bar\\":true}]",
|
||||
"timeRestore": false,
|
||||
"uiStateJSON": "{}",
|
||||
"useMargins": true,
|
||||
"version": 1,
|
||||
},
|
||||
"id": "1",
|
||||
|
@ -58,3 +69,34 @@ Object {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('dashboard migration 7.3.0 migrates panels', () => {
|
||||
const doc: DashboardDoc700To720 = {
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
references: [],
|
||||
attributes: {
|
||||
useMargins: true,
|
||||
description: '',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
timeRestore: false,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{"filter":[],"highlightAll":true,"version":true}',
|
||||
},
|
||||
panelsJSON:
|
||||
'[{"size_x":6,"size_y":3,"panelIndex":1,"type":"visualization","id":"AWtIUP8QRNXhJVz2_Mar","col":1,"row":1}]',
|
||||
},
|
||||
};
|
||||
|
||||
const newDoc = migrations730(doc, mockLogger) as DashboardDoc730ToLatest;
|
||||
|
||||
const newPanels = JSON.parse(newDoc.attributes.panelsJSON) as RawSavedDashboardPanel730ToLatest[];
|
||||
|
||||
expect(newPanels.length).toBe(1);
|
||||
expect(newPanels[0].gridData.w).toEqual(24);
|
||||
expect(newPanels[0].gridData.h).toEqual(12);
|
||||
expect(newPanels[0].gridData.x).toEqual(0);
|
||||
expect(newPanels[0].gridData.y).toEqual(0);
|
||||
expect(newPanels[0].panelIndex).toEqual('1');
|
||||
});
|
||||
|
|
|
@ -16,17 +16,20 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { DashboardDoc } from './types';
|
||||
import { Logger } from 'target/types/server/saved_objects/migrations/core/migration_logger';
|
||||
import { DashboardDoc730ToLatest, DashboardDoc700To720 } from './types';
|
||||
import { isDashboardDoc } from './is_dashboard_doc';
|
||||
import { moveFiltersToQuery } from './move_filters_to_query';
|
||||
import { migratePanelsTo730 } from './migrate_to_730_panels';
|
||||
|
||||
export function migrations730(
|
||||
doc:
|
||||
| {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
| DashboardDoc
|
||||
): DashboardDoc | { [key: string]: unknown } {
|
||||
| DashboardDoc700To720,
|
||||
logger: Logger
|
||||
): DashboardDoc730ToLatest | { [key: string]: unknown } {
|
||||
if (!isDashboardDoc(doc)) {
|
||||
// NOTE: we should probably throw an error here... but for now following suit and in the
|
||||
// case of errors, just returning the same document.
|
||||
|
@ -38,8 +41,28 @@ export function migrations730(
|
|||
doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(
|
||||
moveFiltersToQuery(searchSource)
|
||||
);
|
||||
return doc;
|
||||
} catch (e) {
|
||||
logger.warning(`Exception @ migrations730 while trying to migrate query filters! ${e}`);
|
||||
return doc;
|
||||
}
|
||||
|
||||
let uiState = {};
|
||||
// Ignore errors, at some point uiStateJSON stopped being used, so it may not exist.
|
||||
if (doc.attributes.uiStateJSON && doc.attributes.uiStateJSON !== '') {
|
||||
uiState = JSON.parse(doc.attributes.uiStateJSON);
|
||||
}
|
||||
|
||||
try {
|
||||
const panels = JSON.parse(doc.attributes.panelsJSON);
|
||||
doc.attributes.panelsJSON = JSON.stringify(
|
||||
migratePanelsTo730(panels, '7.3.0', doc.attributes.useMargins, uiState)
|
||||
);
|
||||
|
||||
delete doc.attributes.uiStateJSON;
|
||||
} catch (e) {
|
||||
logger.warning(`Exception @ migrations730 while trying to migrate dashboard panels! ${e}`);
|
||||
return doc;
|
||||
}
|
||||
|
||||
return doc as DashboardDoc730ToLatest;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,9 @@ export interface Pre600FilterQuery {
|
|||
}
|
||||
|
||||
export interface SearchSourcePre600 {
|
||||
filter: Array<Filter | Pre600FilterQuery>;
|
||||
// I encountered at least one export from 7.0.0-alpha that was missing the filter property in here.
|
||||
// The maps data in esarchives actually has it, but I don't know how/when they created it.
|
||||
filter?: Array<Filter | Pre600FilterQuery>;
|
||||
}
|
||||
|
||||
export interface SearchSource730 {
|
||||
|
@ -54,6 +56,12 @@ export function moveFiltersToQuery(
|
|||
},
|
||||
};
|
||||
|
||||
// I encountered at least one export from 7.0.0-alpha that was missing the filter property in here.
|
||||
// The maps data in esarchives actually has it, but I don't know how/when they created it.
|
||||
if (!searchSource.filter) {
|
||||
searchSource.filter = [];
|
||||
}
|
||||
|
||||
searchSource.filter.forEach(filter => {
|
||||
if (isQueryFilter(filter)) {
|
||||
searchSource730.query = {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { GridData } from '../types';
|
||||
import { Doc, DocPre700 } from '../../../migrations/types';
|
||||
|
||||
export interface SavedObjectAttributes {
|
||||
|
@ -26,13 +27,107 @@ export interface SavedObjectAttributes {
|
|||
}
|
||||
|
||||
interface DashboardAttributes extends SavedObjectAttributes {
|
||||
panelsJSON: string;
|
||||
description: string;
|
||||
version: number;
|
||||
timeRestore: boolean;
|
||||
useMargins: boolean;
|
||||
}
|
||||
|
||||
export type DashboardAttributes730ToLatest = DashboardAttributes;
|
||||
|
||||
interface DashboardAttributesTo720 extends SavedObjectAttributes {
|
||||
panelsJSON: string;
|
||||
description: string;
|
||||
uiStateJSON: string;
|
||||
version: number;
|
||||
timeRestore: boolean;
|
||||
useMargins: boolean;
|
||||
}
|
||||
|
||||
export type DashboardDoc = Doc<DashboardAttributes>;
|
||||
export type DashboardDoc730ToLatest = Doc<DashboardAttributes>;
|
||||
|
||||
export type DashboardDocPre700 = DocPre700<DashboardAttributes>;
|
||||
export type DashboardDoc700To720 = Doc<DashboardAttributesTo720>;
|
||||
|
||||
export type DashboardDocPre700 = DocPre700<DashboardAttributesTo720>;
|
||||
|
||||
// Note that these types are prefixed with `Raw` because there are some post processing steps
|
||||
// that happen before the saved objects even reach the client. Namely, injecting type and id
|
||||
// parameters back into the panels, where the raw saved objects actually have them stored elsewhere.
|
||||
//
|
||||
// Ideally, everywhere in the dashboard code would use references at the top level instead of
|
||||
// embedded in the panels. The reason this is stored at the top level is so the references can be uniformly
|
||||
// updated across all saved object types that have references.
|
||||
|
||||
// Starting in 7.3 we introduced the possibility of embeddables existing without an id
|
||||
// parameter. If there was no id, then type remains on the panel. So it either will have a name,
|
||||
// or a type property.
|
||||
export type RawSavedDashboardPanel730ToLatest = Pick<
|
||||
RawSavedDashboardPanel640To720,
|
||||
Exclude<keyof RawSavedDashboardPanel640To720, 'name'>
|
||||
> & {
|
||||
// Should be either type, and not name (not backed by a saved object), or name but not type (backed by a
|
||||
// saved object and type and id are stored on references). Had trouble with oring the two types
|
||||
// because of optional properties being marked as required: https://github.com/microsoft/TypeScript/issues/20722
|
||||
readonly type?: string;
|
||||
readonly name?: string;
|
||||
|
||||
panelIndex: string;
|
||||
};
|
||||
|
||||
// NOTE!!
|
||||
// All of these types can actually exist in 7.2! The names are pretty confusing because we did
|
||||
// in place migrations for so long. For example, `RawSavedDashboardPanelTo60` is what a panel
|
||||
// created in 6.0 will look like after it's been migrated up to 7.2, *not* what it would look like in 6.0.
|
||||
// That's why it actually doesn't have id or type, but has a name property, because that was a migration
|
||||
// added in 7.0.
|
||||
|
||||
// Hopefully since we finally have a formal saved object migration system and we can do less in place
|
||||
// migrations, this will be easier to understand moving forward.
|
||||
|
||||
// Starting in 6.4 we added an in-place edit on panels to remove columns and sort properties and put them
|
||||
// inside the embeddable config (https://github.com/elastic/kibana/pull/17446).
|
||||
// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in
|
||||
// this shape in v 7.2.
|
||||
export type RawSavedDashboardPanel640To720 = Pick<
|
||||
RawSavedDashboardPanel630,
|
||||
Exclude<keyof RawSavedDashboardPanel630, 'columns' | 'sort'>
|
||||
>;
|
||||
|
||||
// In 6.3.0 we expanded the number of grid columns and rows: https://github.com/elastic/kibana/pull/16763
|
||||
// We added in-place migrations to multiply older x,y,h,w numbers. Note the typescript shape here is the same
|
||||
// because it's just multiplying existing fields.
|
||||
// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in 7.2
|
||||
// that need to be modified.
|
||||
export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620;
|
||||
|
||||
// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property.
|
||||
// Source: https://github.com/elastic/kibana/pull/14949
|
||||
export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & {
|
||||
embeddableConfig: { [key: string]: unknown };
|
||||
version: string;
|
||||
};
|
||||
|
||||
// In 6.1 we switched from an angular grid to react grid layout (https://github.com/elastic/kibana/pull/13853)
|
||||
// This used gridData instead of size_x, size_y, row and col. We also started tracking the version this panel
|
||||
// was created in to make future migrations easier.
|
||||
// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in
|
||||
// this shape in v 7.2.
|
||||
export type RawSavedDashboardPanel610 = Pick<
|
||||
RawSavedDashboardPanelTo60,
|
||||
Exclude<keyof RawSavedDashboardPanelTo60, 'size_x' | 'size_y' | 'col' | 'row'>
|
||||
> & { gridData: GridData; version: string };
|
||||
|
||||
export interface RawSavedDashboardPanelTo60 {
|
||||
readonly columns?: string[];
|
||||
readonly sort?: string;
|
||||
readonly size_x?: number;
|
||||
readonly size_y?: number;
|
||||
readonly row: number;
|
||||
readonly col: number;
|
||||
panelIndex: number | string; // earlier versions allowed this to be number or string
|
||||
readonly name: string;
|
||||
|
||||
// This is where custom panel titles are stored prior to Embeddable API v2
|
||||
title?: string;
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ class DashboardPanelUi extends React.Component<DashboardPanelUiProps, State> {
|
|||
if (!initialized) {
|
||||
embeddableIsInitializing();
|
||||
embeddableFactory
|
||||
// @ts-ignore -- going away with Embeddable V2
|
||||
.create(panel, embeddableStateChanged)
|
||||
.then((embeddable: Embeddable) => {
|
||||
if (this.mounted) {
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock(
|
||||
'ui/chrome',
|
||||
() => ({
|
||||
getKibanaVersion: () => '6.3.0',
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants';
|
||||
import { PanelUtils } from './panel_utils';
|
||||
import { createPanelState } from './panel_state';
|
||||
|
||||
test('parseVersion', () => {
|
||||
const { major, minor } = PanelUtils.parseVersion('6.2.0');
|
||||
expect(major).toBe(6);
|
||||
expect(minor).toBe(2);
|
||||
});
|
||||
|
||||
test('convertPanelDataPre_6_1 gives supplies width and height when missing', () => {
|
||||
const panelData = [
|
||||
{
|
||||
col: 3,
|
||||
id: 'foo1',
|
||||
row: 1,
|
||||
type: 'visualization',
|
||||
panelIndex: 1,
|
||||
gridData: createPanelState,
|
||||
},
|
||||
{
|
||||
col: 3,
|
||||
id: 'foo2',
|
||||
row: 1,
|
||||
size_x: 3,
|
||||
size_y: 2,
|
||||
type: 'visualization',
|
||||
panelIndex: 2,
|
||||
gridData: createPanelState,
|
||||
},
|
||||
];
|
||||
panelData.forEach(oldPanel => PanelUtils.convertPanelDataPre_6_1(oldPanel));
|
||||
expect(panelData[0].gridData.w).toBe(DEFAULT_PANEL_WIDTH);
|
||||
expect(panelData[0].gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
|
||||
expect(panelData[0].version).toBe('6.3.0');
|
||||
|
||||
expect(panelData[1].gridData.w).toBe(3);
|
||||
expect(panelData[1].gridData.h).toBe(2);
|
||||
expect(panelData[1].version).toBe('6.3.0');
|
||||
});
|
||||
|
||||
test('convertPanelDataPre_6_3 scales panel dimensions', () => {
|
||||
const oldPanel = {
|
||||
gridData: {
|
||||
h: 3,
|
||||
w: 7,
|
||||
x: 2,
|
||||
y: 5,
|
||||
},
|
||||
version: '6.2.0',
|
||||
};
|
||||
const updatedPanel = PanelUtils.convertPanelDataPre_6_3(oldPanel, false);
|
||||
expect(updatedPanel.gridData.w).toBe(28);
|
||||
expect(updatedPanel.gridData.h).toBe(15);
|
||||
expect(updatedPanel.gridData.x).toBe(8);
|
||||
expect(updatedPanel.gridData.y).toBe(25);
|
||||
expect(updatedPanel.version).toBe('6.3.0');
|
||||
});
|
||||
|
||||
test('convertPanelDataPre_6_3 with margins scales panel dimensions', () => {
|
||||
const oldPanel = {
|
||||
gridData: {
|
||||
h: 3,
|
||||
w: 7,
|
||||
x: 2,
|
||||
y: 5,
|
||||
},
|
||||
version: '6.2.0',
|
||||
};
|
||||
const updatedPanel = PanelUtils.convertPanelDataPre_6_3(oldPanel, true);
|
||||
expect(updatedPanel.gridData.w).toBe(28);
|
||||
expect(updatedPanel.gridData.h).toBe(12);
|
||||
expect(updatedPanel.gridData.x).toBe(8);
|
||||
expect(updatedPanel.gridData.y).toBe(20);
|
||||
expect(updatedPanel.version).toBe('6.3.0');
|
||||
});
|
|
@ -17,114 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import _ from 'lodash';
|
||||
import chrome from 'ui/chrome';
|
||||
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants';
|
||||
import { GridData, SavedDashboardPanel } from '../types';
|
||||
|
||||
const PANEL_HEIGHT_SCALE_FACTOR = 5;
|
||||
const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4;
|
||||
const PANEL_WIDTH_SCALE_FACTOR = 4;
|
||||
|
||||
export interface SemanticVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
}
|
||||
import { SavedDashboardPanel } from '../types';
|
||||
|
||||
export class PanelUtils {
|
||||
// 6.1 switched from gridster to react grid. React grid uses different variables for tracking layout
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
public static convertPanelDataPre_6_1(panel: any): SavedDashboardPanel {
|
||||
['col', 'row'].forEach(key => {
|
||||
if (!_.has(panel, key)) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', {
|
||||
defaultMessage:
|
||||
'Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected field: {key}',
|
||||
values: { key },
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
panel.gridData = {
|
||||
x: panel.col - 1,
|
||||
y: panel.row - 1,
|
||||
w: panel.size_x || DEFAULT_PANEL_WIDTH,
|
||||
h: panel.size_y || DEFAULT_PANEL_HEIGHT,
|
||||
i: panel.panelIndex.toString(),
|
||||
};
|
||||
panel.version = chrome.getKibanaVersion();
|
||||
panel.panelIndex = panel.panelIndex.toString();
|
||||
delete panel.size_x;
|
||||
delete panel.size_y;
|
||||
delete panel.row;
|
||||
delete panel.col;
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
// 6.3 changed the panel dimensions to allow finer control over sizing
|
||||
// 1) decrease column height from 100 to 20.
|
||||
// 2) increase rows from 12 to 48
|
||||
// Need to scale pre 6.3 panels so they maintain the same layout
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
public static convertPanelDataPre_6_3(
|
||||
panel: {
|
||||
gridData: GridData;
|
||||
version: string;
|
||||
},
|
||||
useMargins: boolean
|
||||
) {
|
||||
['w', 'x', 'h', 'y'].forEach(key => {
|
||||
if (!_.has(panel.gridData, key)) {
|
||||
throw new Error(
|
||||
i18n.translate(
|
||||
'kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: {key}',
|
||||
values: { key },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// see https://github.com/elastic/kibana/issues/20635 on why the scale factor changes when margins are being used
|
||||
const heightScaleFactor = useMargins
|
||||
? PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS
|
||||
: PANEL_HEIGHT_SCALE_FACTOR;
|
||||
|
||||
panel.gridData.w = panel.gridData.w * PANEL_WIDTH_SCALE_FACTOR;
|
||||
panel.gridData.x = panel.gridData.x * PANEL_WIDTH_SCALE_FACTOR;
|
||||
panel.gridData.h = panel.gridData.h * heightScaleFactor;
|
||||
panel.gridData.y = panel.gridData.y * heightScaleFactor;
|
||||
panel.version = chrome.getKibanaVersion();
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
public static parseVersion(version = '6.0.0'): SemanticVersion {
|
||||
const versionSplit = version.split('.');
|
||||
if (versionSplit.length < 3) {
|
||||
throw new Error(
|
||||
i18n.translate('kbn.dashboard.panel.invalidVersionErrorMessage', {
|
||||
defaultMessage: 'Invalid version, {version}, expected {semver}',
|
||||
values: {
|
||||
version,
|
||||
semver: '<major>.<minor>.<patch>',
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return {
|
||||
major: parseInt(versionSplit[0], 10),
|
||||
minor: parseInt(versionSplit[1], 10),
|
||||
};
|
||||
}
|
||||
|
||||
public static initPanelIndexes(panels: SavedDashboardPanel[]): void {
|
||||
// find the largest panelIndex in all the panels
|
||||
let maxIndex = this.getMaxPanelIndex(panels);
|
||||
|
|
|
@ -38,6 +38,7 @@ const originalPanelData = {
|
|||
|
||||
beforeEach(() => {
|
||||
// init store
|
||||
// @ts-ignore all this is going away soon, just ignore type errors.
|
||||
store.dispatch(updatePanels({ '1': originalPanelData }));
|
||||
});
|
||||
|
||||
|
@ -53,6 +54,7 @@ describe('UpdatePanel', () => {
|
|||
y: 5,
|
||||
},
|
||||
};
|
||||
// @ts-ignore all this is going away soon, just ignore type errors.
|
||||
store.dispatch(updatePanel(newPanelData));
|
||||
|
||||
const panel = getPanel(store.getState(), '1');
|
||||
|
@ -70,6 +72,7 @@ describe('UpdatePanel', () => {
|
|||
columns: ['field1', 'field2', 'field3'],
|
||||
},
|
||||
};
|
||||
// @ts-ignore all this is going away soon, just ignore type errors.
|
||||
store.dispatch(updatePanels({ '1': panelData }));
|
||||
const newPanelData = {
|
||||
...originalPanelData,
|
||||
|
@ -77,12 +80,13 @@ describe('UpdatePanel', () => {
|
|||
columns: ['field2', 'field3'],
|
||||
},
|
||||
};
|
||||
// @ts-ignore all this is going away soon, just ignore type errors.
|
||||
store.dispatch(updatePanel(newPanelData));
|
||||
|
||||
const panel = getPanel(store.getState(), '1');
|
||||
expect(panel.embeddableConfig.columns.length).toBe(2);
|
||||
expect(panel.embeddableConfig.columns[0]).toBe('field2');
|
||||
expect(panel.embeddableConfig.columns[1]).toBe('field3');
|
||||
expect((panel.embeddableConfig as any).columns.length).toBe(2);
|
||||
expect((panel.embeddableConfig as any).columns[0]).toBe('field2');
|
||||
expect((panel.embeddableConfig as any).columns[1]).toBe('field3');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -84,10 +84,6 @@ module.factory('SavedDashboard', function (Private) {
|
|||
description: 'text',
|
||||
panelsJSON: 'text',
|
||||
optionsJSON: 'text',
|
||||
// Note: this field is no longer used for dashboards created or saved in version 6.2 onward. We keep it around
|
||||
// due to BWC, until we can ensure a migration step for all old dashboards saved in an index, as well as
|
||||
// migration steps for importing. See https://github.com/elastic/kibana/issues/15204 for more info.
|
||||
uiStateJSON: 'text',
|
||||
version: 'integer',
|
||||
timeRestore: 'boolean',
|
||||
timeTo: 'keyword',
|
||||
|
|
|
@ -25,7 +25,8 @@ export function extractReferences({ attributes, references = [] }) {
|
|||
throw new Error(`"type" attribute is missing from panel "${i}"`);
|
||||
}
|
||||
if (!panel.id) {
|
||||
throw new Error(`"id" attribute is missing from panel "${i}"`);
|
||||
// Embeddables are not required to be backed off a saved object.
|
||||
return;
|
||||
}
|
||||
panel.panelRefName = `panel_${i}`;
|
||||
panelReferences.push({
|
||||
|
|
|
@ -82,7 +82,7 @@ Object {
|
|||
);
|
||||
});
|
||||
|
||||
test('fails when "id" attribute is missing from a panel', () => {
|
||||
test('passes when "id" attribute is missing from a panel', () => {
|
||||
const doc = {
|
||||
id: '1',
|
||||
attributes: {
|
||||
|
@ -95,9 +95,15 @@ Object {
|
|||
]),
|
||||
},
|
||||
};
|
||||
expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"id\\" attribute is missing from panel \\"0\\""`
|
||||
);
|
||||
expect(extractReferences(doc)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"foo": true,
|
||||
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]",
|
||||
},
|
||||
"references": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -24,6 +24,14 @@ import { Filter } from '@kbn/es-query';
|
|||
import { Query } from 'src/legacy/core_plugins/data/public';
|
||||
import { AppState as TAppState } from 'ui/state_management/app_state';
|
||||
import { DashboardViewMode } from './dashboard_view_mode';
|
||||
import {
|
||||
RawSavedDashboardPanelTo60,
|
||||
RawSavedDashboardPanel610,
|
||||
RawSavedDashboardPanel620,
|
||||
RawSavedDashboardPanel630,
|
||||
RawSavedDashboardPanel640To720,
|
||||
RawSavedDashboardPanel730ToLatest,
|
||||
} from './migrations/types';
|
||||
|
||||
export interface EmbeddableFactoryRegistry extends UIRegistry<EmbeddableFactory> {
|
||||
byName: { [key: string]: EmbeddableFactory };
|
||||
|
@ -39,56 +47,63 @@ export interface GridData {
|
|||
i: string;
|
||||
}
|
||||
|
||||
export interface SavedDashboardPanel {
|
||||
// TODO: Make id optional when embeddable API V2 is merged. At that point, it's okay to store panels
|
||||
// that aren't backed by saved object ids.
|
||||
readonly id: string;
|
||||
/**
|
||||
* This should always represent the latest dashboard panel shape, after all possible migrations.
|
||||
*/
|
||||
export type SavedDashboardPanel = SavedDashboardPanel730ToLatest;
|
||||
|
||||
readonly version: string;
|
||||
readonly type: string;
|
||||
panelIndex: string;
|
||||
embeddableConfig: any;
|
||||
readonly gridData: GridData;
|
||||
readonly title?: string;
|
||||
}
|
||||
|
||||
export interface Pre61SavedDashboardPanel {
|
||||
readonly size_x: number;
|
||||
readonly size_y: number;
|
||||
readonly row: number;
|
||||
readonly col: number;
|
||||
readonly panelIndex: number | string; // earlier versions allowed this to be number or string
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
embeddableConfig: any;
|
||||
}
|
||||
|
||||
export interface Pre64SavedDashboardPanel {
|
||||
columns?: string;
|
||||
sort?: string;
|
||||
// id becomes optional starting in 7.3.0
|
||||
export type SavedDashboardPanel730ToLatest = Pick<
|
||||
RawSavedDashboardPanel730ToLatest,
|
||||
Exclude<keyof RawSavedDashboardPanel730ToLatest, 'name'>
|
||||
> & {
|
||||
readonly id?: string;
|
||||
readonly version: string;
|
||||
readonly type: string;
|
||||
readonly panelIndex: string;
|
||||
readonly gridData: GridData;
|
||||
readonly title?: string;
|
||||
embeddableConfig: any;
|
||||
}
|
||||
};
|
||||
|
||||
export interface DashboardAppStateDefaults {
|
||||
panels: SavedDashboardPanel[];
|
||||
fullScreenMode: boolean;
|
||||
title: string;
|
||||
export type SavedDashboardPanel640To720 = Pick<
|
||||
RawSavedDashboardPanel640To720,
|
||||
Exclude<keyof RawSavedDashboardPanel640To720, 'name'>
|
||||
> & {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
};
|
||||
|
||||
export type SavedDashboardPanel630 = Pick<
|
||||
RawSavedDashboardPanel630,
|
||||
Exclude<keyof RawSavedDashboardPanel620, 'name'>
|
||||
> & {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
};
|
||||
|
||||
export type SavedDashboardPanel620 = Pick<
|
||||
RawSavedDashboardPanel620,
|
||||
Exclude<keyof RawSavedDashboardPanel620, 'name'>
|
||||
> & {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
};
|
||||
|
||||
export type SavedDashboardPanel610 = Pick<
|
||||
RawSavedDashboardPanel610,
|
||||
Exclude<keyof RawSavedDashboardPanel610, 'name'>
|
||||
> & {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
};
|
||||
|
||||
export type SavedDashboardPanelTo60 = Pick<
|
||||
RawSavedDashboardPanelTo60,
|
||||
Exclude<keyof RawSavedDashboardPanelTo60, 'name'>
|
||||
> & {
|
||||
readonly id: string;
|
||||
readonly type: string;
|
||||
};
|
||||
|
||||
export type DashboardAppStateDefaults = DashboardAppStateParameters & {
|
||||
description?: string;
|
||||
timeRestore: boolean;
|
||||
options: {
|
||||
useMargins: boolean;
|
||||
hidePanelTitles: boolean;
|
||||
};
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
viewMode: DashboardViewMode;
|
||||
}
|
||||
};
|
||||
|
||||
export interface DashboardAppStateParameters {
|
||||
panels: SavedDashboardPanel[];
|
||||
|
|
|
@ -23,7 +23,7 @@ import { TimeRange } from 'ui/timefilter/time_history';
|
|||
import { Query } from 'src/legacy/core_plugins/data/public';
|
||||
|
||||
export interface EmbeddableCustomization {
|
||||
[key: string]: object | string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ContainerState {
|
||||
|
@ -65,7 +65,7 @@ export interface EmbeddableState {
|
|||
* Any customization data that should be stored at the panel level. For
|
||||
* example, pie slice colors, or custom per panel sort order or columns.
|
||||
*/
|
||||
customization?: object;
|
||||
customization?: { [key: string]: unknown };
|
||||
/**
|
||||
* A possible filter the embeddable wishes dashboard to apply.
|
||||
*/
|
||||
|
|
|
@ -196,7 +196,6 @@ export default function ({ getService }) {
|
|||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
|
@ -248,7 +247,6 @@ export default function ({ getService }) {
|
|||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
|
@ -305,7 +303,6 @@ export default function ({ getService }) {
|
|||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
title: 'Requests',
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
},
|
||||
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
|
||||
|
|
|
@ -1315,7 +1315,6 @@
|
|||
"kbn.dashboard.panel.customizePanelTitle": "パネルをカスタマイズ",
|
||||
"kbn.dashboard.panel.dashboardPanelAriaLabel": "ダッシュボードパネル: {title}",
|
||||
"kbn.dashboard.panel.inspectorPanel.displayName": "検査",
|
||||
"kbn.dashboard.panel.invalidVersionErrorMessage": "無効なバージョン {version}、{semver} が必要です",
|
||||
"kbn.dashboard.panel.noEmbeddableFactoryErrorMessage": "このパネルを並べ替える機能が欠けています。",
|
||||
"kbn.dashboard.panel.noFoundEmbeddableFactoryErrorMessage": "パネルタイプ {panelType} をレンダリングする機能がありません",
|
||||
"kbn.dashboard.panel.optionsMenu.optionsContextMenuTitle": "オプション",
|
||||
|
@ -1326,7 +1325,6 @@
|
|||
"kbn.dashboard.panel.removePanel.displayName": "ダッシュボードから削除",
|
||||
"kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName": "最小化",
|
||||
"kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName": "全画面",
|
||||
"kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}",
|
||||
"kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}",
|
||||
"kbn.dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード",
|
||||
"kbn.dashboard.savedDashboardsTitle": "ダッシュボード",
|
||||
|
|
|
@ -1061,7 +1061,6 @@
|
|||
"kbn.dashboard.panel.customizePanelTitle": "定制面板",
|
||||
"kbn.dashboard.panel.dashboardPanelAriaLabel": "仪表板面板:{title}",
|
||||
"kbn.dashboard.panel.inspectorPanel.displayName": "检查",
|
||||
"kbn.dashboard.panel.invalidVersionErrorMessage": "版本 {version} 无效,应为 {semver}",
|
||||
"kbn.dashboard.panel.noFoundEmbeddableFactoryErrorMessage": "未找到面板类型 {panelType} 的 Embeddable 工厂",
|
||||
"kbn.dashboard.panel.optionsMenu.optionsContextMenuTitle": "选项",
|
||||
"kbn.dashboard.panel.optionsMenu.panelOptionsButtonAriaLabel": "面板选项",
|
||||
|
@ -1071,7 +1070,6 @@
|
|||
"kbn.dashboard.panel.removePanel.displayName": "从仪表板删除",
|
||||
"kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName": "最小化",
|
||||
"kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName": "全屏",
|
||||
"kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含预期字段:{key}",
|
||||
"kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}",
|
||||
"kbn.dashboard.savedDashboard.newDashboardTitle": "新建仪表板",
|
||||
"kbn.dashboard.savedDashboardsTitle": "仪表板",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue