Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard] Public CRUD API MVP #193067

Merged
merged 73 commits into from
Nov 8, 2024
Merged

Conversation

nickpeihl
Copy link
Member

@nickpeihl nickpeihl commented Sep 16, 2024

Closes #192618

Adds public CRUD+List endpoints for the Dashboards API.

The schema for the endpoints are generated from Content Management schemas so that the RPC and Public APIs use the same schemas for CRUD operations. A new version (v3) has been added to the Dashboards content management specification that decouples Content from Saved Objects using a translation layer in Content Management. When retrieving a saved object the Content Management layer parses and validates the panelJSON, optionsListJSON, and savedSearchJSON properties against the defines schema and passes the translated content to the consumer (user interface or API).

When writing a saved object, the Content Management layer serializes (JSON.stringify) the Content object into the saved object schema. So the saved object schema continues to store as stringified JSON, but the user interface and public API see and use the JSON objects.

These planned features are out of scope for this PR and may be added in subsequent PRs.

  1. [Dashboards] Move reference injection/extraction from client to the server #192758
  2. [Dashboards] Embeddables schema registry for Dashboard API validation #192622

Reviewers, please test both UI and endpoints.

cURL examples:

First, yarn start --no-base-path. Assumes elastic:changeme is the username:password.

Create

Create an empty dashboard with the minimum required properties
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "attributes": { "title": "my empty dashboard" }
  }'
