-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Dashboard] Public CRUD API MVP #193067
[Dashboard] Public CRUD API MVP #193067
Conversation
src/plugins/dashboard/server/content_management/schema/v3/cm_services.ts
Outdated
Show resolved
Hide resolved
packages/kbn-content-management-utils/src/saved_object_content_storage.ts
Outdated
Show resolved
Hide resolved
This schema is not strongly typed and validation may be too strong without allowing unknowns
Discussed this offline with @nickpeihl . Overall scope of this PR is good for initial MVP. A few high level remarks:
Later on, as we create implementations for Maps, Lens, ..., we could envision introducing more abstractions but we should not start with this. |
🤦 This one line broke only Lens panels for some reason.
src/plugins/controls/public/control_group/init_controls_manager.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- 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.
- 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Foo who?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😆 still working on tests. 😛
export default function ({ getService }: FtrProviderContext) { | ||
const supertest = getService('supertest'); | ||
describe('main', () => { | ||
it('can create a dashboard with controls', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have API integration tests for other RUD parts too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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: [] }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- delete the
"
from around the panels value - 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
We are tracking this in issue elastic#196609
Pinging @elastic/kibana-presentation (Team:Presentation) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kibana-presentation changes LGTM. Nice effort. This is a great starting point for a more human understandable API.
Pinging @elastic/security-solution (Team: SecuritySolution) |
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
Async chunks
Public APIs missing exports
Page load bundle
Unknown metric groupsAPI count
ESLint disabled line counts
Total ESLint disabled count
History
|
Starting backport for target branches: 8.x https://github.com/elastic/kibana/actions/runs/11745932479 |
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)
💔 All backports failed
Manual backportTo create the backport manually run:
Questions ?Please refer to the Backport tool documentation |
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)
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.
Reviewers, please test both UI and endpoints.
cURL examples:
First,
yarn start --no-base-path
. Assumeselastic:changeme
is the username:password.Create
Create an empty dashboard with the minimum required properties
Create a dashboard of a specific ID with some ES|QL panels
Create a dashboard with a Links panel
Create a dashboard with a Maps panel
Create a dashboard with a Filter pill and a Field statistics panel
Create a dashboard with a Lens panel
Create a dashboard in a specific Space
Update
Update an existing dashboard
Get / List
Get a dashboard
Get a paginated list of dashboards
Delete
Delete a dashboard
Open API specification
Retrieve the Open API specification