{
id: string,
title: string,
timeRange: {
from: string,
to: string,
},
autoRefreshInterval: string,
options: {
timeStoredWithDashboard: boolean,
useDarkTheme: boolean,
useMargins: boolean
}
// We might need appliedFilters and pending filters...
filters: {},
panels: {
[id]: {
gridData: { // Used for positioning by ReactGridLayout
x: int,
y: int,
w: int,
h: int,
i: string, // id
},
// Configuration state is per panel data that is generally updated via the panel
// context menu.
configuration: {
// Custom time range per panel
customTimeRange: {},
// if not undefined, this will be shown instead of the
// title supplied by the embeddable
customTitle: string,
drillDownLinks: [
{
id: string,
label: string,
type: ?,
url: string?,
objectId: id?
}
],
// A JSON object for the embeddable to store whatever it wants here which can
// override other settings on the embeddable instance (for example, columns
// on a saved search, or colors in a pie chart). This can be any shape dictated
// by the embeddable, dashboard knows nothing.
// While not currently something exposed on the panel context menu, this is probably
// something we want to do, to allow users to "reset" this - so all values go back
// to the stored state on the embeddable (e.g. whatever colors or columns, have most
// recently been saved with the embeddable).
// This is TWO WAY data - dashboard can tell the embeddable it changed (got reset, maybe
// a manual modification of JSON data), *and* the embeddable can tell dashboard it changed
// (user changed the color of a pie slice).
embeddablePersonalization: {}
},
// State that dashboard expects the embeddable to give it.
// @typedef {Object} EmbeddableData
embeddable: {
// This is state that might change after the embeddable has been initially rendered.
// If we offer the embeddable a changeState function to call with new state, this is
// the shape of the data we should expect back, while below is metadata that we only need
// to grab once per render.
// @typedef {Object} EmbeddableViewData
view: {
drillDownLink: {
// Ids of the filters that were clicked on. These will be dynamically
// applied to the drill down links customized for this panel. If these
// are supplied the drill down menu should be displayed at the current
// mouse location
stagedFilters: FilterObject
},
},
// Once set, metadata shouldn't change.
// @typedef {Object} EmbeddableMetaData
metadata {
// Right now this is either visualization or saved search.
type: string,
// If this is true, then dashboard will show a "configure drill down
// links" menu option in the context menu for the panel.
supportsDrillDowns: boolean,
editUrl: string,
title: string,
indexPatterns: {...},
// Any shape (make no assumptions)!
// But currently, it will have the id for visualizations and saved searches.
embeddableConfiguration: {
objectId: string?,
}
}
}
}
},
}
State we need to store with each dashboard object, essentially what the dashboard saved object should look like
_id: string // The id of the saved object is a special doc id field in elasticsearch.
_source: {
type: string, // always will be "dashboard",
updated_at: string,
dashboard: {
title: string,
description: string,
// Stores all panel data, including embeddablePersonalization data, and data needed to recreate the
// embeddable (both data dashboard knows about and expects, e.g. type, and data it might not
// need to care about, e.g. id of the saved object for visualizations.
panelsJSON: string,
// Stuff in the options panel, like dark theme and use margins.
optionsJSON: string,
// A deprecated field that needs an official migration path before we can get rid of it completely.
// Currently only migrated once an older dashboard is opened.
uiStateJSON: string,
// I'm actually not sure what this is for. Shows up as just 1 in my index. We store the kibana version
// with panel data, but we should probably pull that out and store it only once per dashboard, not once
// per panel.
version: number,
// Should the time range be saved with the dashboard?
timeRestore: boolean,
// If timeRestore is true:
timeTo: string,
timeFrom: string,
refreshInterval: {...}
// Not sure why this is nested under a generic term rather than something dashboard specific. If it's
// common to all saved objects, it should probably be under _source, not under _source.dashboard
kibanaSavedObjectMeta: {
// Stores filters saved with the dashboard.
searchSourceJSON: string
},
}
}
If we can make fields easily updateable and migratable, we should parse all that stuff out of JSON objects. Then we could search/query for different aspects.
_id: string // The id of the saved object is a special doc id field in elasticsearch.
_source: {
type: string, // always will be "dashboard",
updated_at: string,
dashboard: {
title: string,
description: string,
kibanaVersion: string,
// Should the time range be saved with the dashboard?
timeRestore: boolean,
timeRange: {...},
filters: {...},
useDarkTheme: boolean,
useMargins: boolean,
// Should we keep this as JSON? The benefit is that it's a lot easier to modify it without needing
// to adjust the kibana index mapping. The downside is that then you can easily change something and
// forget to handle BWC for older style dashboards.
panels: {
[id:string]: {
gridData: {
x: int,
y: int,
w: int,
h: int,
},
// Optional panel specific time range
timeRange: {...},
// Optionally override the title given by the embeddable,
customTitle: string,
drillDownLinks: [
{
name: string,
url: string?,
objectId: string?,
}
],
// The type of embeddable. Used to look up the embeddable factory plugin so the
// embeddable can be rendered.
type: string,
// Since we don't know the shape of this, we'll have to store it as JSON - we can't
// create a mapping for it in our index. This will contain per panel embeddable state, such
// as pie colors and saved search columns.
embeddablePersonalizationJSON: string,
// We won't know the shape of this either. This will contain data required by the embeddable
// to render itself. Such as, visualizations and saved searches need an id in order to load
// themselves. It differs from the personalization above because it's the same for every
// embeddable and can't be changed by dashboard. We do want to expose functionality for
// dashboard to wipe out the embeddable personalization data.
// ** Is this separation from the above neccessary for storage purposes?? Should we reconsider this?
embeddableConfigurationJSON: string,
}
}
}
}
dashboard_app.js
// grab dashboard object from storage
// loop through panels array
For each panel:
const metadata = embeddableFactoriers[panel.type].initialize(
{
type: panel.type,
configuration: panel.embeddableConfiguration
});
// Keep this out of redux to avoid re-renders on data that doesn't change.
// This is derived data that the selectors can get from. Prefetched!
embeddableMetaDataCache[panel.id] = metadata;
dashboard_panel.js:
// @typedef {Object} EmbeddableState - Data that dashbaord needs from the embeddable
// I think this will just be EmbeddableViewData plus a field for embeddablePersonalization
{
// Before we have drilldown links expose, we need a way to communicate the filters to apply
// immediately on a dashboard (but once applied don't really matter anymore to a visualization).
applyFilters: {},
// Once we expose drillDownLinks
drillDownLink: {
// Ids of the filters that were clicked on. These will be dynamically
// applied to the drill down links customized for this panel. If these
// are supplied the drill down menu should be displayed at the current
// mouse location
stagedFilters: FilterObject
},
// Could be anything - colors on a pie slice, or columns in a saved search. Can be modified by
// the embeddable or possibly by the dashboard - whether reset or if expose the actual object
// for manual modification at the panel level on a dashboard...
personalizationData: {},
}
// @typedef {Object} PanelState - data embeddable needs from dashboard in order to render itself.
{
// Data stored on the dashboard, given to us by the embeddable. We don't know or care what is in
// here, but the embeddable needs it to recreate itself from storage.
embeddableConfiguration: {
objectId: string
},
filters: {...},
timeRange: {...},
}
/**
* @param {EmbeddableState} newEmbeddableState
*/
const changeState = (newEmbeddableState) => {
// Convert newEmbeddableState to the Redux tree representation. It's not an exact transformation because:
// 1. There is some data that is two way - both given to dashboard and dashboard can tell the embeddable to
// change. The redux tree formation separates these types of data.
// 2. We can change our minds much easier about our redux tree set up if it's decoupled from our
// plugin communication layer which we will have to support long term and BWC.
For each known value inside of EmbeddableState that maps to our redux tree:
dispatch(ACTION_TO_UPDATE, newEmbeddableState.value)
};
// Once again panelState should not map directly to our redux tree
const { update: fn } = embeddableFactory.render({ node, containerState, changeState });
EmbeddableFactory::render({ panelState }) {
// mount myself then call
}
update(panelState);
// Add new panel
dispatch({
type: "DASHBOARD_ADD_PANEL",
embeddableType: "bar-chart",
// Whatever data the embeddable needs to create an instance of this type. It will vary based on
// the plugin type.
embeddableConfiguration: {
objectId: string
},
});
// Selector
function getPanel(state, id) {
const panel = state.panels[id];
return {
panel,
embeddableMeta: embeddableFactories[panel.embeddableType].metadata,
};
}
Embeddable state is the data that the embeddable gives dashboard. It differs slightly from dashboard redux tree because some data we
{
// Data
metadata: {
// Right now this is either visualization or saved search.
type: string,
// If this is true, then dashboard will show a "configure drill down
// links" menu option in the context menu for the panel.
supportsDrillDowns: boolean,
},
},
// Index patterns used by this embeddable. This information is currently
// used by the filter on a dashboard for which fields to show in the
// dropdown. Otherwise we'd have to show all fields over all indexes and
// if no embeddables use those index patterns, there really is no point
// to filtering on them.
indexPatternIds: [id1, id2],
// Dashboard navigates to this url when the user clicks 'Edit visualization'
// in the panel context menu.
editUrl: string,
// Title to be shown in the panel. Can be overridden at the panel level.
title: string,
//
esQuery {...},
}
- We must ensure that if embeddables communicate changes to dashbaord via a state tree, that untouched state won't overwrite two way data.
For instance: embeddablePersonalization state goes both ways. Dashboard might tell the embeddable to wipe it out, but the embeddable might tell dashboard to udpate it.