Create a dashboard of a specific ID with some ES|QL panels
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "description": "",
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                  "name": "kibana_sample_data_ecommerce",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "title": "kibana_sample_data_ecommerce",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "title": "kibana_sample_data_ecommerce"
                    }
                  ],
                  "layers": {
                    "44866844-8fca-482a-a769-006e7d029b9b": {
                      "columns": [
                        {
                          "columnId": "6376af5c-fdd1-4d72-a3ec-5686b5049664",
                          "fieldName": "customer_gender",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        },
                        {
                          "columnId": "a2e3e039-dff6-4893-9c9d-9f0a816207dd",
                          "fieldName": "taxless_total_price",
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        }
                      ],
                      "index": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "query": {
                        "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
                      }
                    },
                    "781db49e-f4f1-42e0-975f-7118d2ef7a18": {
                      "columns": [],
                      "index": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "query": {
                        "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
                      }
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
              },
              "visualization": {
                "layers": [
                  {
                    "categoryDisplay": "default",
                    "colorMapping": {
                      "assignments": [],
                      "colorMode": {
                        "type": "categorical"
                      },
                      "paletteId": "eui_amsterdam_color_blind",
                      "specialAssignments": [
                        {
                          "color": {
                            "type": "loop"
                          },
                          "rule": {
                            "type": "other"
                          },
                          "touched": false
                        }
                      ]
                    },
                    "layerId": "44866844-8fca-482a-a769-006e7d029b9b",
                    "layerType": "data",
                    "legendDisplay": "default",
                    "metrics": [
                      "a2e3e039-dff6-4893-9c9d-9f0a816207dd"
                    ],
                    "nestedLegend": false,
                    "numberDisplay": "percent",
                    "primaryGroups": [
                      "6376af5c-fdd1-4d72-a3ec-5686b5049664"
                    ]
                  }
                ],
                "shape": "pie"
              }
            },
            "title": "Table category & category.keyword & currency & customer_first_name & customer_first_name.keyword",
            "type": "lens",
            "visualizationType": "lnsPie"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 0,
          "y": 0
        },
        "type": "lens"
      },
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                  "name": "kibana_sample_data_logs",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "timeFieldName": "@timestamp",
                  "title": "kibana_sample_data_logs",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                      "timeField": "@timestamp",
                      "title": "kibana_sample_data_logs"
                    }
                  ],
                  "layers": {
                    "2e3f211d-289f-4a24-87bb-1ccacd678adb": {
                      "columns": [
                        {
                          "columnId": "AVG(machine.ram)",
                          "fieldName": "AVG(machine.ram)",
                          "inMetricDimension": true,
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        },
                        {
                          "columnId": "machine.os.keyword",
                          "fieldName": "machine.os.keyword",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        }
                      ],
                      "index": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                      "query": {
                        "esql": "FROM kibana_sample_data_logs| STATS AVG(machine.ram) BY machine.os.keyword "
                      },
                      "timeField": "@timestamp"
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_logs| STATS AVG(machine.ram) BY machine.os.keyword "
              },
              "visualization": {
                "axisTitlesVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "fittingFunction": "None",
                "gridlinesVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "labelsOrientation": {
                  "x": 0,
                  "yLeft": 0,
                  "yRight": 0
                },
                "layers": [
                  {
                    "accessors": [
                      "AVG(machine.ram)"
                    ],
                    "colorMapping": {
                      "assignments": [],
                      "colorMode": {
                        "type": "categorical"
                      },
                      "paletteId": "eui_amsterdam_color_blind",
                      "specialAssignments": [
                        {
                          "color": {
                            "type": "loop"
                          },
                          "rule": {
                            "type": "other"
                          },
                          "touched": false
                        }
                      ]
                    },
                    "layerId": "2e3f211d-289f-4a24-87bb-1ccacd678adb",
                    "layerType": "data",
                    "seriesType": "bar_stacked",
                    "xAccessor": "machine.os.keyword"
                  }
                ],
                "legend": {
                  "isVisible": true,
                  "position": "right"
                },
                "preferredSeriesType": "bar_stacked",
                "tickLabelsVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "valueLabels": "hide"
              }
            },
            "title": "Bar vertical stacked",
            "type": "lens",
            "visualizationType": "lnsXY"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 24,
          "y": 0
        },
        "type": "lens"
      },
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                  "name": "kibana_sample_data_flights",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "title": "kibana_sample_data_flights",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                      "title": "kibana_sample_data_flights"
                    }
                  ],
                  "layers": {
                    "4451c40f-b3ef-464e-b3d4-b10469f65c2a": {
                      "columns": [
                        {
                          "columnId": "AvgDelayMins",
                          "fieldName": "AvgDelayMins",
                          "inMetricDimension": true,
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        },
                        {
                          "columnId": "Carrier",
                          "fieldName": "Carrier",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        }
                      ],
                      "index": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                      "query": {
                        "esql": "FROM kibana_sample_data_flights| STATS AvgDelayMins = AVG(FlightDelayMin) BY Carrier "
                      }
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_flights| STATS AvgDelayMins = AVG(FlightDelayMin) BY Carrier "
              },
              "visualization": {
                "breakdownByAccessor": "Carrier",
                "layerId": "4451c40f-b3ef-464e-b3d4-b10469f65c2a",
                "layerType": "data",
                "metricAccessor": "AvgDelayMins",
                "palette": {
                  "name": "status",
                  "params": {
                    "colorStops": [],
                    "continuity": "all",
                    "maxSteps": 5,
                    "name": "status",
                    "progression": "fixed",
                    "rangeMax": 100,
                    "rangeMin": 0,
                    "rangeType": "percent",
                    "reverse": false,
                    "steps": 3,
                    "stops": [
                      {
                        "color": "#209280",
                        "stop": 33.33
                      },
                      {
                        "color": "#d6bf57",
                        "stop": 66.66
                      },
                      {
                        "color": "#cc5642",
                        "stop": 100
                      }
                    ]
                  },
                  "type": "palette"
                }
              }
            },
            "title": "Bar vertical stacked",
            "type": "lens",
            "visualizationType": "lnsMetric"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 0,
          "y": 15
        },
        "type": "lens"
      }
    ],
    "timeRestore": false,
    "title": "several es|ql panels",
    "version": 3
  }
}'
Create a dashboard with a Links panel
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "layout": "vertical",
            "links": [
              {
                "destinationRefName": "link_1981a00f-8120-4c80-b37f-ed38969afe09_dashboard",
                "id": "1981a00f-8120-4c80-b37f-ed38969afe09",
                "order": 0,
                "type": "dashboardLink"
              },
              {
                "destinationRefName": "link_f2e1a75c-fbca-4f41-a290-d5d89a60a797_dashboard",
                "id": "f2e1a75c-fbca-4f41-a290-d5d89a60a797",
                "order": 1,
                "type": "dashboardLink"
              },
              {
                "destination": "https://example.com",
                "id": "63342ea6-f686-42b2-a526-ec0bcf4476b0",
                "order": 2,
                "type": "externalLink"
              }
            ]
          },
          "enhancements": {},
          "id": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b"
        },
        "gridData": {
          "h": 7,
          "i": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b",
          "w": 8,
          "x": 0,
          "y": 0
        },
        "panelIndex": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b",
        "type": "links"
      }
    ],
    "timeRestore": false,
    "title": "a links panel",
    "version": 3
  },
  "references": [
    {
      "id": "722b74f0-b882-11e8-a6d9-e546fe2bba5f",
      "name": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b:link_1981a00f-8120-4c80-b37f-ed38969afe09_dashboard",
      "type": "dashboard"
    },
    {
      "id": "edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b",
      "name": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b:link_f2e1a75c-fbca-4f41-a290-d5d89a60a797_dashboard",
      "type": "dashboard"
    }
  ]
}'
Create a dashboard with a Maps panel
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "description": "",
            "layerListJSON": "[{\"locale\":\"autoselect\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true,\"lightModeDefault\":\"road_map_desaturated\"},\"id\":\"db63eee8-3dfc-48c6-8c8b-7f2c4e32329d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"EMS_VECTOR_TILE\",\"color\":\"\"},\"includeInFitToBounds\":true,\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geoip.location\",\"scalingType\":\"MVT\",\"id\":\"9ee192e4-18f0-41b2-b8b7-89eb91d0e529\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"65710bbc-f41c-4fe7-b0c3-a6dbc0613220\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"category.keyword\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"MVT_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
            "mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":1.57,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":60000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":\"males only\",\"index\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"key\":\"customer_gender\",\"field\":\"customer_gender\",\"params\":{\"query\":\"MALE\"},\"type\":\"phrase\"},\"query\":{\"match_phrase\":{\"customer_gender\":\"MALE\"}},\"$state\":{\"store\":\"appState\"}}],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
            "title": "",
            "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"65710bbc-f41c-4fe7-b0c3-a6dbc0613220\"]}"
          },
          "enhancements": {
            "dynamicActions": {
              "events": []
            }
          },
          "hiddenLayers": [],
          "id": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
          "isLayerTOCOpen": false,
          "mapBuffer": {
            "maxLat": 85.05113,
            "maxLon": 180,
            "minLat": -66.51326,
            "minLon": -180
          },
          "mapCenter": {
            "lat": 19.94277,
            "lon": 0,
            "zoom": 1.57
          },
          "openTOCDetails": [
            "65710bbc-f41c-4fe7-b0c3-a6dbc0613220"
          ]
        },
        "gridData": {
          "h": 25,
          "i": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
          "w": 38,
          "x": 0,
          "y": 0
        },
        "panelIndex": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
        "type": "map"
      }
    ],
    "timeRestore": false,
    "title": "a maps panel",
    "version": 3
  },
  "references": [
    {
      "type": "tag",
      "id": "662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "name": "tag-ref-662b28f2-71e4-4c04-b4e5-0c6249b1c08a"
    },
    {
      "name": "108b2f72-0101-4e09-b8a9-22f7aa9573b0:layer_1_source_index_pattern",
      "type": "index-pattern",
      "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f"
    }
  ]
}'
Create a dashboard with a Filter pill and a Field statistics panel
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "description": "",
    "kibanaSavedObjectMeta": {
      "searchSource": {
        "filter": [
          {
            "$state": {
              "store": "appState"
            },
            "meta": {
              "alias": "gnomehouse",
              "disabled": false,
              "field": "products.manufacturer.keyword",
              "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
              "key": "products.manufacturer.keyword",
              "negate": false,
              "params": [
                "Gnomehouse",
                "Gnomehouse mom"
              ],
              "type": "phrases"
            },
            "query": {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "match_phrase": {
                      "products.manufacturer.keyword": "Gnomehouse"
                    }
                  },
                  {
                    "match_phrase": {
                      "products.manufacturer.keyword": "Gnomehouse mom"
                    }
                  }
                ]
              }
            }
          }
        ],
        "query": {
          "language": "kuery",
          "query": ""
        }
      }
    },
    "panels": [
      {
        "panelConfig": {
          "dataViewId": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
          "enhancements": {},
          "id": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
          "query": {
            "esql": "from kibana_sample_data_ecommerce | limit 10"
          },
          "viewType": "esql"
        },
        "gridData": {
          "h": 18,
          "i": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
          "w": 48,
          "x": 0,
          "y": 0
        },
        "panelIndex": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
        "type": "field_stats_table"
      }
    ],
    "timeRestore": false,
    "title": "field stats panel",
    "version": 2
  },
  "references": [
    {
      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
      "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
      "type": "index-pattern"
    },
    {
      "id": "662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "name": "tag-ref-662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "type": "tag"
    },
    {
      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
      "name": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4:fieldStatsTableDataViewId",
      "type": "index-pattern"
    }
  ]
}'
Create a dashboard with a Lens panel
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard/' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "title": "a lens panel",
    "kibanaSavedObjectMeta": {
      "searchSource": {}
    },
    "timeRestore": false,
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "title": "",
            "visualizationType": "lnsDatatable",
            "type": "lens",
            "references": [
              {
                "type": "index-pattern",
                "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
                "name": "indexpattern-datasource-layer-b9789655-f916-4732-9bf2-641a88075210"
              }
            ],
            "state": {
              "visualization": {
                "layerId": "b9789655-f916-4732-9bf2-641a88075210",
                "layerType": "data",
                "columns": [
                  {
                    "isTransposed": false,
                    "columnId": "4175e737-76b9-46db-894b-57106a06b9cb"
                  },
                  {
                    "isTransposed": false,
                    "columnId": "1494f183-3bfa-4602-a780-6a41624f6c69"
                  },
                  {
                    "isTransposed": false,
                    "columnId": "8eb92ea9-5b76-45a2-865e-d78511c1e506"
                  }
                ]
              },
              "query": {
                "query": "",
                "language": "kuery"
              },
              "filters": [],
              "datasourceStates": {
                "formBased": {
                  "layers": {
                    "b9789655-f916-4732-9bf2-641a88075210": {
                      "columns": {
                        "4175e737-76b9-46db-894b-57106a06b9cb": {
                          "label": "Top 5 values of Carrier",
                          "dataType": "string",
                          "operationType": "terms",
                          "scale": "ordinal",
                          "sourceField": "Carrier",
                          "isBucketed": true,
                          "params": {
                            "size": 5,
                            "orderBy": {
                              "type": "column",
                              "columnId": "1494f183-3bfa-4602-a780-6a41624f6c69"
                            },
                            "orderDirection": "desc",
                            "otherBucket": true,
                            "missingBucket": false,
                            "parentFormat": {
                              "id": "terms"
                            },
                            "include": [],
                            "exclude": [],
                            "includeIsRegex": false,
                            "excludeIsRegex": false
                          }
                        },
                        "1494f183-3bfa-4602-a780-6a41624f6c69": {
                          "label": "Count of records",
                          "dataType": "number",
                          "operationType": "count",
                          "isBucketed": false,
                          "scale": "ratio",
                          "sourceField": "___records___",
                          "params": {
                            "emptyAsNull": true
                          }
                        },
                        "8eb92ea9-5b76-45a2-865e-d78511c1e506": {
                          "label": "Median of AvgTicketPrice",
                          "dataType": "number",
                          "operationType": "median",
                          "sourceField": "AvgTicketPrice",
                          "isBucketed": false,
                          "scale": "ratio",
                          "params": {
                            "emptyAsNull": true
                          }
                        }
                      },
                      "columnOrder": [
                        "4175e737-76b9-46db-894b-57106a06b9cb",
                        "1494f183-3bfa-4602-a780-6a41624f6c69",
                        "8eb92ea9-5b76-45a2-865e-d78511c1e506"
                      ],
                      "incompleteColumns": {},
                      "sampling": 1
                    }
                  }
                },
                "indexpattern": {
                  "layers": {}
                },
                "textBased": {
                  "layers": {}
                }
              },
              "internalReferences": [],
              "adHocDataViews": {}
            }
          },
          "enhancements": {}
        },
        "gridData": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "type": "lens"
      }
    ],
    "options": {
      "hidePanelTitles": false,
      "useMargins": true,
      "syncColors": false,
      "syncTooltips": true,
      "syncCursor": true
    },
    "version": 3
  },
  "references": [
    {
      "type": "index-pattern",
      "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
      "name": "indexpattern-datasource-layer-b9789655-f916-4732-9bf2-641a88075210"
    }
  ]
}'
Create a dashboard in a specific Space
curl  -X POST \
  'http://localhost:5601/s/space-1/api/dashboards/dashboard/' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
  "title": "my other demo dashboard",
  "kibanaSavedObjectMeta": {
    "searchSource": {}
  },
  "timeRestore": false,
  "panels": [
    {
      "panelConfig": {
        "savedVis": {
          "description": "",
          "type": "markdown",
          "params": {
            "fontSize": 12,
            "openLinksInNewTab": false,
            "markdown": "## Sample eCommerce Data\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."
          },
          "uiState": {},
          "data": {
            "aggs": [],
            "searchSource": {
              "query": {
                "query": "",
                "language": "kuery"
              },
              "filter": []
            }
          }
        },
        "enhancements": {}
      },
      "gridData": {
        "x": 0,
        "y": 0,
        "w": 24,
        "h": 15,
        "i": "1"
      },
      "type": "visualization",
      "version": "7.9.2"
    }
  ],
  "options": {
    "hidePanelTitles": false,
    "useMargins": true,
    "syncColors": false,
    "syncTooltips": true,
    "syncCursor": true
  },
  "version": 3
  },
  "references": [],
  "spaces": ["space-1"]
}'

