mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Dashboard] Add collapsible sections (#220877)
Closes https://github.com/elastic/kibana/issues/1547 Closes https://github.com/elastic/kibana/issues/190342 Closes https://github.com/elastic/kibana/issues/197716 ## Summary This PR adds the ability for collapsible sections to be created and managed on Dashboards. https://github.com/user-attachments/assets/c5c046d0-58f1-45e1-88b3-33421f3ec002 > [!NOTE] > Most of the work for developing collapsible sections occurred in PRs contained to the `kbn-grid-layout` package (see [this meta issue](https://github.com/elastic/kibana/issues/190342) to track this work) - this PR simply makes them available on Dashboards by adding them as a widget that can be added through the "Add panel" menu. As a result of this, most work is contained in the Dashboard plugin - changes made to the `kbn-grid-layout` package only include adding IDs for additional tests that were added for the Dashboard integration. ### Technical Details #### Content Management Schema The content management schema allows for panels and sections to be mixed within the single `panels` key for a dashboard **without** worrying about section IDs; for example: ``` { "panels": [ { // this is a simplified panel "gridData": { "x": 0, "y": 0, "w": 12, "h": 8, }, "panelConfig": { ... }, }, { // this is a section "gridData": { "y": 9, }, "collapsed": false, "title": "Section title", "panels": [ { // this is a simplified panel "gridData": { "x": 0, "y": 0, "w": 24, "h": 16, }, "panelConfig": { ... }, }, ], }, ] } ``` #### Saved Object Schema The dashboard saved object schema, on the other hand, separates out sections and panels under different keys - this is because, while we are stuck with panels being stored as `panelJSON`, I didn't want to add much to this. So, under grid data for each panel, they have an optional `sectionId` which then links to a section in the `sections` array in the saved object: ``` { "panelsJSON": "<...> \"gridData\":{\"i\":\"panelId\",\"y\":0,\"x\":0,\"w\":12,\"h\":8,\"sectionId\":\"someSectionId\"} <...>" "sections": [ { "collapsed": false, "title": "Section title", "gridData": { "i": "someSectionId", "y": 8. } } ], } ``` This allows sections to be serialized **without** being stringified. This storage also matches how we store this data in runtime using `layout`. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ## Release note Adds collapsible sections to Dashboard, which allow panels to grouped into sections that will not load their contents when their assigned section is collapsed. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b3f79c809f
commit
74ee116780
78 changed files with 5615 additions and 1982 deletions
|
@ -7469,6 +7469,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -7568,6 +7570,147 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData",
|
||||
"panelIndex"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData",
|
||||
"panels"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -8130,6 +8273,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -8229,6 +8374,148 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"description": "The unique identifier of the section",
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"default": [],
|
||||
"description": "The panels that belong to the section.",
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"description": "The unique identifier of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"description": "The unique ID of the panel.",
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -8675,6 +8962,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -8774,6 +9063,147 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData",
|
||||
"panelIndex"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData",
|
||||
"panels"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -9308,6 +9738,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -9407,6 +9839,148 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"description": "The unique identifier of the section",
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"default": [],
|
||||
"description": "The panels that belong to the section.",
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"description": "The unique identifier of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"description": "The unique ID of the panel.",
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -9847,6 +10421,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -9946,6 +10522,147 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData",
|
||||
"panelIndex"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData",
|
||||
"panels"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
|
|
@ -7469,6 +7469,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -7568,6 +7570,147 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData",
|
||||
"panelIndex"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData",
|
||||
"panels"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -8130,6 +8273,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -8229,6 +8374,148 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"description": "The unique identifier of the section",
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"default": [],
|
||||
"description": "The panels that belong to the section.",
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"description": "The unique identifier of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"description": "The unique ID of the panel.",
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -8675,6 +8962,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -8774,6 +9063,147 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData",
|
||||
"panelIndex"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData",
|
||||
"panels"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -9308,6 +9738,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -9407,6 +9839,148 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"description": "The unique identifier of the section",
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"default": [],
|
||||
"description": "The panels that belong to the section.",
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"description": "The unique identifier of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"description": "The unique ID of the panel.",
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
@ -9847,6 +10421,8 @@
|
|||
"panels": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
|
@ -9946,6 +10522,147 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"description": "The collapsed state of the section.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the section in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"panels": {
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gridData": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"h": {
|
||||
"default": 15,
|
||||
"description": "The height of the panel in grid units",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"i": {
|
||||
"type": "string"
|
||||
},
|
||||
"w": {
|
||||
"default": 24,
|
||||
"description": "The width of the panel in grid units",
|
||||
"maximum": 48,
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"x": {
|
||||
"description": "The x coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"description": "The y coordinate of the panel in grid units",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"i"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"id": {
|
||||
"description": "The saved object id for by reference panels",
|
||||
"type": "string"
|
||||
},
|
||||
"panelConfig": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "The description of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"enhancements": {
|
||||
"additionalProperties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"hidePanelTitles": {
|
||||
"description": "Set to true to hide the panel title in its container.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"savedObjectId": {
|
||||
"description": "The unique id of the library item to construct the embeddable.",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the embeddable in the panel.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"panelIndex": {
|
||||
"type": "string"
|
||||
},
|
||||
"panelRefName": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the panel",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "The embeddable type",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"deprecated": true,
|
||||
"description": "The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"panelConfig",
|
||||
"type",
|
||||
"gridData",
|
||||
"panelIndex"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the section.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"title",
|
||||
"gridData",
|
||||
"panels"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"refreshInterval": {
|
||||
|
|
|
@ -6731,6 +6731,101 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
- i
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- panelIndex
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
- i
|
||||
panels:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -6807,6 +6902,14 @@ paths:
|
|||
- gridData
|
||||
- panelIndex
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
- panels
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -7201,6 +7304,103 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
description: The unique identifier of the panel
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
description: The unique ID of the panel.
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
description: The unique identifier of the section
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
panels:
|
||||
default: []
|
||||
description: The panels that belong to the section.
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -7277,6 +7477,13 @@ paths:
|
|||
- type
|
||||
- gridData
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -7594,6 +7801,101 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
- i
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- panelIndex
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
- i
|
||||
panels:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -7670,6 +7972,14 @@ paths:
|
|||
- gridData
|
||||
- panelIndex
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
- panels
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -8044,6 +8354,103 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
description: The unique identifier of the panel
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
description: The unique ID of the panel.
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
description: The unique identifier of the section
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
panels:
|
||||
default: []
|
||||
description: The panels that belong to the section.
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -8120,6 +8527,13 @@ paths:
|
|||
- type
|
||||
- gridData
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -8433,6 +8847,101 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
- i
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- panelIndex
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
- i
|
||||
panels:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -8509,6 +9018,14 @@ paths:
|
|||
- gridData
|
||||
- panelIndex
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
- panels
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
|
|
@ -8273,6 +8273,101 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
- i
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- panelIndex
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
- i
|
||||
panels:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -8349,6 +8444,14 @@ paths:
|
|||
- gridData
|
||||
- panelIndex
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
- panels
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -8743,6 +8846,103 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
description: The unique identifier of the panel
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
description: The unique ID of the panel.
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
description: The unique identifier of the section
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
panels:
|
||||
default: []
|
||||
description: The panels that belong to the section.
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -8819,6 +9019,13 @@ paths:
|
|||
- type
|
||||
- gridData
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -9136,6 +9343,101 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
- i
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- panelIndex
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
- i
|
||||
panels:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -9212,6 +9514,14 @@ paths:
|
|||
- gridData
|
||||
- panelIndex
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
- panels
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -9586,6 +9896,103 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
description: The unique identifier of the panel
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
description: The unique ID of the panel.
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
description: The unique identifier of the section
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
panels:
|
||||
default: []
|
||||
description: The panels that belong to the section.
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -9662,6 +10069,13 @@ paths:
|
|||
- type
|
||||
- gridData
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
@ -9975,6 +10389,101 @@ paths:
|
|||
type: boolean
|
||||
panels:
|
||||
default: []
|
||||
items:
|
||||
anyOf:
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
h:
|
||||
default: 15
|
||||
description: The height of the panel in grid units
|
||||
minimum: 1
|
||||
type: number
|
||||
i:
|
||||
type: string
|
||||
w:
|
||||
default: 24
|
||||
description: The width of the panel in grid units
|
||||
maximum: 48
|
||||
minimum: 1
|
||||
type: number
|
||||
x:
|
||||
description: The x coordinate of the panel in grid units
|
||||
type: number
|
||||
'y':
|
||||
description: The y coordinate of the panel in grid units
|
||||
type: number
|
||||
required:
|
||||
- x
|
||||
- 'y'
|
||||
- i
|
||||
id:
|
||||
description: The saved object id for by reference panels
|
||||
type: string
|
||||
panelConfig:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
description: The description of the panel
|
||||
type: string
|
||||
enhancements:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
hidePanelTitles:
|
||||
description: Set to true to hide the panel title in its container.
|
||||
type: boolean
|
||||
savedObjectId:
|
||||
description: The unique id of the library item to construct the embeddable.
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
version:
|
||||
description: The version of the embeddable in the panel.
|
||||
type: string
|
||||
panelIndex:
|
||||
type: string
|
||||
panelRefName:
|
||||
type: string
|
||||
title:
|
||||
description: The title of the panel
|
||||
type: string
|
||||
type:
|
||||
description: The embeddable type
|
||||
type: string
|
||||
version:
|
||||
deprecated: true
|
||||
description: The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).
|
||||
type: string
|
||||
required:
|
||||
- panelConfig
|
||||
- type
|
||||
- gridData
|
||||
- panelIndex
|
||||
- additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
collapsed:
|
||||
description: The collapsed state of the section.
|
||||
type: boolean
|
||||
gridData:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
i:
|
||||
type: string
|
||||
'y':
|
||||
description: The y coordinate of the section in grid units
|
||||
type: number
|
||||
required:
|
||||
- 'y'
|
||||
- i
|
||||
panels:
|
||||
items:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
|
@ -10051,6 +10560,14 @@ paths:
|
|||
- gridData
|
||||
- panelIndex
|
||||
type: array
|
||||
title:
|
||||
description: The title of the section.
|
||||
type: string
|
||||
required:
|
||||
- title
|
||||
- gridData
|
||||
- panels
|
||||
type: array
|
||||
refreshInterval:
|
||||
additionalProperties: false
|
||||
description: A container for various refresh interval settings
|
||||
|
|
|
@ -285,6 +285,7 @@
|
|||
"refreshInterval.pause",
|
||||
"refreshInterval.section",
|
||||
"refreshInterval.value",
|
||||
"sections",
|
||||
"timeFrom",
|
||||
"timeRestore",
|
||||
"timeTo",
|
||||
|
|
|
@ -982,6 +982,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"timeFrom": {
|
||||
"doc_values": false,
|
||||
"index": false,
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"connector_token": "79977ea2cb1530ba7e315b95c1b5a524b622a6b3",
|
||||
"core-usage-stats": "b3c04da317c957741ebcdedfea4524049fdc79ff",
|
||||
"csp-rule-template": "c151324d5f85178169395eecb12bac6b96064654",
|
||||
"dashboard": "211e9ca30f5a95d5f3c27b1bf2b58e6cfa0c9ae9",
|
||||
"dashboard": "7fea2b6f8f860ac4f665fd0d5c91645ac248fd56",
|
||||
"dynamic-config-overrides": "eb3ec7d96a42991068eda5421eecba9349c82d2b",
|
||||
"endpoint:unified-user-artifact-manifest": "71c7fcb52c658b21ea2800a6b6a76972ae1c776e",
|
||||
"endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b",
|
||||
|
|
|
@ -38,6 +38,11 @@ const renderGridLayout = (propsOverrides: Partial<GridLayoutProps> = {}) => {
|
|||
|
||||
const { rerender, ...rtlRest } = render(<GridLayout {...props} />, { wrapper: EuiThemeProvider });
|
||||
|
||||
const gridLayout = screen.getByTestId('kbnGridLayout');
|
||||
jest.spyOn(gridLayout, 'getBoundingClientRect').mockImplementation(() => {
|
||||
return { top: 0, bottom: 500 } as DOMRect;
|
||||
});
|
||||
|
||||
return {
|
||||
...rtlRest,
|
||||
rerender: (overrides: Partial<GridLayoutProps>) => {
|
||||
|
|
|
@ -198,6 +198,7 @@ export const GridLayout = ({
|
|||
<GridLayoutContext.Provider value={memoizedContext}>
|
||||
<GridHeightSmoother>
|
||||
<div
|
||||
data-test-subj="kbnGridLayout"
|
||||
ref={(divElement) => {
|
||||
layoutRef.current = divElement;
|
||||
setDimensionsRef(divElement);
|
||||
|
|
|
@ -35,6 +35,7 @@ export const DeleteGridSectionModal = ({
|
|||
|
||||
return (
|
||||
<EuiModal
|
||||
data-test-subj={`kbnGridLayoutDeleteSectionModal-${sectionId}`}
|
||||
onClose={() => {
|
||||
setDeleteModalVisible(false);
|
||||
}}
|
||||
|
|
|
@ -174,6 +174,10 @@ export const GridSectionHeader = React.memo(({ sectionId }: GridSectionHeaderPro
|
|||
|
||||
section.isCollapsed = !section.isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
|
||||
const buttonRef = collapseButtonRef.current;
|
||||
if (!buttonRef) return;
|
||||
buttonRef.setAttribute('aria-expanded', `${!section.isCollapsed}`);
|
||||
}, [gridLayoutStateManager, sectionId]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -110,6 +110,7 @@ export const GridSectionTitle = React.memo(
|
|||
size="m"
|
||||
id={`kbnGridSectionTitle-${sectionId}`}
|
||||
aria-controls={`kbnGridSection-${sectionId}`}
|
||||
aria-expanded={!currentSection?.isCollapsed}
|
||||
data-test-subj={`kbnGridSectionTitle-${sectionId}`}
|
||||
textProps={false}
|
||||
className={'kbnGridSectionTitle--button'}
|
||||
|
|
|
@ -86,6 +86,7 @@ export const GridSectionWrapper = React.memo(({ sectionId }: GridSectionProps) =
|
|||
gridLayoutStateManager.sectionRefs.current[sectionId] = rowRef;
|
||||
}}
|
||||
className={'kbnGridSection'}
|
||||
data-test-subj={`kbnGridSectionWrapper-${sectionId}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -88,9 +88,17 @@ export const moveAction = (
|
|||
let previousSection;
|
||||
let targetSectionId: string | undefined = (() => {
|
||||
if (isResize) return lastSectionId;
|
||||
// early return - target the first "main" section if the panel is dragged above the layout element
|
||||
if (previewRect.top < (gridLayoutElement?.getBoundingClientRect().top ?? 0)) {
|
||||
const layoutRect = gridLayoutElement?.getBoundingClientRect();
|
||||
// early returns for edge cases
|
||||
if (previewRect.top < (layoutRect?.top ?? 0)) {
|
||||
// target the first "main" section if the panel is dragged above the layout element
|
||||
return `main-0`;
|
||||
} else if (previewRect.top > (layoutRect?.bottom ?? Infinity)) {
|
||||
// target the last "main" section if the panel is dragged below the layout element
|
||||
const sections = Object.values(currentLayout);
|
||||
const maxOrder = sections.length - 1;
|
||||
previousSection = sections.filter(({ order }) => order === maxOrder)[0].id;
|
||||
return `main-${maxOrder}`;
|
||||
}
|
||||
|
||||
const previewBottom = previewRect.top + rowHeight;
|
||||
|
|
|
@ -62,6 +62,7 @@ export const isLayoutEqual = (a: GridLayoutData, b: GridLayoutData) => {
|
|||
for (const key of keys) {
|
||||
const widgetA = a[key];
|
||||
const widgetB = b[key];
|
||||
if (!widgetA || !widgetB) return widgetA === widgetB;
|
||||
|
||||
if (widgetA.type === 'panel' && widgetB.type === 'panel') {
|
||||
isEqual = isGridDataEqual(widgetA, widgetB);
|
||||
|
|
|
@ -17,6 +17,7 @@ export {
|
|||
type CanDuplicatePanels,
|
||||
type CanExpandPanels,
|
||||
} from './interfaces/panel_management';
|
||||
export { type CanAddNewSection, apiCanAddNewSection } from './interfaces/can_add_new_section';
|
||||
export {
|
||||
canTrackContentfulRender,
|
||||
type TrackContentfulRender,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export interface CanAddNewSection {
|
||||
addNewSection: () => void;
|
||||
}
|
||||
|
||||
export const apiCanAddNewSection = (api: unknown): api is CanAddNewSection => {
|
||||
return typeof (api as CanAddNewSection)?.addNewSection === 'function';
|
||||
};
|
|
@ -7,15 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { combineLatest, debounceTime, distinctUntilChanged, map, of, switchMap } from 'rxjs';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import {
|
||||
apiHasUniqueId,
|
||||
apiPublishesUnsavedChanges,
|
||||
HasUniqueId,
|
||||
PublishesUnsavedChanges,
|
||||
PublishingSubject,
|
||||
apiHasUniqueId,
|
||||
apiPublishesUnsavedChanges,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { combineLatest, debounceTime, map, of, switchMap } from 'rxjs';
|
||||
|
||||
export const DEBOUNCE_TIME = 100;
|
||||
|
||||
|
@ -27,8 +26,6 @@ export function childrenUnsavedChanges$<Api extends unknown = unknown>(
|
|||
) {
|
||||
return children$.pipe(
|
||||
map((children) => Object.keys(children)),
|
||||
distinctUntilChanged(deepEqual),
|
||||
|
||||
// children may change, so make sure we subscribe/unsubscribe with switchMap
|
||||
switchMap((newChildIds: string[]) => {
|
||||
if (newChildIds.length === 0) return of([]);
|
||||
|
|
|
@ -7,5 +7,11 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { GridData, DashboardItem, SavedDashboardPanel } from '../v1/types'; // no changes made to types from v1 to v2
|
||||
export type { ControlGroupAttributes, DashboardCrudTypes, DashboardAttributes } from './types';
|
||||
export type { DashboardItem } from '../v1/types'; // no changes made to types from v1 to v2
|
||||
export type {
|
||||
ControlGroupAttributes,
|
||||
DashboardCrudTypes,
|
||||
DashboardAttributes,
|
||||
GridData,
|
||||
SavedDashboardPanel,
|
||||
} from './types';
|
||||
|
|
|
@ -16,7 +16,18 @@ import { DashboardContentType } from '../types';
|
|||
import {
|
||||
ControlGroupAttributesV1,
|
||||
DashboardAttributes as DashboardAttributesV1,
|
||||
GridData as GridDataV1,
|
||||
SavedDashboardPanel as SavedDashboardPanelV1,
|
||||
} from '../v1/types';
|
||||
import { DashboardSectionState } from '../..';
|
||||
|
||||
export type GridData = GridDataV1 & {
|
||||
sectionId?: string;
|
||||
};
|
||||
|
||||
export type SavedDashboardPanel = Omit<SavedDashboardPanelV1, 'gridData'> & {
|
||||
gridData: GridData;
|
||||
};
|
||||
|
||||
export type ControlGroupAttributes = ControlGroupAttributesV1 & {
|
||||
showApplySelections?: boolean;
|
||||
|
@ -24,6 +35,7 @@ export type ControlGroupAttributes = ControlGroupAttributesV1 & {
|
|||
|
||||
export type DashboardAttributes = Omit<DashboardAttributesV1, 'controlGroupInput'> & {
|
||||
controlGroupInput?: ControlGroupAttributes;
|
||||
sections?: DashboardSectionState[];
|
||||
};
|
||||
|
||||
export type DashboardCrudTypes = ContentManagementCrudTypes<
|
||||
|
|
|
@ -26,6 +26,7 @@ const dashboardWithExtractedPanel: ParsedDashboardAttributesWithType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
sections: {},
|
||||
};
|
||||
|
||||
const extractedSavedObjectPanelRef = {
|
||||
|
@ -47,6 +48,7 @@ const unextractedDashboardState: ParsedDashboardAttributesWithType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
sections: {},
|
||||
};
|
||||
|
||||
describe('inject/extract by reference panel', () => {
|
||||
|
@ -85,6 +87,7 @@ const dashboardWithExtractedByValuePanel: ParsedDashboardAttributesWithType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
sections: {},
|
||||
};
|
||||
|
||||
const extractedByValueRef = {
|
||||
|
@ -106,6 +109,7 @@ const unextractedDashboardByValueState: ParsedDashboardAttributesWithType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
sections: {},
|
||||
};
|
||||
|
||||
describe('inject/extract by value panels', () => {
|
||||
|
|
|
@ -11,6 +11,17 @@ import type { Reference } from '@kbn/content-management-utils';
|
|||
|
||||
import type { GridData } from '../../server/content_management';
|
||||
|
||||
export interface DashboardSectionMap {
|
||||
[id: string]: DashboardSectionState;
|
||||
}
|
||||
|
||||
export interface DashboardSectionState {
|
||||
title: string;
|
||||
collapsed?: boolean; // if undefined, then collapsed is false
|
||||
readonly gridData: Pick<GridData, 'i' | 'y'>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DashboardPanelMap {
|
||||
[key: string]: DashboardPanelState;
|
||||
}
|
||||
|
@ -18,7 +29,7 @@ export interface DashboardPanelMap {
|
|||
export interface DashboardPanelState<PanelState = object> {
|
||||
type: string;
|
||||
explicitInput: PanelState;
|
||||
readonly gridData: GridData;
|
||||
readonly gridData: GridData & { sectionId?: string };
|
||||
panelRefName?: string;
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,8 +11,8 @@ import type { Reference } from '@kbn/content-management-utils';
|
|||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types';
|
||||
|
||||
import {
|
||||
convertPanelMapToPanelsArray,
|
||||
convertPanelsArrayToPanelMap,
|
||||
convertPanelSectionMapsToPanelsArray,
|
||||
convertPanelsArrayToPanelSectionMaps,
|
||||
} from '../../lib/dashboard_panel_converters';
|
||||
import { DashboardAttributesAndReferences, ParsedDashboardAttributesWithType } from '../../types';
|
||||
import type { DashboardAttributes } from '../../../server/content_management';
|
||||
|
@ -28,9 +28,11 @@ export interface InjectExtractDeps {
|
|||
function parseDashboardAttributesWithType({
|
||||
panels,
|
||||
}: DashboardAttributes): ParsedDashboardAttributesWithType {
|
||||
const { panels: panelsMap, sections } = convertPanelsArrayToPanelSectionMaps(panels); // drop sections
|
||||
return {
|
||||
type: 'dashboard',
|
||||
panels: convertPanelsArrayToPanelMap(panels),
|
||||
panels: panelsMap,
|
||||
sections,
|
||||
} as ParsedDashboardAttributesWithType;
|
||||
}
|
||||
|
||||
|
@ -43,7 +45,10 @@ export function injectReferences(
|
|||
// inject references back into panels via the Embeddable persistable state service.
|
||||
const inject = createInject(deps.embeddablePersistableStateService);
|
||||
const injectedState = inject(parsedAttributes, references) as ParsedDashboardAttributesWithType;
|
||||
const injectedPanels = convertPanelMapToPanelsArray(injectedState.panels);
|
||||
const injectedPanels = convertPanelSectionMapsToPanelsArray(
|
||||
injectedState.panels,
|
||||
parsedAttributes.sections
|
||||
); // sections don't have references
|
||||
|
||||
const newAttributes = {
|
||||
...attributes,
|
||||
|
@ -58,7 +63,6 @@ export function extractReferences(
|
|||
deps: InjectExtractDeps
|
||||
): DashboardAttributesAndReferences {
|
||||
const parsedAttributes = parseDashboardAttributesWithType(attributes);
|
||||
|
||||
const panels = parsedAttributes.panels;
|
||||
|
||||
const panelMissingType = Object.entries(panels).find(
|
||||
|
@ -73,8 +77,10 @@ export function extractReferences(
|
|||
references: Reference[];
|
||||
state: ParsedDashboardAttributesWithType;
|
||||
};
|
||||
const extractedPanels = convertPanelMapToPanelsArray(extractedState.panels);
|
||||
|
||||
const extractedPanels = convertPanelSectionMapsToPanelsArray(
|
||||
extractedState.panels,
|
||||
parsedAttributes.sections
|
||||
); // sections don't have references
|
||||
const newAttributes = {
|
||||
...attributes,
|
||||
panels: extractedPanels,
|
||||
|
|
|
@ -14,6 +14,12 @@ export type {
|
|||
DashboardState,
|
||||
} from './types';
|
||||
|
||||
export type { DashboardPanelMap, DashboardPanelState } from './dashboard_container/types';
|
||||
export type {
|
||||
DashboardPanelMap,
|
||||
DashboardPanelState,
|
||||
DashboardSectionMap,
|
||||
DashboardSectionState,
|
||||
} from './dashboard_container/types';
|
||||
|
||||
export { type InjectExtractDeps } from './dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
export { isDashboardSection } from './lib/dashboard_panel_converters';
|
||||
|
|
|
@ -7,22 +7,69 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import { omit } from 'lodash';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import type { DashboardPanelMap } from '..';
|
||||
import type { DashboardPanel } from '../../server/content_management';
|
||||
import type { DashboardPanelMap, DashboardSectionMap } from '..';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardPanel,
|
||||
DashboardSection,
|
||||
} from '../../server/content_management';
|
||||
|
||||
import {
|
||||
getReferencesForPanelId,
|
||||
prefixReferencesFromPanel,
|
||||
} from '../dashboard_container/persistable_state/dashboard_container_references';
|
||||
|
||||
export const convertPanelsArrayToPanelMap = (panels?: DashboardPanel[]): DashboardPanelMap => {
|
||||
export const isDashboardSection = (
|
||||
widget: DashboardAttributes['panels'][number]
|
||||
): widget is DashboardSection => {
|
||||
return 'panels' in widget;
|
||||
};
|
||||
|
||||
export const convertPanelsArrayToPanelSectionMaps = (
|
||||
panels?: DashboardAttributes['panels']
|
||||
): { panels: DashboardPanelMap; sections: DashboardSectionMap } => {
|
||||
const panelsMap: DashboardPanelMap = {};
|
||||
panels?.forEach((panel, idx) => {
|
||||
panelsMap![panel.panelIndex ?? String(idx)] = {
|
||||
const sectionsMap: DashboardSectionMap = {};
|
||||
|
||||
/**
|
||||
* panels and sections are mixed in the DashboardAttributes 'panels' key, so we need
|
||||
* to separate them out into separate maps for the dashboard client side code
|
||||
*/
|
||||
panels?.forEach((widget, i) => {
|
||||
if (isDashboardSection(widget)) {
|
||||
const sectionId = widget.gridData.i ?? String(i);
|
||||
const { panels: sectionPanels, ...restOfSection } = widget;
|
||||
sectionsMap[sectionId] = {
|
||||
...restOfSection,
|
||||
gridData: {
|
||||
...widget.gridData,
|
||||
i: sectionId,
|
||||
},
|
||||
id: sectionId,
|
||||
};
|
||||
(sectionPanels as DashboardPanel[]).forEach((panel, j) => {
|
||||
const panelId = panel.panelIndex ?? String(j);
|
||||
const transformed = transformPanel(panel);
|
||||
panelsMap[panelId] = {
|
||||
...transformed,
|
||||
gridData: { ...transformed.gridData, sectionId, i: panelId },
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// if not a section, then this widget is a panel
|
||||
panelsMap[widget.panelIndex ?? String(i)] = transformPanel(widget);
|
||||
}
|
||||
});
|
||||
|
||||
return { panels: panelsMap, sections: sectionsMap };
|
||||
};
|
||||
|
||||
const transformPanel = (panel: DashboardPanel): DashboardPanelMap[string] => {
|
||||
return {
|
||||
type: panel.type,
|
||||
gridData: panel.gridData,
|
||||
panelRefName: panel.panelRefName,
|
||||
|
@ -33,18 +80,24 @@ export const convertPanelsArrayToPanelMap = (panels?: DashboardPanel[]): Dashboa
|
|||
},
|
||||
version: panel.version,
|
||||
};
|
||||
});
|
||||
return panelsMap;
|
||||
};
|
||||
|
||||
export const convertPanelMapToPanelsArray = (
|
||||
export const convertPanelSectionMapsToPanelsArray = (
|
||||
panels: DashboardPanelMap,
|
||||
sections: DashboardSectionMap,
|
||||
removeLegacyVersion?: boolean
|
||||
) => {
|
||||
return Object.entries(panels).map(([panelId, panelState]) => {
|
||||
): DashboardAttributes['panels'] => {
|
||||
const combined: DashboardAttributes['panels'] = [];
|
||||
|
||||
const panelsInSections: { [sectionId: string]: DashboardSection } = {};
|
||||
Object.entries(sections).forEach(([sectionId, sectionState]) => {
|
||||
panelsInSections[sectionId] = { ...omit(sectionState, 'id'), panels: [] };
|
||||
});
|
||||
Object.entries(panels).forEach(([panelId, panelState]) => {
|
||||
const savedObjectId = (panelState.explicitInput as { savedObjectId?: string }).savedObjectId;
|
||||
const title = (panelState.explicitInput as { title?: string }).title;
|
||||
return {
|
||||
const { sectionId, ...gridData } = panelState.gridData; // drop section ID
|
||||
const convertedPanelState = {
|
||||
/**
|
||||
* Version information used to be stored in the panel until 8.11 when it was moved to live inside the
|
||||
* explicit Embeddable Input. If removeLegacyVersion is not passed, we'd like to keep this information for
|
||||
|
@ -53,14 +106,22 @@ export const convertPanelMapToPanelsArray = (
|
|||
...(!removeLegacyVersion ? { version: panelState.version } : {}),
|
||||
|
||||
type: panelState.type,
|
||||
gridData: panelState.gridData,
|
||||
gridData,
|
||||
panelIndex: panelId,
|
||||
panelConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
|
||||
...(title !== undefined && { title }),
|
||||
...(savedObjectId !== undefined && { id: savedObjectId }),
|
||||
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
|
||||
};
|
||||
|
||||
if (sectionId) {
|
||||
panelsInSections[sectionId].panels.push(convertedPanelState);
|
||||
} else {
|
||||
combined.push(convertedPanelState);
|
||||
}
|
||||
});
|
||||
|
||||
return [...combined, ...Object.values(panelsInSections)];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { ScopedHistory } from '@kbn/core-application-browser';
|
|||
|
||||
import { ForwardedDashboardState } from './locator';
|
||||
import type { DashboardState } from '../types';
|
||||
import { convertPanelsArrayToPanelMap } from '../lib/dashboard_panel_converters';
|
||||
import { convertPanelsArrayToPanelSectionMaps } from '../lib/dashboard_panel_converters';
|
||||
|
||||
export const loadDashboardHistoryLocationState = (
|
||||
getScopedHistory: () => ScopedHistory
|
||||
|
@ -29,6 +29,6 @@ export const loadDashboardHistoryLocationState = (
|
|||
|
||||
return {
|
||||
...restOfState,
|
||||
...{ panels: convertPanelsArrayToPanelMap(panels) },
|
||||
...convertPanelsArrayToPanelSectionMaps(panels),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,11 +17,12 @@ import type {
|
|||
ControlGroupSerializedState,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
|
||||
import type { DashboardPanelMap } from './dashboard_container/types';
|
||||
import type { DashboardPanelMap, DashboardSectionMap } from './dashboard_container/types';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardOptions,
|
||||
DashboardPanel,
|
||||
DashboardSection,
|
||||
} from '../server/content_management';
|
||||
|
||||
export interface DashboardCapabilities {
|
||||
|
@ -37,6 +38,7 @@ export interface DashboardCapabilities {
|
|||
export interface ParsedDashboardAttributesWithType {
|
||||
id: string;
|
||||
panels: DashboardPanelMap;
|
||||
sections: DashboardSectionMap;
|
||||
type: 'dashboard';
|
||||
}
|
||||
|
||||
|
@ -59,6 +61,7 @@ export interface DashboardState extends DashboardSettings {
|
|||
refreshInterval?: RefreshInterval;
|
||||
viewMode: ViewMode;
|
||||
panels: DashboardPanelMap;
|
||||
sections: DashboardSectionMap;
|
||||
|
||||
/**
|
||||
* Temporary. Currently Dashboards are in charge of providing references to all of their children.
|
||||
|
@ -78,7 +81,7 @@ export interface DashboardState extends DashboardSettings {
|
|||
* Do not change type without considering BWC of stored URLs
|
||||
*/
|
||||
export type SharedDashboardState = Partial<
|
||||
Omit<DashboardState, 'panels'> & {
|
||||
Omit<DashboardState, 'panels' | 'sections'> & {
|
||||
controlGroupInput?: DashboardState['controlGroupInput'] & SerializableRecord;
|
||||
|
||||
/**
|
||||
|
@ -87,7 +90,7 @@ export type SharedDashboardState = Partial<
|
|||
*/
|
||||
controlGroupState?: Partial<ControlGroupRuntimeState> & SerializableRecord;
|
||||
|
||||
panels: DashboardPanel[];
|
||||
panels: Array<DashboardPanel | DashboardSection>;
|
||||
|
||||
references?: DashboardState['references'] & SerializableRecord;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ADD_PANEL_ANNOTATION_GROUP } from '@kbn/embeddable-plugin/public';
|
||||
import { apiCanAddNewSection, CanAddNewSection } from '@kbn/presentation-containers';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import { ACTION_ADD_SECTION } from './constants';
|
||||
|
||||
type AddSectionActionApi = CanAddNewSection;
|
||||
|
||||
const isApiCompatible = (api: unknown | null): api is AddSectionActionApi =>
|
||||
Boolean(apiCanAddNewSection(api));
|
||||
|
||||
export class AddSectionAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_ADD_SECTION;
|
||||
public readonly id = ACTION_ADD_SECTION;
|
||||
public order = 40;
|
||||
public grouping = [ADD_PANEL_ANNOTATION_GROUP];
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('dashboard.collapsibleSection.displayName', {
|
||||
defaultMessage: 'Collapsible section',
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType() {
|
||||
return 'section';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
return isApiCompatible(embeddable);
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
embeddable.addNewSection();
|
||||
}
|
||||
}
|
|
@ -15,4 +15,5 @@ export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard';
|
|||
export const ACTION_EXPAND_PANEL = 'togglePanel';
|
||||
export const ACTION_EXPORT_CSV = 'ACTION_EXPORT_CSV';
|
||||
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
|
||||
export const ACTION_ADD_SECTION = 'addCollapsibleSection';
|
||||
export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION';
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
*/
|
||||
|
||||
import { CONTEXT_MENU_TRIGGER, PANEL_NOTIFICATION_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public';
|
||||
import { DashboardStartDependencies } from '../plugin';
|
||||
import {
|
||||
ACTION_ADD_SECTION,
|
||||
ACTION_ADD_TO_LIBRARY,
|
||||
ACTION_CLONE_PANEL,
|
||||
ACTION_COPY_TO_DASHBOARD,
|
||||
|
@ -40,6 +42,12 @@ export const registerActions = async (plugins: DashboardStartDependencies) => {
|
|||
});
|
||||
uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, BADGE_FILTERS_NOTIFICATION);
|
||||
|
||||
uiActions.registerActionAsync(ACTION_ADD_SECTION, async () => {
|
||||
const { AddSectionAction } = await import('../dashboard_renderer/dashboard_module');
|
||||
return new AddSectionAction();
|
||||
});
|
||||
uiActions.attachAction(ADD_PANEL_TRIGGER, ACTION_ADD_SECTION);
|
||||
|
||||
if (share) {
|
||||
uiActions.registerActionAsync(ACTION_EXPORT_CSV, async () => {
|
||||
const { ExportCSVAction } = await import('../dashboard_renderer/dashboard_module');
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 deepEqual from 'fast-deep-equal';
|
||||
import { xor } from 'lodash';
|
||||
import { DashboardLayout } from './types';
|
||||
|
||||
/**
|
||||
* Checks whether the layouts have the same keys, and if they do, checks whether every layout item in the
|
||||
* original layout is deep equal to the layout item at the same ID in the new layout
|
||||
*/
|
||||
export const areLayoutsEqual = (originalLayout?: DashboardLayout, newLayout?: DashboardLayout) => {
|
||||
/**
|
||||
* It is safe to assume that there are **usually** more panels than sections, so do cheaper section ID comparison first
|
||||
*/
|
||||
const newSectionUuids = Object.keys(newLayout?.sections ?? {});
|
||||
const sectionIdDiff = xor(Object.keys(originalLayout?.sections ?? {}), newSectionUuids);
|
||||
if (sectionIdDiff.length > 0) return false;
|
||||
|
||||
/**
|
||||
* Since section IDs are equal, check for more expensive panel ID equality
|
||||
*/
|
||||
const newPanelUuids = Object.keys(newLayout?.panels ?? {});
|
||||
const panelIdDiff = xor(Object.keys(originalLayout?.panels ?? {}), newPanelUuids);
|
||||
if (panelIdDiff.length > 0) return false;
|
||||
|
||||
/**
|
||||
* IDs of all widgets are equal, so now actually compare contents - this is the most expensive equality comparison step
|
||||
*/
|
||||
// again, start with section comparison since it is most likely cheaper
|
||||
for (const sectionId of newSectionUuids) {
|
||||
if (!deepEqual(originalLayout?.sections[sectionId], newLayout?.sections[sectionId])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// then compare panel grid data
|
||||
for (const embeddableId of newPanelUuids) {
|
||||
if (
|
||||
!deepEqual(
|
||||
originalLayout?.panels[embeddableId]?.gridData,
|
||||
newLayout?.panels[embeddableId]?.gridData
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { xor } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { DashboardLayout } from './types';
|
||||
|
||||
/**
|
||||
* Checks whether the panel maps have the same keys, and if they do, whether the grid data and types of each panel
|
||||
* are equal.
|
||||
*/
|
||||
export const arePanelLayoutsEqual = (
|
||||
originalPanels?: DashboardLayout,
|
||||
newPanels?: DashboardLayout
|
||||
) => {
|
||||
const originalUuids = Object.keys(originalPanels ?? {});
|
||||
const newUuids = Object.keys(newPanels ?? {});
|
||||
|
||||
const idDiff = xor(originalUuids, newUuids);
|
||||
if (idDiff.length > 0) return false;
|
||||
|
||||
for (const embeddableId of newUuids) {
|
||||
if (originalPanels?.[embeddableId]?.type !== newPanels?.[embeddableId]?.type) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(originalPanels?.[embeddableId]?.gridData, newPanels?.[embeddableId]?.gridData)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -16,6 +16,7 @@ export const DEFAULT_DASHBOARD_STATE: DashboardState = {
|
|||
description: '',
|
||||
filters: [],
|
||||
panels: {},
|
||||
sections: {},
|
||||
title: '',
|
||||
tags: [],
|
||||
|
||||
|
|
|
@ -11,18 +11,23 @@ import type { Reference } from '@kbn/content-management-utils';
|
|||
import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
|
||||
import { BehaviorSubject, debounceTime, merge } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
import { DASHBOARD_APP_ID } from '../../common/constants';
|
||||
import {
|
||||
getReferencesForControls,
|
||||
getReferencesForPanelId,
|
||||
} from '../../common/dashboard_container/persistable_state/dashboard_container_references';
|
||||
import { DASHBOARD_APP_ID } from '../../common/constants';
|
||||
import type { DashboardState } from '../../common/types';
|
||||
import { getDashboardContentManagementService } from '../services/dashboard_content_management_service';
|
||||
import { LoadDashboardReturn } from '../services/dashboard_content_management_service/types';
|
||||
import {
|
||||
CONTROL_GROUP_EMBEDDABLE_ID,
|
||||
initializeControlGroupManager,
|
||||
} from './control_group_manager';
|
||||
import { initializeDataLoadingManager } from './data_loading_manager';
|
||||
import { initializeDataViewsManager } from './data_views_manager';
|
||||
import { DEFAULT_DASHBOARD_STATE } from './default_dashboard_state';
|
||||
import { getSerializedState } from './get_serialized_state';
|
||||
import { initializePanelsManager } from './panels_manager';
|
||||
import { initializeLayoutManager } from './layout_manager';
|
||||
import { openSaveModal } from './save_modal/open_save_modal';
|
||||
import { initializeSearchSessionManager } from './search_sessions/search_session_manager';
|
||||
import { initializeSettingsManager } from './settings_manager';
|
||||
|
@ -35,14 +40,9 @@ import {
|
|||
DashboardCreationOptions,
|
||||
DashboardInternalApi,
|
||||
} from './types';
|
||||
import type { DashboardState } from '../../common/types';
|
||||
import { initializeUnifiedSearchManager } from './unified_search_manager';
|
||||
import { initializeUnsavedChangesManager } from './unsaved_changes_manager';
|
||||
import { initializeViewModeManager } from './view_mode_manager';
|
||||
import {
|
||||
CONTROL_GROUP_EMBEDDABLE_ID,
|
||||
initializeControlGroupManager,
|
||||
} from './control_group_manager';
|
||||
|
||||
export function getDashboardApi({
|
||||
creationOptions,
|
||||
|
@ -63,7 +63,7 @@ export function getDashboardApi({
|
|||
|
||||
const viewModeManager = initializeViewModeManager(incomingEmbeddable, savedObjectResult);
|
||||
const trackPanel = initializeTrackPanel(async (id: string) => {
|
||||
await panelsManager.api.getChildApi(id);
|
||||
await layoutManager.api.getChildApi(id);
|
||||
});
|
||||
|
||||
const references$ = new BehaviorSubject<Reference[] | undefined>(initialState.references);
|
||||
|
@ -78,9 +78,10 @@ export function getDashboardApi({
|
|||
return panelReferences.length > 0 ? panelReferences : references$.value ?? [];
|
||||
};
|
||||
|
||||
const panelsManager = initializePanelsManager(
|
||||
const layoutManager = initializeLayoutManager(
|
||||
incomingEmbeddable,
|
||||
initialState.panels,
|
||||
initialState.sections,
|
||||
trackPanel,
|
||||
getReferences
|
||||
);
|
||||
|
@ -88,10 +89,10 @@ export function getDashboardApi({
|
|||
initialState.controlGroupInput,
|
||||
getReferences
|
||||
);
|
||||
const dataLoadingManager = initializeDataLoadingManager(panelsManager.api.children$);
|
||||
const dataLoadingManager = initializeDataLoadingManager(layoutManager.api.children$);
|
||||
const dataViewsManager = initializeDataViewsManager(
|
||||
controlGroupManager.api.controlGroupApi$,
|
||||
panelsManager.api.children$
|
||||
layoutManager.api.children$
|
||||
);
|
||||
const settingsManager = initializeSettingsManager(initialState);
|
||||
const unifiedSearchManager = initializeUnifiedSearchManager(
|
||||
|
@ -107,7 +108,7 @@ export function getDashboardApi({
|
|||
creationOptions,
|
||||
controlGroupManager,
|
||||
lastSavedState: savedObjectResult?.dashboardInput ?? DEFAULT_DASHBOARD_STATE,
|
||||
panelsManager,
|
||||
layoutManager,
|
||||
savedObjectId$,
|
||||
settingsManager,
|
||||
unifiedSearchManager,
|
||||
|
@ -115,13 +116,18 @@ export function getDashboardApi({
|
|||
});
|
||||
|
||||
function getState() {
|
||||
const { panels, references: panelReferences } = panelsManager.internalApi.serializePanels();
|
||||
const {
|
||||
panels,
|
||||
sections,
|
||||
references: panelReferences,
|
||||
} = layoutManager.internalApi.serializeLayout();
|
||||
const { state: unifiedSearchState, references: searchSourceReferences } =
|
||||
unifiedSearchManager.internalApi.getState();
|
||||
const dashboardState: DashboardState = {
|
||||
...settingsManager.api.getSettings(),
|
||||
...unifiedSearchState,
|
||||
panels,
|
||||
sections,
|
||||
viewMode: viewModeManager.api.viewMode$.value,
|
||||
};
|
||||
|
||||
|
@ -143,7 +149,7 @@ export function getDashboardApi({
|
|||
...viewModeManager.api,
|
||||
...dataLoadingManager.api,
|
||||
...dataViewsManager.api,
|
||||
...panelsManager.api,
|
||||
...layoutManager.api,
|
||||
...settingsManager.api,
|
||||
...trackPanel,
|
||||
...unifiedSearchManager.api,
|
||||
|
@ -223,7 +229,7 @@ export function getDashboardApi({
|
|||
getSerializedStateForChild: (childId: string) => {
|
||||
return childId === CONTROL_GROUP_EMBEDDABLE_ID
|
||||
? controlGroupManager.internalApi.getStateForControlGroup()
|
||||
: panelsManager.internalApi.getSerializedStateForPanel(childId);
|
||||
: layoutManager.internalApi.getSerializedStateForPanel(childId);
|
||||
},
|
||||
setSavedObjectId: (id: string | undefined) => savedObjectId$.next(id),
|
||||
type: DASHBOARD_API_TYPE as 'dashboard',
|
||||
|
@ -231,7 +237,7 @@ export function getDashboardApi({
|
|||
} as Omit<DashboardApi, 'searchSessionId$'>;
|
||||
|
||||
const internalApi: DashboardInternalApi = {
|
||||
...panelsManager.internalApi,
|
||||
...layoutManager.internalApi,
|
||||
...unifiedSearchManager.internalApi,
|
||||
setControlGroupApi: controlGroupManager.internalApi.setControlGroupApi,
|
||||
};
|
||||
|
|
|
@ -167,4 +167,70 @@ describe('getSerializedState', () => {
|
|||
|
||||
expect(result.references).toEqual(panelReferences);
|
||||
});
|
||||
|
||||
it('should serialize sections', () => {
|
||||
const dashboardState = {
|
||||
...getSampleDashboardState(),
|
||||
panels: {
|
||||
oldPanelId: {
|
||||
type: 'visualization',
|
||||
gridData: { sectionId: 'section1' },
|
||||
} as unknown as DashboardPanelState,
|
||||
},
|
||||
sections: {
|
||||
section1: {
|
||||
id: 'section1',
|
||||
title: 'Section One',
|
||||
collapsed: false,
|
||||
gridData: { y: 1, i: 'section1' },
|
||||
},
|
||||
section2: {
|
||||
id: 'section2',
|
||||
title: 'Section Two',
|
||||
collapsed: true,
|
||||
gridData: { y: 2, i: 'section2' },
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = getSerializedState({
|
||||
controlGroupReferences: [],
|
||||
generateNewIds: true,
|
||||
dashboardState,
|
||||
panelReferences: [],
|
||||
searchSourceReferences: [],
|
||||
});
|
||||
|
||||
expect(result.attributes.panels).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"collapsed": false,
|
||||
"gridData": Object {
|
||||
"i": "section1",
|
||||
"y": 1,
|
||||
},
|
||||
"panels": Array [
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"i": "54321",
|
||||
},
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "54321",
|
||||
"type": "visualization",
|
||||
"version": undefined,
|
||||
},
|
||||
],
|
||||
"title": "Section One",
|
||||
},
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"gridData": Object {
|
||||
"i": "section2",
|
||||
"y": 2,
|
||||
},
|
||||
"panels": Array [],
|
||||
"title": "Section Two",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,27 +7,30 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import { pick } from 'lodash';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { extractReferences } from '../../common/dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
import {
|
||||
convertPanelMapToPanelsArray,
|
||||
convertPanelSectionMapsToPanelsArray,
|
||||
generateNewPanelIds,
|
||||
} from '../../common/lib/dashboard_panel_converters';
|
||||
import { extractReferences } from '../../common/dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
import type { DashboardAttributes } from '../../server';
|
||||
|
||||
import { convertDashboardVersionToNumber } from '../services/dashboard_content_management_service/lib/dashboard_versioning';
|
||||
import type { DashboardState } from '../../common';
|
||||
import { LATEST_VERSION } from '../../common/content_management';
|
||||
import {
|
||||
convertDashboardVersionToNumber,
|
||||
convertNumberToDashboardVersion,
|
||||
} from '../services/dashboard_content_management_service/lib/dashboard_versioning';
|
||||
import {
|
||||
dataService,
|
||||
embeddableService,
|
||||
savedObjectsTaggingService,
|
||||
} from '../services/kibana_services';
|
||||
import type { DashboardState } from '../../common';
|
||||
import { LATEST_VERSION } from '../../common/content_management';
|
||||
import { convertNumberToDashboardVersion } from '../services/dashboard_content_management_service/lib/dashboard_versioning';
|
||||
import { DashboardApi } from './types';
|
||||
|
||||
const LATEST_DASHBOARD_CONTAINER_VERSION = convertNumberToDashboardVersion(LATEST_VERSION);
|
||||
|
||||
|
@ -53,13 +56,12 @@ export const getSerializedState = ({
|
|||
dashboardState: DashboardState;
|
||||
panelReferences?: Reference[];
|
||||
searchSourceReferences?: Reference[];
|
||||
}) => {
|
||||
}): ReturnType<DashboardApi['getSerializedState']> => {
|
||||
const {
|
||||
query: {
|
||||
timefilter: { timefilter },
|
||||
},
|
||||
} = dataService;
|
||||
|
||||
const {
|
||||
tags,
|
||||
query,
|
||||
|
@ -67,6 +69,7 @@ export const getSerializedState = ({
|
|||
filters,
|
||||
timeRestore,
|
||||
description,
|
||||
sections,
|
||||
|
||||
// Dashboard options
|
||||
useMargins,
|
||||
|
@ -100,7 +103,7 @@ export const getSerializedState = ({
|
|||
syncTooltips,
|
||||
hidePanelTitles,
|
||||
};
|
||||
const savedPanels = convertPanelMapToPanelsArray(panels, true);
|
||||
const savedPanels = convertPanelSectionMapsToPanelsArray(panels, sections, true);
|
||||
|
||||
/**
|
||||
* Parse global time filter settings
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { filter, map as lodashMap, max } from 'lodash';
|
||||
import { BehaviorSubject, Observable, combineLatestWith, debounceTime, map, merge } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import {
|
||||
|
@ -14,12 +18,8 @@ import {
|
|||
EmbeddablePackageState,
|
||||
PanelNotFoundError,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
CanDuplicatePanels,
|
||||
HasSerializedChildState,
|
||||
PanelPackage,
|
||||
PresentationContainer,
|
||||
} from '@kbn/presentation-containers';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PanelPackage } from '@kbn/presentation-containers';
|
||||
import {
|
||||
SerializedPanelState,
|
||||
SerializedTitles,
|
||||
|
@ -32,10 +32,8 @@ import {
|
|||
shouldLogStateDiff,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { filter, map as lodashMap, max } from 'lodash';
|
||||
import { BehaviorSubject, Observable, combineLatestWith, debounceTime, map, merge } from 'rxjs';
|
||||
import { v4 } from 'uuid';
|
||||
import type { DashboardState } from '../../common';
|
||||
|
||||
import type { DashboardSectionMap, DashboardState } from '../../common';
|
||||
import { DashboardPanelMap } from '../../common';
|
||||
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../common/content_management';
|
||||
import { prefixReferencesFromPanel } from '../../common/dashboard_container/persistable_state/dashboard_container_references';
|
||||
|
@ -47,83 +45,84 @@ import { runPanelPlacementStrategy } from '../panel_placement/place_new_panel_st
|
|||
import { PanelPlacementStrategy } from '../plugin_constants';
|
||||
import { coreServices, usageCollectionService } from '../services/kibana_services';
|
||||
import { DASHBOARD_UI_METRIC_ID } from '../utils/telemetry_constants';
|
||||
import { arePanelLayoutsEqual } from './are_panel_layouts_equal';
|
||||
import { areLayoutsEqual } from './are_layouts_equal';
|
||||
import type { initializeTrackPanel } from './track_panel';
|
||||
import {
|
||||
DashboardApi,
|
||||
DashboardChildState,
|
||||
DashboardChildren,
|
||||
DashboardLayout,
|
||||
DashboardLayoutItem,
|
||||
} from './types';
|
||||
import { DashboardChildState, DashboardChildren, DashboardLayout, DashboardPanel } from './types';
|
||||
|
||||
export function initializePanelsManager(
|
||||
export function initializeLayoutManager(
|
||||
incomingEmbeddable: EmbeddablePackageState | undefined,
|
||||
initialPanels: DashboardPanelMap, // SERIALIZED STATE ONLY TODO Remove the DashboardPanelMap layer. We could take the Saved Dashboard Panels array here directly.
|
||||
initialSections: DashboardSectionMap,
|
||||
trackPanel: ReturnType<typeof initializeTrackPanel>,
|
||||
getReferences: (id: string) => Reference[]
|
||||
): {
|
||||
internalApi: {
|
||||
startComparing$: (
|
||||
lastSavedState$: BehaviorSubject<DashboardState>
|
||||
) => Observable<{ panels?: DashboardPanelMap }>;
|
||||
getSerializedStateForPanel: HasSerializedChildState['getSerializedStateForChild'];
|
||||
layout$: BehaviorSubject<DashboardLayout>;
|
||||
registerChildApi: (api: DefaultEmbeddableApi) => void;
|
||||
resetPanels: (lastSavedPanels: DashboardPanelMap) => void;
|
||||
setChildState: (uuid: string, state: SerializedPanelState<object>) => void;
|
||||
serializePanels: () => { panels: DashboardPanelMap; references: Reference[] };
|
||||
};
|
||||
api: PresentationContainer<DefaultEmbeddableApi> &
|
||||
CanDuplicatePanels & { getDashboardPanelFromId: DashboardApi['getDashboardPanelFromId'] };
|
||||
} {
|
||||
) {
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Set up panel state manager
|
||||
// --------------------------------------------------------------------------------------
|
||||
const children$ = new BehaviorSubject<DashboardChildren>({});
|
||||
const { layout: initialLayout, childState: initialChildState } = deserializePanels(initialPanels);
|
||||
const { layout: initialLayout, childState: initialChildState } = deserializeLayout(
|
||||
initialPanels,
|
||||
initialSections
|
||||
);
|
||||
const layout$ = new BehaviorSubject<DashboardLayout>(initialLayout); // layout is the source of truth for which panels are in the dashboard.
|
||||
let currentChildState = initialChildState; // childState is the source of truth for the state of each panel.
|
||||
|
||||
function deserializePanels(panelMap: DashboardPanelMap) {
|
||||
const layout: DashboardLayout = {};
|
||||
function deserializeLayout(panelMap: DashboardPanelMap, sectionMap: DashboardSectionMap) {
|
||||
const layout: DashboardLayout = {
|
||||
panels: {},
|
||||
sections: {},
|
||||
};
|
||||
const childState: DashboardChildState = {};
|
||||
Object.keys(panelMap).forEach((uuid) => {
|
||||
const { gridData, explicitInput, type } = panelMap[uuid];
|
||||
layout[uuid] = { type, gridData };
|
||||
childState[uuid] = {
|
||||
Object.keys(sectionMap).forEach((sectionId) => {
|
||||
layout.sections[sectionId] = { collapsed: false, ...sectionMap[sectionId] };
|
||||
});
|
||||
Object.keys(panelMap).forEach((panelId) => {
|
||||
const { gridData, explicitInput, type } = panelMap[panelId];
|
||||
layout.panels[panelId] = { type, gridData } as DashboardPanel;
|
||||
childState[panelId] = {
|
||||
rawState: explicitInput,
|
||||
references: getReferences(uuid),
|
||||
references: getReferences(panelId),
|
||||
};
|
||||
});
|
||||
return { layout, childState };
|
||||
}
|
||||
|
||||
const serializePanels = (): { references: Reference[]; panels: DashboardPanelMap } => {
|
||||
const serializeLayout = (): {
|
||||
references: Reference[];
|
||||
panels: DashboardPanelMap;
|
||||
sections: DashboardSectionMap;
|
||||
} => {
|
||||
const references: Reference[] = [];
|
||||
const layout = layout$.value;
|
||||
const panels: DashboardPanelMap = {};
|
||||
for (const uuid of Object.keys(layout$.value)) {
|
||||
|
||||
for (const panelId of Object.keys(layout.panels)) {
|
||||
references.push(
|
||||
...prefixReferencesFromPanel(uuid, currentChildState[uuid]?.references ?? [])
|
||||
...prefixReferencesFromPanel(panelId, currentChildState[panelId]?.references ?? [])
|
||||
);
|
||||
panels[uuid] = {
|
||||
...layout$.value[uuid],
|
||||
explicitInput: currentChildState[uuid]?.rawState ?? {},
|
||||
panels[panelId] = {
|
||||
...layout.panels[panelId],
|
||||
explicitInput: currentChildState[panelId]?.rawState ?? {},
|
||||
};
|
||||
}
|
||||
return { panels, references };
|
||||
return { panels, sections: { ...layout.sections }, references };
|
||||
};
|
||||
|
||||
const resetPanels = (lastSavedPanels: DashboardPanelMap) => {
|
||||
const { layout: lastSavedLayout, childState: lstSavedChildState } =
|
||||
deserializePanels(lastSavedPanels);
|
||||
const resetLayout = ({
|
||||
panels: lastSavedPanels,
|
||||
sections: lastSavedSections,
|
||||
}: DashboardState) => {
|
||||
const { layout: lastSavedLayout, childState: lastSavedChildState } = deserializeLayout(
|
||||
lastSavedPanels,
|
||||
lastSavedSections
|
||||
);
|
||||
|
||||
layout$.next(lastSavedLayout);
|
||||
currentChildState = lstSavedChildState;
|
||||
currentChildState = lastSavedChildState;
|
||||
let childrenModified = false;
|
||||
const currentChildren = { ...children$.value };
|
||||
for (const uuid of Object.keys(currentChildren)) {
|
||||
if (lastSavedLayout[uuid]) {
|
||||
if (lastSavedLayout.panels[uuid]) {
|
||||
const child = currentChildren[uuid];
|
||||
if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges();
|
||||
} else {
|
||||
|
@ -145,7 +144,7 @@ export function initializePanelsManager(
|
|||
{
|
||||
width: size?.width ?? DEFAULT_PANEL_WIDTH,
|
||||
height: size?.height ?? DEFAULT_PANEL_HEIGHT,
|
||||
currentPanels: layout$.value,
|
||||
currentPanels: layout$.value.panels,
|
||||
}
|
||||
);
|
||||
return { ...newPanelPlacement, i: uuid };
|
||||
|
@ -154,11 +153,17 @@ export function initializePanelsManager(
|
|||
const placeNewPanel = async (
|
||||
uuid: string,
|
||||
panelPackage: PanelPackage,
|
||||
gridData?: DashboardLayoutItem['gridData']
|
||||
gridData?: DashboardPanel['gridData']
|
||||
): Promise<DashboardLayout> => {
|
||||
const { panelType: type, serializedState } = panelPackage;
|
||||
if (gridData) {
|
||||
return { ...layout$.value, [uuid]: { gridData: { ...gridData, i: uuid }, type } };
|
||||
return {
|
||||
...layout$.value,
|
||||
panels: {
|
||||
...layout$.value.panels,
|
||||
[uuid]: { gridData: { ...gridData, i: uuid }, type } as DashboardPanel,
|
||||
},
|
||||
};
|
||||
}
|
||||
const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting(type);
|
||||
const customPlacementSettings = getCustomPlacementSettingFunc
|
||||
|
@ -167,12 +172,18 @@ export function initializePanelsManager(
|
|||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
customPlacementSettings?.strategy ?? PanelPlacementStrategy.findTopLeftMostOpenSpace,
|
||||
{
|
||||
currentPanels: layout$.value,
|
||||
currentPanels: layout$.value.panels,
|
||||
height: customPlacementSettings?.height ?? DEFAULT_PANEL_HEIGHT,
|
||||
width: customPlacementSettings?.width ?? DEFAULT_PANEL_WIDTH,
|
||||
}
|
||||
);
|
||||
return { ...otherPanels, [uuid]: { gridData: { ...newPanelPlacement, i: uuid }, type } };
|
||||
return {
|
||||
...layout$.value,
|
||||
panels: {
|
||||
...otherPanels,
|
||||
[uuid]: { gridData: { ...newPanelPlacement, i: uuid }, type } as DashboardPanel,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
@ -181,7 +192,7 @@ export function initializePanelsManager(
|
|||
if (incomingEmbeddable) {
|
||||
const { serializedState, size, type } = incomingEmbeddable;
|
||||
const uuid = incomingEmbeddable.embeddableId ?? v4();
|
||||
const existingPanel: DashboardLayoutItem | undefined = layout$.value[uuid];
|
||||
const existingPanel: DashboardPanel | undefined = layout$.value.panels[uuid];
|
||||
const sameType = existingPanel?.type === type;
|
||||
|
||||
const gridData = existingPanel ? existingPanel.gridData : placeIncomingPanel(uuid, size);
|
||||
|
@ -195,14 +206,17 @@ export function initializePanelsManager(
|
|||
|
||||
layout$.next({
|
||||
...layout$.value,
|
||||
[uuid]: { gridData, type },
|
||||
panels: {
|
||||
...layout$.value.panels,
|
||||
[uuid]: { gridData, type } as DashboardPanel,
|
||||
},
|
||||
});
|
||||
trackPanel.setScrollToPanelId(uuid);
|
||||
trackPanel.setHighlightPanelId(uuid);
|
||||
}
|
||||
|
||||
function getDashboardPanelFromId(panelId: string) {
|
||||
const childLayout = layout$.value[panelId];
|
||||
const childLayout = layout$.value.panels[panelId];
|
||||
const childApi = children$.value[panelId];
|
||||
if (!childApi || !childLayout) throw new PanelNotFoundError();
|
||||
return {
|
||||
|
@ -216,7 +230,7 @@ export function initializePanelsManager(
|
|||
|
||||
async function getPanelTitles(): Promise<string[]> {
|
||||
const titles: string[] = [];
|
||||
await asyncForEach(Object.keys(layout$.value), async (id) => {
|
||||
await asyncForEach(Object.keys(layout$.value.panels), async (id) => {
|
||||
const childApi = await getChildApi(id);
|
||||
const title = apiPublishesTitle(childApi) ? getTitle(childApi) : '';
|
||||
if (title) titles.push(title);
|
||||
|
@ -230,7 +244,7 @@ export function initializePanelsManager(
|
|||
const addNewPanel = async <ApiType>(
|
||||
panelPackage: PanelPackage,
|
||||
displaySuccessMessage?: boolean,
|
||||
gridData?: DashboardLayoutItem['gridData']
|
||||
gridData?: DashboardPanel['gridData']
|
||||
) => {
|
||||
const uuid = v4();
|
||||
const { panelType: type, serializedState } = panelPackage;
|
||||
|
@ -246,17 +260,17 @@ export function initializePanelsManager(
|
|||
title: getPanelAddedSuccessString(title),
|
||||
'data-test-subj': 'addEmbeddableToDashboardSuccess',
|
||||
});
|
||||
}
|
||||
trackPanel.setScrollToPanelId(uuid);
|
||||
trackPanel.setHighlightPanelId(uuid);
|
||||
}
|
||||
return (await getChildApi(uuid)) as ApiType;
|
||||
};
|
||||
|
||||
const removePanel = (uuid: string) => {
|
||||
const layout = { ...layout$.value };
|
||||
if (layout[uuid]) {
|
||||
delete layout[uuid];
|
||||
layout$.next(layout);
|
||||
const panels = { ...layout$.value.panels };
|
||||
if (panels[uuid]) {
|
||||
delete panels[uuid];
|
||||
layout$.next({ ...layout$.value, panels });
|
||||
}
|
||||
const children = { ...children$.value };
|
||||
if (children[uuid]) {
|
||||
|
@ -269,7 +283,7 @@ export function initializePanelsManager(
|
|||
};
|
||||
|
||||
const replacePanel = async (idToRemove: string, panelPackage: PanelPackage) => {
|
||||
const existingGridData = layout$.value[idToRemove]?.gridData;
|
||||
const existingGridData = layout$.value.panels[idToRemove]?.gridData;
|
||||
if (!existingGridData) throw new PanelNotFoundError();
|
||||
|
||||
removePanel(idToRemove);
|
||||
|
@ -278,7 +292,7 @@ export function initializePanelsManager(
|
|||
};
|
||||
|
||||
const duplicatePanel = async (uuidToDuplicate: string) => {
|
||||
const layoutItemToDuplicate = layout$.value[uuidToDuplicate];
|
||||
const layoutItemToDuplicate = layout$.value.panels[uuidToDuplicate];
|
||||
const apiToDuplicate = children$.value[uuidToDuplicate];
|
||||
if (!apiToDuplicate || !layoutItemToDuplicate) throw new PanelNotFoundError();
|
||||
|
||||
|
@ -297,15 +311,23 @@ export function initializePanelsManager(
|
|||
const { newPanelPlacement, otherPanels } = placeClonePanel({
|
||||
width: layoutItemToDuplicate.gridData.w,
|
||||
height: layoutItemToDuplicate.gridData.h,
|
||||
currentPanels: layout$.value,
|
||||
sectionId: layoutItemToDuplicate.gridData.sectionId,
|
||||
currentPanels: layout$.value.panels,
|
||||
placeBesideId: uuidToDuplicate,
|
||||
});
|
||||
layout$.next({
|
||||
...layout$.value,
|
||||
panels: {
|
||||
...otherPanels,
|
||||
[uuidOfDuplicate]: {
|
||||
gridData: { ...newPanelPlacement, i: uuidOfDuplicate },
|
||||
gridData: {
|
||||
...newPanelPlacement,
|
||||
i: uuidOfDuplicate,
|
||||
sectionId: layoutItemToDuplicate.gridData.sectionId,
|
||||
},
|
||||
type: layoutItemToDuplicate.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
coreServices.notifications.toasts.addSuccess({
|
||||
|
@ -315,7 +337,7 @@ export function initializePanelsManager(
|
|||
};
|
||||
|
||||
const getChildApi = async (uuid: string): Promise<DefaultEmbeddableApi | undefined> => {
|
||||
if (!layout$.value[uuid]) throw new PanelNotFoundError();
|
||||
if (!layout$.value.panels[uuid]) throw new PanelNotFoundError();
|
||||
if (children$.value[uuid]) return children$.value[uuid];
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
@ -326,7 +348,7 @@ export function initializePanelsManager(
|
|||
}
|
||||
|
||||
// If we hit this, the panel was removed before the embeddable finished loading.
|
||||
if (layout$.value[uuid] === undefined) {
|
||||
if (layout$.value.panels[uuid] === undefined) {
|
||||
subscription.unsubscribe();
|
||||
resolve(undefined);
|
||||
}
|
||||
|
@ -338,25 +360,34 @@ export function initializePanelsManager(
|
|||
internalApi: {
|
||||
getSerializedStateForPanel: (uuid: string) => currentChildState[uuid],
|
||||
layout$,
|
||||
resetPanels,
|
||||
serializePanels,
|
||||
reset: resetLayout,
|
||||
serializeLayout,
|
||||
startComparing$: (
|
||||
lastSavedState$: BehaviorSubject<DashboardState>
|
||||
): Observable<{ panels?: DashboardPanelMap }> => {
|
||||
): Observable<{ panels?: DashboardPanelMap; sections?: DashboardSectionMap }> => {
|
||||
return layout$.pipe(
|
||||
debounceTime(100),
|
||||
combineLatestWith(lastSavedState$.pipe(map((lastSaved) => lastSaved.panels))),
|
||||
map(([, lastSavedPanels]) => {
|
||||
const panels = serializePanels().panels;
|
||||
if (!arePanelLayoutsEqual(lastSavedPanels, panels)) {
|
||||
combineLatestWith(
|
||||
lastSavedState$.pipe(
|
||||
map((lastSaved) => ({ panels: lastSaved.panels, sections: lastSaved.sections }))
|
||||
)
|
||||
),
|
||||
map(([, { panels: lastSavedPanels, sections: lastSavedSections }]) => {
|
||||
const { panels, sections } = serializeLayout();
|
||||
if (
|
||||
!areLayoutsEqual(
|
||||
{ panels: lastSavedPanels, sections: lastSavedSections },
|
||||
{ panels, sections }
|
||||
)
|
||||
) {
|
||||
if (shouldLogStateDiff()) {
|
||||
logStateDiff(
|
||||
'dashboard layout',
|
||||
deserializePanels(lastSavedPanels).layout,
|
||||
deserializePanels(panels).layout
|
||||
deserializeLayout(lastSavedPanels, lastSavedSections).layout,
|
||||
deserializeLayout(panels, sections).layout
|
||||
);
|
||||
}
|
||||
return { panels };
|
||||
return { panels, sections };
|
||||
}
|
||||
return {};
|
||||
})
|
||||
|
@ -371,8 +402,13 @@ export function initializePanelsManager(
|
|||
setChildState: (uuid: string, state: SerializedPanelState<object>) => {
|
||||
currentChildState[uuid] = state;
|
||||
},
|
||||
isSectionCollapsed: (sectionId?: string): boolean => {
|
||||
const { sections } = layout$.getValue();
|
||||
return Boolean(sectionId && sections[sectionId].collapsed);
|
||||
},
|
||||
},
|
||||
api: {
|
||||
/** Panels */
|
||||
children$,
|
||||
getChildApi,
|
||||
addNewPanel,
|
||||
|
@ -380,8 +416,39 @@ export function initializePanelsManager(
|
|||
replacePanel,
|
||||
duplicatePanel,
|
||||
getDashboardPanelFromId,
|
||||
getPanelCount: () => Object.keys(layout$.value).length,
|
||||
getPanelCount: () => Object.keys(layout$.value.panels).length,
|
||||
canRemovePanels: () => trackPanel.expandedPanelId$.value === undefined,
|
||||
|
||||
/** Sections */
|
||||
addNewSection: () => {
|
||||
const currentLayout = layout$.getValue();
|
||||
|
||||
// find the max y so we know where to add the section
|
||||
let maxY = 0;
|
||||
[...Object.values(currentLayout.panels), ...Object.values(currentLayout.sections)].forEach(
|
||||
(widget) => {
|
||||
const { y, h } = { h: 1, ...widget.gridData };
|
||||
maxY = Math.max(maxY, y + h);
|
||||
}
|
||||
);
|
||||
|
||||
// add the new section
|
||||
const sections = { ...currentLayout.sections };
|
||||
const newId = v4();
|
||||
sections[newId] = {
|
||||
id: newId,
|
||||
gridData: { i: newId, y: maxY },
|
||||
title: i18n.translate('dashboard.defaultSectionTitle', {
|
||||
defaultMessage: 'New collapsible section',
|
||||
}),
|
||||
collapsed: false,
|
||||
};
|
||||
layout$.next({
|
||||
...currentLayout,
|
||||
sections,
|
||||
});
|
||||
trackPanel.scrollToBottom$.next();
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -7,13 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
|
||||
export function initializeTrackPanel(untilLoaded: (id: string) => Promise<undefined>) {
|
||||
const expandedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
const focusedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
const highlightPanelId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
const scrollToPanelId$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
const scrollToBottom$ = new Subject<void>();
|
||||
let scrollPosition: number | undefined;
|
||||
|
||||
function setScrollToPanelId(id: string | undefined) {
|
||||
|
@ -62,15 +63,19 @@ export function initializeTrackPanel(untilLoaded: (id: string) => Promise<undefi
|
|||
untilLoaded(id).then(() => {
|
||||
setScrollToPanelId(undefined);
|
||||
if (scrollPosition !== undefined) {
|
||||
window.scrollTo({ top: scrollPosition });
|
||||
window.scrollTo({ top: scrollPosition, behavior: 'smooth' });
|
||||
scrollPosition = undefined;
|
||||
} else {
|
||||
panelRef.scrollIntoView({ block: 'start' });
|
||||
panelRef.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
},
|
||||
scrollToTop: () => {
|
||||
window.scroll(0, 0);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
scrollToBottom$,
|
||||
scrollToBottom: () => {
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||
},
|
||||
setFocusedPanelId: (id: string | undefined) => {
|
||||
if (focusedPanelId$.value !== id) focusedPanelId$.next(id);
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Filter, Query, TimeRange } from '@kbn/es-query';
|
|||
import { PublishesESQLVariables } from '@kbn/esql-types';
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import {
|
||||
CanAddNewSection,
|
||||
CanExpandPanels,
|
||||
HasLastSavedChildState,
|
||||
HasSerializedChildState,
|
||||
|
@ -47,6 +48,8 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
|||
import {
|
||||
DashboardLocatorParams,
|
||||
DashboardPanelMap,
|
||||
DashboardPanelState,
|
||||
DashboardSectionMap,
|
||||
DashboardSettings,
|
||||
DashboardState,
|
||||
} from '../../common';
|
||||
|
@ -58,9 +61,12 @@ import {
|
|||
|
||||
export const DASHBOARD_API_TYPE = 'dashboard';
|
||||
|
||||
export type DashboardLayoutItem = { gridData: GridData } & HasType;
|
||||
export const ReservedLayoutItemTypes: readonly string[] = ['section'] as const;
|
||||
|
||||
export type DashboardPanel = Pick<DashboardPanelState, 'gridData'> & HasType;
|
||||
export interface DashboardLayout {
|
||||
[uuid: string]: DashboardLayoutItem;
|
||||
panels: { [uuid: string]: DashboardPanel }; // partial of DashboardPanelState
|
||||
sections: DashboardSectionMap;
|
||||
}
|
||||
|
||||
export interface DashboardChildState {
|
||||
|
@ -102,6 +108,7 @@ export interface DashboardCreationOptions {
|
|||
}
|
||||
|
||||
export type DashboardApi = CanExpandPanels &
|
||||
CanAddNewSection &
|
||||
HasAppContext &
|
||||
HasExecutionContext &
|
||||
HasLastSavedChildState &
|
||||
|
@ -151,6 +158,8 @@ export type DashboardApi = CanExpandPanels &
|
|||
scrollToPanel: (panelRef: HTMLDivElement) => void;
|
||||
scrollToPanelId$: PublishingSubject<string | undefined>;
|
||||
scrollToTop: () => void;
|
||||
scrollToBottom: () => void;
|
||||
scrollToBottom$: Subject<void>;
|
||||
setFilters: (filters?: Filter[] | undefined) => void;
|
||||
setFullScreenMode: (fullScreenMode: boolean) => void;
|
||||
setHighlightPanelId: (id: string | undefined) => void;
|
||||
|
@ -168,5 +177,10 @@ export interface DashboardInternalApi {
|
|||
layout$: BehaviorSubject<DashboardLayout>;
|
||||
registerChildApi: (api: DefaultEmbeddableApi) => void;
|
||||
setControlGroupApi: (controlGroupApi: ControlGroupApi) => void;
|
||||
serializePanels: () => { references: Reference[]; panels: DashboardPanelMap };
|
||||
serializeLayout: () => {
|
||||
references: Reference[];
|
||||
panels: DashboardPanelMap;
|
||||
sections: DashboardSectionMap;
|
||||
};
|
||||
isSectionCollapsed: (sectionId?: string) => boolean;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
tap,
|
||||
} from 'rxjs';
|
||||
import { getDashboardBackupService } from '../services/dashboard_backup_service';
|
||||
import { initializePanelsManager } from './panels_manager';
|
||||
import { initializeLayoutManager } from './layout_manager';
|
||||
import { initializeSettingsManager } from './settings_manager';
|
||||
import { DashboardCreationOptions } from './types';
|
||||
import { DashboardState } from '../../common';
|
||||
|
@ -40,7 +40,7 @@ import {
|
|||
const DEBOUNCE_TIME = 100;
|
||||
|
||||
export function initializeUnsavedChangesManager({
|
||||
panelsManager,
|
||||
layoutManager,
|
||||
savedObjectId$,
|
||||
lastSavedState,
|
||||
settingsManager,
|
||||
|
@ -55,7 +55,7 @@ export function initializeUnsavedChangesManager({
|
|||
getReferences: (id: string) => Reference[];
|
||||
savedObjectId$: PublishesSavedObjectId['savedObjectId$'];
|
||||
controlGroupManager: ReturnType<typeof initializeControlGroupManager>;
|
||||
panelsManager: ReturnType<typeof initializePanelsManager>;
|
||||
layoutManager: ReturnType<typeof initializeLayoutManager>;
|
||||
viewModeManager: ReturnType<typeof initializeViewModeManager>;
|
||||
settingsManager: ReturnType<typeof initializeSettingsManager>;
|
||||
unifiedSearchManager: ReturnType<typeof initializeUnifiedSearchManager>;
|
||||
|
@ -75,14 +75,14 @@ export function initializeUnsavedChangesManager({
|
|||
// references injected while loading dashboard saved object in loadDashboardState
|
||||
const lastSavedState$ = new BehaviorSubject<DashboardState>(lastSavedState);
|
||||
|
||||
const hasPanelChanges$ = childrenUnsavedChanges$(panelsManager.api.children$).pipe(
|
||||
const hasPanelChanges$ = childrenUnsavedChanges$(layoutManager.api.children$).pipe(
|
||||
tap((childrenWithChanges) => {
|
||||
// propagate the latest serialized state back to the panels manager.
|
||||
// propagate the latest serialized state back to the layout manager.
|
||||
for (const { uuid, hasUnsavedChanges } of childrenWithChanges) {
|
||||
const childApi = panelsManager.api.children$.value[uuid];
|
||||
const childApi = layoutManager.api.children$.value[uuid];
|
||||
if (!hasUnsavedChanges || !childApi || !apiHasSerializableState(childApi)) continue;
|
||||
|
||||
panelsManager.internalApi.setChildState(uuid, childApi.serializeState());
|
||||
layoutManager.internalApi.setChildState(uuid, childApi.serializeState());
|
||||
}
|
||||
}),
|
||||
map((childrenWithChanges) => {
|
||||
|
@ -93,7 +93,7 @@ export function initializeUnsavedChangesManager({
|
|||
const dashboardStateChanges$: Observable<Partial<DashboardState>> = combineLatest([
|
||||
settingsManager.internalApi.startComparing$(lastSavedState$),
|
||||
unifiedSearchManager.internalApi.startComparing$(lastSavedState$),
|
||||
panelsManager.internalApi.startComparing$(lastSavedState$),
|
||||
layoutManager.internalApi.startComparing$(lastSavedState$),
|
||||
]).pipe(
|
||||
map(([settings, unifiedSearch, panels]) => {
|
||||
return { ...settings, ...unifiedSearch, ...panels };
|
||||
|
@ -129,10 +129,9 @@ export function initializeUnsavedChangesManager({
|
|||
|
||||
// always back up view mode. This allows us to know which Dashboards were last changed while in edit mode.
|
||||
dashboardStateToBackup.viewMode = viewMode;
|
||||
|
||||
// Backup latest state from children that have unsaved changes
|
||||
if (hasPanelChanges || hasControlGroupChanges) {
|
||||
const { panels, references } = panelsManager.internalApi.serializePanels();
|
||||
const { panels, references } = layoutManager.internalApi.serializeLayout();
|
||||
const { controlGroupInput, controlGroupReferences } =
|
||||
controlGroupManager.internalApi.serializeControlGroup();
|
||||
// dashboardStateToBackup.references will be used instead of savedObjectResult.references
|
||||
|
@ -169,9 +168,10 @@ export function initializeUnsavedChangesManager({
|
|||
return {
|
||||
api: {
|
||||
asyncResetToLastSavedState: async () => {
|
||||
panelsManager.internalApi.resetPanels(lastSavedState$.value.panels);
|
||||
unifiedSearchManager.internalApi.reset(lastSavedState$.value);
|
||||
settingsManager.internalApi.reset(lastSavedState$.value);
|
||||
const savedState = lastSavedState$.value;
|
||||
layoutManager.internalApi.reset(savedState);
|
||||
unifiedSearchManager.internalApi.reset(savedState);
|
||||
settingsManager.internalApi.reset(savedState);
|
||||
|
||||
await controlGroupManager.api.controlGroupApi$.value?.resetUnsavedChanges();
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { Capabilities } from '@kbn/core/public';
|
||||
import { convertPanelMapToPanelsArray } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import { convertPanelSectionMapsToPanelsArray } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import { DashboardLocatorParams } from '../../../../common/types';
|
||||
import { getDashboardBackupService } from '../../../services/dashboard_backup_service';
|
||||
import { shareService } from '../../../services/kibana_services';
|
||||
|
@ -124,7 +124,7 @@ describe('ShowShareModal', () => {
|
|||
).locatorParams.params;
|
||||
const rawDashboardState = {
|
||||
...unsavedDashboardState,
|
||||
panels: convertPanelMapToPanelsArray(unsavedDashboardState.panels),
|
||||
panels: convertPanelSectionMapsToPanelsArray(unsavedDashboardState.panels, {}),
|
||||
};
|
||||
unsavedStateKeys.forEach((key) => {
|
||||
expect(shareLocatorParams[key]).toStrictEqual(
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
|
||||
import { EuiCallOut, EuiCheckboxGroup } from '@elastic/eui';
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
import { QueryState } from '@kbn/data-plugin/common';
|
||||
|
@ -14,12 +18,10 @@ import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { getStateFromKbnUrl, setStateToKbnUrl, unhashUrl } from '@kbn/kibana-utils-plugin/public';
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import { LocatorPublic } from '@kbn/share-plugin/common';
|
||||
|
||||
import { DashboardLocatorParams } from '../../../../common';
|
||||
import { convertPanelMapToPanelsArray } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import { convertPanelSectionMapsToPanelsArray } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import { SharedDashboardState } from '../../../../common/types';
|
||||
import { getDashboardBackupService } from '../../../services/dashboard_backup_service';
|
||||
import { coreServices, dataService, shareService } from '../../../services/kibana_services';
|
||||
|
@ -110,8 +112,11 @@ export function ShowShareModal({
|
|||
);
|
||||
};
|
||||
|
||||
const { panels: allUnsavedPanelsMap, ...unsavedDashboardState } =
|
||||
getDashboardBackupService().getState(savedObjectId) ?? {};
|
||||
const {
|
||||
panels: allUnsavedPanelsMap,
|
||||
sections: allUnsavedSectionsMap,
|
||||
...unsavedDashboardState
|
||||
} = getDashboardBackupService().getState(savedObjectId) ?? {};
|
||||
|
||||
const hasPanelChanges = allUnsavedPanelsMap !== undefined;
|
||||
|
||||
|
@ -121,8 +126,11 @@ export function ShowShareModal({
|
|||
unsavedDashboardState.controlGroupInput as SharedDashboardState['controlGroupInput'],
|
||||
references: unsavedDashboardState.references as SharedDashboardState['references'],
|
||||
};
|
||||
if (allUnsavedPanelsMap) {
|
||||
unsavedDashboardStateForLocator.panels = convertPanelMapToPanelsArray(allUnsavedPanelsMap);
|
||||
if (allUnsavedPanelsMap || allUnsavedSectionsMap) {
|
||||
unsavedDashboardStateForLocator.panels = convertPanelSectionMapsToPanelsArray(
|
||||
allUnsavedPanelsMap ?? {},
|
||||
allUnsavedSectionsMap ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
const locatorParams: DashboardLocatorParams = {
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { History } from 'history';
|
||||
import { map } from 'rxjs';
|
||||
import { SEARCH_SESSION_ID } from '../../../common/constants';
|
||||
import { convertPanelMapToPanelsArray } from '../../../common/lib/dashboard_panel_converters';
|
||||
import { convertPanelSectionMapsToPanelsArray } from '../../../common/lib/dashboard_panel_converters';
|
||||
import { DashboardLocatorParams } from '../../../common/types';
|
||||
import { DashboardApi, DashboardInternalApi } from '../../dashboard_api/types';
|
||||
import { dataService } from '../../services/kibana_services';
|
||||
|
@ -81,7 +81,7 @@ function getLocatorParams({
|
|||
shouldRestoreSearchSession: boolean;
|
||||
}): DashboardLocatorParams {
|
||||
const savedObjectId = dashboardApi.savedObjectId$.value;
|
||||
const { panels, references } = dashboardInternalApi.serializePanels();
|
||||
const { panels, sections, references } = dashboardInternalApi.serializeLayout();
|
||||
return {
|
||||
viewMode: dashboardApi.viewMode$.value ?? 'view',
|
||||
useHash: false,
|
||||
|
@ -104,7 +104,10 @@ function getLocatorParams({
|
|||
...(savedObjectId
|
||||
? {}
|
||||
: {
|
||||
panels: convertPanelMapToPanelsArray(panels) as DashboardLocatorParams['panels'],
|
||||
panels: convertPanelSectionMapsToPanelsArray(
|
||||
panels,
|
||||
sections
|
||||
) as DashboardLocatorParams['panels'],
|
||||
references: references as DashboardLocatorParams['references'],
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -7,17 +7,22 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { serializeRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { History } from 'history';
|
||||
import _ from 'lodash';
|
||||
import { skip } from 'rxjs';
|
||||
import semverSatisfies from 'semver/functions/satisfies';
|
||||
import type { DashboardPanelMap } from '../../../common/dashboard_container/types';
|
||||
import { convertPanelsArrayToPanelMap } from '../../../common/lib/dashboard_panel_converters';
|
||||
|
||||
import { serializeRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { replaceUrlHashQuery } from '@kbn/kibana-utils-plugin/common';
|
||||
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import type {
|
||||
DashboardPanelMap,
|
||||
DashboardSectionMap,
|
||||
} from '../../../common/dashboard_container/types';
|
||||
import { convertPanelsArrayToPanelSectionMaps } from '../../../common/lib/dashboard_panel_converters';
|
||||
import type { DashboardState, SharedDashboardState } from '../../../common/types';
|
||||
import type { DashboardPanel } from '../../../server/content_management';
|
||||
import type { DashboardPanel, DashboardSection } from '../../../server/content_management';
|
||||
import type { SavedDashboardPanel } from '../../../server/dashboard_saved_object';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
import { migrateLegacyQuery } from '../../services/dashboard_content_management_service/lib/load_dashboard_state';
|
||||
|
@ -33,8 +38,14 @@ const panelIsLegacy = (panel: unknown): panel is SavedDashboardPanel => {
|
|||
* We no longer support loading panels from a version older than 7.3 in the URL.
|
||||
* @returns whether or not there is a panel in the URL state saved with a version before 7.3
|
||||
*/
|
||||
export const isPanelVersionTooOld = (panels: DashboardPanel[] | SavedDashboardPanel[]) => {
|
||||
export const isPanelVersionTooOld = (
|
||||
panels: Array<DashboardPanel | DashboardSection> | SavedDashboardPanel[]
|
||||
) => {
|
||||
for (const panel of panels) {
|
||||
if ('panels' in panel) {
|
||||
// can't use isDashboardSection type guard because of SavedDashboardPanel type
|
||||
continue; // ignore sections
|
||||
}
|
||||
if (
|
||||
!panel.gridData ||
|
||||
!((panel as DashboardPanel).panelConfig || (panel as SavedDashboardPanel).embeddableConfig) ||
|
||||
|
@ -45,13 +56,15 @@ export const isPanelVersionTooOld = (panels: DashboardPanel[] | SavedDashboardPa
|
|||
return false;
|
||||
};
|
||||
|
||||
function getPanelsMap(panels?: DashboardPanel[]): DashboardPanelMap | undefined {
|
||||
function getPanelSectionMaps(
|
||||
panels?: Array<DashboardPanel | DashboardSection>
|
||||
): { panels: DashboardPanelMap; sections: DashboardSectionMap } | undefined {
|
||||
if (!panels) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (panels.length === 0) {
|
||||
return {};
|
||||
return { panels: {}, sections: {} };
|
||||
}
|
||||
|
||||
if (isPanelVersionTooOld(panels)) {
|
||||
|
@ -71,7 +84,7 @@ function getPanelsMap(panels?: DashboardPanel[]): DashboardPanelMap | undefined
|
|||
return panel;
|
||||
});
|
||||
|
||||
return convertPanelsArrayToPanelMap(standardizedPanels);
|
||||
return convertPanelsArrayToPanelSectionMaps(standardizedPanels);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,8 +98,7 @@ export const loadAndRemoveDashboardState = (
|
|||
);
|
||||
|
||||
if (!rawAppStateInUrl) return {};
|
||||
|
||||
const panelsMap = getPanelsMap(rawAppStateInUrl.panels);
|
||||
const converted = getPanelSectionMaps(rawAppStateInUrl.panels);
|
||||
|
||||
const nextUrl = replaceUrlHashQuery(window.location.href, (hashQuery) => {
|
||||
delete hashQuery[DASHBOARD_STATE_STORAGE_KEY];
|
||||
|
@ -100,7 +112,8 @@ export const loadAndRemoveDashboardState = (
|
|||
controlGroupInput: serializeRuntimeState(rawAppStateInUrl.controlGroupState).rawState,
|
||||
}
|
||||
: {}),
|
||||
...(panelsMap ? { panels: panelsMap } : {}),
|
||||
...(converted?.panels ? { panels: converted.panels } : {}),
|
||||
...(converted?.sections ? { sections: converted.sections } : {}),
|
||||
...(rawAppStateInUrl.query ? { query: migrateLegacyQuery(rawAppStateInUrl.query) } : {}),
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
.dshEmptyPromptParent {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dshEmptyPromptPageTemplate {
|
||||
|
|
|
@ -17,3 +17,4 @@ export { ExportCSVAction } from '../dashboard_actions/export_csv_action';
|
|||
export { AddToLibraryAction } from '../dashboard_actions/library_add_action';
|
||||
export { UnlinkFromLibraryAction } from '../dashboard_actions/library_unlink_action';
|
||||
export { CopyToDashboardAction } from '../dashboard_actions/copy_to_dashboard_action';
|
||||
export { AddSectionAction } from '../dashboard_actions/add_section_action';
|
||||
|
|
|
@ -8,19 +8,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiThemeProvider } from '@elastic/eui';
|
||||
|
||||
import { EuiThemeProvider } from '@elastic/eui';
|
||||
import { useBatchedPublishingSubjects as mockUseBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { DashboardPanelMap } from '../../../common';
|
||||
import { RenderResult, act, getByLabelText, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { DashboardPanelMap, DashboardSectionMap } from '../../../common';
|
||||
import {
|
||||
DashboardContext,
|
||||
useDashboardApi as mockUseDashboardApi,
|
||||
} from '../../dashboard_api/use_dashboard_api';
|
||||
import { DashboardInternalContext } from '../../dashboard_api/use_dashboard_internal_api';
|
||||
import { buildMockDashboardApi } from '../../mocks';
|
||||
import { buildMockDashboardApi, getMockDashboardPanels } from '../../mocks';
|
||||
import { DashboardGrid } from './dashboard_grid';
|
||||
import type { Props as DashboardGridItemProps } from './dashboard_grid_item';
|
||||
import { RenderResult, act, render, waitFor } from '@testing-library/react';
|
||||
|
||||
jest.mock('./dashboard_grid_item', () => {
|
||||
return {
|
||||
|
@ -56,19 +58,6 @@ jest.mock('./dashboard_grid_item', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const PANELS = {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
'2': {
|
||||
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
};
|
||||
|
||||
const verifyElementHasClass = (
|
||||
component: RenderResult,
|
||||
elementSelector: string,
|
||||
|
@ -79,10 +68,16 @@ const verifyElementHasClass = (
|
|||
expect(itemToCheck!.classList.contains(className)).toBe(true);
|
||||
};
|
||||
|
||||
const createAndMountDashboardGrid = async (panels: DashboardPanelMap = PANELS) => {
|
||||
const createAndMountDashboardGrid = async (overrides?: {
|
||||
panels?: DashboardPanelMap;
|
||||
sections?: DashboardSectionMap;
|
||||
}) => {
|
||||
const panels = overrides?.panels ?? getMockDashboardPanels().panels;
|
||||
const sections = overrides?.sections;
|
||||
const { api, internalApi } = buildMockDashboardApi({
|
||||
overrides: {
|
||||
panels,
|
||||
...(sections && { sections }),
|
||||
},
|
||||
});
|
||||
const component = render(
|
||||
|
@ -95,35 +90,45 @@ const createAndMountDashboardGrid = async (panels: DashboardPanelMap = PANELS) =
|
|||
</EuiThemeProvider>
|
||||
);
|
||||
|
||||
// panels in collapsed sections should not render
|
||||
const panelRenderCount = sections
|
||||
? Object.values(panels).filter((value) => {
|
||||
const sectionId = value.gridData.sectionId;
|
||||
return sectionId ? !sections[sectionId].collapsed : true;
|
||||
}).length
|
||||
: Object.keys(panels).length;
|
||||
|
||||
// wait for first render
|
||||
await waitFor(() => {
|
||||
expect(component.queryAllByTestId('dashboardGridItem').length).toBe(Object.keys(panels).length);
|
||||
expect(component.queryAllByTestId('dashboardGridItem').length).toBe(panelRenderCount);
|
||||
});
|
||||
|
||||
return { dashboardApi: api, component };
|
||||
return { dashboardApi: api, internalApi, component };
|
||||
};
|
||||
|
||||
test('renders DashboardGrid', async () => {
|
||||
await createAndMountDashboardGrid(PANELS);
|
||||
describe('DashboardGrid', () => {
|
||||
test('renders', async () => {
|
||||
await createAndMountDashboardGrid();
|
||||
});
|
||||
|
||||
test('renders DashboardGrid with no visualizations', async () => {
|
||||
await createAndMountDashboardGrid({});
|
||||
describe('panels', () => {
|
||||
test('renders with no visualizations', async () => {
|
||||
await createAndMountDashboardGrid();
|
||||
});
|
||||
|
||||
test('DashboardGrid removes panel when removed from container', async () => {
|
||||
const { dashboardApi, component } = await createAndMountDashboardGrid(PANELS);
|
||||
test('removes panel when removed from container', async () => {
|
||||
const { dashboardApi, component } = await createAndMountDashboardGrid();
|
||||
|
||||
// remove panel
|
||||
await act(async () => {
|
||||
dashboardApi.removePanel('1');
|
||||
dashboardApi.removePanel('2');
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
});
|
||||
|
||||
expect(component.getAllByTestId('dashboardGridItem').length).toBe(1);
|
||||
});
|
||||
|
||||
test('DashboardGrid renders expanded panel', async () => {
|
||||
test('renders expanded panel', async () => {
|
||||
const { dashboardApi, component } = await createAndMountDashboardGrid();
|
||||
|
||||
// maximize panel
|
||||
|
@ -148,7 +153,7 @@ test('DashboardGrid renders expanded panel', async () => {
|
|||
verifyElementHasClass(component, '#mockDashboardGridItem_2', 'regularPanel');
|
||||
});
|
||||
|
||||
test('DashboardGrid renders focused panel', async () => {
|
||||
test('renders focused panel', async () => {
|
||||
const { dashboardApi, component } = await createAndMountDashboardGrid();
|
||||
const overlayMock = {
|
||||
onClose: new Promise<void>((resolve) => {
|
||||
|
@ -176,3 +181,134 @@ test('DashboardGrid renders focused panel', async () => {
|
|||
verifyElementHasClass(component, '#mockDashboardGridItem_1', 'regularPanel');
|
||||
verifyElementHasClass(component, '#mockDashboardGridItem_2', 'regularPanel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sections', () => {
|
||||
test('renders sections', async () => {
|
||||
const { panels, sections } = getMockDashboardPanels(true);
|
||||
await createAndMountDashboardGrid({
|
||||
panels,
|
||||
sections,
|
||||
});
|
||||
|
||||
const header1 = screen.getByTestId('kbnGridSectionHeader-section1');
|
||||
expect(header1).toBeInTheDocument();
|
||||
expect(header1.classList).toContain('kbnGridSectionHeader--collapsed');
|
||||
const header2 = screen.getByTestId('kbnGridSectionHeader-section2');
|
||||
expect(header2).toBeInTheDocument();
|
||||
expect(header2.classList).not.toContain('kbnGridSectionHeader--collapsed');
|
||||
});
|
||||
|
||||
test('can add new section', async () => {
|
||||
const { panels, sections } = getMockDashboardPanels(true);
|
||||
const { dashboardApi, internalApi } = await createAndMountDashboardGrid({
|
||||
panels,
|
||||
sections,
|
||||
});
|
||||
dashboardApi.addNewSection();
|
||||
await waitFor(() => {
|
||||
const headers = screen.getAllByLabelText('Edit section title'); // aria-label
|
||||
expect(headers.length).toEqual(3);
|
||||
});
|
||||
|
||||
const newHeader = Object.values(internalApi.layout$.getValue().sections).filter(
|
||||
({ gridData: { y } }) => y === 8
|
||||
)[0];
|
||||
|
||||
expect(newHeader.title).toEqual('New collapsible section');
|
||||
expect(screen.getByText(newHeader.title)).toBeInTheDocument();
|
||||
expect(newHeader.collapsed).toBe(false);
|
||||
expect(screen.getByTestId(`kbnGridSectionHeader-${newHeader.id}`).classList).not.toContain(
|
||||
'kbnGridSectionHeader--collapsed'
|
||||
);
|
||||
});
|
||||
|
||||
test('dashboard state updates on collapse', async () => {
|
||||
const { panels, sections } = getMockDashboardPanels(true);
|
||||
const { internalApi } = await createAndMountDashboardGrid({
|
||||
panels,
|
||||
sections,
|
||||
});
|
||||
|
||||
const headerButton = screen.getByTestId(`kbnGridSectionTitle-section2`);
|
||||
expect(headerButton.nodeName.toLowerCase()).toBe('button');
|
||||
userEvent.click(headerButton);
|
||||
await waitFor(() => {
|
||||
expect(internalApi.layout$.getValue().sections.section2.collapsed).toBe(true);
|
||||
});
|
||||
expect(headerButton.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
test('dashboard state updates on section deletion', async () => {
|
||||
const { panels, sections } = getMockDashboardPanels(true, {
|
||||
sections: {
|
||||
emptySection: {
|
||||
id: 'emptySection',
|
||||
title: 'Empty section',
|
||||
collapsed: false,
|
||||
gridData: { i: 'emptySection', y: 8 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { internalApi } = await createAndMountDashboardGrid({
|
||||
panels,
|
||||
sections,
|
||||
});
|
||||
|
||||
// can delete empty section
|
||||
const deleteEmptySectionButton = getByLabelText(
|
||||
screen.getByTestId('kbnGridSectionHeader-emptySection'),
|
||||
'Delete section'
|
||||
);
|
||||
await act(async () => {
|
||||
await userEvent.click(deleteEmptySectionButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(Object.keys(internalApi.layout$.getValue().sections)).not.toContain('emptySection');
|
||||
});
|
||||
|
||||
// can delete non-empty section
|
||||
const deleteSection1Button = getByLabelText(
|
||||
screen.getByTestId('kbnGridSectionHeader-section1'),
|
||||
'Delete section'
|
||||
);
|
||||
await userEvent.click(deleteSection1Button);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('kbnGridLayoutDeleteSectionModal-section1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmDeleteButton = screen.getByText('Delete section and 1 panel');
|
||||
await userEvent.click(confirmDeleteButton);
|
||||
await waitFor(() => {
|
||||
expect(Object.keys(internalApi.layout$.getValue().sections)).not.toContain('section1');
|
||||
expect(Object.keys(internalApi.layout$.getValue().panels)).not.toContain('3'); // this is the panel in section1
|
||||
});
|
||||
});
|
||||
|
||||
test('layout responds to dashboard state update', async () => {
|
||||
const withoutSections = getMockDashboardPanels();
|
||||
const withSections = getMockDashboardPanels(true);
|
||||
|
||||
const { internalApi } = await createAndMountDashboardGrid({
|
||||
panels: withoutSections.panels,
|
||||
sections: {},
|
||||
});
|
||||
|
||||
let sectionContainers = screen.getAllByTestId(`kbnGridSectionWrapper-`, {
|
||||
exact: false,
|
||||
});
|
||||
expect(sectionContainers.length).toBe(1); // only the first top section is rendered
|
||||
|
||||
internalApi.layout$.next(withSections);
|
||||
|
||||
await waitFor(() => {
|
||||
sectionContainers = screen.getAllByTestId(`kbnGridSectionWrapper-`, {
|
||||
exact: false,
|
||||
});
|
||||
expect(sectionContainers.length).toBe(2); // section wrappers are not rendered for collapsed sections
|
||||
expect(screen.getAllByTestId('dashboardGridItem').length).toBe(3); // one panel is in a collapsed section
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,19 +10,20 @@
|
|||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useAppFixedViewport } from '@kbn/core-rendering-browser';
|
||||
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
|
||||
import { GridLayout, GridPanelData, GridSectionData, type GridLayoutData } from '@kbn/grid-layout';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { default as React, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../common/content_management/constants';
|
||||
import { arePanelLayoutsEqual } from '../../dashboard_api/are_panel_layouts_equal';
|
||||
import { GridData } from '../../../common/content_management/v2/types';
|
||||
import { areLayoutsEqual } from '../../dashboard_api/are_layouts_equal';
|
||||
import { DashboardLayout } from '../../dashboard_api/types';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
import { useDashboardInternalApi } from '../../dashboard_api/use_dashboard_internal_api';
|
||||
import {
|
||||
DEFAULT_DASHBOARD_DRAG_TOP_OFFSET,
|
||||
DASHBOARD_GRID_HEIGHT,
|
||||
DASHBOARD_MARGIN_SIZE,
|
||||
DEFAULT_DASHBOARD_DRAG_TOP_OFFSET,
|
||||
} from './constants';
|
||||
import { DashboardGridItem } from './dashboard_grid_item';
|
||||
import { useLayoutStyles } from './use_layout_styles';
|
||||
|
@ -34,11 +35,13 @@ export const DashboardGrid = ({
|
|||
}) => {
|
||||
const dashboardApi = useDashboardApi();
|
||||
const dashboardInternalApi = useDashboardInternalApi();
|
||||
const layoutRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const layoutStyles = useLayoutStyles();
|
||||
const panelRefs = useRef<{ [panelId: string]: React.Ref<HTMLDivElement> }>({});
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [topOffset, setTopOffset] = useState(DEFAULT_DASHBOARD_DRAG_TOP_OFFSET);
|
||||
const [expandedPanelId, layout, useMargins, viewMode] = useBatchedPublishingSubjects(
|
||||
dashboardApi.expandedPanelId$,
|
||||
dashboardInternalApi.layout$,
|
||||
|
@ -46,57 +49,93 @@ export const DashboardGrid = ({
|
|||
dashboardApi.viewMode$
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTopOffset(
|
||||
dashboardContainerRef?.current?.getBoundingClientRect().top ??
|
||||
DEFAULT_DASHBOARD_DRAG_TOP_OFFSET
|
||||
);
|
||||
}, [dashboardContainerRef]);
|
||||
|
||||
const appFixedViewport = useAppFixedViewport();
|
||||
|
||||
const currentLayout: GridLayoutData = useMemo(() => {
|
||||
const singleRow: GridLayoutData = {};
|
||||
|
||||
Object.keys(layout).forEach((panelId) => {
|
||||
const gridData = layout[panelId].gridData;
|
||||
singleRow[panelId] = {
|
||||
const newLayout: GridLayoutData = {};
|
||||
Object.keys(layout.sections).forEach((sectionId) => {
|
||||
const section = layout.sections[sectionId];
|
||||
newLayout[sectionId] = {
|
||||
id: sectionId,
|
||||
type: 'section',
|
||||
row: section.gridData.y,
|
||||
isCollapsed: Boolean(section.collapsed),
|
||||
title: section.title,
|
||||
panels: {},
|
||||
};
|
||||
});
|
||||
Object.keys(layout.panels).forEach((panelId) => {
|
||||
const gridData = layout.panels[panelId].gridData;
|
||||
const basePanel = {
|
||||
id: panelId,
|
||||
row: gridData.y,
|
||||
column: gridData.x,
|
||||
width: gridData.w,
|
||||
height: gridData.h,
|
||||
} as GridPanelData;
|
||||
if (gridData.sectionId) {
|
||||
(newLayout[gridData.sectionId] as GridSectionData).panels[panelId] = basePanel;
|
||||
} else {
|
||||
newLayout[panelId] = {
|
||||
...basePanel,
|
||||
type: 'panel',
|
||||
};
|
||||
}
|
||||
// update `data-grid-row` attribute for all panels because it is used for some styling
|
||||
const panelRef = panelRefs.current[panelId];
|
||||
if (typeof panelRef !== 'function' && panelRef?.current) {
|
||||
panelRef.current.setAttribute('data-grid-row', `${gridData.y}`);
|
||||
}
|
||||
});
|
||||
|
||||
return singleRow;
|
||||
return newLayout;
|
||||
}, [layout]);
|
||||
|
||||
const onLayoutChange = useCallback(
|
||||
(newLayout: GridLayoutData) => {
|
||||
if (viewMode !== 'edit') return;
|
||||
|
||||
const currentPanels = dashboardInternalApi.layout$.getValue();
|
||||
const updatedPanels: DashboardLayout = Object.values(newLayout).reduce(
|
||||
(updatedPanelsAcc, widget) => {
|
||||
const currLayout = dashboardInternalApi.layout$.getValue();
|
||||
const updatedLayout: DashboardLayout = {
|
||||
sections: {},
|
||||
panels: {},
|
||||
};
|
||||
Object.values(newLayout).forEach((widget) => {
|
||||
if (widget.type === 'section') {
|
||||
return updatedPanelsAcc; // sections currently aren't supported
|
||||
}
|
||||
updatedPanelsAcc[widget.id] = {
|
||||
...currentPanels[widget.id],
|
||||
updatedLayout.sections[widget.id] = {
|
||||
collapsed: widget.isCollapsed,
|
||||
title: widget.title,
|
||||
id: widget.id,
|
||||
gridData: {
|
||||
i: widget.id,
|
||||
y: widget.row,
|
||||
x: widget.column,
|
||||
w: widget.width,
|
||||
h: widget.height,
|
||||
},
|
||||
};
|
||||
return updatedPanelsAcc;
|
||||
Object.values(widget.panels).forEach((panel) => {
|
||||
updatedLayout.panels[panel.id] = {
|
||||
...currLayout.panels[panel.id],
|
||||
gridData: {
|
||||
...convertGridPanelToDashboardGridData(panel),
|
||||
sectionId: widget.id,
|
||||
},
|
||||
{} as DashboardLayout
|
||||
);
|
||||
if (!arePanelLayoutsEqual(currentPanels, updatedPanels)) {
|
||||
dashboardInternalApi.layout$.next(updatedPanels);
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// widget is a panel
|
||||
updatedLayout.panels[widget.id] = {
|
||||
...currLayout.panels[widget.id],
|
||||
gridData: convertGridPanelToDashboardGridData(widget),
|
||||
};
|
||||
}
|
||||
});
|
||||
if (!areLayoutsEqual(currLayout, updatedLayout)) {
|
||||
dashboardInternalApi.layout$.next(updatedLayout);
|
||||
}
|
||||
},
|
||||
[dashboardInternalApi.layout$, viewMode]
|
||||
|
@ -104,13 +143,14 @@ export const DashboardGrid = ({
|
|||
|
||||
const renderPanelContents = useCallback(
|
||||
(id: string, setDragHandles: (refs: Array<HTMLElement | null>) => void) => {
|
||||
const currentPanels = dashboardInternalApi.layout$.getValue();
|
||||
if (!currentPanels[id]) return;
|
||||
const panels = dashboardInternalApi.layout$.getValue().panels;
|
||||
if (!panels[id]) return;
|
||||
|
||||
if (!panelRefs.current[id]) {
|
||||
panelRefs.current[id] = React.createRef();
|
||||
}
|
||||
const type = currentPanels[id].type;
|
||||
|
||||
const type = panels[id].type;
|
||||
return (
|
||||
<DashboardGridItem
|
||||
ref={panelRefs.current[id]}
|
||||
|
@ -120,13 +160,45 @@ export const DashboardGrid = ({
|
|||
setDragHandles={setDragHandles}
|
||||
appFixedViewport={appFixedViewport}
|
||||
dashboardContainerRef={dashboardContainerRef}
|
||||
data-grid-row={currentPanels[id].gridData.y} // initialize data-grid-row
|
||||
data-grid-row={panels[id].gridData.y} // initialize data-grid-row
|
||||
/>
|
||||
);
|
||||
},
|
||||
[appFixedViewport, dashboardContainerRef, dashboardInternalApi.layout$]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* ResizeObserver fires the callback on `.observe()` with the initial size of the observed
|
||||
* element; we want to ignore this first call and scroll to the bottom on the **second**
|
||||
* callback - i.e. after the row is actually added to the DOM
|
||||
*/
|
||||
let first = false;
|
||||
const scrollToBottomOnResize = new ResizeObserver(() => {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
dashboardApi.scrollToBottom();
|
||||
scrollToBottomOnResize.disconnect(); // once scrolled, stop observing resize events
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* When `scrollToBottom$` emits, wait for the `layoutRef` size to change then scroll
|
||||
* to the bottom of the screen
|
||||
*/
|
||||
const scrollToBottomSubscription = dashboardApi.scrollToBottom$.subscribe(() => {
|
||||
if (!layoutRef.current) return;
|
||||
first = true; // ensure that only the second resize callback actually triggers scrolling
|
||||
scrollToBottomOnResize.observe(layoutRef.current);
|
||||
});
|
||||
|
||||
return () => {
|
||||
scrollToBottomOnResize.disconnect();
|
||||
scrollToBottomSubscription.unsubscribe();
|
||||
};
|
||||
}, [dashboardApi]);
|
||||
|
||||
const memoizedgridLayout = useMemo(() => {
|
||||
// memoizing this component reduces the number of times it gets re-rendered to a minimum
|
||||
return (
|
||||
|
@ -137,9 +209,7 @@ export const DashboardGrid = ({
|
|||
gutterSize: useMargins ? DASHBOARD_MARGIN_SIZE : 0,
|
||||
rowHeight: DASHBOARD_GRID_HEIGHT,
|
||||
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
|
||||
keyboardDragTopLimit:
|
||||
dashboardContainerRef?.current?.getBoundingClientRect().top ||
|
||||
DEFAULT_DASHBOARD_DRAG_TOP_OFFSET,
|
||||
keyboardDragTopLimit: topOffset,
|
||||
}}
|
||||
useCustomDragHandle={true}
|
||||
renderPanelContents={renderPanelContents}
|
||||
|
@ -156,7 +226,7 @@ export const DashboardGrid = ({
|
|||
onLayoutChange,
|
||||
expandedPanelId,
|
||||
viewMode,
|
||||
dashboardContainerRef,
|
||||
topOffset,
|
||||
]);
|
||||
|
||||
const { dashboardClasses, dashboardStyles } = useMemo(() => {
|
||||
|
@ -183,8 +253,18 @@ export const DashboardGrid = ({
|
|||
}, [useMargins, viewMode, expandedPanelId, euiTheme.levels.toast]);
|
||||
|
||||
return (
|
||||
<div className={dashboardClasses} css={dashboardStyles}>
|
||||
<div ref={layoutRef} className={dashboardClasses} css={dashboardStyles}>
|
||||
{memoizedgridLayout}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const convertGridPanelToDashboardGridData = (panel: GridPanelData): GridData => {
|
||||
return {
|
||||
i: panel.id,
|
||||
y: panel.row,
|
||||
x: panel.column,
|
||||
w: panel.width,
|
||||
h: panel.height,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -50,7 +50,11 @@ export const useLayoutStyles = () => {
|
|||
background-origin: content-box;
|
||||
}
|
||||
|
||||
.kbnGridPanel--dragPreview {
|
||||
// styles for the area where the panel and/or section header will be dropped
|
||||
.kbnGridPanel--dragPreview,
|
||||
.kbnGridSection--dragPreview {
|
||||
border-radius: ${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium};
|
||||
|
||||
background-color: ${transparentize(euiTheme.colors.vis.euiColorVis0, 0.2)};
|
||||
}
|
||||
|
||||
|
@ -91,6 +95,50 @@ export const useLayoutStyles = () => {
|
|||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
// styling for what the grid section header looks like when being dragged
|
||||
.kbnGridSectionHeader--active {
|
||||
background-color: ${euiTheme.colors.backgroundBasePlain};
|
||||
outline: var(--dashboardActivePanelBorderStyle);
|
||||
border-radius: ${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium};
|
||||
padding-left: 8px;
|
||||
// hide accordian arrow + panel count text when row is being dragged
|
||||
& .kbnGridSectionTitle--button svg,
|
||||
& .kbnGridLayout--panelCount {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// styling for the section footer
|
||||
.kbnGridSectionFooter {
|
||||
height: ${euiTheme.size.s};
|
||||
display: block;
|
||||
border-top: ${euiTheme.border.thin};
|
||||
// highlight the footer of a targeted section to make it clear where the section ends
|
||||
&--targeted {
|
||||
border-top: ${euiTheme.border.width.thick} solid
|
||||
${transparentize(euiTheme.colors.vis.euiColorVis0, 0.5)};
|
||||
}
|
||||
}
|
||||
// hide footer border when section is being dragged
|
||||
&:has(.kbnGridSectionHeader--active) .kbnGridSectionHeader--active + .kbnGridSectionFooter {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
// apply a "fade out" effect when dragging a section header over another section, indicating that dropping is not allowed
|
||||
.kbnGridSection--blocked {
|
||||
z-index: 1;
|
||||
background-color: ${transparentize(euiTheme.colors.backgroundBaseSubdued, 0.5)};
|
||||
// the oulines of panels extend past 100% by 1px on each side, so adjust for that
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
}
|
||||
|
||||
&:has(.kbnGridSection--blocked) .kbnGridSection--dragHandle {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
`;
|
||||
}, [euiTheme]);
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
.dshDashboardViewport {
|
||||
width: 100%;
|
||||
|
||||
&.dshDashboardViewport--empty {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--panelExpanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@ import { EuiPortal } from '@elastic/eui';
|
|||
import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public';
|
||||
import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
|
||||
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { CONTROL_GROUP_EMBEDDABLE_ID } from '../../dashboard_api/control_group_manager';
|
||||
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
|
||||
import { useDashboardInternalApi } from '../../dashboard_api/use_dashboard_internal_api';
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { DashboardEmptyScreen } from './empty_screen/dashboard_empty_screen';
|
||||
import { CONTROL_GROUP_EMBEDDABLE_ID } from '../../dashboard_api/control_group_manager';
|
||||
|
||||
export const DashboardViewport = ({
|
||||
dashboardContainerRef,
|
||||
|
@ -54,12 +54,21 @@ export const DashboardViewport = ({
|
|||
dashboardApi.setFullScreenMode(false);
|
||||
}, [dashboardApi]);
|
||||
|
||||
const panelCount = useMemo(() => {
|
||||
return Object.keys(layout).length;
|
||||
}, [layout]);
|
||||
const { panelCount, visiblePanelCount, sectionCount } = useMemo(() => {
|
||||
const panels = Object.values(layout.panels);
|
||||
const visiblePanels = panels.filter(({ gridData }) => {
|
||||
return !dashboardInternalApi.isSectionCollapsed(gridData.sectionId);
|
||||
});
|
||||
return {
|
||||
panelCount: panels.length,
|
||||
visiblePanelCount: visiblePanels.length,
|
||||
sectionCount: Object.keys(layout.sections).length,
|
||||
};
|
||||
}, [layout, dashboardInternalApi]);
|
||||
|
||||
const classes = classNames({
|
||||
dshDashboardViewport: true,
|
||||
'dshDashboardViewport--empty': panelCount === 0 && sectionCount === 0,
|
||||
'dshDashboardViewport--print': viewMode === 'print',
|
||||
'dshDashboardViewport--panelExpanded': Boolean(expandedPanelId),
|
||||
});
|
||||
|
@ -124,15 +133,18 @@ export const DashboardViewport = ({
|
|||
<ExitFullScreenButton onExit={onExit} toggleChrome={!dashboardApi.isEmbeddedExternally} />
|
||||
</EuiPortal>
|
||||
)}
|
||||
{panelCount === 0 && <DashboardEmptyScreen />}
|
||||
<div
|
||||
className={classes}
|
||||
data-shared-items-container
|
||||
data-title={dashboardTitle}
|
||||
data-description={description}
|
||||
data-shared-items-count={panelCount}
|
||||
data-shared-items-count={visiblePanelCount}
|
||||
>
|
||||
{panelCount === 0 && sectionCount === 0 ? (
|
||||
<DashboardEmptyScreen />
|
||||
) : (
|
||||
<DashboardGrid dashboardContainerRef={dashboardContainerRef} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import { DashboardStart } from './plugin';
|
||||
import { DashboardState } from '../common/types';
|
||||
import { getDashboardApi } from './dashboard_api/get_dashboard_api';
|
||||
import { DashboardPanelState } from '../common/dashboard_container/types';
|
||||
import { DashboardPanelMap, DashboardSectionMap } from '../common';
|
||||
|
||||
export type Start = jest.Mocked<DashboardStart>;
|
||||
|
||||
|
@ -126,24 +126,67 @@ export function getSampleDashboardState(overrides?: Partial<DashboardState>): Da
|
|||
timeRestore: false,
|
||||
viewMode: 'view',
|
||||
panels: {},
|
||||
sections: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSampleDashboardPanel(
|
||||
overrides: Partial<DashboardPanelState> & {
|
||||
explicitInput: { id: string };
|
||||
type: string;
|
||||
export function getMockDashboardPanels(
|
||||
withSections: boolean = false,
|
||||
overrides?: {
|
||||
panels?: DashboardPanelMap;
|
||||
sections?: DashboardSectionMap;
|
||||
}
|
||||
): DashboardPanelState {
|
||||
return {
|
||||
gridData: {
|
||||
h: 15,
|
||||
w: 15,
|
||||
x: 0,
|
||||
y: 0,
|
||||
i: overrides.explicitInput.id,
|
||||
): { panels: DashboardPanelMap; sections: DashboardSectionMap } {
|
||||
const panels = {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
...overrides,
|
||||
'2': {
|
||||
gridData: { x: 6, y: 0, w: 6, h: 6, i: '2' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
...overrides?.panels,
|
||||
};
|
||||
if (!withSections) return { panels, sections: {} };
|
||||
|
||||
return {
|
||||
panels: {
|
||||
...panels,
|
||||
'3': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '3', sectionId: 'section1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '3' },
|
||||
},
|
||||
'4': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '4', sectionId: 'section2' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '4' },
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
section1: {
|
||||
id: 'section1',
|
||||
title: 'Section One',
|
||||
collapsed: true,
|
||||
gridData: {
|
||||
y: 6,
|
||||
i: 'section1',
|
||||
},
|
||||
},
|
||||
section2: {
|
||||
id: 'section2',
|
||||
title: 'Section Two',
|
||||
collapsed: false,
|
||||
gridData: {
|
||||
y: 7,
|
||||
i: 'section2',
|
||||
},
|
||||
},
|
||||
...overrides?.sections,
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { getMockDashboardPanels } from '../mocks';
|
||||
import { placeClonePanel } from './place_clone_panel_strategy';
|
||||
|
||||
describe('Clone panel placement strategies', () => {
|
||||
it('no other panels', () => {
|
||||
const currentPanels = {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
};
|
||||
const { newPanelPlacement, otherPanels } = placeClonePanel({
|
||||
width: 6,
|
||||
height: 6,
|
||||
currentPanels,
|
||||
placeBesideId: '1',
|
||||
});
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 6, // placed right beside the other panel
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(currentPanels);
|
||||
});
|
||||
|
||||
it('panel collision at desired clone location', () => {
|
||||
const { panels } = getMockDashboardPanels();
|
||||
const { newPanelPlacement, otherPanels } = placeClonePanel({
|
||||
width: 6,
|
||||
height: 6,
|
||||
currentPanels: panels,
|
||||
placeBesideId: '1',
|
||||
});
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 6, // instead of being placed beside the cloned panel, it is placed right below
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(panels);
|
||||
});
|
||||
|
||||
it('ignores panels in other sections', () => {
|
||||
const { panels } = getMockDashboardPanels(true);
|
||||
|
||||
const { newPanelPlacement, otherPanels } = placeClonePanel({
|
||||
width: 6,
|
||||
height: 6,
|
||||
currentPanels: panels,
|
||||
placeBesideId: '3',
|
||||
sectionId: 'section1',
|
||||
});
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 6, // placed beside panel 3, since is has space beside it in section1
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(panels);
|
||||
});
|
||||
});
|
|
@ -9,10 +9,11 @@
|
|||
|
||||
import { PanelNotFoundError } from '@kbn/embeddable-plugin/public';
|
||||
import { cloneDeep, forOwn } from 'lodash';
|
||||
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../common/content_management';
|
||||
import type { GridData } from '../../server/content_management';
|
||||
import { DashboardLayoutItem } from '../dashboard_api/types';
|
||||
import { PanelPlacementProps, PanelPlacementReturn } from './types';
|
||||
import { DashboardPanel } from '../dashboard_api/types';
|
||||
|
||||
interface IplacementDirection {
|
||||
grid: Omit<GridData, 'i'>;
|
||||
|
@ -42,6 +43,7 @@ function comparePanels(a: GridData, b: GridData): number {
|
|||
export function placeClonePanel({
|
||||
width,
|
||||
height,
|
||||
sectionId,
|
||||
currentPanels,
|
||||
placeBesideId,
|
||||
}: PanelPlacementProps & { placeBesideId: string }): PanelPlacementReturn {
|
||||
|
@ -51,8 +53,11 @@ export function placeClonePanel({
|
|||
}
|
||||
const beside = panelToPlaceBeside.gridData;
|
||||
const otherPanelGridData: GridData[] = [];
|
||||
forOwn(currentPanels, (panel: DashboardLayoutItem, key: string | undefined) => {
|
||||
forOwn(currentPanels, (panel: DashboardPanel) => {
|
||||
if (panel.gridData.sectionId === sectionId) {
|
||||
// only check against panels that are in the same section as the cloned panel
|
||||
otherPanelGridData.push(panel.gridData);
|
||||
}
|
||||
});
|
||||
|
||||
const possiblePlacementDirections: IplacementDirection[] = [
|
||||
|
@ -109,8 +114,11 @@ export function placeClonePanel({
|
|||
for (let j = position + 1; j < grid.length; j++) {
|
||||
originalPositionInTheGrid = grid[j].i;
|
||||
const { gridData, ...movedPanel } = cloneDeep(otherPanels[originalPositionInTheGrid]);
|
||||
if (gridData.sectionId === sectionId) {
|
||||
// only move panels in the cloned panel's section
|
||||
const newGridData = { ...gridData, y: gridData.y + diff };
|
||||
otherPanels[originalPositionInTheGrid] = { ...movedPanel, gridData: newGridData };
|
||||
}
|
||||
}
|
||||
return { newPanelPlacement: bottomPlacement.grid, otherPanels };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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 { getMockDashboardPanels } from '../mocks';
|
||||
import { PanelPlacementStrategy } from '../plugin_constants';
|
||||
import { runPanelPlacementStrategy } from './place_new_panel_strategies';
|
||||
|
||||
describe('new panel placement strategies', () => {
|
||||
describe('place at top', () => {
|
||||
it('no other panels', () => {
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.placeAtTop,
|
||||
{ width: 6, height: 6, currentPanels: {} }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual({});
|
||||
});
|
||||
|
||||
it('push other panels down', () => {
|
||||
const { panels } = getMockDashboardPanels();
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.placeAtTop,
|
||||
{ width: 6, height: 6, currentPanels: panels }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(
|
||||
Object.keys(panels).reduce((prev, panelId) => {
|
||||
const originalGridData = panels[panelId].gridData;
|
||||
return {
|
||||
...prev,
|
||||
[panelId]: {
|
||||
...panels[panelId],
|
||||
gridData: {
|
||||
...originalGridData,
|
||||
y: originalGridData.y + 6, // panel was pushed down by height of new panel
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores panels in other sections', () => {
|
||||
const { panels } = getMockDashboardPanels(true);
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.placeAtTop,
|
||||
{ width: 6, height: 6, currentPanels: panels, sectionId: 'section1' }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(
|
||||
Object.keys(panels).reduce((prev, panelId) => {
|
||||
const originalGridData = panels[panelId].gridData;
|
||||
return {
|
||||
...prev,
|
||||
[panelId]: {
|
||||
...panels[panelId],
|
||||
gridData: {
|
||||
...originalGridData,
|
||||
// only panels in the targetted section should get pushed down
|
||||
...(originalGridData.sectionId === 'section1' && {
|
||||
y: originalGridData.y + 6,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Find top left most open space', () => {
|
||||
it('no other panels', () => {
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.findTopLeftMostOpenSpace,
|
||||
{ width: 6, height: 6, currentPanels: {} }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual({});
|
||||
});
|
||||
|
||||
it('top left most space is available', () => {
|
||||
const { panels } = getMockDashboardPanels(false, {
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 6, y: 0, w: 6, h: 6, i: '1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.findTopLeftMostOpenSpace,
|
||||
{ width: 6, height: 6, currentPanels: panels }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0, // placed in the first available spot
|
||||
y: 0,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(panels); // other panels don't move with this strategy
|
||||
});
|
||||
|
||||
it('panel must be pushed down', () => {
|
||||
const { panels } = getMockDashboardPanels(true, {
|
||||
panels: {
|
||||
'5': {
|
||||
gridData: { x: 6, y: 0, w: 42, h: 6, i: '5' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.findTopLeftMostOpenSpace,
|
||||
{ width: 6, height: 6, currentPanels: panels }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 6,
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(panels); // other panels don't move with this strategy
|
||||
});
|
||||
|
||||
it('ignores panels in other sections', () => {
|
||||
const { panels } = getMockDashboardPanels(true, {
|
||||
panels: {
|
||||
'1': {
|
||||
gridData: { x: 0, y: 0, w: 6, h: 100, i: '1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
'2': {
|
||||
gridData: { x: 6, y: 6, w: 42, h: 100, i: '2' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '2' },
|
||||
},
|
||||
'6': {
|
||||
gridData: { x: 0, y: 6, w: 6, h: 6, i: '6', sectionId: 'section1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
'7': {
|
||||
gridData: { x: 6, y: 0, w: 42, h: 12, i: '7', sectionId: 'section1' },
|
||||
type: 'lens',
|
||||
explicitInput: { id: '1' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
|
||||
PanelPlacementStrategy.findTopLeftMostOpenSpace,
|
||||
{ width: 6, height: 6, currentPanels: panels, sectionId: 'section1' }
|
||||
);
|
||||
expect(newPanelPlacement).toEqual({
|
||||
x: 0,
|
||||
y: 12, // maxY is 12 for section1; maxY of 100 in section 0 is ignored
|
||||
w: 6,
|
||||
h: 6,
|
||||
});
|
||||
expect(otherPanels).toEqual(panels); // other panels don't move with this strategy
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,16 +15,19 @@ import { PanelPlacementProps, PanelPlacementReturn } from './types';
|
|||
|
||||
export const runPanelPlacementStrategy = (
|
||||
strategy: PanelPlacementStrategy,
|
||||
{ width, height, currentPanels }: PanelPlacementProps
|
||||
{ width, height, currentPanels, sectionId }: PanelPlacementProps
|
||||
): PanelPlacementReturn => {
|
||||
switch (strategy) {
|
||||
case PanelPlacementStrategy.placeAtTop:
|
||||
const otherPanels = { ...currentPanels };
|
||||
for (const [id, panel] of Object.entries(currentPanels)) {
|
||||
// only consider collisions with panels in the same section
|
||||
if (!sectionId || panel.gridData.sectionId === sectionId) {
|
||||
const { gridData, ...currentPanel } = cloneDeep(panel);
|
||||
const newGridData = { ...gridData, y: gridData.y + height };
|
||||
otherPanels[id] = { ...currentPanel, gridData: newGridData };
|
||||
}
|
||||
}
|
||||
return {
|
||||
newPanelPlacement: { x: 0, y: 0, w: width, h: height },
|
||||
otherPanels,
|
||||
|
@ -35,7 +38,10 @@ export const runPanelPlacementStrategy = (
|
|||
|
||||
const currentPanelsArray = Object.values(currentPanels);
|
||||
currentPanelsArray.forEach((panel) => {
|
||||
// only consider panels in the same section when calculating maxY
|
||||
if (panel.gridData.sectionId === sectionId) {
|
||||
maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle case of empty grid.
|
||||
|
@ -52,6 +58,7 @@ export const runPanelPlacementStrategy = (
|
|||
}
|
||||
|
||||
currentPanelsArray.forEach((panel) => {
|
||||
if (panel.gridData.sectionId === sectionId) {
|
||||
for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
|
||||
for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
|
||||
const row = grid[y];
|
||||
|
@ -65,6 +72,7 @@ export const runPanelPlacementStrategy = (
|
|||
grid[y][x] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (let y = 0; y < maxY; y++) {
|
||||
|
|
|
@ -21,13 +21,14 @@ export interface PanelPlacementSettings {
|
|||
|
||||
export interface PanelPlacementReturn {
|
||||
newPanelPlacement: Omit<GridData, 'i'>;
|
||||
otherPanels: DashboardLayout;
|
||||
otherPanels: DashboardLayout['panels'];
|
||||
}
|
||||
|
||||
export interface PanelPlacementProps {
|
||||
width: number;
|
||||
height: number;
|
||||
currentPanels: DashboardLayout;
|
||||
currentPanels: DashboardLayout['panels'];
|
||||
sectionId?: string; // section where panel is being placed
|
||||
}
|
||||
|
||||
export type GetPanelPlacementSettings<SerializedState extends object = object> = (
|
||||
|
|
|
@ -7,18 +7,18 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { has } from 'lodash';
|
||||
import { injectSearchSourceReferences } from '@kbn/data-plugin/public';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import { has } from 'lodash';
|
||||
|
||||
import { cleanFiltersForSerialize } from '../../../utils/clean_filters_for_serialize';
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { convertPanelsArrayToPanelMap } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import { injectReferences } from '../../../../common/dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
import { convertPanelsArrayToPanelSectionMaps } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants';
|
||||
import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state';
|
||||
import { cleanFiltersForSerialize } from '../../../utils/clean_filters_for_serialize';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants';
|
||||
import {
|
||||
contentManagementService,
|
||||
dataService,
|
||||
|
@ -73,6 +73,7 @@ export const loadDashboardState = async ({
|
|||
let resolveMeta: DashboardGetOut['meta'];
|
||||
|
||||
const cachedDashboard = dashboardContentManagementCache.fetchDashboard(id);
|
||||
|
||||
if (cachedDashboard) {
|
||||
/** If the dashboard exists in the cache, use the cached version to load the dashboard */
|
||||
({ item: rawDashboardContent, meta: resolveMeta } = cachedDashboard);
|
||||
|
@ -149,7 +150,6 @@ export const loadDashboardState = async ({
|
|||
const query = migrateLegacyQuery(
|
||||
searchSource?.getOwnField('query') || queryString.getDefaultQuery() // TODO SAVED DASHBOARDS determine if migrateLegacyQuery is still needed
|
||||
);
|
||||
|
||||
const {
|
||||
refreshInterval,
|
||||
description,
|
||||
|
@ -170,7 +170,9 @@ export const loadDashboardState = async ({
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const panelMap = convertPanelsArrayToPanelMap(panels ?? []);
|
||||
const { panels: panelMap, sections: sectionsMap } = convertPanelsArrayToPanelSectionMaps(
|
||||
panels ?? []
|
||||
);
|
||||
|
||||
return {
|
||||
managed,
|
||||
|
@ -187,6 +189,7 @@ export const loadDashboardState = async ({
|
|||
panels: panelMap,
|
||||
query,
|
||||
title,
|
||||
sections: sectionsMap,
|
||||
|
||||
viewMode: 'view', // dashboards loaded from saved object default to view mode. If it was edited recently, the view mode from session storage will override this.
|
||||
tags:
|
||||
|
|
|
@ -11,6 +11,7 @@ export type {
|
|||
ControlGroupAttributes,
|
||||
GridData,
|
||||
DashboardPanel,
|
||||
DashboardSection,
|
||||
DashboardAttributes,
|
||||
DashboardItem,
|
||||
DashboardGetIn,
|
||||
|
|
|
@ -229,7 +229,16 @@ const searchSourceSchema = schema.object(
|
|||
{ defaultValue: {}, unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const gridDataSchema = schema.object({
|
||||
const sectionGridDataSchema = schema.object({
|
||||
y: schema.number({ meta: { description: 'The y coordinate of the section in grid units' } }),
|
||||
i: schema.maybe(
|
||||
schema.string({
|
||||
meta: { description: 'The unique identifier of the section' },
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const panelGridDataSchema = schema.object({
|
||||
x: schema.number({ meta: { description: 'The x coordinate of the panel in grid units' } }),
|
||||
y: schema.number({ meta: { description: 'The y coordinate of the panel in grid units' } }),
|
||||
w: schema.number({
|
||||
|
@ -284,7 +293,7 @@ export const panelSchema = schema.object({
|
|||
),
|
||||
type: schema.string({ meta: { description: 'The embeddable type' } }),
|
||||
panelRefName: schema.maybe(schema.string()),
|
||||
gridData: gridDataSchema,
|
||||
gridData: panelGridDataSchema,
|
||||
panelIndex: schema.maybe(
|
||||
schema.string({
|
||||
meta: { description: 'The unique ID of the panel.' },
|
||||
|
@ -302,6 +311,23 @@ export const panelSchema = schema.object({
|
|||
),
|
||||
});
|
||||
|
||||
export const sectionSchema = schema.object({
|
||||
title: schema.string({
|
||||
meta: { description: 'The title of the section.' },
|
||||
}),
|
||||
collapsed: schema.maybe(
|
||||
schema.boolean({
|
||||
meta: { description: 'The collapsed state of the section.' },
|
||||
defaultValue: false,
|
||||
})
|
||||
),
|
||||
gridData: sectionGridDataSchema,
|
||||
panels: schema.arrayOf(panelSchema, {
|
||||
meta: { description: 'The panels that belong to the section.' },
|
||||
defaultValue: [],
|
||||
}),
|
||||
});
|
||||
|
||||
export const optionsSchema = schema.object({
|
||||
hidePanelTitles: schema.boolean({
|
||||
defaultValue: DEFAULT_DASHBOARD_OPTIONS.hidePanelTitles,
|
||||
|
@ -402,7 +428,7 @@ export const dashboardAttributesSchema = searchResultsAttributesSchema.extends({
|
|||
|
||||
// Dashboard Content
|
||||
controlGroupInput: schema.maybe(controlGroupInputSchema),
|
||||
panels: schema.arrayOf(panelSchema, { defaultValue: [] }),
|
||||
panels: schema.arrayOf(schema.oneOf([panelSchema, sectionSchema]), { defaultValue: [] }),
|
||||
options: optionsSchema,
|
||||
version: schema.maybe(schema.number({ meta: { deprecated: true } })),
|
||||
});
|
||||
|
@ -417,14 +443,29 @@ export const referenceSchema = schema.object(
|
|||
);
|
||||
|
||||
const dashboardAttributesSchemaResponse = dashboardAttributesSchema.extends({
|
||||
// Responses always include the panel index (for panels) and gridData.i (for panels + sections)
|
||||
panels: schema.arrayOf(
|
||||
schema.oneOf([
|
||||
panelSchema.extends({
|
||||
// Responses always include the panel index and gridData.i
|
||||
panelIndex: schema.string(),
|
||||
gridData: gridDataSchema.extends({
|
||||
gridData: panelGridDataSchema.extends({
|
||||
i: schema.string(),
|
||||
}),
|
||||
}),
|
||||
sectionSchema.extends({
|
||||
gridData: sectionGridDataSchema.extends({
|
||||
i: schema.string(),
|
||||
}),
|
||||
panels: schema.arrayOf(
|
||||
panelSchema.extends({
|
||||
panelIndex: schema.string(),
|
||||
gridData: panelGridDataSchema.extends({
|
||||
i: schema.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
]),
|
||||
{ defaultValue: [] }
|
||||
),
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ export type {
|
|||
ControlGroupAttributes,
|
||||
GridData,
|
||||
DashboardPanel,
|
||||
DashboardSection,
|
||||
DashboardAttributes,
|
||||
DashboardItem,
|
||||
DashboardGetIn,
|
||||
|
|
|
@ -10,21 +10,11 @@
|
|||
import { pick } from 'lodash';
|
||||
|
||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardGetOut,
|
||||
DashboardItem,
|
||||
ItemAttrsToSavedObjectParams,
|
||||
ItemAttrsToSavedObjectReturn,
|
||||
ItemAttrsToSavedObjectWithTagsParams,
|
||||
PartialDashboardItem,
|
||||
SavedObjectToItemReturn,
|
||||
} from './types';
|
||||
import type { DashboardSavedObjectAttributes } from '../../dashboard_saved_object';
|
||||
import type {
|
||||
ControlGroupAttributes as ControlGroupAttributesV2,
|
||||
DashboardCrudTypes as DashboardCrudTypesV2,
|
||||
} from '../../../common/content_management/v2';
|
||||
import type { DashboardSavedObjectAttributes } from '../../dashboard_saved_object';
|
||||
import {
|
||||
transformControlGroupIn,
|
||||
transformControlGroupOut,
|
||||
|
@ -34,6 +24,16 @@ import {
|
|||
transformSearchSourceIn,
|
||||
transformSearchSourceOut,
|
||||
} from './transforms';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardGetOut,
|
||||
DashboardItem,
|
||||
ItemAttrsToSavedObjectParams,
|
||||
ItemAttrsToSavedObjectReturn,
|
||||
ItemAttrsToSavedObjectWithTagsParams,
|
||||
PartialDashboardItem,
|
||||
SavedObjectToItemReturn,
|
||||
} from './types';
|
||||
|
||||
export function dashboardAttributesOut(
|
||||
attributes: DashboardSavedObjectAttributes | Partial<DashboardSavedObjectAttributes>,
|
||||
|
@ -46,6 +46,7 @@ export function dashboardAttributesOut(
|
|||
kibanaSavedObjectMeta,
|
||||
optionsJSON,
|
||||
panelsJSON,
|
||||
sections,
|
||||
refreshInterval,
|
||||
timeFrom,
|
||||
timeRestore,
|
||||
|
@ -53,7 +54,6 @@ export function dashboardAttributesOut(
|
|||
title,
|
||||
version,
|
||||
} = attributes;
|
||||
|
||||
// Inject any tag names from references into the attributes
|
||||
let tags: string[] | undefined;
|
||||
if (getTagNamesFromReferences && references && references.length) {
|
||||
|
@ -68,7 +68,7 @@ export function dashboardAttributesOut(
|
|||
kibanaSavedObjectMeta: transformSearchSourceOut(kibanaSavedObjectMeta),
|
||||
}),
|
||||
...(optionsJSON && { options: transformOptionsOut(optionsJSON) }),
|
||||
...(panelsJSON && { panels: transformPanelsOut(panelsJSON) }),
|
||||
...((panelsJSON || sections) && { panels: transformPanelsOut(panelsJSON, sections) }),
|
||||
...(refreshInterval && {
|
||||
refreshInterval: { pause: refreshInterval.pause, value: refreshInterval.value },
|
||||
}),
|
||||
|
@ -107,7 +107,7 @@ export const getResultV3ToV2 = (result: DashboardGetOut): DashboardCrudTypesV2['
|
|||
kibanaSavedObjectMeta: transformSearchSourceIn(kibanaSavedObjectMeta),
|
||||
}),
|
||||
...(options && { optionsJSON: JSON.stringify(options) }),
|
||||
panelsJSON: panels ? transformPanelsIn(panels) : '[]',
|
||||
panelsJSON: panels ? transformPanelsIn(panels, true).panelsJSON : '[]',
|
||||
refreshInterval,
|
||||
...(timeFrom && { timeFrom }),
|
||||
timeRestore,
|
||||
|
@ -130,6 +130,8 @@ export const itemAttrsToSavedObject = ({
|
|||
}: ItemAttrsToSavedObjectParams): ItemAttrsToSavedObjectReturn => {
|
||||
try {
|
||||
const { controlGroupInput, kibanaSavedObjectMeta, options, panels, tags, ...rest } = attributes;
|
||||
const { panelsJSON, sections } = transformPanelsIn(panels);
|
||||
|
||||
const soAttributes = {
|
||||
...rest,
|
||||
...(controlGroupInput && {
|
||||
|
@ -139,8 +141,9 @@ export const itemAttrsToSavedObject = ({
|
|||
optionsJSON: JSON.stringify(options),
|
||||
}),
|
||||
...(panels && {
|
||||
panelsJSON: transformPanelsIn(panels),
|
||||
panelsJSON,
|
||||
}),
|
||||
...(sections?.length && { sections }),
|
||||
...(kibanaSavedObjectMeta && {
|
||||
kibanaSavedObjectMeta: transformSearchSourceIn(kibanaSavedObjectMeta),
|
||||
}),
|
||||
|
@ -217,7 +220,6 @@ export function savedObjectToItem(
|
|||
version,
|
||||
managed,
|
||||
} = savedObject;
|
||||
|
||||
try {
|
||||
const attributesOut = allowedAttributes
|
||||
? pick(
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('transformPanelsIn', () => {
|
|||
},
|
||||
];
|
||||
const result = transformPanelsIn(panels as DashboardPanel[]);
|
||||
expect(result).toEqual(
|
||||
expect(result.panelsJSON).toEqual(
|
||||
JSON.stringify([
|
||||
{
|
||||
type: 'foo',
|
||||
|
|
|
@ -9,13 +9,46 @@
|
|||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object';
|
||||
import { DashboardAttributes } from '../../types';
|
||||
import { isDashboardSection } from '../../../../../common/lib/dashboard_panel_converters';
|
||||
import {
|
||||
DashboardSavedObjectAttributes,
|
||||
SavedDashboardPanel,
|
||||
SavedDashboardSection,
|
||||
} from '../../../../dashboard_saved_object';
|
||||
import { DashboardAttributes, DashboardPanel, DashboardSection } from '../../types';
|
||||
|
||||
export function transformPanelsIn(
|
||||
panels: DashboardAttributes['panels']
|
||||
): DashboardSavedObjectAttributes['panelsJSON'] {
|
||||
const updatedPanels = panels.map(({ panelIndex, gridData, panelConfig, ...restPanel }) => {
|
||||
widgets: DashboardAttributes['panels'] | undefined,
|
||||
dropSections: boolean = false
|
||||
): {
|
||||
panelsJSON: DashboardSavedObjectAttributes['panelsJSON'];
|
||||
sections: DashboardSavedObjectAttributes['sections'];
|
||||
} {
|
||||
const panels: SavedDashboardPanel[] = [];
|
||||
const sections: SavedDashboardSection[] = [];
|
||||
|
||||
widgets?.forEach((widget) => {
|
||||
if (isDashboardSection(widget)) {
|
||||
const { panels: sectionPanels, gridData, ...restOfSection } = widget as DashboardSection;
|
||||
const idx = gridData.i ?? uuidv4();
|
||||
sections.push({ ...restOfSection, gridData: { ...gridData, i: idx } });
|
||||
(sectionPanels as DashboardPanel[]).forEach((panel) => {
|
||||
const transformed = transformPanel(panel);
|
||||
panels.push({
|
||||
...transformed,
|
||||
gridData: { ...transformed.gridData, ...(!dropSections && { sectionId: idx }) },
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// widget is a panel
|
||||
panels.push(transformPanel(widget));
|
||||
}
|
||||
});
|
||||
return { panelsJSON: JSON.stringify(panels), sections };
|
||||
}
|
||||
|
||||
function transformPanel(panel: DashboardPanel): SavedDashboardPanel {
|
||||
const { panelIndex, gridData, panelConfig, ...restPanel } = panel as DashboardPanel;
|
||||
const idx = panelIndex ?? uuidv4();
|
||||
return {
|
||||
...restPanel,
|
||||
|
@ -26,7 +59,4 @@ export function transformPanelsIn(
|
|||
i: idx,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return JSON.stringify(updatedPanels);
|
||||
}
|
||||
|
|
|
@ -7,25 +7,51 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { flow } from 'lodash';
|
||||
import { SavedDashboardPanel } from '../../../../dashboard_saved_object';
|
||||
import { DashboardAttributes } from '../../types';
|
||||
import { SavedDashboardPanel, SavedDashboardSection } from '../../../../dashboard_saved_object';
|
||||
import { DashboardAttributes, DashboardPanel, DashboardSection } from '../../types';
|
||||
|
||||
export function transformPanelsOut(panelsJSON: string): DashboardAttributes['panels'] {
|
||||
return flow(JSON.parse, transformPanelsProperties)(panelsJSON);
|
||||
export function transformPanelsOut(
|
||||
panelsJSON: string = '{}',
|
||||
sections: SavedDashboardSection[] = []
|
||||
): DashboardAttributes['panels'] {
|
||||
const panels = JSON.parse(panelsJSON);
|
||||
const sectionsMap: { [uuid: string]: DashboardPanel | DashboardSection } = sections.reduce(
|
||||
(prev, section) => {
|
||||
const sectionId = section.gridData.i;
|
||||
return { ...prev, [sectionId]: { ...section, panels: [] } };
|
||||
},
|
||||
{}
|
||||
);
|
||||
panels.forEach((panel: SavedDashboardPanel) => {
|
||||
const { sectionId } = panel.gridData;
|
||||
if (sectionId) {
|
||||
(sectionsMap[sectionId] as DashboardSection).panels.push(transformPanelProperties(panel));
|
||||
} else {
|
||||
sectionsMap[panel.panelIndex] = transformPanelProperties(panel);
|
||||
}
|
||||
});
|
||||
return Object.values(sectionsMap);
|
||||
}
|
||||
|
||||
function transformPanelsProperties(panels: SavedDashboardPanel[]) {
|
||||
return panels.map(
|
||||
({ embeddableConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({
|
||||
function transformPanelProperties({
|
||||
embeddableConfig,
|
||||
gridData,
|
||||
id,
|
||||
panelIndex,
|
||||
panelRefName,
|
||||
title,
|
||||
type,
|
||||
version,
|
||||
}: SavedDashboardPanel) {
|
||||
const { sectionId, ...rest } = gridData; // drop section ID, if it exists
|
||||
return {
|
||||
gridData: rest,
|
||||
id,
|
||||
panelConfig: embeddableConfig,
|
||||
panelIndex,
|
||||
panelRefName,
|
||||
title,
|
||||
type,
|
||||
version,
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,8 +20,9 @@ import { WithRequiredProperty } from '@kbn/utility-types';
|
|||
import {
|
||||
dashboardItemSchema,
|
||||
controlGroupInputSchema,
|
||||
gridDataSchema,
|
||||
panelGridDataSchema,
|
||||
panelSchema,
|
||||
sectionSchema,
|
||||
dashboardAttributesSchema,
|
||||
dashboardCreateOptionsSchema,
|
||||
dashboardCreateResultSchema,
|
||||
|
@ -43,8 +44,9 @@ export type DashboardPanel = Omit<TypeOf<typeof panelSchema>, 'panelConfig'> & {
|
|||
panelConfig: TypeOf<typeof panelSchema>['panelConfig'] & { [key: string]: any };
|
||||
gridData: GridData;
|
||||
};
|
||||
export type DashboardSection = TypeOf<typeof sectionSchema>;
|
||||
export type DashboardAttributes = Omit<TypeOf<typeof dashboardAttributesSchema>, 'panels'> & {
|
||||
panels: DashboardPanel[];
|
||||
panels: Array<DashboardPanel | DashboardSection>;
|
||||
};
|
||||
|
||||
export type DashboardItem = TypeOf<typeof dashboardItemSchema>;
|
||||
|
@ -54,7 +56,7 @@ export type PartialDashboardItem = Omit<DashboardItem, 'attributes' | 'reference
|
|||
};
|
||||
|
||||
export type ControlGroupAttributes = TypeOf<typeof controlGroupInputSchema>;
|
||||
export type GridData = WithRequiredProperty<TypeOf<typeof gridDataSchema>, 'i'>;
|
||||
export type GridData = WithRequiredProperty<TypeOf<typeof panelGridDataSchema>, 'i'>;
|
||||
|
||||
export type DashboardGetIn = GetIn<typeof CONTENT_ID>;
|
||||
export type DashboardGetOut = TypeOf<typeof dashboardGetResultSchema>;
|
||||
|
|
|
@ -79,6 +79,10 @@ export const createDashboardSavedObjectType = ({
|
|||
},
|
||||
optionsJSON: { type: 'text', index: false },
|
||||
panelsJSON: { type: 'text', index: false },
|
||||
sections: {
|
||||
properties: {},
|
||||
dynamic: false,
|
||||
},
|
||||
refreshInterval: {
|
||||
properties: {
|
||||
display: { type: 'keyword', index: false, doc_values: false },
|
||||
|
|
|
@ -11,4 +11,9 @@ export {
|
|||
createDashboardSavedObjectType,
|
||||
DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
} from './dashboard_saved_object';
|
||||
export type { DashboardSavedObjectAttributes, GridData, SavedDashboardPanel } from './schema';
|
||||
export type {
|
||||
DashboardSavedObjectAttributes,
|
||||
GridData,
|
||||
SavedDashboardPanel,
|
||||
SavedDashboardSection,
|
||||
} from './schema';
|
||||
|
|
|
@ -7,5 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { DashboardSavedObjectAttributes, GridData, SavedDashboardPanel } from './latest';
|
||||
export type {
|
||||
DashboardSavedObjectAttributes,
|
||||
GridData,
|
||||
SavedDashboardPanel,
|
||||
SavedDashboardSection,
|
||||
} from './latest';
|
||||
export { dashboardSavedObjectSchema } from './latest';
|
||||
|
|
|
@ -13,4 +13,5 @@ export {
|
|||
type DashboardAttributes as DashboardSavedObjectAttributes,
|
||||
type GridData,
|
||||
type SavedDashboardPanel,
|
||||
type SavedDashboardSection,
|
||||
} from './v2';
|
||||
|
|
|
@ -7,5 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { DashboardAttributes, GridData, SavedDashboardPanel } from './types';
|
||||
export type {
|
||||
DashboardAttributes,
|
||||
GridData,
|
||||
SavedDashboardPanel,
|
||||
SavedDashboardSection,
|
||||
} from './types';
|
||||
export { controlGroupInputSchema, dashboardAttributesSchema } from './v2';
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { Serializable } from '@kbn/utility-types';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { dashboardAttributesSchema, gridDataSchema } from './v2';
|
||||
import { dashboardAttributesSchema, gridDataSchema, sectionSchema } from './v2';
|
||||
|
||||
export type DashboardAttributes = TypeOf<typeof dashboardAttributesSchema>;
|
||||
export type GridData = TypeOf<typeof gridDataSchema>;
|
||||
|
@ -33,3 +33,8 @@ export interface SavedDashboardPanel {
|
|||
*/
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A saved dashboard section parsed directly from the Dashboard Attributes
|
||||
*/
|
||||
export type SavedDashboardSection = TypeOf<typeof sectionSchema>;
|
||||
|
|
|
@ -13,6 +13,26 @@ import {
|
|||
dashboardAttributesSchema as dashboardAttributesSchemaV1,
|
||||
} from '../v1';
|
||||
|
||||
// sections only include y + i for grid data
|
||||
export const sectionGridDataSchema = schema.object({
|
||||
y: schema.number(),
|
||||
i: schema.string(),
|
||||
});
|
||||
|
||||
// panels include all grid data keys, including those that sections use
|
||||
export const gridDataSchema = sectionGridDataSchema.extends({
|
||||
x: schema.number(),
|
||||
w: schema.number(),
|
||||
h: schema.number(),
|
||||
sectionId: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export const sectionSchema = schema.object({
|
||||
title: schema.string(),
|
||||
collapsed: schema.maybe(schema.boolean()),
|
||||
gridData: sectionGridDataSchema,
|
||||
});
|
||||
|
||||
export const controlGroupInputSchema = controlGroupInputSchemaV1.extends(
|
||||
{
|
||||
showApplySelections: schema.maybe(schema.boolean()),
|
||||
|
@ -23,14 +43,7 @@ export const controlGroupInputSchema = controlGroupInputSchemaV1.extends(
|
|||
export const dashboardAttributesSchema = dashboardAttributesSchemaV1.extends(
|
||||
{
|
||||
controlGroupInput: schema.maybe(controlGroupInputSchema),
|
||||
sections: schema.maybe(schema.arrayOf(sectionSchema)),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export const gridDataSchema = schema.object({
|
||||
x: schema.number(),
|
||||
y: schema.number(),
|
||||
w: schema.number(),
|
||||
h: schema.number(),
|
||||
i: schema.string(),
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ export async function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export type { DashboardPluginSetup, DashboardPluginStart } from './types';
|
||||
export type { DashboardAttributes, DashboardPanel } from './content_management';
|
||||
export type { DashboardAttributes, DashboardPanel, DashboardSection } from './content_management';
|
||||
export type { DashboardSavedObjectAttributes } from './dashboard_saved_object';
|
||||
|
||||
export { PUBLIC_API_PATH } from './api/constants';
|
||||
|
|
|
@ -12,6 +12,7 @@ import chroma from 'chroma-js';
|
|||
import rison from '@kbn/rison';
|
||||
import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/common/content_management/constants';
|
||||
import { SharedDashboardState } from '@kbn/dashboard-plugin/common/types';
|
||||
import { DashboardPanel } from '@kbn/dashboard-plugin/server';
|
||||
import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../../page_objects/dashboard_page';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
|
@ -231,7 +232,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
(appState: Partial<SharedDashboardState>) => {
|
||||
log.debug(JSON.stringify(appState, null, ' '));
|
||||
return {
|
||||
panels: (appState.panels ?? []).map((panel) => {
|
||||
panels: (appState.panels ?? []).map((widget) => {
|
||||
const panel = widget as DashboardPanel;
|
||||
return {
|
||||
...panel,
|
||||
gridData: {
|
||||
|
@ -306,7 +308,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
currentUrl,
|
||||
(appState: Partial<SharedDashboardState>) => {
|
||||
return {
|
||||
panels: (appState.panels ?? []).map((panel) => {
|
||||
panels: (appState.panels ?? []).map((widget) => {
|
||||
const panel = widget as DashboardPanel;
|
||||
return {
|
||||
...panel,
|
||||
panelConfig: {
|
||||
|
@ -350,7 +353,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
currentUrl,
|
||||
(appState: Partial<SharedDashboardState>) => {
|
||||
return {
|
||||
panels: (appState.panels ?? []).map((panel) => {
|
||||
panels: (appState.panels ?? []).map((widget) => {
|
||||
const panel = widget as DashboardPanel;
|
||||
return {
|
||||
...panel,
|
||||
panelConfig: {
|
||||
|
|
|
@ -70,7 +70,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
]);
|
||||
|
||||
// Any changes to the number of panels needs to be audited by @elastic/kibana-presentation
|
||||
expect(panelTypes.length).to.eql(10);
|
||||
expect(panelTypes.length).to.eql(11);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import { IContentClient } from '@kbn/content-management-plugin/server/types';
|
||||
import type { Logger, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
import { isDashboardSection } from '@kbn/dashboard-plugin/common';
|
||||
import type { DashboardAttributes, DashboardPanel } from '@kbn/dashboard-plugin/server';
|
||||
import type { LensAttributes } from '@kbn/lens-embeddable-utils';
|
||||
import type {
|
||||
FieldBasedIndexPatternColumn,
|
||||
GenericIndexPatternColumn,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
import type { LensAttributes } from '@kbn/lens-embeddable-utils';
|
||||
import type { RelevantPanel, RelatedDashboard } from '@kbn/observability-schema';
|
||||
import type { DashboardAttributes, DashboardPanel } from '@kbn/dashboard-plugin/server';
|
||||
import type { InvestigateAlertsClient } from './investigate_alerts_client';
|
||||
import type { RelatedDashboard, RelevantPanel } from '@kbn/observability-schema';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { AlertData } from './alert_data';
|
||||
import type { InvestigateAlertsClient } from './investigate_alerts_client';
|
||||
|
||||
type Dashboard = SavedObjectsFindResult<DashboardAttributes>;
|
||||
export class RelatedDashboardsClient {
|
||||
|
@ -177,19 +177,21 @@ export class RelatedDashboardsClient {
|
|||
return { dashboards: relevantDashboards };
|
||||
}
|
||||
|
||||
getPanelsByIndex(index: string, panels: DashboardPanel[]): DashboardPanel[] {
|
||||
getPanelsByIndex(index: string, panels: DashboardAttributes['panels']): DashboardPanel[] {
|
||||
const panelsByIndex = panels.filter((p) => {
|
||||
if (isDashboardSection(p)) return false; // filter out sections
|
||||
const panelIndices = this.getPanelIndices(p);
|
||||
return panelIndices.has(index);
|
||||
});
|
||||
}) as DashboardPanel[]; // filtering with type guard doesn't actually limit type, so need to cast
|
||||
return panelsByIndex;
|
||||
}
|
||||
|
||||
getPanelsByField(
|
||||
fields: string[],
|
||||
panels: DashboardPanel[]
|
||||
panels: DashboardAttributes['panels']
|
||||
): Array<{ matchingFields: Set<string>; panel: DashboardPanel }> {
|
||||
const panelsByField = panels.reduce((acc, p) => {
|
||||
if (isDashboardSection(p)) return acc; // filter out sections
|
||||
const panelFields = this.getPanelFields(p);
|
||||
const matchingFields = fields.filter((f) => panelFields.has(f));
|
||||
if (matchingFields.length) {
|
||||
|
|
|
@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
]);
|
||||
|
||||
// Any changes to the number of panels needs to be audited by @elastic/kibana-presentation
|
||||
expect(panelTypes.length).to.eql(20);
|
||||
expect(panelTypes.length).to.eql(21);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue