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:
Stacey Gammon 2019-06-28 13:59:02 -04:00 committed by GitHub
parent b519f36333
commit 698efe7566
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1226 additions and 502 deletions

View file

@ -137,9 +137,6 @@
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
*/

View file

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

View file

@ -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": "ダッシュボード",

View file

@ -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": "仪表板",