Update

Update an existing dashboard
curl  -X PUT \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "title": "my demo dashboard",
    "kibanaSavedObjectMeta": {
      "searchSource": {}
    },
    "timeRestore": false,
    "panels": [
      {
        "panelConfig": {
          "savedVis": {
            "description": "",
            "type": "markdown",
            "params": {
              "fontSize": 12,
              "openLinksInNewTab": false,
              "markdown": "## Sample eCommerce Data\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html).\nWubba lubba dub-dub!"
            },
            "uiState": {},
            "data": {
              "aggs": [],
              "searchSource": {
                "query": {
                  "query": "",
                  "language": "kuery"
                },
                "filter": []
              }
            }
          },
          "enhancements": {}
        },
        "gridData": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "type": "visualization"
      }
    ],
    "version": 3
  },
  "references": []
}'

Get / List

Get a dashboard
curl  -X GET \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
    --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' 
Get a paginated list of dashboards
curl  -X GET \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' 

Delete

Delete a dashboard
curl  -X DELETE \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' 

Open API specification

Retrieve the Open API specification
curl  -X GET \
  'http://localhost:5601/api/oas?pathStartsWith=%2Fapi%2Fdashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' 

This schema is not strongly typed and validation may be too strong without allowing unknowns
@thomasneirynck
Copy link
Contributor

Discussed this offline with @nickpeihl . Overall scope of this PR is good for initial MVP.

A few high level remarks:

  • Schemas for Content and SO should be defined separately. By preserving the link we are cluttering the overall picture.

    • Content-types should not reference Saved Object types (or vice versa!)
    • If we would like to avoid boilerplate in these schema-definitions, we can introduce a new shared/... folder where a base-schema for a dashboard can live. This can then both be imported by the SO and the CM schema definitions.
    • the DashboardStorage module is the only module where both the SO-schema and the CM-schema are imported
  • In this first pass, we should not introduce unnecesary abstractions.

    • define routes explicitly in server-dashboard. Do not create utility code to create those routes.
    • DashboardStorage should not derive from the SOContentStorage-utility type. It should be defined as a simple class, with all CRUD methods implemented in-line.
    • The "vertical" transform from SO<--->Content-type should be implemented in these CRUD-methods of this DashboardStorage class
    • the "horizontal" transform between Content-versions should be satisfied by the up-down CM-transforms
    • It's OK to have some utility functions that do the stringification-destringification and call it where needed (ie. in the "horizontal" CM-version-to-version transforms, "vertical"-CM-to-SO transforms)

Later on, as we create implementations for Maps, Lens, ..., we could envision introducing more abstractions but we should not start with this.

@nickpeihl nickpeihl changed the title [Dashboard] Publish content management schema v3 [Dashboard] Public CRUD API MVP Sep 17, 2024
@nickpeihl nickpeihl added the ci:cloud-deploy Create or update a Cloud deployment label Sep 30, 2024
@nickpeihl nickpeihl requested review from a team September 30, 2024 14:57
@nickpeihl nickpeihl marked this pull request as ready for review September 30, 2024 14:58
@nickpeihl nickpeihl requested review from a team as code owners September 30, 2024 14:58
@nreese nreese self-requested a review September 30, 2024 15:00
Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @nickpeihl , this is a huge step forward and a great precedent for other domain-specific HTTP APIs to follow!

Focused my review on the server-side and in there on the HTTP API and OAS bits.


Couple of post-PR thoughts:

  1. Are we planning on writing a guide for end users somewhere? I think we should mention the new dashboard APIs in our deprecation guide of the SO APIs, perhaps something to chat about in the near future.
  2. Continuing with this line, perhaps a similar mention can be made of GitOps management and by-reference values in guides to the new API: if you want to add a by reference (from lib) Map (for ex) you need to do this via the UI then "get" the JSON?

logger: Logger;
}

