-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Dashboards] Add getSerializedState method to Dashboard API #204140
Changes from 3 commits
cca4f65
a9159a4
6b6818f
b58c33d
ed68550
457f24d
3bcba56
3dc3af4
535ffd2
16e29ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the "Elastic License | ||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
* Public License v 1"; you may not use this file except in compliance with, at | ||
* your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
* License v3.0 only", or the "Server Side Public License, v 1". | ||
*/ | ||
|
||
import { dataService, embeddableService, savedObjectsTaggingService } from '../../kibana_services'; | ||
import { getSampleDashboardState } from '../../../mocks'; | ||
import { DashboardState } from '../../../dashboard_api/types'; | ||
import { getDashboardState } from './get_dashboard_state'; | ||
|
||
dataService.search.searchSource.create = jest.fn().mockResolvedValue({ | ||
setField: jest.fn(), | ||
getSerializedFields: jest.fn().mockReturnValue({}), | ||
}); | ||
|
||
dataService.query.timefilter.timefilter.getTime = jest | ||
.fn() | ||
.mockReturnValue({ from: 'now-15m', to: 'now' }); | ||
|
||
dataService.query.timefilter.timefilter.getRefreshInterval = jest | ||
.fn() | ||
.mockReturnValue({ pause: true, value: 0 }); | ||
|
||
embeddableService.extract = jest | ||
.fn() | ||
.mockImplementation((attributes) => ({ state: attributes, references: [] })); | ||
|
||
if (savedObjectsTaggingService) { | ||
savedObjectsTaggingService.getTaggingApi = jest.fn().mockReturnValue({ | ||
ui: { | ||
updateTagsReferences: jest.fn((references, tags) => references), | ||
}, | ||
}); | ||
} | ||
|
||
describe('getDashboardState', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should return the current state attributes and references', async () => { | ||
const currentState = getSampleDashboardState(); | ||
const result = await getDashboardState({ | ||
controlGroupReferences: [], | ||
generateNewIds: false, | ||
currentState, | ||
panelReferences: [], | ||
}); | ||
|
||
expect(result.attributes.panels).toEqual([]); | ||
expect(result.references).toEqual([]); | ||
}); | ||
|
||
it('should generate new IDs for panels and references when generateNewIds is true', async () => { | ||
const currentState = { | ||
...getSampleDashboardState(), | ||
panels: { oldPanelId: { type: 'visualization' } }, | ||
} as unknown as DashboardState; | ||
const result = await getDashboardState({ | ||
controlGroupReferences: [], | ||
generateNewIds: true, | ||
currentState, | ||
panelReferences: [ | ||
{ | ||
name: 'oldPanelId:indexpattern_foobar', | ||
type: 'index-pattern', | ||
id: 'bizzbuzz', | ||
}, | ||
], | ||
}); | ||
|
||
expect(result.attributes.panels).toEqual( | ||
expect.arrayContaining([ | ||
expect.objectContaining({ | ||
panelIndex: expect.not.stringMatching('oldPanelId'), | ||
type: 'visualization', | ||
}), | ||
]) | ||
); | ||
expect(result.references).toEqual( | ||
expect.arrayContaining([ | ||
{ | ||
name: expect.not.stringMatching(/^oldPanelId:/), | ||
id: 'bizzbuzz', | ||
type: 'index-pattern', | ||
}, | ||
]) | ||
); | ||
}); | ||
|
||
it('should include control group references', async () => { | ||
const currentState = getSampleDashboardState(); | ||
const controlGroupReferences = [ | ||
{ name: 'control1:indexpattern', type: 'index-pattern', id: 'foobar' }, | ||
]; | ||
const result = await getDashboardState({ | ||
controlGroupReferences, | ||
generateNewIds: false, | ||
currentState, | ||
panelReferences: [], | ||
}); | ||
|
||
expect(result.references).toEqual(controlGroupReferences); | ||
}); | ||
|
||
it('should include panel references', async () => { | ||
const currentState = getSampleDashboardState(); | ||
const panelReferences = [ | ||
{ name: 'panel1:boogiewoogie', type: 'index-pattern', id: 'fizzbuzz' }, | ||
]; | ||
const result = await getDashboardState({ | ||
controlGroupReferences: [], | ||
generateNewIds: false, | ||
currentState, | ||
panelReferences, | ||
}); | ||
|
||
expect(result.references).toEqual(panelReferences); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the "Elastic License | ||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
* Public License v 1"; you may not use this file except in compliance with, at | ||
* your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
* License v3.0 only", or the "Server Side Public License, v 1". | ||
*/ | ||
|
||
import { pick } from 'lodash'; | ||
import moment, { Moment } from 'moment'; | ||
import { isFilterPinned } from '@kbn/es-query'; | ||
import type { SavedObjectReference } from '@kbn/core/server'; | ||
import { RefreshInterval, extractSearchSourceReferences } from '@kbn/data-plugin/public'; | ||
|
||
import { | ||
convertPanelMapToPanelsArray, | ||
extractReferences, | ||
generateNewPanelIds, | ||
} from '../../../../common'; | ||
import type { DashboardAttributes } from '../../../../server'; | ||
|
||
import { convertDashboardVersionToNumber } from './dashboard_versioning'; | ||
import { DashboardSearchSource, GetDashboardStateProps } from '../types'; | ||
import { dataService, embeddableService, savedObjectsTaggingService } from '../../kibana_services'; | ||
import { LATEST_DASHBOARD_CONTAINER_VERSION } from '../../../dashboard_container'; | ||
|
||
export const convertTimeToUTCString = (time?: string | Moment): undefined | string => { | ||
if (moment(time).isValid()) { | ||
return moment(time).utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); | ||
} else { | ||
// If it's not a valid moment date, then it should be a string representing a relative time | ||
// like 'now' or 'now-15m'. | ||
return time as string; | ||
} | ||
}; | ||
|
||
export const getDashboardState = async ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this part of content management service instead of just a function that can be imported where needed? Does it need to be part of content management service? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does not need to be on content management service. I moved the module into the |
||
controlGroupReferences, | ||
generateNewIds, | ||
currentState, | ||
panelReferences, | ||
}: GetDashboardStateProps) => { | ||
const { | ||
search: dataSearchService, | ||
query: { | ||
timefilter: { timefilter }, | ||
}, | ||
} = dataService; | ||
|
||
const { | ||
tags, | ||
query, | ||
title, | ||
filters, | ||
timeRestore, | ||
description, | ||
|
||
// Dashboard options | ||
useMargins, | ||
syncColors, | ||
syncCursor, | ||
syncTooltips, | ||
hidePanelTitles, | ||
controlGroupInput, | ||
} = currentState; | ||
|
||
let { panels } = currentState; | ||
let prefixedPanelReferences = panelReferences; | ||
if (generateNewIds) { | ||
const { panels: newPanels, references: newPanelReferences } = generateNewPanelIds( | ||
panels, | ||
panelReferences | ||
); | ||
panels = newPanels; | ||
prefixedPanelReferences = newPanelReferences; | ||
// | ||
// do not need to generate new ids for controls. | ||
// ControlGroup Component is keyed on dashboard id so changing dashboard id mounts new ControlGroup Component. | ||
// | ||
} | ||
|
||
const { searchSource, searchSourceReferences } = await (async () => { | ||
const searchSourceFields = await dataSearchService.searchSource.create(); | ||
searchSourceFields.setField( | ||
'filter', // save only unpinned filters | ||
filters.filter((filter) => !isFilterPinned(filter)) | ||
); | ||
searchSourceFields.setField('query', query); | ||
|
||
const rawSearchSourceFields = searchSourceFields.getSerializedFields(); | ||
const [fields, references] = extractSearchSourceReferences(rawSearchSourceFields) as [ | ||
DashboardSearchSource, | ||
SavedObjectReference[] | ||
]; | ||
return { searchSourceReferences: references, searchSource: fields }; | ||
})(); | ||
|
||
const options = { | ||
useMargins, | ||
syncColors, | ||
syncCursor, | ||
syncTooltips, | ||
hidePanelTitles, | ||
}; | ||
const savedPanels = convertPanelMapToPanelsArray(panels, true); | ||
|
||
/** | ||
* Parse global time filter settings | ||
*/ | ||
const { from, to } = timefilter.getTime(); | ||
const timeFrom = timeRestore ? convertTimeToUTCString(from) : undefined; | ||
const timeTo = timeRestore ? convertTimeToUTCString(to) : undefined; | ||
const refreshInterval = timeRestore | ||
? (pick(timefilter.getRefreshInterval(), [ | ||
'display', | ||
'pause', | ||
'section', | ||
'value', | ||
]) as RefreshInterval) | ||
: undefined; | ||
|
||
const rawDashboardAttributes: DashboardAttributes = { | ||
version: convertDashboardVersionToNumber(LATEST_DASHBOARD_CONTAINER_VERSION), | ||
controlGroupInput: controlGroupInput as DashboardAttributes['controlGroupInput'], | ||
kibanaSavedObjectMeta: { searchSource }, | ||
description: description ?? '', | ||
refreshInterval, | ||
timeRestore, | ||
options, | ||
panels: savedPanels, | ||
timeFrom, | ||
title, | ||
timeTo, | ||
}; | ||
|
||
/** | ||
* Extract references from raw attributes and tags into the references array. | ||
*/ | ||
const { attributes, references: dashboardReferences } = extractReferences( | ||
{ | ||
attributes: rawDashboardAttributes, | ||
references: searchSourceReferences, | ||
}, | ||
{ embeddablePersistableStateService: embeddableService } | ||
); | ||
|
||
const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi(); | ||
const references = savedObjectsTaggingApi?.ui.updateTagsReferences | ||
? savedObjectsTaggingApi?.ui.updateTagsReferences(dashboardReferences, tags) | ||
: dashboardReferences; | ||
|
||
const allReferences = [ | ||
...references, | ||
...(prefixedPanelReferences ?? []), | ||
...(controlGroupReferences ?? []), | ||
]; | ||
return { attributes, references: allReferences }; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#203662 made getState sync so this method could now become sync as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good call! Updated in b58c33d.
Unfortunately, the
getSerializedState
method still needs to be async for now since thegetDashboardState
method on the CM service relies on a Promise to extract searchSource references. But this might change when we move reference handling to the server in #192758.