From 74ee116780cf98fb4aaba41770f538d9eb6bae30 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 30 May 2025 11:40:28 -0600 Subject: [PATCH] [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> --- oas_docs/bundle.json | 1517 ++++++++++++----- oas_docs/bundle.serverless.json | 1517 ++++++++++++----- oas_docs/output/kibana.serverless.yaml | 1177 +++++++++---- oas_docs/output/kibana.yaml | 1177 +++++++++---- .../current_fields.json | 1 + .../current_mappings.json | 4 + .../check_registered_types.test.ts | 2 +- .../kbn-grid-layout/grid/grid_layout.test.tsx | 5 + .../kbn-grid-layout/grid/grid_layout.tsx | 1 + .../delete_grid_section_modal.tsx | 1 + .../grid/grid_section/grid_section_header.tsx | 4 + .../grid/grid_section/grid_section_title.tsx | 1 + .../grid_section/grid_section_wrapper.tsx | 1 + .../panel/state_manager_actions.ts | 12 +- .../grid/utils/equality_checks.ts | 1 + .../presentation_containers/index.ts | 1 + .../interfaces/can_add_new_section.ts | 16 + .../children_unsaved_changes.ts | 9 +- .../common/content_management/v2/index.ts | 10 +- .../common/content_management/v2/types.ts | 12 + .../dashboard_container_references.test.ts | 4 + .../common/dashboard_container/types.ts | 13 +- .../dashboard_saved_object_references.ts | 20 +- .../plugins/shared/dashboard/common/index.ts | 8 +- .../common/lib/dashboard_panel_converters.ts | 109 +- .../load_dashboard_history_location_state.ts | 4 +- .../plugins/shared/dashboard/common/types.ts | 9 +- .../dashboard_actions/add_section_action.tsx | 47 + .../public/dashboard_actions/constants.ts | 1 + .../dashboard_actions/register_actions.ts | 8 + .../public/dashboard_api/are_layouts_equal.ts | 54 + .../dashboard_api/are_panel_layouts_equal.ts | 37 - .../dashboard_api/default_dashboard_state.ts | 1 + .../public/dashboard_api/get_dashboard_api.ts | 38 +- .../get_serialized_state.test.ts | 66 + .../dashboard_api/get_serialized_state.ts | 23 +- .../{panels_manager.ts => layout_manager.ts} | 247 ++- .../public/dashboard_api/track_panel.ts | 13 +- .../dashboard/public/dashboard_api/types.ts | 20 +- .../dashboard_api/unsaved_changes_manager.ts | 26 +- .../top_nav/share/show_share_modal.test.tsx | 4 +- .../top_nav/share/show_share_modal.tsx | 24 +- .../url/search_sessions_integration.ts | 9 +- .../public/dashboard_app/url/url_utils.ts | 39 +- .../_dashboard_container.scss | 1 + .../dashboard_renderer/dashboard_module.ts | 1 + .../grid/dashboard_grid.test.tsx | 316 +++- .../grid/dashboard_grid.tsx | 158 +- .../grid/use_layout_styles.tsx | 50 +- .../viewport/_dashboard_viewport.scss | 4 + .../viewport/dashboard_viewport.tsx | 30 +- .../plugins/shared/dashboard/public/mocks.tsx | 71 +- .../place_clone_panel_strategy.test.ts | 72 + .../place_clone_panel_strategy.ts | 18 +- .../place_new_panel_strategies.test.ts | 192 +++ .../place_new_panel_strategies.ts | 38 +- .../dashboard/public/panel_placement/types.ts | 5 +- .../lib/load_dashboard_state.ts | 15 +- .../server/content_management/index.ts | 1 + .../content_management/v3/cm_services.ts | 59 +- .../server/content_management/v3/index.ts | 1 + .../content_management/v3/transform_utils.ts | 34 +- .../in/panels_in_transforms.test.ts | 2 +- .../v3/transforms/in/panels_in_transforms.ts | 64 +- .../transforms/out/panels_out_transforms.ts | 64 +- .../server/content_management/v3/types.ts | 8 +- .../dashboard_saved_object.ts | 4 + .../server/dashboard_saved_object/index.ts | 7 +- .../dashboard_saved_object/schema/index.ts | 7 +- .../dashboard_saved_object/schema/latest.ts | 1 + .../dashboard_saved_object/schema/v2/index.ts | 7 +- .../dashboard_saved_object/schema/v2/types.ts | 7 +- .../dashboard_saved_object/schema/v2/v2.ts | 29 +- .../plugins/shared/dashboard/server/index.ts | 2 +- .../apps/dashboard/group3/dashboard_state.ts | 10 +- .../group5/dashboard_panel_listing.ts | 2 +- .../services/related_dashboards_client.ts | 22 +- .../group2/dashboard_panel_listing.ts | 2 +- 78 files changed, 5615 insertions(+), 1982 deletions(-) create mode 100644 src/platform/packages/shared/presentation/presentation_containers/interfaces/can_add_new_section.ts create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_actions/add_section_action.tsx create mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/are_layouts_equal.ts delete mode 100644 src/platform/plugins/shared/dashboard/public/dashboard_api/are_panel_layouts_equal.ts rename src/platform/plugins/shared/dashboard/public/dashboard_api/{panels_manager.ts => layout_manager.ts} (67%) create mode 100644 src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts create mode 100644 src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 907605056ec3..2c95106199dc 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -7469,104 +7469,247 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData", - "panelIndex" - ], - "type": "object" + ] }, "type": "array" }, @@ -8130,104 +8273,248 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData" - ], - "type": "object" + ] }, "type": "array" }, @@ -8675,104 +8962,247 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData", - "panelIndex" - ], - "type": "object" + ] }, "type": "array" }, @@ -9308,104 +9738,248 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData" - ], - "type": "object" + ] }, "type": "array" }, @@ -9847,104 +10421,247 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData", - "panelIndex" - ], - "type": "object" + ] }, "type": "array" }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 043524a75ce6..91bdf37bddff 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -7469,104 +7469,247 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData", - "panelIndex" - ], - "type": "object" + ] }, "type": "array" }, @@ -8130,104 +8273,248 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData" - ], - "type": "object" + ] }, "type": "array" }, @@ -8675,104 +8962,247 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData", - "panelIndex" - ], - "type": "object" + ] }, "type": "array" }, @@ -9308,104 +9738,248 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData" - ], - "type": "object" + ] }, "type": "array" }, @@ -9847,104 +10421,247 @@ "panels": { "default": [], "items": { - "additionalProperties": false, - "properties": { - "gridData": { + "anyOf": [ + { "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": {}, + "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" }, - "hidePanelTitles": { - "description": "Set to true to hide the panel title in its container.", - "type": "boolean" + "id": { + "description": "The saved object id for by reference panels", + "type": "string" }, - "savedObjectId": { - "description": "The unique id of the library item to construct the embeddable.", + "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": { - "description": "The version of the embeddable in the panel.", + "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" }, - "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" + { + "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" } - }, - "required": [ - "panelConfig", - "type", - "gridData", - "panelIndex" - ], - "type": "object" + ] }, "type": "array" }, diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 14d266f3d40e..9fb21af3488a 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -6732,80 +6732,183 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData + - panels type: array refreshInterval: additionalProperties: false @@ -7202,80 +7305,184 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData type: array refreshInterval: additionalProperties: false @@ -7595,80 +7802,183 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData + - panels type: array refreshInterval: additionalProperties: false @@ -8045,80 +8355,184 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData type: array refreshInterval: additionalProperties: false @@ -8434,80 +8848,183 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData + - panels type: array refreshInterval: additionalProperties: false diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index c5cb2ae8b41f..5fe70280f4ca 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -8274,80 +8274,183 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData + - panels type: array refreshInterval: additionalProperties: false @@ -8744,80 +8847,184 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData type: array refreshInterval: additionalProperties: false @@ -9137,80 +9344,183 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData + - panels type: array refreshInterval: additionalProperties: false @@ -9587,80 +9897,184 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData type: array refreshInterval: additionalProperties: false @@ -9976,80 +10390,183 @@ paths: panels: default: [] items: - additionalProperties: false - type: object - properties: - gridData: - additionalProperties: false + anyOf: + - 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: {} + gridData: + additionalProperties: false 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. + 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 - version: - description: The version of the embeddable in the panel. + type: + description: The embeddable type 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 + 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 + 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 + type: array + title: + description: The title of the section. + type: string + required: + - title + - gridData + - panels type: array refreshInterval: additionalProperties: false diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 8a3808a5d669..eb88d4388675 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -285,6 +285,7 @@ "refreshInterval.pause", "refreshInterval.section", "refreshInterval.value", + "sections", "timeFrom", "timeRestore", "timeTo", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 81e932d27dfc..2cff82f93bf2 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -982,6 +982,10 @@ } } }, + "sections": { + "dynamic": false, + "properties": {} + }, "timeFrom": { "doc_values": false, "index": false, diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 1089475dd728..2cfcc74474bc 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -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", diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx index 2f6ac821e461..1d656090e6a3 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.test.tsx @@ -38,6 +38,11 @@ const renderGridLayout = (propsOverrides: Partial = {}) => { const { rerender, ...rtlRest } = render(, { wrapper: EuiThemeProvider }); + const gridLayout = screen.getByTestId('kbnGridLayout'); + jest.spyOn(gridLayout, 'getBoundingClientRect').mockImplementation(() => { + return { top: 0, bottom: 500 } as DOMRect; + }); + return { ...rtlRest, rerender: (overrides: Partial) => { diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx index 81cae2fc492d..9903dc87a7d6 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_layout.tsx @@ -198,6 +198,7 @@ export const GridLayout = ({
{ layoutRef.current = divElement; setDimensionsRef(divElement); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/delete_grid_section_modal.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/delete_grid_section_modal.tsx index 6b679a8dfee7..61c989942a49 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/delete_grid_section_modal.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/delete_grid_section_modal.tsx @@ -35,6 +35,7 @@ export const DeleteGridSectionModal = ({ return ( { setDeleteModalVisible(false); }} diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_header.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_header.tsx index 50ad66a4e337..ed2cf965b3e0 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_header.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_header.tsx @@ -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 ( diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_title.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_title.tsx index 837793e7a587..a245b57b813f 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_title.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_title.tsx @@ -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'} diff --git a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_wrapper.tsx b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_wrapper.tsx index 397a2211251a..383692507cc6 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_wrapper.tsx +++ b/src/platform/packages/private/kbn-grid-layout/grid/grid_section/grid_section_wrapper.tsx @@ -86,6 +86,7 @@ export const GridSectionWrapper = React.memo(({ sectionId }: GridSectionProps) = gridLayoutStateManager.sectionRefs.current[sectionId] = rowRef; }} className={'kbnGridSection'} + data-test-subj={`kbnGridSectionWrapper-${sectionId}`} /> ); }); diff --git a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel/state_manager_actions.ts b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel/state_manager_actions.ts index d9f623cd7b30..c14c8c67439f 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel/state_manager_actions.ts +++ b/src/platform/packages/private/kbn-grid-layout/grid/use_grid_layout_events/panel/state_manager_actions.ts @@ -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; diff --git a/src/platform/packages/private/kbn-grid-layout/grid/utils/equality_checks.ts b/src/platform/packages/private/kbn-grid-layout/grid/utils/equality_checks.ts index 2f22c680ec01..53c9ce2abc24 100644 --- a/src/platform/packages/private/kbn-grid-layout/grid/utils/equality_checks.ts +++ b/src/platform/packages/private/kbn-grid-layout/grid/utils/equality_checks.ts @@ -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); diff --git a/src/platform/packages/shared/presentation/presentation_containers/index.ts b/src/platform/packages/shared/presentation/presentation_containers/index.ts index 7fb0a712ea45..5b23aaba0cb4 100644 --- a/src/platform/packages/shared/presentation/presentation_containers/index.ts +++ b/src/platform/packages/shared/presentation/presentation_containers/index.ts @@ -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, diff --git a/src/platform/packages/shared/presentation/presentation_containers/interfaces/can_add_new_section.ts b/src/platform/packages/shared/presentation/presentation_containers/interfaces/can_add_new_section.ts new file mode 100644 index 000000000000..5d6c4c4ed01c --- /dev/null +++ b/src/platform/packages/shared/presentation/presentation_containers/interfaces/can_add_new_section.ts @@ -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'; +}; diff --git a/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts b/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts index c0a498a06bd6..3d930ce0496f 100644 --- a/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts +++ b/src/platform/packages/shared/presentation/presentation_containers/interfaces/unsaved_changes/children_unsaved_changes.ts @@ -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$( ) { 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([]); diff --git a/src/platform/plugins/shared/dashboard/common/content_management/v2/index.ts b/src/platform/plugins/shared/dashboard/common/content_management/v2/index.ts index bd687ff0dd60..37188bf3a560 100644 --- a/src/platform/plugins/shared/dashboard/common/content_management/v2/index.ts +++ b/src/platform/plugins/shared/dashboard/common/content_management/v2/index.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts b/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts index ae2c2a798d81..80033007232e 100644 --- a/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts +++ b/src/platform/plugins/shared/dashboard/common/content_management/v2/types.ts @@ -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 & { + gridData: GridData; +}; export type ControlGroupAttributes = ControlGroupAttributesV1 & { showApplySelections?: boolean; @@ -24,6 +35,7 @@ export type ControlGroupAttributes = ControlGroupAttributesV1 & { export type DashboardAttributes = Omit & { controlGroupInput?: ControlGroupAttributes; + sections?: DashboardSectionState[]; }; export type DashboardCrudTypes = ContentManagementCrudTypes< diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts b/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts index d04205884c2b..aa00108929b2 100644 --- a/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts +++ b/src/platform/plugins/shared/dashboard/common/dashboard_container/persistable_state/dashboard_container_references.test.ts @@ -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', () => { diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts b/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts index 7ada87b12b25..a70c171124fd 100644 --- a/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts +++ b/src/platform/plugins/shared/dashboard/common/dashboard_container/types.ts @@ -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; + id: string; +} + export interface DashboardPanelMap { [key: string]: DashboardPanelState; } @@ -18,7 +29,7 @@ export interface DashboardPanelMap { export interface DashboardPanelState { type: string; explicitInput: PanelState; - readonly gridData: GridData; + readonly gridData: GridData & { sectionId?: string }; panelRefName?: string; /** diff --git a/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts b/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts index ca8354c3d5ed..4918306b61b1 100644 --- a/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts +++ b/src/platform/plugins/shared/dashboard/common/dashboard_saved_object/persistable_state/dashboard_saved_object_references.ts @@ -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, diff --git a/src/platform/plugins/shared/dashboard/common/index.ts b/src/platform/plugins/shared/dashboard/common/index.ts index dae523f3f0fd..4d3dd779d1e2 100644 --- a/src/platform/plugins/shared/dashboard/common/index.ts +++ b/src/platform/plugins/shared/dashboard/common/index.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts b/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts index 1db50541aeed..1c58750987b2 100644 --- a/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts +++ b/src/platform/plugins/shared/dashboard/common/lib/dashboard_panel_converters.ts @@ -7,44 +7,97 @@ * 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 => { - const panelsMap: DashboardPanelMap = {}; - panels?.forEach((panel, idx) => { - panelsMap![panel.panelIndex ?? String(idx)] = { - type: panel.type, - gridData: panel.gridData, - panelRefName: panel.panelRefName, - explicitInput: { - ...(panel.id !== undefined && { savedObjectId: panel.id }), - ...(panel.title !== undefined && { title: panel.title }), - ...panel.panelConfig, - }, - version: panel.version, - }; - }); - return panelsMap; +export const isDashboardSection = ( + widget: DashboardAttributes['panels'][number] +): widget is DashboardSection => { + return 'panels' in widget; }; -export const convertPanelMapToPanelsArray = ( +export const convertPanelsArrayToPanelSectionMaps = ( + panels?: DashboardAttributes['panels'] +): { panels: DashboardPanelMap; sections: DashboardSectionMap } => { + const panelsMap: DashboardPanelMap = {}; + 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, + explicitInput: { + ...(panel.id !== undefined && { savedObjectId: panel.id }), + ...(panel.title !== undefined && { title: panel.title }), + ...panel.panelConfig, + }, + version: panel.version, + }; +}; + +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)]; }; /** diff --git a/src/platform/plugins/shared/dashboard/common/locator/load_dashboard_history_location_state.ts b/src/platform/plugins/shared/dashboard/common/locator/load_dashboard_history_location_state.ts index c9980386bf91..fb08d5f37eab 100644 --- a/src/platform/plugins/shared/dashboard/common/locator/load_dashboard_history_location_state.ts +++ b/src/platform/plugins/shared/dashboard/common/locator/load_dashboard_history_location_state.ts @@ -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), }; }; diff --git a/src/platform/plugins/shared/dashboard/common/types.ts b/src/platform/plugins/shared/dashboard/common/types.ts index ef9c859c4e37..9d207a6dc7bb 100644 --- a/src/platform/plugins/shared/dashboard/common/types.ts +++ b/src/platform/plugins/shared/dashboard/common/types.ts @@ -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 & { + Omit & { controlGroupInput?: DashboardState['controlGroupInput'] & SerializableRecord; /** @@ -87,7 +90,7 @@ export type SharedDashboardState = Partial< */ controlGroupState?: Partial & SerializableRecord; - panels: DashboardPanel[]; + panels: Array; references?: DashboardState['references'] & SerializableRecord; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_actions/add_section_action.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_actions/add_section_action.tsx new file mode 100644 index 000000000000..9ac5624eb2e3 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_actions/add_section_action.tsx @@ -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 { + 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(); + } +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_actions/constants.ts b/src/platform/plugins/shared/dashboard/public/dashboard_actions/constants.ts index 33ae533e9f50..a0341bedb1db 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_actions/constants.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_actions/constants.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_actions/register_actions.ts b/src/platform/plugins/shared/dashboard/public/dashboard_actions/register_actions.ts index 3d7945375e8c..06bcec7f68ca 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_actions/register_actions.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_actions/register_actions.ts @@ -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'); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/are_layouts_equal.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/are_layouts_equal.ts new file mode 100644 index 000000000000..c0e29680588d --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/are_layouts_equal.ts @@ -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; +}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/are_panel_layouts_equal.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/are_panel_layouts_equal.ts deleted file mode 100644 index a881f7bb6214..000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/are_panel_layouts_equal.ts +++ /dev/null @@ -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; -}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts index 88ab9681eb35..a296c75dd814 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts @@ -16,6 +16,7 @@ export const DEFAULT_DASHBOARD_STATE: DashboardState = { description: '', filters: [], panels: {}, + sections: {}, title: '', tags: [], diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index 7fc631bae3da..5e490569e491 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -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(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; const internalApi: DashboardInternalApi = { - ...panelsManager.internalApi, + ...layoutManager.internalApi, ...unifiedSearchManager.internalApi, setControlGroupApi: controlGroupManager.internalApi.setControlGroupApi, }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts index 9cae8584e7f4..c3ba9cb87c8b 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts @@ -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", + }, + ] + `); + }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts index acc80d6de801..f2fb74f8eb49 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts @@ -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 => { 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 diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/panels_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.ts similarity index 67% rename from src/platform/plugins/shared/dashboard/public/dashboard_api/panels_manager.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.ts index 80068a6f2a34..edf2164b2cbc 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/panels_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager.ts @@ -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, getReferences: (id: string) => Reference[] -): { - internalApi: { - startComparing$: ( - lastSavedState$: BehaviorSubject - ) => Observable<{ panels?: DashboardPanelMap }>; - getSerializedStateForPanel: HasSerializedChildState['getSerializedStateForChild']; - layout$: BehaviorSubject; - registerChildApi: (api: DefaultEmbeddableApi) => void; - resetPanels: (lastSavedPanels: DashboardPanelMap) => void; - setChildState: (uuid: string, state: SerializedPanelState) => void; - serializePanels: () => { panels: DashboardPanelMap; references: Reference[] }; - }; - api: PresentationContainer & - CanDuplicatePanels & { getDashboardPanelFromId: DashboardApi['getDashboardPanelFromId'] }; -} { +) { // -------------------------------------------------------------------------------------- // Set up panel state manager // -------------------------------------------------------------------------------------- const children$ = new BehaviorSubject({}); - const { layout: initialLayout, childState: initialChildState } = deserializePanels(initialPanels); + const { layout: initialLayout, childState: initialChildState } = deserializeLayout( + initialPanels, + initialSections + ); const layout$ = new BehaviorSubject(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 => { 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 { 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 ( 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); } + 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,14 +311,22 @@ 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({ - ...otherPanels, - [uuidOfDuplicate]: { - gridData: { ...newPanelPlacement, i: uuidOfDuplicate }, - type: layoutItemToDuplicate.type, + ...layout$.value, + panels: { + ...otherPanels, + [uuidOfDuplicate]: { + gridData: { + ...newPanelPlacement, + i: uuidOfDuplicate, + sectionId: layoutItemToDuplicate.gridData.sectionId, + }, + type: layoutItemToDuplicate.type, + }, }, }); @@ -315,7 +337,7 @@ export function initializePanelsManager( }; const getChildApi = async (uuid: string): Promise => { - 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 - ): 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) => { 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(); + }, }, }; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/track_panel.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/track_panel.ts index 4b266b8a53d1..65245eb774a7 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/track_panel.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/track_panel.ts @@ -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) { const expandedPanelId$ = new BehaviorSubject(undefined); const focusedPanelId$ = new BehaviorSubject(undefined); const highlightPanelId$ = new BehaviorSubject(undefined); const scrollToPanelId$ = new BehaviorSubject(undefined); + const scrollToBottom$ = new Subject(); let scrollPosition: number | undefined; function setScrollToPanelId(id: string | undefined) { @@ -62,15 +63,19 @@ export function initializeTrackPanel(untilLoaded: (id: string) => Promise { 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); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts index fb18255a9846..78e579cb13b5 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts @@ -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 & 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; scrollToTop: () => void; + scrollToBottom: () => void; + scrollToBottom$: Subject; setFilters: (filters?: Filter[] | undefined) => void; setFullScreenMode: (fullScreenMode: boolean) => void; setHighlightPanelId: (id: string | undefined) => void; @@ -168,5 +177,10 @@ export interface DashboardInternalApi { layout$: BehaviorSubject; registerChildApi: (api: DefaultEmbeddableApi) => void; setControlGroupApi: (controlGroupApi: ControlGroupApi) => void; - serializePanels: () => { references: Reference[]; panels: DashboardPanelMap }; + serializeLayout: () => { + references: Reference[]; + panels: DashboardPanelMap; + sections: DashboardSectionMap; + }; + isSectionCollapsed: (sectionId?: string) => boolean; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts index 24216b9846d8..cb8705170639 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts @@ -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; - panelsManager: ReturnType; + layoutManager: ReturnType; viewModeManager: ReturnType; settingsManager: ReturnType; unifiedSearchManager: ReturnType; @@ -75,14 +75,14 @@ export function initializeUnsavedChangesManager({ // references injected while loading dashboard saved object in loadDashboardState const lastSavedState$ = new BehaviorSubject(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> = 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(); }, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index 0c113e0ff531..80d5a88c66dd 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx @@ -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( diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index b886bd070415..6b1bb097f690 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -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 = { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts index ffb2ed8f2b48..a55ed7dfa01c 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/search_sessions_integration.ts @@ -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'], }), }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/url_utils.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/url_utils.ts index d92920bebc5c..950875d8d1a8 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/url_utils.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/url_utils.ts @@ -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 | 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 +): { 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) } : {}), }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/_dashboard_container.scss b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/_dashboard_container.scss index cd1faa183cac..e2b039ee8b93 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/_dashboard_container.scss +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/_dashboard_container.scss @@ -15,6 +15,7 @@ .dshEmptyPromptParent { flex-grow: 1; display: flex; + height: 100%; } .dshEmptyPromptPageTemplate { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts index 5034d2d73cc0..9724ba92217e 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx index f6c267987b17..c3eee737f3e7 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.test.tsx @@ -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,84 +90,225 @@ const createAndMountDashboardGrid = async (panels: DashboardPanelMap = PANELS) = ); + // 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); -}); - -test('renders DashboardGrid with no visualizations', async () => { - await createAndMountDashboardGrid({}); -}); - -test('DashboardGrid removes panel when removed from container', async () => { - const { dashboardApi, component } = await createAndMountDashboardGrid(PANELS); - - // remove panel - await act(async () => { - dashboardApi.removePanel('1'); - await new Promise((resolve) => setTimeout(resolve, 1)); +describe('DashboardGrid', () => { + test('renders', async () => { + await createAndMountDashboardGrid(); }); - expect(component.getAllByTestId('dashboardGridItem').length).toBe(1); -}); - -test('DashboardGrid renders expanded panel', async () => { - const { dashboardApi, component } = await createAndMountDashboardGrid(); - - // maximize panel - await act(async () => { - dashboardApi.expandPanel('1'); - await new Promise((resolve) => setTimeout(resolve, 1)); - }); - // Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized. - expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); - - verifyElementHasClass(component, '#mockDashboardGridItem_1', 'expandedPanel'); - verifyElementHasClass(component, '#mockDashboardGridItem_2', 'hiddenPanel'); - - // minimize panel - await act(async () => { - dashboardApi.expandPanel('1'); - await new Promise((resolve) => setTimeout(resolve, 1)); - }); - expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); - - verifyElementHasClass(component, '#mockDashboardGridItem_1', 'regularPanel'); - verifyElementHasClass(component, '#mockDashboardGridItem_2', 'regularPanel'); -}); - -test('DashboardGrid renders focused panel', async () => { - const { dashboardApi, component } = await createAndMountDashboardGrid(); - const overlayMock = { - onClose: new Promise((resolve) => { - resolve(); - }), - close: async () => {}, - }; - - await act(async () => { - dashboardApi.openOverlay(overlayMock, { focusedPanelId: '2' }); - await new Promise((resolve) => setTimeout(resolve, 1)); - }); - // Both panels should still exist in the dom, so nothing needs to be re-fetched once focused/blurred. - expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); - - verifyElementHasClass(component, '#mockDashboardGridItem_1', 'blurredPanel'); - verifyElementHasClass(component, '#mockDashboardGridItem_2', 'focusedPanel'); - - await act(async () => { - dashboardApi.clearOverlays(); - await new Promise((resolve) => setTimeout(resolve, 1)); - }); - expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); - - verifyElementHasClass(component, '#mockDashboardGridItem_1', 'regularPanel'); - verifyElementHasClass(component, '#mockDashboardGridItem_2', 'regularPanel'); + describe('panels', () => { + test('renders with no visualizations', async () => { + await createAndMountDashboardGrid(); + }); + + test('removes panel when removed from container', async () => { + const { dashboardApi, component } = await createAndMountDashboardGrid(); + + // remove panel + await act(async () => { + dashboardApi.removePanel('2'); + await new Promise((resolve) => setTimeout(resolve, 1)); + }); + + expect(component.getAllByTestId('dashboardGridItem').length).toBe(1); + }); + + test('renders expanded panel', async () => { + const { dashboardApi, component } = await createAndMountDashboardGrid(); + + // maximize panel + await act(async () => { + dashboardApi.expandPanel('1'); + await new Promise((resolve) => setTimeout(resolve, 1)); + }); + // Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized. + expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); + + verifyElementHasClass(component, '#mockDashboardGridItem_1', 'expandedPanel'); + verifyElementHasClass(component, '#mockDashboardGridItem_2', 'hiddenPanel'); + + // minimize panel + await act(async () => { + dashboardApi.expandPanel('1'); + await new Promise((resolve) => setTimeout(resolve, 1)); + }); + expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); + + verifyElementHasClass(component, '#mockDashboardGridItem_1', 'regularPanel'); + verifyElementHasClass(component, '#mockDashboardGridItem_2', 'regularPanel'); + }); + + test('renders focused panel', async () => { + const { dashboardApi, component } = await createAndMountDashboardGrid(); + const overlayMock = { + onClose: new Promise((resolve) => { + resolve(); + }), + close: async () => {}, + }; + + await act(async () => { + dashboardApi.openOverlay(overlayMock, { focusedPanelId: '2' }); + await new Promise((resolve) => setTimeout(resolve, 1)); + }); + // Both panels should still exist in the dom, so nothing needs to be re-fetched once focused/blurred. + expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); + + verifyElementHasClass(component, '#mockDashboardGridItem_1', 'blurredPanel'); + verifyElementHasClass(component, '#mockDashboardGridItem_2', 'focusedPanel'); + + await act(async () => { + dashboardApi.clearOverlays(); + await new Promise((resolve) => setTimeout(resolve, 1)); + }); + expect(component.getAllByTestId('dashboardGridItem').length).toBe(2); + + 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 + }); + }); + }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx index 148cff6fb288..51511425d71d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid.tsx @@ -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(null); const layoutStyles = useLayoutStyles(); const panelRefs = useRef<{ [panelId: string]: React.Ref }>({}); 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, - type: 'panel', - }; + } 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) => { - if (widget.type === 'section') { - return updatedPanelsAcc; // sections currently aren't supported - } - updatedPanelsAcc[widget.id] = { - ...currentPanels[widget.id], + const currLayout = dashboardInternalApi.layout$.getValue(); + const updatedLayout: DashboardLayout = { + sections: {}, + panels: {}, + }; + Object.values(newLayout).forEach((widget) => { + if (widget.type === 'section') { + 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; - }, - {} as DashboardLayout - ); - if (!arePanelLayoutsEqual(currentPanels, updatedPanels)) { - dashboardInternalApi.layout$.next(updatedPanels); + Object.values(widget.panels).forEach((panel) => { + updatedLayout.panels[panel.id] = { + ...currLayout.panels[panel.id], + gridData: { + ...convertGridPanelToDashboardGridData(panel), + sectionId: widget.id, + }, + }; + }); + } 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) => 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 ( ); }, [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 ( -
+
{memoizedgridLayout}
); }; + +const convertGridPanelToDashboardGridData = (panel: GridPanelData): GridData => { + return { + i: panel.id, + y: panel.row, + x: panel.column, + w: panel.width, + h: panel.height, + }; +}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/use_layout_styles.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/use_layout_styles.tsx index 1be076234d3d..3ac87b862a2b 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/use_layout_styles.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/use_layout_styles.tsx @@ -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]); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/_dashboard_viewport.scss b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/_dashboard_viewport.scss index 9a57af03d3c1..e476fdf3f34a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/_dashboard_viewport.scss +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/_dashboard_viewport.scss @@ -12,6 +12,10 @@ .dshDashboardViewport { width: 100%; + &.dshDashboardViewport--empty { + height: 100%; + } + &--panelExpanded { flex: 1; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx index e7eb0c532f09..e1acf71ca40d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx @@ -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 = ({ )} - {panelCount === 0 && }
- + {panelCount === 0 && sectionCount === 0 ? ( + + ) : ( + + )}
); diff --git a/src/platform/plugins/shared/dashboard/public/mocks.tsx b/src/platform/plugins/shared/dashboard/public/mocks.tsx index 8e5049d31382..a673e8a0433a 100644 --- a/src/platform/plugins/shared/dashboard/public/mocks.tsx +++ b/src/platform/plugins/shared/dashboard/public/mocks.tsx @@ -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; @@ -126,24 +126,67 @@ export function getSampleDashboardState(overrides?: Partial): Da timeRestore: false, viewMode: 'view', panels: {}, + sections: {}, ...overrides, }; } -export function getSampleDashboardPanel( - overrides: Partial & { - 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; } diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts new file mode 100644 index 000000000000..3a2c8fc8ee10 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.test.ts @@ -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); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts index f34f4bcf7dbe..5bd33508d610 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_clone_panel_strategy.ts @@ -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; @@ -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) => { - otherPanelGridData.push(panel.gridData); + 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]); - const newGridData = { ...gridData, y: gridData.y + diff }; - otherPanels[originalPositionInTheGrid] = { ...movedPanel, gridData: newGridData }; + 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 }; } diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts new file mode 100644 index 000000000000..eff226705f9d --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.test.ts @@ -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 + }); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.ts index 20f2a144131a..5f8327fe77bb 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/place_new_panel_strategies.ts @@ -15,15 +15,18 @@ 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)) { - const { gridData, ...currentPanel } = cloneDeep(panel); - const newGridData = { ...gridData, y: gridData.y + height }; - otherPanels[id] = { ...currentPanel, gridData: newGridData }; + // 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 }, @@ -35,7 +38,10 @@ export const runPanelPlacementStrategy = ( const currentPanelsArray = Object.values(currentPanels); currentPanelsArray.forEach((panel) => { - maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY); + // 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,17 +58,19 @@ export const runPanelPlacementStrategy = ( } currentPanelsArray.forEach((panel) => { - 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]; - if (row === undefined) { - throw new Error( - `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify( - 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]; + if (row === undefined) { + throw new Error( + `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify( + panel + )}` + ); + } + grid[y][x] = 1; } - grid[y][x] = 1; } } }); diff --git a/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts b/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts index d7b575369409..92b60ae7d197 100644 --- a/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts +++ b/src/platform/plugins/shared/dashboard/public/panel_placement/types.ts @@ -21,13 +21,14 @@ export interface PanelPlacementSettings { export interface PanelPlacementReturn { newPanelPlacement: Omit; - 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 = ( diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts index 4b2c13dedeba..206fce2da818 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts @@ -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: diff --git a/src/platform/plugins/shared/dashboard/server/content_management/index.ts b/src/platform/plugins/shared/dashboard/server/content_management/index.ts index 8ff43345aa9c..dfef14827b0b 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/index.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/index.ts @@ -11,6 +11,7 @@ export type { ControlGroupAttributes, GridData, DashboardPanel, + DashboardSection, DashboardAttributes, DashboardItem, DashboardGetIn, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts index 36aa421ade3a..dd210909e9df 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts @@ -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( - panelSchema.extends({ - // Responses always include the panel index and gridData.i - panelIndex: schema.string(), - gridData: gridDataSchema.extends({ - i: schema.string(), + schema.oneOf([ + panelSchema.extends({ + panelIndex: schema.string(), + 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: [] } ), }); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/index.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/index.ts index 5c26a0d7fed9..12b273e98591 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/index.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/index.ts @@ -11,6 +11,7 @@ export type { ControlGroupAttributes, GridData, DashboardPanel, + DashboardSection, DashboardAttributes, DashboardItem, DashboardGetIn, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts index 5a4485765727..8870951928d7 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts @@ -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, @@ -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( diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts index e8e5e39a79ec..1e01fd50d6e2 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.test.ts @@ -30,7 +30,7 @@ describe('transformPanelsIn', () => { }, ]; const result = transformPanelsIn(panels as DashboardPanel[]); - expect(result).toEqual( + expect(result.panelsJSON).toEqual( JSON.stringify([ { type: 'foo', diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts index 71645e641b6b..042f06d75556 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts @@ -9,24 +9,54 @@ 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 }) => { - const idx = panelIndex ?? uuidv4(); - return { - ...restPanel, - embeddableConfig: panelConfig, - panelIndex: idx, - gridData: { - ...gridData, - i: idx, - }, - }; - }); + widgets: DashboardAttributes['panels'] | undefined, + dropSections: boolean = false +): { + panelsJSON: DashboardSavedObjectAttributes['panelsJSON']; + sections: DashboardSavedObjectAttributes['sections']; +} { + const panels: SavedDashboardPanel[] = []; + const sections: SavedDashboardSection[] = []; - return JSON.stringify(updatedPanels); + 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, + embeddableConfig: panelConfig, + panelIndex: idx, + gridData: { + ...gridData, + i: idx, + }, + }; } diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts index 1f67c436ec9b..7dfebd643cb4 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts @@ -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); -} - -function transformPanelsProperties(panels: SavedDashboardPanel[]) { - return panels.map( - ({ embeddableConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({ - gridData, - id, - panelConfig: embeddableConfig, - panelIndex, - panelRefName, - title, - type, - version, - }) +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 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, + }; } diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts index 0c55985162e3..32e5484b90b5 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts @@ -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, 'panelConfig'> & { panelConfig: TypeOf['panelConfig'] & { [key: string]: any }; gridData: GridData; }; +export type DashboardSection = TypeOf; export type DashboardAttributes = Omit, 'panels'> & { - panels: DashboardPanel[]; + panels: Array; }; export type DashboardItem = TypeOf; @@ -54,7 +56,7 @@ export type PartialDashboardItem = Omit; -export type GridData = WithRequiredProperty, 'i'>; +export type GridData = WithRequiredProperty, 'i'>; export type DashboardGetIn = GetIn; export type DashboardGetOut = TypeOf; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/dashboard_saved_object.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/dashboard_saved_object.ts index 524bd59a4925..b90a9ca7ae37 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/dashboard_saved_object.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/dashboard_saved_object.ts @@ -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 }, diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/index.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/index.ts index 23c91f2c6ea3..80403b74d4d1 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/index.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/index.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/index.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/index.ts index 4c50de472f53..12df48448590 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/index.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/index.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/latest.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/latest.ts index a40e476abe79..3f2af886f863 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/latest.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/latest.ts @@ -13,4 +13,5 @@ export { type DashboardAttributes as DashboardSavedObjectAttributes, type GridData, type SavedDashboardPanel, + type SavedDashboardSection, } from './v2'; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/index.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/index.ts index 2fda02230ed6..9b8d210c7ae1 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/index.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/index.ts @@ -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'; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/types.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/types.ts index e50a27efe2b3..ce90044689fa 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/types.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/types.ts @@ -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; export type GridData = TypeOf; @@ -33,3 +33,8 @@ export interface SavedDashboardPanel { */ version?: string; } + +/** + * A saved dashboard section parsed directly from the Dashboard Attributes + */ +export type SavedDashboardSection = TypeOf; diff --git a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/v2.ts b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/v2.ts index dc0ed3eb84cb..a1ff99e8068f 100644 --- a/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/v2.ts +++ b/src/platform/plugins/shared/dashboard/server/dashboard_saved_object/schema/v2/v2.ts @@ -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(), -}); diff --git a/src/platform/plugins/shared/dashboard/server/index.ts b/src/platform/plugins/shared/dashboard/server/index.ts index 842004b836d3..3343faa6efd4 100644 --- a/src/platform/plugins/shared/dashboard/server/index.ts +++ b/src/platform/plugins/shared/dashboard/server/index.ts @@ -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'; diff --git a/src/platform/test/functional/apps/dashboard/group3/dashboard_state.ts b/src/platform/test/functional/apps/dashboard/group3/dashboard_state.ts index 7d277ab6c722..0f4348c172b4 100644 --- a/src/platform/test/functional/apps/dashboard/group3/dashboard_state.ts +++ b/src/platform/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -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) => { 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) => { 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) => { return { - panels: (appState.panels ?? []).map((panel) => { + panels: (appState.panels ?? []).map((widget) => { + const panel = widget as DashboardPanel; return { ...panel, panelConfig: { diff --git a/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts b/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts index 789eae535751..8ded18765501 100644 --- a/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts +++ b/src/platform/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts @@ -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); }); }); } diff --git a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts index b15ca35464df..5dccf68d196b 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts @@ -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; 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; 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) { diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_panel_listing.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_panel_listing.ts index b45d808d4bff..abeb6d9896cb 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_panel_listing.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_panel_listing.ts @@ -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); }); }); }