function recursiveSortObjectByKeys(obj: unknown): unknown {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, given there can be panel objects in here, how deep can this recursion get? Is this serving a functional purpose of the HTTP API?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very deep! Is there a performance concern with virtually unlimited recursion? If so I'm not opposed to removing this. We don't perform any validation for attributes.panels[0].embeddableConfig, so it would possible for someone to insert a very complex object.

This was kind of a hack to see if we could have a deterministic ordering of keys in a JSON response. This may be useful for GitOps users to get more readable diffs if storing their objects in a git repository. But we could consider leaving that up to the users handle when storing as JSON.

Copy link
Contributor

@jloleysens jloleysens Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Key ordering for GitOps is a valid concern. In my experience recursion in JavaScript, unless bounded in some way, is a latent bug. Perhaps we can keep this but just specify a max recursion depth (hard to pick the "right" nr here) or put this in a try catch to make it best effort?

Could also make sorting an input to the API as a future enhancement? Like a "pretty print" option

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One technique to avoid the stack limit is called trampolining (stack overflow), but yeah don't want to introduce unnecessary complexity here, there are a few options to consider.

const response = await supertest.post('/api/dashboards/create').send({});
expect(response.status).to.be(400);
expect(response.body.statusCode).to.be(400);
expect(response.body.message).to.be('foo');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Foo who? :trollface:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆 still working on tests. 😛

export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('main', () => {
it('can create a dashboard with controls', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have API integration tests for other RUD parts too!

Copy link
Member Author

@nickpeihl nickpeihl Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very "RUDe" of me not to include those tests yet. 😝 They are coming soon.


// Dashboard Content
controlGroupInput: schema.maybe(controlGroupInputSchema),
panels: schema.arrayOf(panelSchema, { defaultValue: [] }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, if I have a dashboard SO in a JSON file compat with (deprecated) SO APIs and I want to create it in a new Kibana, would the migration path be:

  1. delete the " from around the panels value
  2. Point my requests to /api/dashboards/dashboard/{id?}

I know we have thought through this already for existing dashboards: simply "get" from the new APIs and you'll have the transformed object to replace your existing JSON with. Was just curious for the case where we may want to create an object and don't have SO APIs around at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's more involved than that. Below is a table that breaks down the difference between the Dashboard API attributes and Dashboard saved object attributes.

Saved Object key Example SO value Public API key Example API value Reason
panelsJSON “[{\"type\":\"lens\",\"embeddableConfig\":{ … }, … }]” panels [{ “type”:“lens”, “embeddableConfig”: {...}, …}] The panelsJSON value is stringified JSON which has no defined schema or validation. The panels value is a JSON object which can be validated against a defined schema.
optionsJSON "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}" options { "hidePanelTitles": false, "syncColors": false, "syncCursor": true, "syncTooltips": false, "useMargins": true } As with the panelsJSON property, the optionsJSON property is destringified into a JSON object that is validated against a defined schema.
kibanaSavedObject.searchSourceJSON "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" kibanaSavedObject.searchSearch { "query": { "language":"kuery", "query": "" }, "filter": [] } Just like the previous <foo>JSON properties, the searchSource value is the destringified JSON object that can be validated against a defined schema.
controlGroupInput.panelsJSON "{\"612f8db8-9ba9-41cf-a809-d133fe9b83a8\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\", … }” controlGroupInput.panels [ { "grow": true, "order": 0, "type": "optionsListControl", … } ] Unlike the top-level panelsJSON which is stored as an array of configs, the controlGrouptInput.panelsJSON is stored as an object mapping unique ids to config values. For the API, we don’t necessarily want consumers to need to define ids for each control when we could do that for them. So the API schema for the controlGroupInput.panels is an array of configs. Specifying an id in the config is optional and a uuid is generated if the id is not specified. When persisting to the saved object, the array is converted back to the object.
controlGroupInput.ignoreParentSettingsJSON "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}" controlGroupInput.ignoreParentSettings { "ignoreFilters": false, "ignoreQuery": false, "ignoreTimerange": false, "ignoreValidations": false } The ignoreParentSettings value is the destringified JSON object containing boolean options. This allows the object to be validated against a defined schema.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so it might be wise to surface this in some kind of guide... if you're on a new Kibana it might be best to import the objects then GET them from the new endpoints. This could merit a "feature" deprecation in the UA in addition to a docs guide (example)

src/plugins/dashboard/server/api/register_routes.ts Outdated Show resolved Hide resolved
We are tracking this in issue elastic#196609
@nickpeihl nickpeihl added the Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas label Nov 5, 2024
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-presentation (Team:Presentation)

@nickpeihl nickpeihl added release_note:skip Skip the PR/issue when compiling release notes backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) labels Nov 5, 2024
Copy link
Contributor

@nreese nreese left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kibana-presentation changes LGTM. Nice effort. This is a great starting point for a more human understandable API.

@nickpeihl nickpeihl enabled auto-merge (squash) November 5, 2024 19:21
@nickpeihl nickpeihl added the Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. label Nov 6, 2024
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-solution (Team: SecuritySolution)

@elasticmachine
Copy link
Contributor

elasticmachine commented Nov 8, 2024

💚 Build Succeeded

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
dashboard 125 111 -14

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
controls 459.9KB 459.8KB -41.0B
dashboard 646.3KB 646.0KB -307.0B
ml 4.5MB 4.5MB -26.0B
total -374.0B

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
controls 14 15 +1
dashboard 14 13 -1
total -0

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
controls 13.3KB 13.9KB +589.0B
dashboard 51.8KB 51.8KB +40.0B
lens 50.8KB 50.8KB +35.0B
total +664.0B
Unknown metric groups

API count

id before after diff
dashboard 130 114 -16

ESLint disabled line counts

id before after diff
dashboard 16 17 +1

Total ESLint disabled count

id before after diff
dashboard 16 17 +1

History

@nickpeihl nickpeihl merged commit a227021 into elastic:main Nov 8, 2024
52 checks passed
@kibanamachine
Copy link
Contributor

Starting backport for target branches: 8.x

https://github.com/elastic/kibana/actions/runs/11745932479

kibanamachine pushed a commit to kibanamachine/kibana that referenced this pull request Nov 8, 2024
Closes #[192618](elastic#192618)

Adds public CRUD+List endpoints for the Dashboards API.

The schema for the endpoints are generated from Content Management
schemas so that the RPC and Public APIs use the same schemas for CRUD
operations. A new version (v3) has been added to the Dashboards content
management specification that decouples Content from Saved Objects using
a translation layer in Content Management. When retrieving a saved
object the Content Management layer parses and validates the panelJSON,
optionsListJSON, and savedSearchJSON properties against the defines
schema and passes the translated content to the consumer (user interface
or API).

When writing a saved object, the Content Management layer serializes
(`JSON.stringify`) the Content object into the saved object schema. So
the saved object schema continues to store as stringified JSON, but the
user interface and public API see and use the JSON objects.

These planned features are out of scope for this PR and may be added in
subsequent PRs.
1) elastic#192758
2) elastic#192622

Reviewers, please test both UI and endpoints.

# cURL examples:

First, `yarn start --no-base-path`. Assumes `elastic:changeme` is the
username:password.

## Create

<details>
<summary>Create an empty dashboard with the minimum required
properties</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "attributes": { "title": "my empty dashboard" }
  }'
```

</details>

<details>
<summary>Create a dashboard of a specific ID with some ES|QL
panels</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "description": "",
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                  "name": "kibana_sample_data_ecommerce",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "title": "kibana_sample_data_ecommerce",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "title": "kibana_sample_data_ecommerce"
                    }
                  ],
                  "layers": {
                    "44866844-8fca-482a-a769-006e7d029b9b": {
                      "columns": [
                        {
                          "columnId": "6376af5c-fdd1-4d72-a3ec-5686b5049664",
                          "fieldName": "customer_gender",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        },
                        {
                          "columnId": "a2e3e039-dff6-4893-9c9d-9f0a816207dd",
                          "fieldName": "taxless_total_price",
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        }
                      ],
                      "index": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "query": {
                        "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
                      }
                    },
                    "781db49e-f4f1-42e0-975f-7118d2ef7a18": {
                      "columns": [],
                      "index": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "query": {
                        "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
                      }
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
              },
              "visualization": {
                "layers": [
                  {
                    "categoryDisplay": "default",
                    "colorMapping": {
                      "assignments": [],
                      "colorMode": {
                        "type": "categorical"
                      },
                      "paletteId": "eui_amsterdam_color_blind",
                      "specialAssignments": [
                        {
                          "color": {
                            "type": "loop"
                          },
                          "rule": {
                            "type": "other"
                          },
                          "touched": false
                        }
                      ]
                    },
                    "layerId": "44866844-8fca-482a-a769-006e7d029b9b",
                    "layerType": "data",
                    "legendDisplay": "default",
                    "metrics": [
                      "a2e3e039-dff6-4893-9c9d-9f0a816207dd"
                    ],
                    "nestedLegend": false,
                    "numberDisplay": "percent",
                    "primaryGroups": [
                      "6376af5c-fdd1-4d72-a3ec-5686b5049664"
                    ]
                  }
                ],
                "shape": "pie"
              }
            },
            "title": "Table category & category.keyword & currency & customer_first_name & customer_first_name.keyword",
            "type": "lens",
            "visualizationType": "lnsPie"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 0,
          "y": 0
        },
        "type": "lens"
      },
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                  "name": "kibana_sample_data_logs",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "timeFieldName": "@timestamp",
                  "title": "kibana_sample_data_logs",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                      "timeField": "@timestamp",
                      "title": "kibana_sample_data_logs"
                    }
                  ],
                  "layers": {
                    "2e3f211d-289f-4a24-87bb-1ccacd678adb": {
                      "columns": [
                        {
                          "columnId": "AVG(machine.ram)",
                          "fieldName": "AVG(machine.ram)",
                          "inMetricDimension": true,
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        },
                        {
                          "columnId": "machine.os.keyword",
                          "fieldName": "machine.os.keyword",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        }
                      ],
                      "index": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                      "query": {
                        "esql": "FROM kibana_sample_data_logs| STATS AVG(machine.ram) BY machine.os.keyword "
                      },
                      "timeField": "@timestamp"
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_logs| STATS AVG(machine.ram) BY machine.os.keyword "
              },
              "visualization": {
                "axisTitlesVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "fittingFunction": "None",
                "gridlinesVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "labelsOrientation": {
                  "x": 0,
                  "yLeft": 0,
                  "yRight": 0
                },
                "layers": [
                  {
                    "accessors": [
                      "AVG(machine.ram)"
                    ],
                    "colorMapping": {
                      "assignments": [],
                      "colorMode": {
                        "type": "categorical"
                      },
                      "paletteId": "eui_amsterdam_color_blind",
                      "specialAssignments": [
                        {
                          "color": {
                            "type": "loop"
                          },
                          "rule": {
                            "type": "other"
                          },
                          "touched": false
                        }
                      ]
                    },
                    "layerId": "2e3f211d-289f-4a24-87bb-1ccacd678adb",
                    "layerType": "data",
                    "seriesType": "bar_stacked",
                    "xAccessor": "machine.os.keyword"
                  }
                ],
                "legend": {
                  "isVisible": true,
                  "position": "right"
                },
                "preferredSeriesType": "bar_stacked",
                "tickLabelsVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "valueLabels": "hide"
              }
            },
            "title": "Bar vertical stacked",
            "type": "lens",
            "visualizationType": "lnsXY"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 24,
          "y": 0
        },
        "type": "lens"
      },
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                  "name": "kibana_sample_data_flights",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "title": "kibana_sample_data_flights",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                      "title": "kibana_sample_data_flights"
                    }
                  ],
                  "layers": {
                    "4451c40f-b3ef-464e-b3d4-b10469f65c2a": {
                      "columns": [
                        {
                          "columnId": "AvgDelayMins",
                          "fieldName": "AvgDelayMins",
                          "inMetricDimension": true,
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        },
                        {
                          "columnId": "Carrier",
                          "fieldName": "Carrier",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        }
                      ],
                      "index": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                      "query": {
                        "esql": "FROM kibana_sample_data_flights| STATS AvgDelayMins = AVG(FlightDelayMin) BY Carrier "
                      }
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_flights| STATS AvgDelayMins = AVG(FlightDelayMin) BY Carrier "
              },
              "visualization": {
                "breakdownByAccessor": "Carrier",
                "layerId": "4451c40f-b3ef-464e-b3d4-b10469f65c2a",
                "layerType": "data",
                "metricAccessor": "AvgDelayMins",
                "palette": {
                  "name": "status",
                  "params": {
                    "colorStops": [],
                    "continuity": "all",
                    "maxSteps": 5,
                    "name": "status",
                    "progression": "fixed",
                    "rangeMax": 100,
                    "rangeMin": 0,
                    "rangeType": "percent",
                    "reverse": false,
                    "steps": 3,
                    "stops": [
                      {
                        "color": "#209280",
                        "stop": 33.33
                      },
                      {
                        "color": "#d6bf57",
                        "stop": 66.66
                      },
                      {
                        "color": "#cc5642",
                        "stop": 100
                      }
                    ]
                  },
                  "type": "palette"
                }
              }
            },
            "title": "Bar vertical stacked",
            "type": "lens",
            "visualizationType": "lnsMetric"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 0,
          "y": 15
        },
        "type": "lens"
      }
    ],
    "timeRestore": false,
    "title": "several es|ql panels",
    "version": 3
  }
}'
```
</details>

<details>
<summary>Create a dashboard with a Links panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "layout": "vertical",
            "links": [
              {
                "destinationRefName": "link_1981a00f-8120-4c80-b37f-ed38969afe09_dashboard",
                "id": "1981a00f-8120-4c80-b37f-ed38969afe09",
                "order": 0,
                "type": "dashboardLink"
              },
              {
                "destinationRefName": "link_f2e1a75c-fbca-4f41-a290-d5d89a60a797_dashboard",
                "id": "f2e1a75c-fbca-4f41-a290-d5d89a60a797",
                "order": 1,
                "type": "dashboardLink"
              },
              {
                "destination": "https://example.com",
                "id": "63342ea6-f686-42b2-a526-ec0bcf4476b0",
                "order": 2,
                "type": "externalLink"
              }
            ]
          },
          "enhancements": {},
          "id": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b"
        },
        "gridData": {
          "h": 7,
          "i": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b",
          "w": 8,
          "x": 0,
          "y": 0
        },
        "panelIndex": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b",
        "type": "links"
      }
    ],
    "timeRestore": false,
    "title": "a links panel",
    "version": 3
  },
  "references": [
    {
      "id": "722b74f0-b882-11e8-a6d9-e546fe2bba5f",
      "name": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b:link_1981a00f-8120-4c80-b37f-ed38969afe09_dashboard",
      "type": "dashboard"
    },
    {
      "id": "edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b",
      "name": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b:link_f2e1a75c-fbca-4f41-a290-d5d89a60a797_dashboard",
      "type": "dashboard"
    }
  ]
}'
```
</details>

<details>
<summary>Create a dashboard with a Maps panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "description": "",
            "layerListJSON": "[{\"locale\":\"autoselect\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true,\"lightModeDefault\":\"road_map_desaturated\"},\"id\":\"db63eee8-3dfc-48c6-8c8b-7f2c4e32329d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"EMS_VECTOR_TILE\",\"color\":\"\"},\"includeInFitToBounds\":true,\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geoip.location\",\"scalingType\":\"MVT\",\"id\":\"9ee192e4-18f0-41b2-b8b7-89eb91d0e529\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"65710bbc-f41c-4fe7-b0c3-a6dbc0613220\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"category.keyword\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"MVT_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
            "mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":1.57,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":60000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":\"males only\",\"index\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"key\":\"customer_gender\",\"field\":\"customer_gender\",\"params\":{\"query\":\"MALE\"},\"type\":\"phrase\"},\"query\":{\"match_phrase\":{\"customer_gender\":\"MALE\"}},\"$state\":{\"store\":\"appState\"}}],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
            "title": "",
            "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"65710bbc-f41c-4fe7-b0c3-a6dbc0613220\"]}"
          },
          "enhancements": {
            "dynamicActions": {
              "events": []
            }
          },
          "hiddenLayers": [],
          "id": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
          "isLayerTOCOpen": false,
          "mapBuffer": {
            "maxLat": 85.05113,
            "maxLon": 180,
            "minLat": -66.51326,
            "minLon": -180
          },
          "mapCenter": {
            "lat": 19.94277,
            "lon": 0,
            "zoom": 1.57
          },
          "openTOCDetails": [
            "65710bbc-f41c-4fe7-b0c3-a6dbc0613220"
          ]
        },
        "gridData": {
          "h": 25,
          "i": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
          "w": 38,
          "x": 0,
          "y": 0
        },
        "panelIndex": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
        "type": "map"
      }
    ],
    "timeRestore": false,
    "title": "a maps panel",
    "version": 3
  },
  "references": [
    {
      "type": "tag",
      "id": "662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "name": "tag-ref-662b28f2-71e4-4c04-b4e5-0c6249b1c08a"
    },
    {
      "name": "108b2f72-0101-4e09-b8a9-22f7aa9573b0:layer_1_source_index_pattern",
      "type": "index-pattern",
      "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f"
    }
  ]
}'
```

</details>

<details>
<summary>Create a dashboard with a Filter pill and a Field statistics
panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "description": "",
    "kibanaSavedObjectMeta": {
      "searchSource": {
        "filter": [
          {
            "$state": {
              "store": "appState"
            },
            "meta": {
              "alias": "gnomehouse",
              "disabled": false,
              "field": "products.manufacturer.keyword",
              "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
              "key": "products.manufacturer.keyword",
              "negate": false,
              "params": [
                "Gnomehouse",
                "Gnomehouse mom"
              ],
              "type": "phrases"
            },
            "query": {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "match_phrase": {
                      "products.manufacturer.keyword": "Gnomehouse"
                    }
                  },
                  {
                    "match_phrase": {
                      "products.manufacturer.keyword": "Gnomehouse mom"
                    }
                  }
                ]
              }
            }
          }
        ],
        "query": {
          "language": "kuery",
          "query": ""
        }
      }
    },
    "panels": [
      {
        "panelConfig": {
          "dataViewId": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
          "enhancements": {},
          "id": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
          "query": {
            "esql": "from kibana_sample_data_ecommerce | limit 10"
          },
          "viewType": "esql"
        },
        "gridData": {
          "h": 18,
          "i": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
          "w": 48,
          "x": 0,
          "y": 0
        },
        "panelIndex": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
        "type": "field_stats_table"
      }
    ],
    "timeRestore": false,
    "title": "field stats panel",
    "version": 2
  },
  "references": [
    {
      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
      "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
      "type": "index-pattern"
    },
    {
      "id": "662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "name": "tag-ref-662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "type": "tag"
    },
    {
      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
      "name": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4:fieldStatsTableDataViewId",
      "type": "index-pattern"
    }
  ]
}'
```
</details>

<details>
<summary>Create a dashboard with a Lens panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard/' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "title": "a lens panel",
    "kibanaSavedObjectMeta": {
      "searchSource": {}
    },
    "timeRestore": false,
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "title": "",
            "visualizationType": "lnsDatatable",
            "type": "lens",
            "references": [
              {
                "type": "index-pattern",
                "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
                "name": "indexpattern-datasource-layer-b9789655-f916-4732-9bf2-641a88075210"
              }
            ],
            "state": {
              "visualization": {
                "layerId": "b9789655-f916-4732-9bf2-641a88075210",
                "layerType": "data",
                "columns": [
                  {
                    "isTransposed": false,
                    "columnId": "4175e737-76b9-46db-894b-57106a06b9cb"
                  },
                  {
                    "isTransposed": false,
                    "columnId": "1494f183-3bfa-4602-a780-6a41624f6c69"
                  },
                  {
                    "isTransposed": false,
                    "columnId": "8eb92ea9-5b76-45a2-865e-d78511c1e506"
                  }
                ]
              },
              "query": {
                "query": "",
                "language": "kuery"
              },
              "filters": [],
              "datasourceStates": {
                "formBased": {
                  "layers": {
                    "b9789655-f916-4732-9bf2-641a88075210": {
                      "columns": {
                        "4175e737-76b9-46db-894b-57106a06b9cb": {
                          "label": "Top 5 values of Carrier",
                          "dataType": "string",
                          "operationType": "terms",
                          "scale": "ordinal",
                          "sourceField": "Carrier",
                          "isBucketed": true,
                          "params": {
                            "size": 5,
                            "orderBy": {
                              "type": "column",
                              "columnId": "1494f183-3bfa-4602-a780-6a41624f6c69"
                            },
                            "orderDirection": "desc",
                            "otherBucket": true,
                            "missingBucket": false,
                            "parentFormat": {
                              "id": "terms"
                            },
                            "include": [],
                            "exclude": [],
                            "includeIsRegex": false,
                            "excludeIsRegex": false
                          }
                        },
                        "1494f183-3bfa-4602-a780-6a41624f6c69": {
                          "label": "Count of records",
                          "dataType": "number",
                          "operationType": "count",
                          "isBucketed": false,
                          "scale": "ratio",
                          "sourceField": "___records___",
                          "params": {
                            "emptyAsNull": true
                          }
                        },
                        "8eb92ea9-5b76-45a2-865e-d78511c1e506": {
                          "label": "Median of AvgTicketPrice",
                          "dataType": "number",
                          "operationType": "median",
                          "sourceField": "AvgTicketPrice",
                          "isBucketed": false,
                          "scale": "ratio",
                          "params": {
                            "emptyAsNull": true
                          }
                        }
                      },
                      "columnOrder": [
                        "4175e737-76b9-46db-894b-57106a06b9cb",
                        "1494f183-3bfa-4602-a780-6a41624f6c69",
                        "8eb92ea9-5b76-45a2-865e-d78511c1e506"
                      ],
                      "incompleteColumns": {},
                      "sampling": 1
                    }
                  }
                },
                "indexpattern": {
                  "layers": {}
                },
                "textBased": {
                  "layers": {}
                }
              },
              "internalReferences": [],
              "adHocDataViews": {}
            }
          },
          "enhancements": {}
        },
        "gridData": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "type": "lens"
      }
    ],
    "options": {
      "hidePanelTitles": false,
      "useMargins": true,
      "syncColors": false,
      "syncTooltips": true,
      "syncCursor": true
    },
    "version": 3
  },
  "references": [
    {
      "type": "index-pattern",
      "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
      "name": "indexpattern-datasource-layer-b9789655-f916-4732-9bf2-641a88075210"
    }
  ]
}'
```

</details>

<details>
<summary>Create a dashboard in a specific Space</summary>

```
curl  -X POST \
  'http://localhost:5601/s/space-1/api/dashboards/dashboard/' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
  "title": "my other demo dashboard",
  "kibanaSavedObjectMeta": {
    "searchSource": {}
  },
  "timeRestore": false,
  "panels": [
    {
      "panelConfig": {
        "savedVis": {
          "description": "",
          "type": "markdown",
          "params": {
            "fontSize": 12,
            "openLinksInNewTab": false,
            "markdown": "## Sample eCommerce Data\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."
          },
          "uiState": {},
          "data": {
            "aggs": [],
            "searchSource": {
              "query": {
                "query": "",
                "language": "kuery"
              },
              "filter": []
            }
          }
        },
        "enhancements": {}
      },
      "gridData": {
        "x": 0,
        "y": 0,
        "w": 24,
        "h": 15,
        "i": "1"
      },
      "type": "visualization",
      "version": "7.9.2"
    }
  ],
  "options": {
    "hidePanelTitles": false,
    "useMargins": true,
    "syncColors": false,
    "syncTooltips": true,
    "syncCursor": true
  },
  "version": 3
  },
  "references": [],
  "spaces": ["space-1"]
}'
```

</details>

## Update

<details>
<summary>Update an existing dashboard</summary>

```
curl  -X PUT \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "title": "my demo dashboard",
    "kibanaSavedObjectMeta": {
      "searchSource": {}
    },
    "timeRestore": false,
    "panels": [
      {
        "panelConfig": {
          "savedVis": {
            "description": "",
            "type": "markdown",
            "params": {
              "fontSize": 12,
              "openLinksInNewTab": false,
              "markdown": "## Sample eCommerce Data\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html).\nWubba lubba dub-dub!"
            },
            "uiState": {},
            "data": {
              "aggs": [],
              "searchSource": {
                "query": {
                  "query": "",
                  "language": "kuery"
                },
                "filter": []
              }
            }
          },
          "enhancements": {}
        },
        "gridData": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "type": "visualization"
      }
    ],
    "version": 3
  },
  "references": []
}'
```

</details>

## Get / List

<details>
<summary>Get a dashboard</summary>

```
curl  -X GET \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
    --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31'
```
</details>

<details>
<summary>Get a paginated list of dashboards</summary>

```
curl  -X GET \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31'
```
</details>

## Delete
<details>
<summary>Delete a dashboard</summary>

```
curl  -X DELETE \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true'
```

</details>

## Open API specification

<details>
<summary>Retrieve the Open API specification</summary>

```
curl  -X GET \
  'http://localhost:5601/api/oas?pathStartsWith=%2Fapi%2Fdashboard' \
  --user elastic:changeme \
  --header 'Accept: */*'
```

</details>

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit a227021)
@kibanamachine
Copy link
Contributor

💔 All backports failed

Status Branch Result
8.x Could not create pull request: Validation Failed: {"resource":"Issue","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"}

Manual backport

To create the backport manually run:

node scripts/backport --pr 193067

Questions ?

Please refer to the Backport tool documentation

nickpeihl added a commit to nickpeihl/kibana that referenced this pull request Nov 8, 2024
Closes #[192618](elastic#192618)

Adds public CRUD+List endpoints for the Dashboards API.

The schema for the endpoints are generated from Content Management
schemas so that the RPC and Public APIs use the same schemas for CRUD
operations. A new version (v3) has been added to the Dashboards content
management specification that decouples Content from Saved Objects using
a translation layer in Content Management. When retrieving a saved
object the Content Management layer parses and validates the panelJSON,
optionsListJSON, and savedSearchJSON properties against the defines
schema and passes the translated content to the consumer (user interface
or API).

When writing a saved object, the Content Management layer serializes
(`JSON.stringify`) the Content object into the saved object schema. So
the saved object schema continues to store as stringified JSON, but the
user interface and public API see and use the JSON objects.

These planned features are out of scope for this PR and may be added in
subsequent PRs.
1) elastic#192758
2) elastic#192622

Reviewers, please test both UI and endpoints.

# cURL examples:

First, `yarn start --no-base-path`. Assumes `elastic:changeme` is the
username:password.

## Create

<details>
<summary>Create an empty dashboard with the minimum required
properties</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "attributes": { "title": "my empty dashboard" }
  }'
```

</details>

<details>
<summary>Create a dashboard of a specific ID with some ES|QL
panels</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "description": "",
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                  "name": "kibana_sample_data_ecommerce",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "title": "kibana_sample_data_ecommerce",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "title": "kibana_sample_data_ecommerce"
                    }
                  ],
                  "layers": {
                    "44866844-8fca-482a-a769-006e7d029b9b": {
                      "columns": [
                        {
                          "columnId": "6376af5c-fdd1-4d72-a3ec-5686b5049664",
                          "fieldName": "customer_gender",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        },
                        {
                          "columnId": "a2e3e039-dff6-4893-9c9d-9f0a816207dd",
                          "fieldName": "taxless_total_price",
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        }
                      ],
                      "index": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "query": {
                        "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
                      }
                    },
                    "781db49e-f4f1-42e0-975f-7118d2ef7a18": {
                      "columns": [],
                      "index": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
                      "query": {
                        "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
                      }
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_ecommerce | LIMIT 100"
              },
              "visualization": {
                "layers": [
                  {
                    "categoryDisplay": "default",
                    "colorMapping": {
                      "assignments": [],
                      "colorMode": {
                        "type": "categorical"
                      },
                      "paletteId": "eui_amsterdam_color_blind",
                      "specialAssignments": [
                        {
                          "color": {
                            "type": "loop"
                          },
                          "rule": {
                            "type": "other"
                          },
                          "touched": false
                        }
                      ]
                    },
                    "layerId": "44866844-8fca-482a-a769-006e7d029b9b",
                    "layerType": "data",
                    "legendDisplay": "default",
                    "metrics": [
                      "a2e3e039-dff6-4893-9c9d-9f0a816207dd"
                    ],
                    "nestedLegend": false,
                    "numberDisplay": "percent",
                    "primaryGroups": [
                      "6376af5c-fdd1-4d72-a3ec-5686b5049664"
                    ]
                  }
                ],
                "shape": "pie"
              }
            },
            "title": "Table category & category.keyword & currency & customer_first_name & customer_first_name.keyword",
            "type": "lens",
            "visualizationType": "lnsPie"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 0,
          "y": 0
        },
        "type": "lens"
      },
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                  "name": "kibana_sample_data_logs",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "timeFieldName": "@timestamp",
                  "title": "kibana_sample_data_logs",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                      "timeField": "@timestamp",
                      "title": "kibana_sample_data_logs"
                    }
                  ],
                  "layers": {
                    "2e3f211d-289f-4a24-87bb-1ccacd678adb": {
                      "columns": [
                        {
                          "columnId": "AVG(machine.ram)",
                          "fieldName": "AVG(machine.ram)",
                          "inMetricDimension": true,
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        },
                        {
                          "columnId": "machine.os.keyword",
                          "fieldName": "machine.os.keyword",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        }
                      ],
                      "index": "e3465e67bdeced2befff9f9dca7ecf9c48504cad68a10efd881f4c7dd5ade28a",
                      "query": {
                        "esql": "FROM kibana_sample_data_logs| STATS AVG(machine.ram) BY machine.os.keyword "
                      },
                      "timeField": "@timestamp"
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_logs| STATS AVG(machine.ram) BY machine.os.keyword "
              },
              "visualization": {
                "axisTitlesVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "fittingFunction": "None",
                "gridlinesVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "labelsOrientation": {
                  "x": 0,
                  "yLeft": 0,
                  "yRight": 0
                },
                "layers": [
                  {
                    "accessors": [
                      "AVG(machine.ram)"
                    ],
                    "colorMapping": {
                      "assignments": [],
                      "colorMode": {
                        "type": "categorical"
                      },
                      "paletteId": "eui_amsterdam_color_blind",
                      "specialAssignments": [
                        {
                          "color": {
                            "type": "loop"
                          },
                          "rule": {
                            "type": "other"
                          },
                          "touched": false
                        }
                      ]
                    },
                    "layerId": "2e3f211d-289f-4a24-87bb-1ccacd678adb",
                    "layerType": "data",
                    "seriesType": "bar_stacked",
                    "xAccessor": "machine.os.keyword"
                  }
                ],
                "legend": {
                  "isVisible": true,
                  "position": "right"
                },
                "preferredSeriesType": "bar_stacked",
                "tickLabelsVisibilitySettings": {
                  "x": true,
                  "yLeft": true,
                  "yRight": true
                },
                "valueLabels": "hide"
              }
            },
            "title": "Bar vertical stacked",
            "type": "lens",
            "visualizationType": "lnsXY"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 24,
          "y": 0
        },
        "type": "lens"
      },
      {
        "panelConfig": {
          "attributes": {
            "references": [],
            "state": {
              "adHocDataViews": {
                "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03": {
                  "allowHidden": false,
                  "allowNoIndex": false,
                  "fieldFormats": {},
                  "id": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                  "name": "kibana_sample_data_flights",
                  "runtimeFieldMap": {},
                  "sourceFilters": [],
                  "title": "kibana_sample_data_flights",
                  "type": "esql"
                }
              },
              "datasourceStates": {
                "textBased": {
                  "indexPatternRefs": [
                    {
                      "id": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                      "title": "kibana_sample_data_flights"
                    }
                  ],
                  "layers": {
                    "4451c40f-b3ef-464e-b3d4-b10469f65c2a": {
                      "columns": [
                        {
                          "columnId": "AvgDelayMins",
                          "fieldName": "AvgDelayMins",
                          "inMetricDimension": true,
                          "meta": {
                            "esType": "double",
                            "type": "number"
                          }
                        },
                        {
                          "columnId": "Carrier",
                          "fieldName": "Carrier",
                          "meta": {
                            "esType": "keyword",
                            "type": "string"
                          }
                        }
                      ],
                      "index": "5d671714fc025d173ee40f0825b86d59b6e432344593b725be28f1f8f17a8a03",
                      "query": {
                        "esql": "FROM kibana_sample_data_flights| STATS AvgDelayMins = AVG(FlightDelayMin) BY Carrier "
                      }
                    }
                  }
                }
              },
              "filters": [],
              "query": {
                "esql": "FROM kibana_sample_data_flights| STATS AvgDelayMins = AVG(FlightDelayMin) BY Carrier "
              },
              "visualization": {
                "breakdownByAccessor": "Carrier",
                "layerId": "4451c40f-b3ef-464e-b3d4-b10469f65c2a",
                "layerType": "data",
                "metricAccessor": "AvgDelayMins",
                "palette": {
                  "name": "status",
                  "params": {
                    "colorStops": [],
                    "continuity": "all",
                    "maxSteps": 5,
                    "name": "status",
                    "progression": "fixed",
                    "rangeMax": 100,
                    "rangeMin": 0,
                    "rangeType": "percent",
                    "reverse": false,
                    "steps": 3,
                    "stops": [
                      {
                        "color": "#209280",
                        "stop": 33.33
                      },
                      {
                        "color": "#d6bf57",
                        "stop": 66.66
                      },
                      {
                        "color": "#cc5642",
                        "stop": 100
                      }
                    ]
                  },
                  "type": "palette"
                }
              }
            },
            "title": "Bar vertical stacked",
            "type": "lens",
            "visualizationType": "lnsMetric"
          }
        },
        "gridData": {
          "h": 15,
          "w": 24,
          "x": 0,
          "y": 15
        },
        "type": "lens"
      }
    ],
    "timeRestore": false,
    "title": "several es|ql panels",
    "version": 3
  }
}'
```
</details>

<details>
<summary>Create a dashboard with a Links panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "layout": "vertical",
            "links": [
              {
                "destinationRefName": "link_1981a00f-8120-4c80-b37f-ed38969afe09_dashboard",
                "id": "1981a00f-8120-4c80-b37f-ed38969afe09",
                "order": 0,
                "type": "dashboardLink"
              },
              {
                "destinationRefName": "link_f2e1a75c-fbca-4f41-a290-d5d89a60a797_dashboard",
                "id": "f2e1a75c-fbca-4f41-a290-d5d89a60a797",
                "order": 1,
                "type": "dashboardLink"
              },
              {
                "destination": "https://example.com",
                "id": "63342ea6-f686-42b2-a526-ec0bcf4476b0",
                "order": 2,
                "type": "externalLink"
              }
            ]
          },
          "enhancements": {},
          "id": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b"
        },
        "gridData": {
          "h": 7,
          "i": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b",
          "w": 8,
          "x": 0,
          "y": 0
        },
        "panelIndex": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b",
        "type": "links"
      }
    ],
    "timeRestore": false,
    "title": "a links panel",
    "version": 3
  },
  "references": [
    {
      "id": "722b74f0-b882-11e8-a6d9-e546fe2bba5f",
      "name": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b:link_1981a00f-8120-4c80-b37f-ed38969afe09_dashboard",
      "type": "dashboard"
    },
    {
      "id": "edf84fe0-e1a0-11e7-b6d5-4dc382ef7f5b",
      "name": "abbbaedc-62f5-46ee-9d17-8367dcf4f52b:link_f2e1a75c-fbca-4f41-a290-d5d89a60a797_dashboard",
      "type": "dashboard"
    }
  ]
}'
```
</details>

<details>
<summary>Create a dashboard with a Maps panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "description": "",
            "layerListJSON": "[{\"locale\":\"autoselect\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true,\"lightModeDefault\":\"road_map_desaturated\"},\"id\":\"db63eee8-3dfc-48c6-8c8b-7f2c4e32329d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"EMS_VECTOR_TILE\",\"color\":\"\"},\"includeInFitToBounds\":true,\"type\":\"EMS_VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geoip.location\",\"scalingType\":\"MVT\",\"id\":\"9ee192e4-18f0-41b2-b8b7-89eb91d0e529\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"applyForceRefresh\":true,\"filterByMapBounds\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"topHitsGroupByTimeseries\":false,\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"65710bbc-f41c-4fe7-b0c3-a6dbc0613220\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"category.keyword\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelZoomRange\":{\"options\":{\"useLayerZoomRange\":true,\"minZoom\":0,\"maxZoom\":24}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}},\"labelPosition\":{\"options\":{\"position\":\"CENTER\"}}},\"isTimeAware\":true},\"includeInFitToBounds\":true,\"type\":\"MVT_VECTOR\",\"joins\":[],\"disableTooltips\":false}]",
            "mapStateJSON": "{\"adHocDataViews\":[],\"zoom\":1.57,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-7d\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":60000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"disabled\":false,\"negate\":false,\"alias\":\"males only\",\"index\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"key\":\"customer_gender\",\"field\":\"customer_gender\",\"params\":{\"query\":\"MALE\"},\"type\":\"phrase\"},\"query\":{\"match_phrase\":{\"customer_gender\":\"MALE\"}},\"$state\":{\"store\":\"appState\"}}],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"customIcons\":[],\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"keydownScrollZoom\":false,\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"showTimesliderToggleButton\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}",
            "title": "",
            "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"65710bbc-f41c-4fe7-b0c3-a6dbc0613220\"]}"
          },
          "enhancements": {
            "dynamicActions": {
              "events": []
            }
          },
          "hiddenLayers": [],
          "id": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
          "isLayerTOCOpen": false,
          "mapBuffer": {
            "maxLat": 85.05113,
            "maxLon": 180,
            "minLat": -66.51326,
            "minLon": -180
          },
          "mapCenter": {
            "lat": 19.94277,
            "lon": 0,
            "zoom": 1.57
          },
          "openTOCDetails": [
            "65710bbc-f41c-4fe7-b0c3-a6dbc0613220"
          ]
        },
        "gridData": {
          "h": 25,
          "i": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
          "w": 38,
          "x": 0,
          "y": 0
        },
        "panelIndex": "108b2f72-0101-4e09-b8a9-22f7aa9573b0",
        "type": "map"
      }
    ],
    "timeRestore": false,
    "title": "a maps panel",
    "version": 3
  },
  "references": [
    {
      "type": "tag",
      "id": "662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "name": "tag-ref-662b28f2-71e4-4c04-b4e5-0c6249b1c08a"
    },
    {
      "name": "108b2f72-0101-4e09-b8a9-22f7aa9573b0:layer_1_source_index_pattern",
      "type": "index-pattern",
      "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f"
    }
  ]
}'
```

</details>

<details>
<summary>Create a dashboard with a Filter pill and a Field statistics
panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "description": "",
    "kibanaSavedObjectMeta": {
      "searchSource": {
        "filter": [
          {
            "$state": {
              "store": "appState"
            },
            "meta": {
              "alias": "gnomehouse",
              "disabled": false,
              "field": "products.manufacturer.keyword",
              "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
              "key": "products.manufacturer.keyword",
              "negate": false,
              "params": [
                "Gnomehouse",
                "Gnomehouse mom"
              ],
              "type": "phrases"
            },
            "query": {
              "bool": {
                "minimum_should_match": 1,
                "should": [
                  {
                    "match_phrase": {
                      "products.manufacturer.keyword": "Gnomehouse"
                    }
                  },
                  {
                    "match_phrase": {
                      "products.manufacturer.keyword": "Gnomehouse mom"
                    }
                  }
                ]
              }
            }
          }
        ],
        "query": {
          "language": "kuery",
          "query": ""
        }
      }
    },
    "panels": [
      {
        "panelConfig": {
          "dataViewId": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
          "enhancements": {},
          "id": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
          "query": {
            "esql": "from kibana_sample_data_ecommerce | limit 10"
          },
          "viewType": "esql"
        },
        "gridData": {
          "h": 18,
          "i": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
          "w": 48,
          "x": 0,
          "y": 0
        },
        "panelIndex": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4",
        "type": "field_stats_table"
      }
    ],
    "timeRestore": false,
    "title": "field stats panel",
    "version": 2
  },
  "references": [
    {
      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
      "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
      "type": "index-pattern"
    },
    {
      "id": "662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "name": "tag-ref-662b28f2-71e4-4c04-b4e5-0c6249b1c08a",
      "type": "tag"
    },
    {
      "id": "32eec79c9673ab1b9265f3e422e8f952778f02c82eaf13147a9c0ba86290337a",
      "name": "3c9dee70-4a01-4c2f-9ccd-0c2812e2a5d4:fieldStatsTableDataViewId",
      "type": "index-pattern"
    }
  ]
}'
```
</details>

<details>
<summary>Create a dashboard with a Lens panel</summary>

```
curl  -X POST \
  'http://localhost:5601/api/dashboards/dashboard/' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "title": "a lens panel",
    "kibanaSavedObjectMeta": {
      "searchSource": {}
    },
    "timeRestore": false,
    "panels": [
      {
        "panelConfig": {
          "attributes": {
            "title": "",
            "visualizationType": "lnsDatatable",
            "type": "lens",
            "references": [
              {
                "type": "index-pattern",
                "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
                "name": "indexpattern-datasource-layer-b9789655-f916-4732-9bf2-641a88075210"
              }
            ],
            "state": {
              "visualization": {
                "layerId": "b9789655-f916-4732-9bf2-641a88075210",
                "layerType": "data",
                "columns": [
                  {
                    "isTransposed": false,
                    "columnId": "4175e737-76b9-46db-894b-57106a06b9cb"
                  },
                  {
                    "isTransposed": false,
                    "columnId": "1494f183-3bfa-4602-a780-6a41624f6c69"
                  },
                  {
                    "isTransposed": false,
                    "columnId": "8eb92ea9-5b76-45a2-865e-d78511c1e506"
                  }
                ]
              },
              "query": {
                "query": "",
                "language": "kuery"
              },
              "filters": [],
              "datasourceStates": {
                "formBased": {
                  "layers": {
                    "b9789655-f916-4732-9bf2-641a88075210": {
                      "columns": {
                        "4175e737-76b9-46db-894b-57106a06b9cb": {
                          "label": "Top 5 values of Carrier",
                          "dataType": "string",
                          "operationType": "terms",
                          "scale": "ordinal",
                          "sourceField": "Carrier",
                          "isBucketed": true,
                          "params": {
                            "size": 5,
                            "orderBy": {
                              "type": "column",
                              "columnId": "1494f183-3bfa-4602-a780-6a41624f6c69"
                            },
                            "orderDirection": "desc",
                            "otherBucket": true,
                            "missingBucket": false,
                            "parentFormat": {
                              "id": "terms"
                            },
                            "include": [],
                            "exclude": [],
                            "includeIsRegex": false,
                            "excludeIsRegex": false
                          }
                        },
                        "1494f183-3bfa-4602-a780-6a41624f6c69": {
                          "label": "Count of records",
                          "dataType": "number",
                          "operationType": "count",
                          "isBucketed": false,
                          "scale": "ratio",
                          "sourceField": "___records___",
                          "params": {
                            "emptyAsNull": true
                          }
                        },
                        "8eb92ea9-5b76-45a2-865e-d78511c1e506": {
                          "label": "Median of AvgTicketPrice",
                          "dataType": "number",
                          "operationType": "median",
                          "sourceField": "AvgTicketPrice",
                          "isBucketed": false,
                          "scale": "ratio",
                          "params": {
                            "emptyAsNull": true
                          }
                        }
                      },
                      "columnOrder": [
                        "4175e737-76b9-46db-894b-57106a06b9cb",
                        "1494f183-3bfa-4602-a780-6a41624f6c69",
                        "8eb92ea9-5b76-45a2-865e-d78511c1e506"
                      ],
                      "incompleteColumns": {},
                      "sampling": 1
                    }
                  }
                },
                "indexpattern": {
                  "layers": {}
                },
                "textBased": {
                  "layers": {}
                }
              },
              "internalReferences": [],
              "adHocDataViews": {}
            }
          },
          "enhancements": {}
        },
        "gridData": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "type": "lens"
      }
    ],
    "options": {
      "hidePanelTitles": false,
      "useMargins": true,
      "syncColors": false,
      "syncTooltips": true,
      "syncCursor": true
    },
    "version": 3
  },
  "references": [
    {
      "type": "index-pattern",
      "id": "d3d7af60-4c81-11e8-b3d7-01146121b73d",
      "name": "indexpattern-datasource-layer-b9789655-f916-4732-9bf2-641a88075210"
    }
  ]
}'
```

</details>

<details>
<summary>Create a dashboard in a specific Space</summary>

```
curl  -X POST \
  'http://localhost:5601/s/space-1/api/dashboards/dashboard/' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
  "title": "my other demo dashboard",
  "kibanaSavedObjectMeta": {
    "searchSource": {}
  },
  "timeRestore": false,
  "panels": [
    {
      "panelConfig": {
        "savedVis": {
          "description": "",
          "type": "markdown",
          "params": {
            "fontSize": 12,
            "openLinksInNewTab": false,
            "markdown": "## Sample eCommerce Data\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."
          },
          "uiState": {},
          "data": {
            "aggs": [],
            "searchSource": {
              "query": {
                "query": "",
                "language": "kuery"
              },
              "filter": []
            }
          }
        },
        "enhancements": {}
      },
      "gridData": {
        "x": 0,
        "y": 0,
        "w": 24,
        "h": 15,
        "i": "1"
      },
      "type": "visualization",
      "version": "7.9.2"
    }
  ],
  "options": {
    "hidePanelTitles": false,
    "useMargins": true,
    "syncColors": false,
    "syncTooltips": true,
    "syncCursor": true
  },
  "version": 3
  },
  "references": [],
  "spaces": ["space-1"]
}'
```

</details>

## Update

<details>
<summary>Update an existing dashboard</summary>

```
curl  -X PUT \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "attributes": {
    "title": "my demo dashboard",
    "kibanaSavedObjectMeta": {
      "searchSource": {}
    },
    "timeRestore": false,
    "panels": [
      {
        "panelConfig": {
          "savedVis": {
            "description": "",
            "type": "markdown",
            "params": {
              "fontSize": 12,
              "openLinksInNewTab": false,
              "markdown": "## Sample eCommerce Data\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html).\nWubba lubba dub-dub!"
            },
            "uiState": {},
            "data": {
              "aggs": [],
              "searchSource": {
                "query": {
                  "query": "",
                  "language": "kuery"
                },
                "filter": []
              }
            }
          },
          "enhancements": {}
        },
        "gridData": {
          "x": 0,
          "y": 0,
          "w": 24,
          "h": 15
        },
        "type": "visualization"
      }
    ],
    "version": 3
  },
  "references": []
}'
```

</details>

## Get / List

<details>
<summary>Get a dashboard</summary>

```
curl  -X GET \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
    --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31'
```
</details>

<details>
<summary>Get a paginated list of dashboards</summary>

```
curl  -X GET \
  'http://localhost:5601/api/dashboards/dashboard' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31'
```
</details>

## Delete
<details>
<summary>Delete a dashboard</summary>

```
curl  -X DELETE \
  'http://localhost:5601/api/dashboards/dashboard/foo-123' \
  --user elastic:changeme \
  --header 'Accept: */*' \
  --header 'elastic-api-version: 2023-10-31' \
  --header 'kbn-xsrf: true'
```

</details>

## Open API specification

<details>
<summary>Retrieve the Open API specification</summary>

```
curl  -X GET \
  'http://localhost:5601/api/oas?pathStartsWith=%2Fapi%2Fdashboard' \
  --user elastic:changeme \
  --header 'Accept: */*'
```

</details>

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit a227021)
nickpeihl added a commit that referenced this pull request Nov 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) ci:cloud-deploy Create or update a Cloud deployment ci:project-deploy-observability Create an Observability project release_note:skip Skip the PR/issue when compiling release notes Team:obs-ux-infra_services Observability Infrastructure & Services User Experience Team Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v8.17.0 v9.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.