mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Dashboard] Public CRUD API MVP (#193067)
Closes #[192618](https://github.com/elastic/kibana/issues/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) https://github.com/elastic/kibana/issues/192758 2) https://github.com/elastic/kibana/issues/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 <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4b6b1c3eff
commit
a227021302
117 changed files with 4056 additions and 682 deletions
|
@ -7,11 +7,25 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ControlGroupChainingSystem } from './control_group';
|
||||
import { ControlLabelPosition, ControlWidth } from './types';
|
||||
|
||||
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'medium';
|
||||
export const CONTROL_WIDTH_OPTIONS = { SMALL: 'small', MEDIUM: 'medium', LARGE: 'large' } as const;
|
||||
export const CONTROL_LABEL_POSITION_OPTIONS = { ONE_LINE: 'oneLine', TWO_LINE: 'twoLine' } as const;
|
||||
export const CONTROL_CHAINING_OPTIONS = { NONE: 'NONE', HIERARCHICAL: 'HIERARCHICAL' } as const;
|
||||
export const DEFAULT_CONTROL_WIDTH: ControlWidth = CONTROL_WIDTH_OPTIONS.MEDIUM;
|
||||
export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition =
|
||||
CONTROL_LABEL_POSITION_OPTIONS.ONE_LINE;
|
||||
export const DEFAULT_CONTROL_GROW: boolean = true;
|
||||
export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition = 'oneLine';
|
||||
export const DEFAULT_CONTROL_CHAINING: ControlGroupChainingSystem =
|
||||
CONTROL_CHAINING_OPTIONS.HIERARCHICAL;
|
||||
export const DEFAULT_IGNORE_PARENT_SETTINGS = {
|
||||
ignoreFilters: false,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
} as const;
|
||||
export const DEFAULT_AUTO_APPLY_SELECTIONS = true;
|
||||
|
||||
export const TIME_SLIDER_CONTROL = 'timeSlider';
|
||||
export const RANGE_SLIDER_CONTROL = 'rangeSliderControl';
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { ControlLabelPosition, DefaultControlState, ParentIgnoreSettings } from '../types';
|
||||
import { CONTROL_CHAINING_OPTIONS } from '../constants';
|
||||
|
||||
export const CONTROL_GROUP_TYPE = 'control_group';
|
||||
|
||||
export type ControlGroupChainingSystem = 'HIERARCHICAL' | 'NONE';
|
||||
export type ControlGroupChainingSystem =
|
||||
(typeof CONTROL_CHAINING_OPTIONS)[keyof typeof CONTROL_CHAINING_OPTIONS];
|
||||
|
||||
export type FieldFilterPredicate = (f: DataViewField) => boolean;
|
||||
|
||||
|
@ -45,15 +47,11 @@ export interface ControlGroupRuntimeState<State extends DefaultControlState = De
|
|||
}
|
||||
|
||||
export interface ControlGroupSerializedState
|
||||
extends Pick<ControlGroupRuntimeState, 'chainingSystem' | 'editorConfig'> {
|
||||
panelsJSON: string; // stringified version of ControlSerializedState
|
||||
ignoreParentSettingsJSON: string;
|
||||
// In runtime state, we refer to this property as `labelPosition`;
|
||||
// to avoid migrations, we will continue to refer to this property as `controlStyle` in the serialized state
|
||||
controlStyle: ControlLabelPosition;
|
||||
// In runtime state, we refer to the inverse of this property as `autoApplySelections`
|
||||
// to avoid migrations, we will continue to refer to this property as `showApplySelections` in the serialized state
|
||||
showApplySelections?: boolean;
|
||||
extends Omit<ControlGroupRuntimeState, 'initialChildControlState'> {
|
||||
// In runtime state, we refer to this property as `initialChildControlState`, but in
|
||||
// the serialized state we transform the state object into an array of state objects
|
||||
// to make it easier for API consumers to add new controls without specifying a uuid key.
|
||||
controls: Array<ControlPanelState & { id?: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,9 +17,15 @@ export type {
|
|||
} from './types';
|
||||
|
||||
export {
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_GROW,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
CONTROL_WIDTH_OPTIONS,
|
||||
CONTROL_CHAINING_OPTIONS,
|
||||
CONTROL_LABEL_POSITION_OPTIONS,
|
||||
OPTIONS_LIST_CONTROL,
|
||||
RANGE_SLIDER_CONTROL,
|
||||
TIME_SLIDER_CONTROL,
|
||||
|
|
|
@ -7,12 +7,16 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type ControlWidth = 'small' | 'medium' | 'large';
|
||||
export type ControlLabelPosition = 'twoLine' | 'oneLine';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { CONTROL_LABEL_POSITION_OPTIONS, CONTROL_WIDTH_OPTIONS } from './constants';
|
||||
|
||||
export type ControlWidth = (typeof CONTROL_WIDTH_OPTIONS)[keyof typeof CONTROL_WIDTH_OPTIONS];
|
||||
export type ControlLabelPosition =
|
||||
(typeof CONTROL_LABEL_POSITION_OPTIONS)[keyof typeof CONTROL_LABEL_POSITION_OPTIONS];
|
||||
|
||||
export type TimeSlice = [number, number];
|
||||
|
||||
export interface ParentIgnoreSettings {
|
||||
export interface ParentIgnoreSettings extends SerializableRecord {
|
||||
ignoreFilters?: boolean;
|
||||
ignoreQuery?: boolean;
|
||||
ignoreTimerange?: boolean;
|
||||
|
|
|
@ -19,8 +19,11 @@ import { useSearchApi, type ViewMode as ViewModeType } from '@kbn/presentation-p
|
|||
import type { ControlGroupApi } from '../..';
|
||||
import {
|
||||
CONTROL_GROUP_TYPE,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
type ControlGroupRuntimeState,
|
||||
type ControlGroupSerializedState,
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
} from '../../../common';
|
||||
import {
|
||||
type ControlGroupStateBuilder,
|
||||
|
@ -136,16 +139,19 @@ export const ControlGroupRenderer = ({
|
|||
...initialState,
|
||||
editorConfig,
|
||||
});
|
||||
const state = {
|
||||
...omit(initialState, ['initialChildControlState', 'ignoreParentSettings']),
|
||||
const state: ControlGroupSerializedState = {
|
||||
...omit(initialState, ['initialChildControlState']),
|
||||
editorConfig,
|
||||
controlStyle: initialState?.labelPosition,
|
||||
panelsJSON: JSON.stringify(initialState?.initialChildControlState ?? {}),
|
||||
ignoreParentSettingsJSON: JSON.stringify(initialState?.ignoreParentSettings ?? {}),
|
||||
autoApplySelections: initialState?.autoApplySelections ?? DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
labelPosition: initialState?.labelPosition ?? DEFAULT_CONTROL_LABEL_POSITION,
|
||||
chainingSystem: initialState?.chainingSystem ?? DEFAULT_CONTROL_CHAINING,
|
||||
controls: Object.entries(initialState?.initialChildControlState ?? {}).map(
|
||||
([controlId, value]) => ({ ...value, id: controlId })
|
||||
),
|
||||
};
|
||||
|
||||
if (!cancelled) {
|
||||
setSerializedState(state as ControlGroupSerializedState);
|
||||
setSerializedState(state);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
|
|
|
@ -33,7 +33,11 @@ import type {
|
|||
ControlPanelsState,
|
||||
ParentIgnoreSettings,
|
||||
} from '../../common';
|
||||
import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_LABEL_POSITION } from '../../common';
|
||||
import {
|
||||
CONTROL_GROUP_TYPE,
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
} from '../../common';
|
||||
import { openDataControlEditor } from '../controls/data_controls/open_data_control_editor';
|
||||
import { coreServices, dataViewsService } from '../services/kibana_services';
|
||||
import { ControlGroup } from './components/control_group';
|
||||
|
@ -45,8 +49,6 @@ import { initSelectionsManager } from './selections_manager';
|
|||
import type { ControlGroupApi } from './types';
|
||||
import { deserializeControlGroup } from './utils/serialization_utils';
|
||||
|
||||
const DEFAULT_CHAINING_SYSTEM = 'HIERARCHICAL';
|
||||
|
||||
export const getControlGroupEmbeddableFactory = () => {
|
||||
const controlGroupEmbeddableFactory: ReactEmbeddableFactory<
|
||||
ControlGroupSerializedState,
|
||||
|
@ -85,7 +87,7 @@ export const getControlGroupEmbeddableFactory = () => {
|
|||
});
|
||||
const dataViews = new BehaviorSubject<DataView[] | undefined>(undefined);
|
||||
const chainingSystem$ = new BehaviorSubject<ControlGroupChainingSystem>(
|
||||
chainingSystem ?? DEFAULT_CHAINING_SYSTEM
|
||||
chainingSystem ?? DEFAULT_CONTROL_CHAINING
|
||||
);
|
||||
const ignoreParentSettings$ = new BehaviorSubject<ParentIgnoreSettings | undefined>(
|
||||
ignoreParentSettings
|
||||
|
@ -108,7 +110,7 @@ export const getControlGroupEmbeddableFactory = () => {
|
|||
chainingSystem: [
|
||||
chainingSystem$,
|
||||
(next: ControlGroupChainingSystem) => chainingSystem$.next(next),
|
||||
(a, b) => (a ?? DEFAULT_CHAINING_SYSTEM) === (b ?? DEFAULT_CHAINING_SYSTEM),
|
||||
(a, b) => (a ?? DEFAULT_CONTROL_CHAINING) === (b ?? DEFAULT_CONTROL_CHAINING),
|
||||
],
|
||||
ignoreParentSettings: [
|
||||
ignoreParentSettings$,
|
||||
|
@ -187,14 +189,14 @@ export const getControlGroupEmbeddableFactory = () => {
|
|||
});
|
||||
},
|
||||
serializeState: () => {
|
||||
const { panelsJSON, references } = controlsManager.serializeControls();
|
||||
const { controls, references } = controlsManager.serializeControls();
|
||||
return {
|
||||
rawState: {
|
||||
chainingSystem: chainingSystem$.getValue(),
|
||||
controlStyle: labelPosition$.getValue(),
|
||||
showApplySelections: !autoApplySelections$.getValue(),
|
||||
ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings$.getValue()),
|
||||
panelsJSON,
|
||||
labelPosition: labelPosition$.getValue(),
|
||||
autoApplySelections: autoApplySelections$.getValue(),
|
||||
ignoreParentSettings: ignoreParentSettings$.getValue(),
|
||||
controls,
|
||||
},
|
||||
references,
|
||||
};
|
||||
|
|
|
@ -147,9 +147,8 @@ export function initControlsManager(
|
|||
},
|
||||
serializeControls: () => {
|
||||
const references: Reference[] = [];
|
||||
const explicitInputPanels: {
|
||||
[panelId: string]: ControlPanelState & { explicitInput: object };
|
||||
} = {};
|
||||
|
||||
const controls: Array<ControlPanelState & { controlConfig: object }> = [];
|
||||
|
||||
controlsInOrder$.getValue().forEach(({ id }, index) => {
|
||||
const controlApi = getControlApi(id);
|
||||
|
@ -166,18 +165,18 @@ export function initControlsManager(
|
|||
references.push(...controlReferences);
|
||||
}
|
||||
|
||||
explicitInputPanels[id] = {
|
||||
controls.push({
|
||||
grow,
|
||||
order: index,
|
||||
type: controlApi.type,
|
||||
width,
|
||||
/** Re-add the `explicitInput` layer on serialize so control group saved object retains shape */
|
||||
explicitInput: { id, ...rest },
|
||||
};
|
||||
/** Re-add the `controlConfig` layer on serialize so control group saved object retains shape */
|
||||
controlConfig: { id, ...rest },
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
panelsJSON: JSON.stringify(explicitInputPanels),
|
||||
controls,
|
||||
references,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -7,17 +7,18 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState } from '../../../common';
|
||||
import {
|
||||
type ControlGroupRuntimeState,
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
} from '../../../common';
|
||||
|
||||
export const getDefaultControlGroupRuntimeState = (): ControlGroupRuntimeState => ({
|
||||
initialChildControlState: {},
|
||||
labelPosition: DEFAULT_CONTROL_LABEL_POSITION,
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
autoApplySelections: true,
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: false,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
},
|
||||
chainingSystem: DEFAULT_CONTROL_CHAINING,
|
||||
autoApplySelections: DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
ignoreParentSettings: DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
});
|
||||
|
|
|
@ -16,37 +16,31 @@ import { parseReferenceName } from '../../controls/data_controls/reference_name_
|
|||
export const deserializeControlGroup = (
|
||||
state: SerializedPanelState<ControlGroupSerializedState>
|
||||
): ControlGroupRuntimeState => {
|
||||
const panels = JSON.parse(state.rawState.panelsJSON);
|
||||
const ignoreParentSettings = JSON.parse(state.rawState.ignoreParentSettingsJSON);
|
||||
const { controls } = state.rawState;
|
||||
const controlsMap = Object.fromEntries(controls.map(({ id, ...rest }) => [id, rest]));
|
||||
|
||||
/** Inject data view references into each individual control */
|
||||
const references = state.references ?? [];
|
||||
references.forEach((reference) => {
|
||||
const referenceName = reference.name;
|
||||
const { controlId } = parseReferenceName(referenceName);
|
||||
if (panels[controlId]) {
|
||||
panels[controlId].dataViewId = reference.id;
|
||||
if (controlsMap[controlId]) {
|
||||
controlsMap[controlId].dataViewId = reference.id;
|
||||
}
|
||||
});
|
||||
|
||||
/** Flatten the state of each panel by removing `explicitInput` */
|
||||
const flattenedPanels = Object.keys(panels).reduce((prev, panelId) => {
|
||||
const currentPanel = panels[panelId];
|
||||
const currentPanelExplicitInput = panels[panelId].explicitInput;
|
||||
/** Flatten the state of each control by removing `controlConfig` */
|
||||
const flattenedControls = Object.keys(controlsMap).reduce((prev, controlId) => {
|
||||
const currentControl = controlsMap[controlId];
|
||||
const currentControlExplicitInput = controlsMap[controlId].controlConfig;
|
||||
return {
|
||||
...prev,
|
||||
[panelId]: { ...omit(currentPanel, 'explicitInput'), ...currentPanelExplicitInput },
|
||||
[controlId]: { ...omit(currentControl, 'controlConfig'), ...currentControlExplicitInput },
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...omit(state.rawState, ['panelsJSON', 'ignoreParentSettingsJSON']),
|
||||
initialChildControlState: flattenedPanels,
|
||||
ignoreParentSettings,
|
||||
autoApplySelections:
|
||||
typeof state.rawState.showApplySelections === 'boolean'
|
||||
? !state.rawState.showApplySelections
|
||||
: true, // Rename "showApplySelections" to "autoApplySelections"
|
||||
labelPosition: state.rawState.controlStyle, // Rename "controlStyle" to "labelPosition"
|
||||
...state.rawState,
|
||||
initialChildControlState: flattenedControls,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -18,10 +18,8 @@ import {
|
|||
import { OptionsListControlState } from '../../common/options_list';
|
||||
import { mockDataControlState, mockOptionsListControlState } from '../mocks';
|
||||
import { removeHideExcludeAndHideExists } from './control_group_migrations';
|
||||
import {
|
||||
SerializableControlGroupState,
|
||||
getDefaultControlGroupState,
|
||||
} from './control_group_persistence';
|
||||
import { getDefaultControlGroupState } from './control_group_persistence';
|
||||
import type { SerializableControlGroupState } from './types';
|
||||
|
||||
describe('migrate control group', () => {
|
||||
const getOptionsListControl = (
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
type SerializedControlState,
|
||||
} from '../../common';
|
||||
import { OptionsListControlState } from '../../common/options_list';
|
||||
import { SerializableControlGroupState } from './control_group_persistence';
|
||||
import { SerializableControlGroupState } from './types';
|
||||
|
||||
export const makeControlOrdersZeroBased = (state: SerializableControlGroupState) => {
|
||||
if (
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
makeControlOrdersZeroBased,
|
||||
removeHideExcludeAndHideExists,
|
||||
} from './control_group_migrations';
|
||||
import type { SerializableControlGroupState } from './control_group_persistence';
|
||||
import { SerializableControlGroupState } from './types';
|
||||
|
||||
const getPanelStatePrefix = (state: SerializedControlState) => `${state.explicitInput.id}:`;
|
||||
|
||||
|
|
|
@ -9,37 +9,22 @@
|
|||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import { ControlGroupSavedObjectState, SerializableControlGroupState } from './types';
|
||||
import {
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
type ControlGroupRuntimeState,
|
||||
type ControlGroupSerializedState,
|
||||
type ControlPanelState,
|
||||
type SerializedControlState,
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
} from '../../common';
|
||||
|
||||
export const getDefaultControlGroupState = (): SerializableControlGroupState => ({
|
||||
panels: {},
|
||||
labelPosition: DEFAULT_CONTROL_LABEL_POSITION,
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
autoApplySelections: true,
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: false,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
},
|
||||
chainingSystem: DEFAULT_CONTROL_CHAINING,
|
||||
autoApplySelections: DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
ignoreParentSettings: DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
});
|
||||
|
||||
// using SerializableRecord to force type to be read as serializable
|
||||
export type SerializableControlGroupState = SerializableRecord &
|
||||
Omit<
|
||||
ControlGroupRuntimeState,
|
||||
'initialChildControlState' | 'ignoreParentSettings' | 'editorConfig' // editor config is not persisted
|
||||
> & {
|
||||
ignoreParentSettings: Record<string, boolean>;
|
||||
panels: Record<string, ControlPanelState<SerializedControlState>> | {};
|
||||
};
|
||||
|
||||
const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
|
||||
if (!jsonString && typeof jsonString !== 'string') return;
|
||||
try {
|
||||
|
@ -49,22 +34,26 @@ const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
|
|||
}
|
||||
};
|
||||
|
||||
export const controlGroupSerializedStateToSerializableRuntimeState = (
|
||||
serializedState: ControlGroupSerializedState
|
||||
export const controlGroupSavedObjectStateToSerializableRuntimeState = (
|
||||
savedObjectState: ControlGroupSavedObjectState
|
||||
): SerializableControlGroupState => {
|
||||
const defaultControlGroupInput = getDefaultControlGroupState();
|
||||
return {
|
||||
chainingSystem: serializedState?.chainingSystem,
|
||||
labelPosition: serializedState?.controlStyle ?? defaultControlGroupInput.labelPosition,
|
||||
autoApplySelections: !serializedState?.showApplySelections,
|
||||
ignoreParentSettings: safeJSONParse(serializedState?.ignoreParentSettingsJSON) ?? {},
|
||||
panels: safeJSONParse(serializedState?.panelsJSON) ?? {},
|
||||
chainingSystem:
|
||||
(savedObjectState?.chainingSystem as SerializableControlGroupState['chainingSystem']) ??
|
||||
defaultControlGroupInput.chainingSystem,
|
||||
labelPosition:
|
||||
(savedObjectState?.controlStyle as SerializableControlGroupState['labelPosition']) ??
|
||||
defaultControlGroupInput.labelPosition,
|
||||
autoApplySelections: !savedObjectState?.showApplySelections,
|
||||
ignoreParentSettings: safeJSONParse(savedObjectState?.ignoreParentSettingsJSON) ?? {},
|
||||
panels: safeJSONParse(savedObjectState?.panelsJSON) ?? {},
|
||||
};
|
||||
};
|
||||
|
||||
export const serializableRuntimeStateToControlGroupSerializedState = (
|
||||
export const serializableRuntimeStateToControlGroupSavedObjectState = (
|
||||
serializable: SerializableRecord // It is safe to treat this as SerializableControlGroupState
|
||||
): ControlGroupSerializedState => {
|
||||
): ControlGroupSavedObjectState => {
|
||||
return {
|
||||
controlStyle: serializable.labelPosition as SerializableControlGroupState['labelPosition'],
|
||||
chainingSystem: serializable.chainingSystem as SerializableControlGroupState['chainingSystem'],
|
||||
|
|
|
@ -7,16 +7,11 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { type ControlGroupSerializedState } from '../../common';
|
||||
import {
|
||||
type ControlGroupTelemetry,
|
||||
controlGroupTelemetry,
|
||||
initializeControlGroupTelemetry,
|
||||
} from './control_group_telemetry';
|
||||
import { controlGroupTelemetry, initializeControlGroupTelemetry } from './control_group_telemetry';
|
||||
import { ControlGroupSavedObjectState, ControlGroupTelemetry } from './types';
|
||||
|
||||
// controls attributes with all settings ignored + 3 options lists + hierarchical chaining + label above
|
||||
const rawControlAttributes1: SerializableRecord & ControlGroupSerializedState = {
|
||||
const rawControlAttributes1: ControlGroupSavedObjectState = {
|
||||
controlStyle: 'twoLine',
|
||||
chainingSystem: 'NONE',
|
||||
showApplySelections: true,
|
||||
|
@ -27,7 +22,7 @@ const rawControlAttributes1: SerializableRecord & ControlGroupSerializedState =
|
|||
};
|
||||
|
||||
// controls attributes with some settings ignored + 2 range sliders, 1 time slider + No chaining + label inline
|
||||
const rawControlAttributes2: SerializableRecord & ControlGroupSerializedState = {
|
||||
const rawControlAttributes2: ControlGroupSavedObjectState = {
|
||||
controlStyle: 'oneLine',
|
||||
chainingSystem: 'NONE',
|
||||
showApplySelections: false,
|
||||
|
@ -38,7 +33,7 @@ const rawControlAttributes2: SerializableRecord & ControlGroupSerializedState =
|
|||
};
|
||||
|
||||
// controls attributes with no settings ignored + 2 options lists, 1 range slider, 1 time slider + hierarchical chaining + label inline
|
||||
const rawControlAttributes3: SerializableRecord & ControlGroupSerializedState = {
|
||||
const rawControlAttributes3: ControlGroupSavedObjectState = {
|
||||
controlStyle: 'oneLine',
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
showApplySelections: false,
|
||||
|
|
|
@ -9,31 +9,15 @@
|
|||
|
||||
import { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
|
||||
import { set } from '@kbn/safer-lodash-set';
|
||||
import type { ControlGroupSerializedState } from '../../common';
|
||||
import {
|
||||
type SerializableControlGroupState,
|
||||
controlGroupSerializedStateToSerializableRuntimeState,
|
||||
controlGroupSavedObjectStateToSerializableRuntimeState,
|
||||
getDefaultControlGroupState,
|
||||
} from './control_group_persistence';
|
||||
|
||||
export interface ControlGroupTelemetry {
|
||||
total: number;
|
||||
chaining_system: {
|
||||
[key: string]: number;
|
||||
};
|
||||
label_position: {
|
||||
[key: string]: number;
|
||||
};
|
||||
ignore_settings: {
|
||||
[key: string]: number;
|
||||
};
|
||||
by_type: {
|
||||
[key: string]: {
|
||||
total: number;
|
||||
details: { [key: string]: number };
|
||||
};
|
||||
};
|
||||
}
|
||||
import {
|
||||
ControlGroupSavedObjectState,
|
||||
ControlGroupTelemetry,
|
||||
SerializableControlGroupState,
|
||||
} from './types';
|
||||
|
||||
export const initializeControlGroupTelemetry = (
|
||||
statsSoFar: Record<string, unknown>
|
||||
|
@ -113,8 +97,8 @@ export const controlGroupTelemetry: PersistableStateService['telemetry'] = (
|
|||
const controlGroupStats = initializeControlGroupTelemetry(stats);
|
||||
const controlGroupState = {
|
||||
...getDefaultControlGroupState(),
|
||||
...controlGroupSerializedStateToSerializableRuntimeState(
|
||||
state as unknown as ControlGroupSerializedState
|
||||
...controlGroupSavedObjectStateToSerializableRuntimeState(
|
||||
state as unknown as ControlGroupSavedObjectState
|
||||
),
|
||||
};
|
||||
if (!controlGroupState) return controlGroupStats;
|
||||
|
|
47
src/plugins/controls/server/control_group/types.ts
Normal file
47
src/plugins/controls/server/control_group/types.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { ControlGroupRuntimeState, ControlPanelState, SerializedControlState } from '../../common';
|
||||
|
||||
// using SerializableRecord to force type to be read as serializable
|
||||
export type SerializableControlGroupState = SerializableRecord &
|
||||
Omit<
|
||||
ControlGroupRuntimeState,
|
||||
'initialChildControlState' | 'editorConfig' // editor config is not persisted
|
||||
> & {
|
||||
panels: Record<string, ControlPanelState<SerializedControlState>> | {};
|
||||
};
|
||||
|
||||
export type ControlGroupSavedObjectState = SerializableRecord & {
|
||||
chainingSystem: SerializableControlGroupState['chainingSystem'];
|
||||
controlStyle: SerializableControlGroupState['labelPosition'];
|
||||
showApplySelections: boolean;
|
||||
ignoreParentSettingsJSON: string;
|
||||
panelsJSON: string;
|
||||
};
|
||||
|
||||
export interface ControlGroupTelemetry {
|
||||
total: number;
|
||||
chaining_system: {
|
||||
[key: string]: number;
|
||||
};
|
||||
label_position: {
|
||||
[key: string]: number;
|
||||
};
|
||||
ignore_settings: {
|
||||
[key: string]: number;
|
||||
};
|
||||
by_type: {
|
||||
[key: string]: {
|
||||
total: number;
|
||||
details: { [key: string]: number };
|
||||
};
|
||||
};
|
||||
}
|
|
@ -13,10 +13,9 @@ export const plugin = async () => {
|
|||
};
|
||||
|
||||
export {
|
||||
controlGroupSerializedStateToSerializableRuntimeState,
|
||||
serializableRuntimeStateToControlGroupSerializedState,
|
||||
controlGroupSavedObjectStateToSerializableRuntimeState,
|
||||
serializableRuntimeStateToControlGroupSavedObjectState,
|
||||
} from './control_group/control_group_persistence';
|
||||
export {
|
||||
type ControlGroupTelemetry,
|
||||
initializeControlGroupTelemetry,
|
||||
} from './control_group/control_group_telemetry';
|
||||
export { initializeControlGroupTelemetry } from './control_group/control_group_telemetry';
|
||||
|
||||
export type { ControlGroupTelemetry } from './control_group/types';
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"@kbn/field-formats-plugin",
|
||||
"@kbn/presentation-panel-plugin",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/std"
|
||||
"@kbn/std",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import type { Serializable } from '@kbn/utility-types';
|
||||
import { GridData } from '../content_management';
|
||||
import type { GridData } from '../../server/dashboard_saved_object';
|
||||
|
||||
interface KibanaAttributes {
|
||||
kibanaSavedObjectMeta: {
|
||||
|
|
|
@ -7,6 +7,18 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export const LATEST_VERSION = 2;
|
||||
export const LATEST_VERSION = 3;
|
||||
|
||||
export const CONTENT_ID = 'dashboard';
|
||||
|
||||
export const DASHBOARD_GRID_COLUMN_COUNT = 48;
|
||||
export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
|
||||
export const DEFAULT_PANEL_HEIGHT = 15;
|
||||
|
||||
export const DEFAULT_DASHBOARD_OPTIONS = {
|
||||
hidePanelTitles: false,
|
||||
useMargins: true,
|
||||
syncColors: true,
|
||||
syncCursor: true,
|
||||
syncTooltips: true,
|
||||
} as const;
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { LATEST_VERSION, CONTENT_ID } from './constants';
|
||||
export {
|
||||
LATEST_VERSION,
|
||||
CONTENT_ID,
|
||||
DASHBOARD_GRID_COLUMN_COUNT,
|
||||
DEFAULT_PANEL_HEIGHT,
|
||||
DEFAULT_PANEL_WIDTH,
|
||||
DEFAULT_DASHBOARD_OPTIONS,
|
||||
} from './constants';
|
||||
|
||||
export type { DashboardContentType } from './types';
|
||||
|
||||
export type {
|
||||
GridData,
|
||||
DashboardItem,
|
||||
DashboardCrudTypes,
|
||||
DashboardAttributes,
|
||||
SavedDashboardPanel,
|
||||
} from './latest';
|
||||
|
|
|
@ -14,7 +14,7 @@ import type {
|
|||
} from '@kbn/content-management-utils';
|
||||
import { Serializable } from '@kbn/utility-types';
|
||||
import { RefreshInterval } from '@kbn/data-plugin/common';
|
||||
import { ControlGroupSerializedState } from '@kbn/controls-plugin/common';
|
||||
import { ControlGroupChainingSystem, ControlLabelPosition } from '@kbn/controls-plugin/common';
|
||||
|
||||
import { DashboardContentType } from '../types';
|
||||
|
||||
|
@ -62,10 +62,13 @@ export interface SavedDashboardPanel {
|
|||
version?: string;
|
||||
}
|
||||
|
||||
type ControlGroupAttributesV1 = Pick<
|
||||
ControlGroupSerializedState,
|
||||
'panelsJSON' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettingsJSON'
|
||||
>;
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type ControlGroupAttributesV1 = {
|
||||
chainingSystem?: ControlGroupChainingSystem;
|
||||
panelsJSON: string; // stringified version of ControlSerializedState
|
||||
ignoreParentSettingsJSON: string;
|
||||
controlStyle?: ControlLabelPosition;
|
||||
};
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */
|
||||
export type DashboardAttributes = {
|
||||
|
@ -77,7 +80,7 @@ export type DashboardAttributes = {
|
|||
description: string;
|
||||
panelsJSON: string;
|
||||
timeFrom?: string;
|
||||
version: number;
|
||||
version?: number;
|
||||
timeTo?: string;
|
||||
title: string;
|
||||
kibanaSavedObjectMeta: {
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
*/
|
||||
|
||||
export type { GridData, DashboardItem, SavedDashboardPanel } from '../v1/types'; // no changes made to types from v1 to v2
|
||||
export type { DashboardCrudTypes, DashboardAttributes } from './types';
|
||||
export type { ControlGroupAttributes, DashboardCrudTypes, DashboardAttributes } from './types';
|
||||
|
|
|
@ -12,21 +12,18 @@ import type {
|
|||
SavedObjectCreateOptions,
|
||||
SavedObjectUpdateOptions,
|
||||
} from '@kbn/content-management-utils';
|
||||
import { ControlGroupSerializedState } from '@kbn/controls-plugin/common';
|
||||
import { DashboardContentType } from '../types';
|
||||
import { DashboardAttributes as DashboardAttributesV1 } from '../v1/types';
|
||||
import {
|
||||
ControlGroupAttributesV1,
|
||||
DashboardAttributes as DashboardAttributesV1,
|
||||
} from '../v1/types';
|
||||
|
||||
type ControlGroupAttributesV2 = Pick<
|
||||
ControlGroupSerializedState,
|
||||
| 'panelsJSON'
|
||||
| 'chainingSystem'
|
||||
| 'controlStyle'
|
||||
| 'ignoreParentSettingsJSON'
|
||||
| 'showApplySelections'
|
||||
>;
|
||||
export type ControlGroupAttributes = ControlGroupAttributesV1 & {
|
||||
showApplySelections?: boolean;
|
||||
};
|
||||
|
||||
export type DashboardAttributes = Omit<DashboardAttributesV1, 'controlGroupInput'> & {
|
||||
controlGroupInput?: ControlGroupAttributesV2;
|
||||
controlGroupInput?: ControlGroupAttributes;
|
||||
};
|
||||
|
||||
export type DashboardCrudTypes = ContentManagementCrudTypes<
|
||||
|
|
|
@ -18,8 +18,7 @@ import type { Reference } from '@kbn/content-management-utils';
|
|||
import { RefreshInterval } from '@kbn/data-plugin/common';
|
||||
import { KibanaExecutionContext } from '@kbn/core-execution-context-common';
|
||||
|
||||
import { DashboardOptions } from '../types';
|
||||
import { GridData } from '../content_management';
|
||||
import type { DashboardOptions, GridData } from '../../server/content_management';
|
||||
|
||||
export interface DashboardPanelMap {
|
||||
[key: string]: DashboardPanelState;
|
||||
|
|
|
@ -18,7 +18,8 @@ import {
|
|||
createInject,
|
||||
} from '../../dashboard_container/persistable_state/dashboard_container_references';
|
||||
import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks';
|
||||
import { DashboardAttributes } from '../../content_management';
|
||||
import type { DashboardAttributes, DashboardItem } from '../../../server/content_management';
|
||||
import { DashboardAttributesAndReferences } from '../../types';
|
||||
|
||||
const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock();
|
||||
const dashboardInject = createInject(embeddablePersistableStateServiceMock);
|
||||
|
@ -44,28 +45,37 @@ const deps: InjectExtractDeps = {
|
|||
};
|
||||
|
||||
const commonAttributes: DashboardAttributes = {
|
||||
kibanaSavedObjectMeta: { searchSourceJSON: '' },
|
||||
kibanaSavedObjectMeta: { searchSource: {} },
|
||||
timeRestore: false,
|
||||
panelsJSON: '',
|
||||
version: 1,
|
||||
options: {
|
||||
hidePanelTitles: false,
|
||||
useMargins: true,
|
||||
syncColors: true,
|
||||
syncCursor: true,
|
||||
syncTooltips: true,
|
||||
},
|
||||
panels: [],
|
||||
description: '',
|
||||
title: '',
|
||||
};
|
||||
|
||||
describe('extractReferences', () => {
|
||||
test('extracts references from panelsJSON', () => {
|
||||
test('extracts references from panels', () => {
|
||||
const doc = {
|
||||
id: '1',
|
||||
attributes: {
|
||||
...commonAttributes,
|
||||
foo: true,
|
||||
panelsJSON: JSON.stringify([
|
||||
panels: [
|
||||
{
|
||||
panelIndex: 'panel-1',
|
||||
type: 'visualization',
|
||||
id: '1',
|
||||
title: 'Title 1',
|
||||
version: '7.9.1',
|
||||
gridData: { x: 0, y: 0, w: 1, h: 1, i: 'panel-1' },
|
||||
panelConfig: {},
|
||||
},
|
||||
{
|
||||
panelIndex: 'panel-2',
|
||||
|
@ -73,8 +83,10 @@ describe('extractReferences', () => {
|
|||
id: '2',
|
||||
title: 'Title 2',
|
||||
version: '7.9.1',
|
||||
gridData: { x: 1, y: 1, w: 2, h: 2, i: 'panel-2' },
|
||||
panelConfig: {},
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
|
@ -86,9 +98,47 @@ describe('extractReferences', () => {
|
|||
"description": "",
|
||||
"foo": true,
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "",
|
||||
"searchSource": Object {},
|
||||
},
|
||||
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-1\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_panel-1\\"},{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"panelIndex\\":\\"panel-2\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_panel-2\\"}]",
|
||||
"options": Object {
|
||||
"hidePanelTitles": false,
|
||||
"syncColors": true,
|
||||
"syncCursor": true,
|
||||
"syncTooltips": true,
|
||||
"useMargins": true,
|
||||
},
|
||||
"panels": Array [
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 1,
|
||||
"i": "panel-1",
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "panel-1",
|
||||
"panelRefName": "panel_panel-1",
|
||||
"title": "Title 1",
|
||||
"type": "visualization",
|
||||
"version": "7.9.1",
|
||||
},
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 2,
|
||||
"i": "panel-2",
|
||||
"w": 2,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
},
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "panel-2",
|
||||
"panelRefName": "panel_panel-2",
|
||||
"title": "Title 2",
|
||||
"type": "visualization",
|
||||
"version": "7.9.1",
|
||||
},
|
||||
],
|
||||
"timeRestore": false,
|
||||
"title": "",
|
||||
"version": 1,
|
||||
|
@ -115,18 +165,18 @@ describe('extractReferences', () => {
|
|||
attributes: {
|
||||
...commonAttributes,
|
||||
foo: true,
|
||||
panelsJSON: JSON.stringify([
|
||||
panels: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Title 1',
|
||||
version: '7.9.1',
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
} as unknown as DashboardAttributesAndReferences;
|
||||
expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"type\\" attribute is missing from panel \\"undefined\\""`
|
||||
`"\\"type\\" attribute is missing from panel \\"0\\""`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -136,25 +186,49 @@ describe('extractReferences', () => {
|
|||
attributes: {
|
||||
...commonAttributes,
|
||||
foo: true,
|
||||
panelsJSON: JSON.stringify([
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
title: 'Title 1',
|
||||
version: '7.9.1',
|
||||
gridData: { x: 0, y: 0, w: 1, h: 1, i: 'panel-1' },
|
||||
panelConfig: {},
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
expect(extractReferences(doc, deps)).toMatchInlineSnapshot(`
|
||||
expect(extractReferences(doc as unknown as DashboardItem, deps)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"description": "",
|
||||
"foo": true,
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "",
|
||||
"searchSource": Object {},
|
||||
},
|
||||
"panelsJSON": "[{\\"version\\":\\"7.9.1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]",
|
||||
"options": Object {
|
||||
"hidePanelTitles": false,
|
||||
"syncColors": true,
|
||||
"syncCursor": true,
|
||||
"syncTooltips": true,
|
||||
"useMargins": true,
|
||||
},
|
||||
"panels": Array [
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 1,
|
||||
"i": "panel-1",
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "0",
|
||||
"title": "Title 1",
|
||||
"type": "visualization",
|
||||
"version": "7.9.1",
|
||||
},
|
||||
],
|
||||
"timeRestore": false,
|
||||
"title": "",
|
||||
"version": 1,
|
||||
|
@ -171,18 +245,26 @@ describe('injectReferences', () => {
|
|||
...commonAttributes,
|
||||
id: '1',
|
||||
title: 'test',
|
||||
panelsJSON: JSON.stringify([
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
panelRefName: 'panel_0',
|
||||
panelIndex: '0',
|
||||
title: 'Title 1',
|
||||
version: '7.9.0',
|
||||
gridData: { x: 0, y: 0, w: 1, h: 1, i: '0' },
|
||||
panelConfig: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
panelRefName: 'panel_1',
|
||||
panelIndex: '1',
|
||||
title: 'Title 2',
|
||||
version: '7.9.0',
|
||||
gridData: { x: 1, y: 1, w: 2, h: 2, i: '1' },
|
||||
panelConfig: {},
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
const references = [
|
||||
{
|
||||
|
@ -203,9 +285,47 @@ describe('injectReferences', () => {
|
|||
"description": "",
|
||||
"id": "1",
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "",
|
||||
"searchSource": Object {},
|
||||
},
|
||||
"panelsJSON": "[{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"version\\":\\"7.9.0\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]",
|
||||
"options": Object {
|
||||
"hidePanelTitles": false,
|
||||
"syncColors": true,
|
||||
"syncCursor": true,
|
||||
"syncTooltips": true,
|
||||
"useMargins": true,
|
||||
},
|
||||
"panels": Array [
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 1,
|
||||
"i": "0",
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": "1",
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "0",
|
||||
"title": "Title 1",
|
||||
"type": "visualization",
|
||||
"version": "7.9.0",
|
||||
},
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 2,
|
||||
"i": "1",
|
||||
"w": 2,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
},
|
||||
"id": "2",
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "1",
|
||||
"title": "Title 2",
|
||||
"type": "visualization",
|
||||
"version": "7.9.0",
|
||||
},
|
||||
],
|
||||
"timeRestore": false,
|
||||
"title": "test",
|
||||
"version": 1,
|
||||
|
@ -213,7 +333,7 @@ describe('injectReferences', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('skips when panelsJSON is missing', () => {
|
||||
test('skips when panels is missing', () => {
|
||||
const attributes = {
|
||||
id: '1',
|
||||
title: 'test',
|
||||
|
@ -222,49 +342,34 @@ describe('injectReferences', () => {
|
|||
expect(newAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"panelsJSON": "[]",
|
||||
"panels": Array [],
|
||||
"title": "test",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('skips when panelsJSON is not an array', () => {
|
||||
const attributes = {
|
||||
...commonAttributes,
|
||||
id: '1',
|
||||
panelsJSON: '{}',
|
||||
title: 'test',
|
||||
};
|
||||
const newAttributes = injectReferences({ attributes, references: [] }, deps);
|
||||
expect(newAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"description": "",
|
||||
"id": "1",
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "",
|
||||
},
|
||||
"panelsJSON": "[]",
|
||||
"timeRestore": false,
|
||||
"title": "test",
|
||||
"version": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('skips a panel when panelRefName is missing', () => {
|
||||
const attributes = {
|
||||
...commonAttributes,
|
||||
id: '1',
|
||||
title: 'test',
|
||||
panelsJSON: JSON.stringify([
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
panelRefName: 'panel_0',
|
||||
panelIndex: '0',
|
||||
title: 'Title 1',
|
||||
gridData: { x: 0, y: 0, w: 1, h: 1, i: '0' },
|
||||
panelConfig: {},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
panelIndex: '1',
|
||||
title: 'Title 2',
|
||||
gridData: { x: 1, y: 1, w: 2, h: 2, i: '1' },
|
||||
panelConfig: {},
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
const references = [
|
||||
{
|
||||
|
@ -279,9 +384,46 @@ describe('injectReferences', () => {
|
|||
"description": "",
|
||||
"id": "1",
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "",
|
||||
"searchSource": Object {},
|
||||
},
|
||||
"panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]",
|
||||
"options": Object {
|
||||
"hidePanelTitles": false,
|
||||
"syncColors": true,
|
||||
"syncCursor": true,
|
||||
"syncTooltips": true,
|
||||
"useMargins": true,
|
||||
},
|
||||
"panels": Array [
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 1,
|
||||
"i": "0",
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": "1",
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "0",
|
||||
"title": "Title 1",
|
||||
"type": "visualization",
|
||||
"version": undefined,
|
||||
},
|
||||
Object {
|
||||
"gridData": Object {
|
||||
"h": 2,
|
||||
"i": "1",
|
||||
"w": 2,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
},
|
||||
"panelConfig": Object {},
|
||||
"panelIndex": "1",
|
||||
"title": "Title 2",
|
||||
"type": "visualization",
|
||||
"version": undefined,
|
||||
},
|
||||
],
|
||||
"timeRestore": false,
|
||||
"title": "test",
|
||||
"version": 1,
|
||||
|
@ -294,12 +436,16 @@ describe('injectReferences', () => {
|
|||
...commonAttributes,
|
||||
id: '1',
|
||||
title: 'test',
|
||||
panelsJSON: JSON.stringify([
|
||||
panels: [
|
||||
{
|
||||
panelIndex: '0',
|
||||
panelRefName: 'panel_0',
|
||||
title: 'Title 1',
|
||||
type: 'visualization',
|
||||
gridData: { x: 0, y: 0, w: 1, h: 1, i: '0' },
|
||||
panelConfig: {},
|
||||
},
|
||||
]),
|
||||
],
|
||||
};
|
||||
expect(() =>
|
||||
injectReferences({ attributes, references: [] }, deps)
|
||||
|
|
|
@ -11,11 +11,11 @@ import type { Reference } from '@kbn/content-management-utils';
|
|||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common/types';
|
||||
|
||||
import {
|
||||
convertPanelMapToSavedPanels,
|
||||
convertSavedPanelsToPanelMap,
|
||||
convertPanelMapToPanelsArray,
|
||||
convertPanelsArrayToPanelMap,
|
||||
} from '../../lib/dashboard_panel_converters';
|
||||
import { DashboardAttributesAndReferences, ParsedDashboardAttributesWithType } from '../../types';
|
||||
import { DashboardAttributes, SavedDashboardPanel } from '../../content_management';
|
||||
import type { DashboardAttributes } from '../../../server/content_management';
|
||||
import {
|
||||
createExtract,
|
||||
createInject,
|
||||
|
@ -25,20 +25,12 @@ export interface InjectExtractDeps {
|
|||
embeddablePersistableStateService: EmbeddablePersistableStateService;
|
||||
}
|
||||
|
||||
function parseDashboardAttributesWithType(
|
||||
attributes: DashboardAttributes
|
||||
): ParsedDashboardAttributesWithType {
|
||||
let parsedPanels = [] as SavedDashboardPanel[];
|
||||
if (typeof attributes.panelsJSON === 'string') {
|
||||
const parsedJSON = JSON.parse(attributes.panelsJSON);
|
||||
if (Array.isArray(parsedJSON)) {
|
||||
parsedPanels = parsedJSON as SavedDashboardPanel[];
|
||||
}
|
||||
}
|
||||
|
||||
function parseDashboardAttributesWithType({
|
||||
panels,
|
||||
}: DashboardAttributes): ParsedDashboardAttributesWithType {
|
||||
return {
|
||||
type: 'dashboard',
|
||||
panels: convertSavedPanelsToPanelMap(parsedPanels),
|
||||
panels: convertPanelsArrayToPanelMap(panels),
|
||||
} as ParsedDashboardAttributesWithType;
|
||||
}
|
||||
|
||||
|
@ -51,12 +43,12 @@ export function injectReferences(
|
|||
// inject references back into panels via the Embeddable persistable state service.
|
||||
const inject = createInject(deps.embeddablePersistableStateService);
|
||||
const injectedState = inject(parsedAttributes, references) as ParsedDashboardAttributesWithType;
|
||||
const injectedPanels = convertPanelMapToSavedPanels(injectedState.panels);
|
||||
const injectedPanels = convertPanelMapToPanelsArray(injectedState.panels);
|
||||
|
||||
const newAttributes = {
|
||||
...attributes,
|
||||
panelsJSON: JSON.stringify(injectedPanels),
|
||||
} as DashboardAttributes;
|
||||
panels: injectedPanels,
|
||||
};
|
||||
|
||||
return newAttributes;
|
||||
}
|
||||
|
@ -81,12 +73,12 @@ export function extractReferences(
|
|||
references: Reference[];
|
||||
state: ParsedDashboardAttributesWithType;
|
||||
};
|
||||
const extractedPanels = convertPanelMapToSavedPanels(extractedState.panels);
|
||||
const extractedPanels = convertPanelMapToPanelsArray(extractedState.panels);
|
||||
|
||||
const newAttributes = {
|
||||
...attributes,
|
||||
panelsJSON: JSON.stringify(extractedPanels),
|
||||
} as DashboardAttributes;
|
||||
panels: extractedPanels,
|
||||
};
|
||||
|
||||
return {
|
||||
references: [...references, ...extractedReferences],
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { DashboardOptions, DashboardCapabilities, SharedDashboardState } from './types';
|
||||
export type { DashboardCapabilities, SharedDashboardState } from './types';
|
||||
|
||||
export type {
|
||||
DashboardPanelMap,
|
||||
|
@ -16,9 +16,8 @@ export type {
|
|||
DashboardContainerByReferenceInput,
|
||||
} from './dashboard_container/types';
|
||||
|
||||
export type { DashboardAttributes, SavedDashboardPanel } from './content_management';
|
||||
|
||||
export {
|
||||
type InjectExtractDeps,
|
||||
injectReferences,
|
||||
extractReferences,
|
||||
} from './dashboard_saved_object/persistable_state/dashboard_saved_object_references';
|
||||
|
@ -31,10 +30,8 @@ export {
|
|||
export { prefixReferencesFromPanel } from './dashboard_container/persistable_state/dashboard_container_references';
|
||||
|
||||
export {
|
||||
convertPanelStateToSavedDashboardPanel,
|
||||
convertSavedDashboardPanelToPanelState,
|
||||
convertSavedPanelsToPanelMap,
|
||||
convertPanelMapToSavedPanels,
|
||||
convertPanelsArrayToPanelMap,
|
||||
convertPanelMapToPanelsArray,
|
||||
} from './lib/dashboard_panel_converters';
|
||||
|
||||
export const UI_SETTINGS = {
|
||||
|
|
|
@ -9,77 +9,63 @@
|
|||
|
||||
import { v4 } from 'uuid';
|
||||
import { omit } from 'lodash';
|
||||
import { EmbeddableInput, SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
|
||||
|
||||
import type { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { DashboardPanelMap, DashboardPanelState } from '..';
|
||||
import { SavedDashboardPanel } from '../content_management';
|
||||
import type { DashboardPanelMap } from '..';
|
||||
import type { DashboardPanel } from '../../server/content_management';
|
||||
|
||||
import {
|
||||
getReferencesForPanelId,
|
||||
prefixReferencesFromPanel,
|
||||
} from '../dashboard_container/persistable_state/dashboard_container_references';
|
||||
|
||||
export function convertSavedDashboardPanelToPanelState<
|
||||
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
|
||||
>(savedDashboardPanel: SavedDashboardPanel): DashboardPanelState<TEmbeddableInput> {
|
||||
return {
|
||||
type: savedDashboardPanel.type,
|
||||
gridData: savedDashboardPanel.gridData,
|
||||
panelRefName: savedDashboardPanel.panelRefName,
|
||||
explicitInput: {
|
||||
id: savedDashboardPanel.panelIndex,
|
||||
...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }),
|
||||
...(savedDashboardPanel.title !== undefined && { title: savedDashboardPanel.title }),
|
||||
...savedDashboardPanel.embeddableConfig,
|
||||
} as TEmbeddableInput,
|
||||
|
||||
/**
|
||||
* Version information used to be stored in the panel until 8.11 when it was moved
|
||||
* to live inside the explicit Embeddable Input. If version information is given here, we'd like to keep it.
|
||||
* It will be removed on Dashboard save
|
||||
*/
|
||||
version: savedDashboardPanel.version,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertPanelStateToSavedDashboardPanel(
|
||||
panelState: DashboardPanelState,
|
||||
removeLegacyVersion?: boolean
|
||||
): SavedDashboardPanel {
|
||||
const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId;
|
||||
return {
|
||||
/**
|
||||
* Version information used to be stored in the panel until 8.11 when it was moved to live inside the
|
||||
* explicit Embeddable Input. If removeLegacyVersion is not passed, we'd like to keep this information for
|
||||
* the time being.
|
||||
*/
|
||||
...(!removeLegacyVersion ? { version: panelState.version } : {}),
|
||||
|
||||
type: panelState.type,
|
||||
gridData: panelState.gridData,
|
||||
panelIndex: panelState.explicitInput.id,
|
||||
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
|
||||
...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }),
|
||||
...(savedObjectId !== undefined && { id: savedObjectId }),
|
||||
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
|
||||
};
|
||||
}
|
||||
|
||||
export const convertSavedPanelsToPanelMap = (panels?: SavedDashboardPanel[]): DashboardPanelMap => {
|
||||
export const convertPanelsArrayToPanelMap = (panels?: DashboardPanel[]): DashboardPanelMap => {
|
||||
const panelsMap: DashboardPanelMap = {};
|
||||
panels?.forEach((panel, idx) => {
|
||||
panelsMap![panel.panelIndex ?? String(idx)] = convertSavedDashboardPanelToPanelState(panel);
|
||||
const panelIndex = panel.panelIndex ?? String(idx);
|
||||
panelsMap![panel.panelIndex ?? String(idx)] = {
|
||||
type: panel.type,
|
||||
gridData: panel.gridData,
|
||||
panelRefName: panel.panelRefName,
|
||||
explicitInput: {
|
||||
id: panelIndex,
|
||||
...(panel.id !== undefined && { savedObjectId: panel.id }),
|
||||
...(panel.title !== undefined && { title: panel.title }),
|
||||
...panel.panelConfig,
|
||||
},
|
||||
version: panel.version,
|
||||
};
|
||||
});
|
||||
return panelsMap;
|
||||
};
|
||||
|
||||
export const convertPanelMapToSavedPanels = (
|
||||
export const convertPanelMapToPanelsArray = (
|
||||
panels: DashboardPanelMap,
|
||||
removeLegacyVersion?: boolean
|
||||
) => {
|
||||
return Object.values(panels).map((panel) =>
|
||||
convertPanelStateToSavedDashboardPanel(panel, removeLegacyVersion)
|
||||
);
|
||||
return Object.values(panels).map((panelState) => {
|
||||
const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId;
|
||||
const panelIndex = panelState.explicitInput.id;
|
||||
return {
|
||||
/**
|
||||
* Version information used to be stored in the panel until 8.11 when it was moved to live inside the
|
||||
* explicit Embeddable Input. If removeLegacyVersion is not passed, we'd like to keep this information for
|
||||
* the time being.
|
||||
*/
|
||||
...(!removeLegacyVersion ? { version: panelState.version } : {}),
|
||||
|
||||
type: panelState.type,
|
||||
gridData: panelState.gridData,
|
||||
panelIndex,
|
||||
panelConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
|
||||
...(panelState.explicitInput.title !== undefined && {
|
||||
title: panelState.explicitInput.title,
|
||||
}),
|
||||
...(savedObjectId !== undefined && { id: savedObjectId }),
|
||||
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,17 +8,9 @@
|
|||
*/
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import { DashboardAttributes, SavedDashboardPanel } from './content_management';
|
||||
import { DashboardContainerInput, DashboardPanelMap } from './dashboard_container/types';
|
||||
|
||||
export interface DashboardOptions {
|
||||
hidePanelTitles: boolean;
|
||||
useMargins: boolean;
|
||||
syncColors: boolean;
|
||||
syncTooltips: boolean;
|
||||
syncCursor: boolean;
|
||||
}
|
||||
import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import type { DashboardContainerInput, DashboardPanelMap } from './dashboard_container/types';
|
||||
import type { DashboardAttributes, DashboardPanel } from '../server/content_management';
|
||||
|
||||
export interface DashboardCapabilities {
|
||||
showWriteControls: boolean;
|
||||
|
@ -32,7 +24,7 @@ export interface DashboardCapabilities {
|
|||
* For BWC reasons, dashboard state is stored with panels as an array instead of a map
|
||||
*/
|
||||
export type SharedDashboardState = Partial<
|
||||
Omit<DashboardContainerInput, 'panels'> & { panels: SavedDashboardPanel[] }
|
||||
Omit<DashboardContainerInput, 'panels'> & { panels: DashboardPanel[] }
|
||||
>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { ScopedHistory } from '@kbn/core-application-browser';
|
||||
|
||||
import { ForwardedDashboardState } from './locator';
|
||||
import { convertSavedPanelsToPanelMap, DashboardContainerInput } from '../../../common';
|
||||
import { convertPanelsArrayToPanelMap, DashboardContainerInput } from '../../../common';
|
||||
|
||||
export const loadDashboardHistoryLocationState = (
|
||||
getScopedHistory: () => ScopedHistory
|
||||
|
@ -28,6 +28,6 @@ export const loadDashboardHistoryLocationState = (
|
|||
|
||||
return {
|
||||
...restOfState,
|
||||
...{ panels: convertSavedPanelsToPanelMap(panels) },
|
||||
...{ panels: convertPanelsArrayToPanelMap(panels) },
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { Capabilities } from '@kbn/core/public';
|
||||
import { convertPanelMapToSavedPanels, DashboardContainerInput } from '../../../../common';
|
||||
import { convertPanelMapToPanelsArray, DashboardContainerInput } from '../../../../common';
|
||||
import { DashboardLocatorParams } from '../../../dashboard_container';
|
||||
|
||||
import { shareService } from '../../../services/kibana_services';
|
||||
|
@ -143,7 +143,7 @@ describe('ShowShareModal', () => {
|
|||
).locatorParams.params;
|
||||
const rawDashboardState = {
|
||||
...unsavedDashboardState,
|
||||
panels: convertPanelMapToSavedPanels(unsavedDashboardState.panels),
|
||||
panels: convertPanelMapToPanelsArray(unsavedDashboardState.panels),
|
||||
};
|
||||
unsavedStateKeys.forEach((key) => {
|
||||
expect(shareLocatorParams[key]).toStrictEqual(
|
||||
|
@ -208,8 +208,8 @@ describe('ShowShareModal', () => {
|
|||
).locatorParams.params;
|
||||
|
||||
expect(shareLocatorParams.panels).toBeDefined();
|
||||
expect(shareLocatorParams.panels![0].embeddableConfig.changedKey1).toBe('changed');
|
||||
expect(shareLocatorParams.panels![1].embeddableConfig.changedKey2).toBe('definitely changed');
|
||||
expect(shareLocatorParams.panels![2].embeddableConfig.changedKey3).toBe('should still exist');
|
||||
expect(shareLocatorParams.panels![0].panelConfig.changedKey1).toBe('changed');
|
||||
expect(shareLocatorParams.panels![1].panelConfig.changedKey2).toBe('definitely changed');
|
||||
expect(shareLocatorParams.panels![2].panelConfig.changedKey3).toBe('should still exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { getStateFromKbnUrl, setStateToKbnUrl, unhashUrl } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { convertPanelMapToSavedPanels, DashboardPanelMap } from '../../../../common';
|
||||
import { convertPanelMapToPanelsArray, DashboardPanelMap } from '../../../../common';
|
||||
import { DashboardLocatorParams } from '../../../dashboard_container';
|
||||
import {
|
||||
getDashboardBackupService,
|
||||
|
@ -151,7 +151,7 @@ export function ShowShareModal({
|
|||
...latestPanels,
|
||||
...modifiedPanels,
|
||||
};
|
||||
return convertPanelMapToSavedPanels(allUnsavedPanelsMap);
|
||||
return convertPanelMapToPanelsArray(allUnsavedPanelsMap);
|
||||
})();
|
||||
|
||||
if (unsavedDashboardState) {
|
||||
|
|
|
@ -22,7 +22,7 @@ import type { ViewMode } from '@kbn/embeddable-plugin/common';
|
|||
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
|
||||
import { SEARCH_SESSION_ID } from '../../dashboard_constants';
|
||||
import { DashboardLocatorParams } from '../../dashboard_container';
|
||||
import { convertPanelMapToSavedPanels } from '../../../common';
|
||||
import { convertPanelMapToPanelsArray } from '../../../common';
|
||||
import { dataService } from '../../services/kibana_services';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
|
||||
|
@ -93,7 +93,7 @@ function getLocatorParams({
|
|||
: undefined,
|
||||
panels: savedObjectId
|
||||
? undefined
|
||||
: (convertPanelMapToSavedPanels(
|
||||
: (convertPanelMapToPanelsArray(
|
||||
dashboardApi.panels$.value
|
||||
) as DashboardLocatorParams['panels']),
|
||||
};
|
||||
|
|
|
@ -19,24 +19,29 @@ import {
|
|||
DashboardContainerInput,
|
||||
DashboardPanelMap,
|
||||
SharedDashboardState,
|
||||
convertSavedPanelsToPanelMap,
|
||||
convertPanelsArrayToPanelMap,
|
||||
} from '../../../common';
|
||||
import { SavedDashboardPanel } from '../../../common/content_management';
|
||||
import type { DashboardPanel } from '../../../server/content_management';
|
||||
import type { SavedDashboardPanel } from '../../../server/dashboard_saved_object';
|
||||
import { DashboardApi } from '../../dashboard_api/types';
|
||||
import { DASHBOARD_STATE_STORAGE_KEY, createDashboardEditUrl } from '../../dashboard_constants';
|
||||
import { migrateLegacyQuery } from '../../services/dashboard_content_management_service/lib/load_dashboard_state';
|
||||
import { coreServices } from '../../services/kibana_services';
|
||||
import { getPanelTooOldErrorString } from '../_dashboard_app_strings';
|
||||
|
||||
const panelIsLegacy = (panel: unknown): panel is SavedDashboardPanel => {
|
||||
return (panel as SavedDashboardPanel).embeddableConfig !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* We no longer support loading panels from a version older than 7.3 in the URL.
|
||||
* @returns whether or not there is a panel in the URL state saved with a version before 7.3
|
||||
*/
|
||||
export const isPanelVersionTooOld = (panels: SavedDashboardPanel[]) => {
|
||||
export const isPanelVersionTooOld = (panels: DashboardPanel[] | SavedDashboardPanel[]) => {
|
||||
for (const panel of panels) {
|
||||
if (
|
||||
!panel.gridData ||
|
||||
!panel.embeddableConfig ||
|
||||
!((panel as DashboardPanel).panelConfig || (panel as SavedDashboardPanel).embeddableConfig) ||
|
||||
(panel.version && semverSatisfies(panel.version, '<7.3'))
|
||||
)
|
||||
return true;
|
||||
|
@ -58,7 +63,19 @@ function getPanelsMap(appStateInUrl: SharedDashboardState): DashboardPanelMap |
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return convertSavedPanelsToPanelMap(appStateInUrl.panels);
|
||||
// convert legacy embeddableConfig keys to panelConfig
|
||||
const panels = appStateInUrl.panels.map((panel) => {
|
||||
if (panelIsLegacy(panel)) {
|
||||
const { embeddableConfig, ...rest } = panel;
|
||||
return {
|
||||
...rest,
|
||||
panelConfig: embeddableConfig,
|
||||
};
|
||||
}
|
||||
return panel;
|
||||
});
|
||||
|
||||
return convertPanelsArrayToPanelMap(panels);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
DashboardPanelMap,
|
||||
prefixReferencesFromPanel,
|
||||
} from '../../../../common';
|
||||
import type { DashboardAttributes } from '../../../../server/content_management';
|
||||
import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../../dashboard_constants';
|
||||
import {
|
||||
SaveDashboardReturn,
|
||||
|
@ -95,7 +96,11 @@ export async function runQuickSave(this: DashboardContainer) {
|
|||
const { rawState: controlGroupSerializedState, references: extractedReferences } =
|
||||
await controlGroupApi.serializeState();
|
||||
controlGroupReferences = extractedReferences;
|
||||
stateToSave = { ...stateToSave, controlGroupInput: controlGroupSerializedState };
|
||||
stateToSave = {
|
||||
...stateToSave,
|
||||
controlGroupInput:
|
||||
controlGroupSerializedState as unknown as DashboardAttributes['controlGroupInput'],
|
||||
};
|
||||
}
|
||||
|
||||
const saveResult = await getDashboardContentManagementService().saveDashboardState({
|
||||
|
@ -186,7 +191,8 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
|
|||
controlGroupReferences = references;
|
||||
dashboardStateToSave = {
|
||||
...dashboardStateToSave,
|
||||
controlGroupInput: controlGroupSerializedState,
|
||||
controlGroupInput:
|
||||
controlGroupSerializedState as unknown as DashboardAttributes['controlGroupInput'],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import { v4 } from 'uuid';
|
|||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
|
||||
import { ControlGroupApi } from '@kbn/controls-plugin/public';
|
||||
import type { KibanaExecutionContext, OverlayRef } from '@kbn/core/public';
|
||||
import { RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
@ -69,12 +69,8 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
|
|||
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
|
||||
|
||||
import { DASHBOARD_CONTAINER_TYPE, DashboardApi, DashboardLocatorParams } from '../..';
|
||||
import {
|
||||
DashboardAttributes,
|
||||
DashboardContainerInput,
|
||||
DashboardPanelMap,
|
||||
DashboardPanelState,
|
||||
} from '../../../common';
|
||||
import type { DashboardAttributes } from '../../../server/content_management';
|
||||
import { DashboardContainerInput, DashboardPanelMap, DashboardPanelState } from '../../../common';
|
||||
import {
|
||||
getReferencesForControls,
|
||||
getReferencesForPanelId,
|
||||
|
@ -887,15 +883,19 @@ export class DashboardContainer
|
|||
public getSerializedStateForControlGroup = () => {
|
||||
return {
|
||||
rawState: this.controlGroupInput
|
||||
? (this.controlGroupInput as ControlGroupSerializedState)
|
||||
: ({
|
||||
controlStyle: 'oneLine',
|
||||
? this.controlGroupInput
|
||||
: {
|
||||
labelPosition: 'oneLine',
|
||||
chainingSystem: 'HIERARCHICAL',
|
||||
showApplySelections: false,
|
||||
panelsJSON: '{}',
|
||||
ignoreParentSettingsJSON:
|
||||
'{"ignoreFilters":false,"ignoreQuery":false,"ignoreTimerange":false,"ignoreValidations":false}',
|
||||
} as ControlGroupSerializedState),
|
||||
autoApplySelections: true,
|
||||
controls: [],
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: false,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
},
|
||||
},
|
||||
references: getReferencesForControls(this.savedObjectReferences),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { cloneDeep, forOwn } from 'lodash';
|
|||
import { PanelNotFoundError } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { DashboardPanelState } from '../../../common';
|
||||
import { GridData } from '../../../common/content_management';
|
||||
import type { GridData } from '../../../server/content_management';
|
||||
import { PanelPlacementProps, PanelPlacementReturn } from './types';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../dashboard_constants';
|
||||
|
||||
|
@ -109,9 +109,9 @@ export function placeClonePanel({
|
|||
|
||||
for (let j = position + 1; j < grid.length; j++) {
|
||||
originalPositionInTheGrid = grid[j].i;
|
||||
const movedPanel = cloneDeep(otherPanels[originalPositionInTheGrid]);
|
||||
movedPanel.gridData.y = movedPanel.gridData.y + diff;
|
||||
otherPanels[originalPositionInTheGrid] = movedPanel;
|
||||
const { gridData, ...movedPanel } = cloneDeep(otherPanels[originalPositionInTheGrid]);
|
||||
const newGridData = { ...gridData, y: gridData.y + diff };
|
||||
otherPanels[originalPositionInTheGrid] = { ...movedPanel, gridData: newGridData };
|
||||
}
|
||||
return { newPanelPlacement: bottomPlacement.grid, otherPanels };
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ export const runPanelPlacementStrategy = (
|
|||
case PanelPlacementStrategy.placeAtTop:
|
||||
const otherPanels = { ...currentPanels };
|
||||
for (const [id, panel] of Object.entries(currentPanels)) {
|
||||
const currentPanel = cloneDeep(panel);
|
||||
currentPanel.gridData.y = currentPanel.gridData.y + height;
|
||||
otherPanels[id] = currentPanel;
|
||||
const { gridData, ...currentPanel } = cloneDeep(panel);
|
||||
const newGridData = { ...gridData, y: gridData.y + height };
|
||||
otherPanels[id] = { ...currentPanel, gridData: newGridData };
|
||||
}
|
||||
return {
|
||||
newPanelPlacement: { x: 0, y: 0, w: width, h: height },
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import { EmbeddableInput } from '@kbn/embeddable-plugin/public';
|
||||
import { MaybePromise } from '@kbn/utility-types';
|
||||
import { DashboardPanelState } from '../../../common';
|
||||
import { GridData } from '../../../common/content_management';
|
||||
import type { GridData } from '../../../server/content_management';
|
||||
import { PanelPlacementStrategy } from '../../dashboard_constants';
|
||||
|
||||
export interface PanelPlacementSettings {
|
||||
|
|
|
@ -12,8 +12,8 @@ import type { ReduxEmbeddableState } from '@kbn/presentation-util-plugin/public'
|
|||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import type { DashboardContainerInput, DashboardOptions } from '../../common';
|
||||
import { SavedDashboardPanel } from '../../common/content_management';
|
||||
import type { DashboardContainerInput } from '../../common';
|
||||
import type { DashboardOptions, DashboardPanel } from '../../server/content_management';
|
||||
|
||||
export interface UnsavedPanelState {
|
||||
[key: string]: object | undefined;
|
||||
|
@ -101,7 +101,7 @@ export type DashboardLocatorParams = Partial<
|
|||
/**
|
||||
* List of dashboard panels
|
||||
*/
|
||||
panels?: Array<SavedDashboardPanel & SerializableRecord>; // used SerializableRecord here to force the GridData type to be read as serializable
|
||||
panels?: Array<DashboardPanel & SerializableRecord>; // used SerializableRecord here to force the GridData type to be read as serializable
|
||||
|
||||
/**
|
||||
* Control group changes
|
||||
|
|
|
@ -20,7 +20,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { DashboardAttributes } from '../../common/content_management';
|
||||
import type { DashboardAttributes } from '../../server/content_management';
|
||||
import {
|
||||
DASHBOARD_PANELS_UNSAVED_ID,
|
||||
getDashboardBackupService,
|
||||
|
|
|
@ -17,7 +17,7 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import { DashboardItem } from '../../../common/content_management';
|
||||
import type { DashboardSearchOut } from '../../../server/content_management';
|
||||
import {
|
||||
DASHBOARD_CONTENT_ID,
|
||||
SAVED_OBJECT_DELETE_TIME,
|
||||
|
@ -42,7 +42,9 @@ type GetDetailViewLink =
|
|||
const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit';
|
||||
const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage';
|
||||
|
||||
const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUserContent => {
|
||||
const toTableListViewSavedObject = (
|
||||
hit: DashboardSearchOut['hits'][number]
|
||||
): DashboardSavedObjectUserContent => {
|
||||
const { title, description, timeRestore } = hit.attributes;
|
||||
return {
|
||||
type: 'dashboard',
|
||||
|
@ -51,7 +53,7 @@ const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUse
|
|||
createdAt: hit.createdAt,
|
||||
createdBy: hit.createdBy,
|
||||
updatedBy: hit.updatedBy,
|
||||
references: hit.references,
|
||||
references: hit.references ?? [],
|
||||
managed: hit.managed,
|
||||
attributes: {
|
||||
title,
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
*/
|
||||
|
||||
import LRUCache from 'lru-cache';
|
||||
import { DashboardCrudTypes } from '../../../common/content_management';
|
||||
import type { DashboardGetOut } from '../../../server/content_management';
|
||||
import { DASHBOARD_CACHE_SIZE, DASHBOARD_CACHE_TTL } from '../../dashboard_constants';
|
||||
|
||||
export class DashboardContentManagementCache {
|
||||
private cache: LRUCache<string, DashboardCrudTypes['GetOut']>;
|
||||
private cache: LRUCache<string, DashboardGetOut>;
|
||||
|
||||
constructor() {
|
||||
this.cache = new LRUCache<string, DashboardCrudTypes['GetOut']>({
|
||||
this.cache = new LRUCache<string, DashboardGetOut>({
|
||||
max: DASHBOARD_CACHE_SIZE,
|
||||
maxAge: DASHBOARD_CACHE_TTL,
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ export class DashboardContentManagementCache {
|
|||
}
|
||||
|
||||
/** Add the fetched dashboard to the cache */
|
||||
public addDashboard({ item: dashboard, meta }: DashboardCrudTypes['GetOut']) {
|
||||
public addDashboard({ item: dashboard, meta }: DashboardGetOut) {
|
||||
this.cache.set(dashboard.id, {
|
||||
meta,
|
||||
item: dashboard,
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { DashboardSearchIn, DashboardSearchOut } from '../../../../server/content_management';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
|
||||
import { DashboardCrudTypes } from '../../../../common/content_management';
|
||||
import { extractTitleAndCount } from '../../../dashboard_container/embeddable/api/lib/extract_title_and_count';
|
||||
import { contentManagementService } from '../../kibana_services';
|
||||
|
||||
|
@ -54,8 +54,8 @@ export async function checkForDuplicateDashboardTitle({
|
|||
const [baseDashboardName] = extractTitleAndCount(title);
|
||||
|
||||
const { hits } = await contentManagementService.client.search<
|
||||
DashboardCrudTypes['SearchIn'],
|
||||
DashboardCrudTypes['SearchOut']
|
||||
DashboardSearchIn,
|
||||
DashboardSearchOut
|
||||
>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
query: {
|
||||
|
|
|
@ -7,18 +7,15 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { DashboardCrudTypes } from '../../../../common/content_management';
|
||||
import type { DeleteIn, DeleteResult } from '@kbn/content-management-plugin/common';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { contentManagementService } from '../../kibana_services';
|
||||
|
||||
export const deleteDashboards = async (ids: string[]) => {
|
||||
const deletePromises = ids.map((id) => {
|
||||
getDashboardContentManagementCache().deleteDashboard(id);
|
||||
return contentManagementService.client.delete<
|
||||
DashboardCrudTypes['DeleteIn'],
|
||||
DashboardCrudTypes['DeleteOut']
|
||||
>({
|
||||
return contentManagementService.client.delete<DeleteIn, DeleteResult>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
id,
|
||||
});
|
||||
|
|
|
@ -10,17 +10,20 @@
|
|||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import { SavedObjectError, SavedObjectsFindOptionsReference } from '@kbn/core/public';
|
||||
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import {
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardCrudTypes,
|
||||
DashboardItem,
|
||||
} from '../../../../common/content_management';
|
||||
DashboardGetIn,
|
||||
DashboardGetOut,
|
||||
DashboardSearchIn,
|
||||
DashboardSearchOut,
|
||||
DashboardSearchOptions,
|
||||
} from '../../../../server/content_management';
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
|
||||
import { contentManagementService } from '../../kibana_services';
|
||||
|
||||
export interface SearchDashboardsArgs {
|
||||
options?: DashboardCrudTypes['SearchIn']['options'];
|
||||
options?: DashboardSearchOptions;
|
||||
hasNoReference?: SavedObjectsFindOptionsReference[];
|
||||
hasReference?: SavedObjectsFindOptionsReference[];
|
||||
search: string;
|
||||
|
@ -29,7 +32,7 @@ export interface SearchDashboardsArgs {
|
|||
|
||||
export interface SearchDashboardsResponse {
|
||||
total: number;
|
||||
hits: DashboardItem[];
|
||||
hits: DashboardSearchOut['hits'];
|
||||
}
|
||||
|
||||
export async function searchDashboards({
|
||||
|
@ -42,10 +45,7 @@ export async function searchDashboards({
|
|||
const {
|
||||
hits,
|
||||
pagination: { total },
|
||||
} = await contentManagementService.client.search<
|
||||
DashboardCrudTypes['SearchIn'],
|
||||
DashboardCrudTypes['SearchOut']
|
||||
>({
|
||||
} = await contentManagementService.client.search<DashboardSearchIn, DashboardSearchOut>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
query: {
|
||||
text: search ? `${search}*` : undefined,
|
||||
|
@ -84,10 +84,7 @@ export async function findDashboardById(id: string): Promise<FindDashboardsByIdR
|
|||
|
||||
/** Otherwise, fetch the dashboard from the content management client, add it to the cache, and return the result */
|
||||
try {
|
||||
const response = await contentManagementService.client.get<
|
||||
DashboardCrudTypes['GetIn'],
|
||||
DashboardCrudTypes['GetOut']
|
||||
>({
|
||||
const response = await contentManagementService.client.get<DashboardGetIn, DashboardGetOut>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
id,
|
||||
});
|
||||
|
@ -119,8 +116,8 @@ export async function findDashboardsByIds(ids: string[]): Promise<FindDashboards
|
|||
|
||||
export async function findDashboardIdByTitle(title: string): Promise<{ id: string } | undefined> {
|
||||
const { hits } = await contentManagementService.client.search<
|
||||
DashboardCrudTypes['SearchIn'],
|
||||
DashboardCrudTypes['SearchOut']
|
||||
DashboardSearchIn,
|
||||
DashboardSearchOut
|
||||
>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
query: {
|
||||
|
|
|
@ -10,19 +10,15 @@
|
|||
import { has } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { injectSearchSourceReferences, parseSearchSourceJSON } from '@kbn/data-plugin/public';
|
||||
import { injectSearchSourceReferences } from '@kbn/data-plugin/public';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import {
|
||||
convertSavedPanelsToPanelMap,
|
||||
injectReferences,
|
||||
type DashboardOptions,
|
||||
} from '../../../../common';
|
||||
import { DashboardCrudTypes } from '../../../../common/content_management';
|
||||
import { convertPanelsArrayToPanelMap, injectReferences } from '../../../../common';
|
||||
import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management';
|
||||
import { DASHBOARD_CONTENT_ID, DEFAULT_DASHBOARD_INPUT } from '../../../dashboard_constants';
|
||||
import {
|
||||
contentManagementService,
|
||||
|
@ -30,7 +26,11 @@ import {
|
|||
embeddableService,
|
||||
savedObjectsTaggingService,
|
||||
} from '../../kibana_services';
|
||||
import type { LoadDashboardFromSavedObjectProps, LoadDashboardReturn } from '../types';
|
||||
import type {
|
||||
DashboardSearchSource,
|
||||
LoadDashboardFromSavedObjectProps,
|
||||
LoadDashboardReturn,
|
||||
} from '../types';
|
||||
import { convertNumberToDashboardVersion } from './dashboard_versioning';
|
||||
import { migrateDashboardInput } from './migrate_dashboard_input';
|
||||
|
||||
|
@ -72,8 +72,8 @@ export const loadDashboardState = async ({
|
|||
/**
|
||||
* Load the saved object from Content Management
|
||||
*/
|
||||
let rawDashboardContent: DashboardCrudTypes['GetOut']['item'];
|
||||
let resolveMeta: DashboardCrudTypes['GetOut']['meta'];
|
||||
let rawDashboardContent: DashboardGetOut['item'];
|
||||
let resolveMeta: DashboardGetOut['meta'];
|
||||
|
||||
const cachedDashboard = dashboardContentManagementCache.fetchDashboard(id);
|
||||
if (cachedDashboard) {
|
||||
|
@ -82,7 +82,7 @@ export const loadDashboardState = async ({
|
|||
} else {
|
||||
/** Otherwise, fetch and load the dashboard from the content management client, and add it to the cache */
|
||||
const result = await contentManagementService.client
|
||||
.get<DashboardCrudTypes['GetIn'], DashboardCrudTypes['GetOut']>({
|
||||
.get<DashboardGetIn, DashboardGetOut>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
id,
|
||||
})
|
||||
|
@ -127,14 +127,16 @@ export const loadDashboardState = async ({
|
|||
/**
|
||||
* Create search source and pull filters and query from it.
|
||||
*/
|
||||
const searchSourceJSON = attributes.kibanaSavedObjectMeta.searchSourceJSON;
|
||||
let searchSourceValues = attributes.kibanaSavedObjectMeta.searchSource;
|
||||
const searchSource = await (async () => {
|
||||
if (!searchSourceJSON) {
|
||||
if (!searchSourceValues) {
|
||||
return await dataSearchService.searchSource.create();
|
||||
}
|
||||
try {
|
||||
let searchSourceValues = parseSearchSourceJSON(searchSourceJSON);
|
||||
searchSourceValues = injectSearchSourceReferences(searchSourceValues as any, references);
|
||||
searchSourceValues = injectSearchSourceReferences(
|
||||
searchSourceValues as any,
|
||||
references
|
||||
) as DashboardSearchSource;
|
||||
return await dataSearchService.searchSource.create(searchSourceValues);
|
||||
} catch (error: any) {
|
||||
return await dataSearchService.searchSource.create();
|
||||
|
@ -151,8 +153,8 @@ export const loadDashboardState = async ({
|
|||
refreshInterval,
|
||||
description,
|
||||
timeRestore,
|
||||
optionsJSON,
|
||||
panelsJSON,
|
||||
options,
|
||||
panels,
|
||||
timeFrom,
|
||||
version,
|
||||
timeTo,
|
||||
|
@ -167,11 +169,7 @@ export const loadDashboardState = async ({
|
|||
}
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* Parse panels and options from JSON
|
||||
*/
|
||||
const options: DashboardOptions = optionsJSON ? JSON.parse(optionsJSON) : undefined;
|
||||
const panels = convertSavedPanelsToPanelMap(panelsJSON ? JSON.parse(panelsJSON) : []);
|
||||
const panelMap = convertPanelsArrayToPanelMap(panels ?? []);
|
||||
|
||||
const { dashboardInput, anyMigrationRun } = migrateDashboardInput({
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
|
@ -183,7 +181,7 @@ export const loadDashboardState = async ({
|
|||
description,
|
||||
timeRange,
|
||||
filters,
|
||||
panels,
|
||||
panels: panelMap,
|
||||
query,
|
||||
title,
|
||||
|
||||
|
@ -192,7 +190,7 @@ export const loadDashboardState = async ({
|
|||
|
||||
controlGroupInput: attributes.controlGroupInput,
|
||||
|
||||
version: convertNumberToDashboardVersion(version),
|
||||
...(version && { version: convertNumberToDashboardVersion(version) }),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -95,7 +95,7 @@ describe('Save dashboard state', () => {
|
|||
currentState: {
|
||||
...getSampleDashboardInput(),
|
||||
title: 'BooThree',
|
||||
panels: { idOne: { type: 'boop' } },
|
||||
panels: { aVerySpecialVeryUniqueId: { type: 'boop' } },
|
||||
} as unknown as DashboardContainerInput,
|
||||
lastSavedId: 'Boogatoonie',
|
||||
saveOptions: { saveAsCopy: true },
|
||||
|
@ -106,7 +106,11 @@ describe('Save dashboard state', () => {
|
|||
expect(contentManagementService.client.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
panelsJSON: expect.not.stringContaining('neverGonnaGetThisId'),
|
||||
panels: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
panelIndex: expect.not.stringContaining('aVerySpecialVeryUniqueId'),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -13,9 +13,16 @@ import moment, { Moment } from 'moment';
|
|||
import { extractSearchSourceReferences, RefreshInterval } from '@kbn/data-plugin/public';
|
||||
import { isFilterPinned } from '@kbn/es-query';
|
||||
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import { getDashboardContentManagementCache } from '..';
|
||||
import { convertPanelMapToSavedPanels, extractReferences } from '../../../../common';
|
||||
import { DashboardAttributes, DashboardCrudTypes } from '../../../../common/content_management';
|
||||
import { convertPanelMapToPanelsArray, extractReferences } from '../../../../common';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardCreateIn,
|
||||
DashboardCreateOut,
|
||||
DashboardUpdateIn,
|
||||
DashboardUpdateOut,
|
||||
} from '../../../../server/content_management';
|
||||
import { generateNewPanelIds } from '../../../../common/lib/dashboard_panel_converters';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
|
||||
import { LATEST_DASHBOARD_CONTAINER_VERSION } from '../../../dashboard_container';
|
||||
|
@ -28,7 +35,7 @@ import {
|
|||
embeddableService,
|
||||
savedObjectsTaggingService,
|
||||
} from '../../kibana_services';
|
||||
import { SaveDashboardProps, SaveDashboardReturn } from '../types';
|
||||
import { DashboardSearchSource, SaveDashboardProps, SaveDashboardReturn } from '../types';
|
||||
import { convertDashboardVersionToNumber } from './dashboard_versioning';
|
||||
|
||||
export const convertTimeToUTCString = (time?: string | Moment): undefined | string => {
|
||||
|
@ -88,33 +95,30 @@ export const saveDashboardState = async ({
|
|||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify filters and query into search source JSON
|
||||
*/
|
||||
const { searchSourceJSON, searchSourceReferences } = await (async () => {
|
||||
const searchSource = await dataSearchService.searchSource.create();
|
||||
searchSource.setField(
|
||||
const { searchSource, searchSourceReferences } = await (async () => {
|
||||
const searchSourceFields = await dataSearchService.searchSource.create();
|
||||
searchSourceFields.setField(
|
||||
'filter', // save only unpinned filters
|
||||
filters.filter((filter) => !isFilterPinned(filter))
|
||||
);
|
||||
searchSource.setField('query', query);
|
||||
searchSourceFields.setField('query', query);
|
||||
|
||||
const rawSearchSourceFields = searchSource.getSerializedFields();
|
||||
const [fields, references] = extractSearchSourceReferences(rawSearchSourceFields);
|
||||
return { searchSourceReferences: references, searchSourceJSON: JSON.stringify(fields) };
|
||||
const rawSearchSourceFields = searchSourceFields.getSerializedFields();
|
||||
const [fields, references] = extractSearchSourceReferences(rawSearchSourceFields) as [
|
||||
DashboardSearchSource,
|
||||
SavedObjectReference[]
|
||||
];
|
||||
return { searchSourceReferences: references, searchSource: fields };
|
||||
})();
|
||||
|
||||
/**
|
||||
* Stringify options and panels
|
||||
*/
|
||||
const optionsJSON = JSON.stringify({
|
||||
const options = {
|
||||
useMargins,
|
||||
syncColors,
|
||||
syncCursor,
|
||||
syncTooltips,
|
||||
hidePanelTitles,
|
||||
});
|
||||
const panelsJSON = JSON.stringify(convertPanelMapToSavedPanels(panels, true));
|
||||
};
|
||||
const savedPanels = convertPanelMapToPanelsArray(panels, true);
|
||||
|
||||
/**
|
||||
* Parse global time filter settings
|
||||
|
@ -134,12 +138,12 @@ export const saveDashboardState = async ({
|
|||
const rawDashboardAttributes: DashboardAttributes = {
|
||||
version: convertDashboardVersionToNumber(LATEST_DASHBOARD_CONTAINER_VERSION),
|
||||
controlGroupInput,
|
||||
kibanaSavedObjectMeta: { searchSourceJSON },
|
||||
kibanaSavedObjectMeta: { searchSource },
|
||||
description: description ?? '',
|
||||
refreshInterval,
|
||||
timeRestore,
|
||||
optionsJSON,
|
||||
panelsJSON,
|
||||
options,
|
||||
panels: savedPanels,
|
||||
timeFrom,
|
||||
title,
|
||||
timeTo,
|
||||
|
@ -174,10 +178,7 @@ export const saveDashboardState = async ({
|
|||
|
||||
try {
|
||||
const result = idToSaveTo
|
||||
? await contentManagementService.client.update<
|
||||
DashboardCrudTypes['UpdateIn'],
|
||||
DashboardCrudTypes['UpdateOut']
|
||||
>({
|
||||
? await contentManagementService.client.update<DashboardUpdateIn, DashboardUpdateOut>({
|
||||
id: idToSaveTo,
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
data: attributes,
|
||||
|
@ -187,10 +188,7 @@ export const saveDashboardState = async ({
|
|||
mergeAttributes: false,
|
||||
},
|
||||
})
|
||||
: await contentManagementService.client.create<
|
||||
DashboardCrudTypes['CreateIn'],
|
||||
DashboardCrudTypes['CreateOut']
|
||||
>({
|
||||
: await contentManagementService.client.create<DashboardCreateIn, DashboardCreateOut>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
data: attributes,
|
||||
options: {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { DashboardContainerInput } from '../../../../common';
|
||||
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants';
|
||||
import { DashboardCrudTypes } from '../../../../common/content_management';
|
||||
import type { DashboardUpdateIn, DashboardUpdateOut } from '../../../../server/content_management';
|
||||
import { findDashboardsByIds } from './find_dashboards';
|
||||
import { contentManagementService, savedObjectsTaggingService } from '../../kibana_services';
|
||||
|
||||
|
@ -35,10 +35,7 @@ export const updateDashboardMeta = async ({
|
|||
? savedObjectsTaggingApi.ui.updateTagsReferences(dashboard.references, tags)
|
||||
: dashboard.references;
|
||||
|
||||
await contentManagementService.client.update<
|
||||
DashboardCrudTypes['UpdateIn'],
|
||||
DashboardCrudTypes['UpdateOut']
|
||||
>({
|
||||
await contentManagementService.client.update<DashboardUpdateIn, DashboardUpdateOut>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
id,
|
||||
data: { title, description },
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
*/
|
||||
|
||||
import type { Reference } from '@kbn/content-management-utils';
|
||||
import type { Query, SerializedSearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public';
|
||||
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
|
||||
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import { DashboardAttributes, DashboardCrudTypes } from '../../../common/content_management';
|
||||
import type { DashboardAttributes, DashboardGetOut } from '../../../server/content_management';
|
||||
import { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title';
|
||||
import {
|
||||
FindDashboardsByIdResponse,
|
||||
|
@ -38,7 +39,7 @@ export interface LoadDashboardFromSavedObjectProps {
|
|||
id?: string;
|
||||
}
|
||||
|
||||
type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta'];
|
||||
type DashboardResolveMeta = DashboardGetOut['meta'];
|
||||
|
||||
export type SavedDashboardInput = DashboardContainerInput & {
|
||||
/**
|
||||
|
@ -54,6 +55,10 @@ export type SavedDashboardInput = DashboardContainerInput & {
|
|||
controlGroupState?: Partial<ControlGroupRuntimeState>;
|
||||
};
|
||||
|
||||
export type DashboardSearchSource = Omit<SerializedSearchSourceFields, 'query'> & {
|
||||
query?: Query;
|
||||
};
|
||||
|
||||
export interface LoadDashboardReturn {
|
||||
dashboardFound: boolean;
|
||||
newDashboardCreated?: boolean;
|
||||
|
|
|
@ -32,7 +32,8 @@ import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks
|
|||
import { visualizationsPluginMock } from '@kbn/visualizations-plugin/public/mocks';
|
||||
|
||||
import { setKibanaServices } from './kibana_services';
|
||||
import { DashboardAttributes, DashboardCapabilities } from '../../common';
|
||||
import { DashboardAttributes } from '../../server/content_management';
|
||||
import { DashboardCapabilities } from '../../common';
|
||||
import { LoadDashboardReturn } from './dashboard_content_management_service/types';
|
||||
import { SearchDashboardsResponse } from './dashboard_content_management_service/lib/find_dashboards';
|
||||
|
||||
|
|
12
src/plugins/dashboard/server/api/constants.ts
Normal file
12
src/plugins/dashboard/server/api/constants.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export const PUBLIC_API_VERSION = '2023-10-31';
|
||||
export const PUBLIC_API_CONTENT_MANAGEMENT_VERSION = 3;
|
||||
export const PUBLIC_API_PATH = '/api/dashboards/dashboard';
|
|
@ -7,8 +7,4 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export {
|
||||
serviceDefinition,
|
||||
dashboardSavedObjectSchema,
|
||||
dashboardAttributesSchema,
|
||||
} from './cm_services';
|
||||
export { registerAPIRoutes } from './register_routes';
|
327
src/plugins/dashboard/server/api/register_routes.ts
Normal file
327
src/plugins/dashboard/server/api/register_routes.ts
Normal file
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
|
||||
import type { HttpServiceSetup } from '@kbn/core/server';
|
||||
import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import {
|
||||
PUBLIC_API_PATH,
|
||||
PUBLIC_API_VERSION,
|
||||
PUBLIC_API_CONTENT_MANAGEMENT_VERSION,
|
||||
} from './constants';
|
||||
import {
|
||||
dashboardAttributesSchema,
|
||||
dashboardGetResultSchema,
|
||||
dashboardCreateResultSchema,
|
||||
dashboardSearchResultsSchema,
|
||||
referenceSchema,
|
||||
} from '../content_management/v3';
|
||||
|
||||
interface RegisterAPIRoutesArgs {
|
||||
http: HttpServiceSetup;
|
||||
contentManagement: ContentManagementServerSetup;
|
||||
restCounter?: UsageCounter;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
const TECHNICAL_PREVIEW_WARNING =
|
||||
'This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.';
|
||||
|
||||
export function registerAPIRoutes({
|
||||
http,
|
||||
contentManagement,
|
||||
restCounter,
|
||||
logger,
|
||||
}: RegisterAPIRoutesArgs) {
|
||||
const { versioned: versionedRouter } = http.createRouter();
|
||||
|
||||
// Create API route
|
||||
const createRoute = versionedRouter.post({
|
||||
path: `${PUBLIC_API_PATH}/{id?}`,
|
||||
access: 'public',
|
||||
summary: 'Create a dashboard',
|
||||
description: TECHNICAL_PREVIEW_WARNING,
|
||||
options: {
|
||||
tags: ['oas-tag:Dashboards'],
|
||||
},
|
||||
});
|
||||
|
||||
createRoute.addVersion(
|
||||
{
|
||||
version: PUBLIC_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.maybe(schema.string()),
|
||||
}),
|
||||
body: schema.object({
|
||||
attributes: dashboardAttributesSchema,
|
||||
references: schema.maybe(schema.arrayOf(referenceSchema)),
|
||||
spaces: schema.maybe(schema.arrayOf(schema.string())),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: () => dashboardCreateResultSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const { id } = req.params;
|
||||
const { attributes, references, spaces: initialNamespaces } = req.body;
|
||||
const client = contentManagement.contentClient
|
||||
.getForRequest({ request: req, requestHandlerContext: ctx })
|
||||
.for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
|
||||
let result;
|
||||
try {
|
||||
({ result } = await client.create(attributes, {
|
||||
id,
|
||||
references,
|
||||
initialNamespaces,
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e.isBoom && e.output.statusCode === 409) {
|
||||
return res.conflict({
|
||||
body: {
|
||||
message: `A dashboard with saved object ID ${id} already exists.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (e.isBoom && e.output.statusCode === 403) {
|
||||
return res.forbidden();
|
||||
}
|
||||
|
||||
return res.badRequest();
|
||||
}
|
||||
|
||||
return res.ok({ body: result });
|
||||
}
|
||||
);
|
||||
|
||||
// Update API route
|
||||
|
||||
const updateRoute = versionedRouter.put({
|
||||
path: `${PUBLIC_API_PATH}/{id}`,
|
||||
access: 'public',
|
||||
summary: `Update an existing dashboard.`,
|
||||
description: TECHNICAL_PREVIEW_WARNING,
|
||||
options: {
|
||||
tags: ['oas-tag:Dashboards'],
|
||||
},
|
||||
});
|
||||
|
||||
updateRoute.addVersion(
|
||||
{
|
||||
version: PUBLIC_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
attributes: dashboardAttributesSchema,
|
||||
references: schema.maybe(schema.arrayOf(referenceSchema)),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: () => dashboardCreateResultSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const { attributes, references } = req.body;
|
||||
const client = contentManagement.contentClient
|
||||
.getForRequest({ request: req, requestHandlerContext: ctx })
|
||||
.for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
|
||||
let result;
|
||||
try {
|
||||
({ result } = await client.update(req.params.id, attributes, { references }));
|
||||
} catch (e) {
|
||||
if (e.isBoom && e.output.statusCode === 404) {
|
||||
return res.notFound({
|
||||
body: {
|
||||
message: `A dashboard with saved object ID ${req.params.id} was not found.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (e.isBoom && e.output.statusCode === 403) {
|
||||
return res.forbidden();
|
||||
}
|
||||
return res.badRequest(e.message);
|
||||
}
|
||||
|
||||
return res.created({ body: result });
|
||||
}
|
||||
);
|
||||
|
||||
// List API route
|
||||
const listRoute = versionedRouter.get({
|
||||
path: `${PUBLIC_API_PATH}`,
|
||||
access: 'public',
|
||||
summary: `Get a list of dashboards.`,
|
||||
description: TECHNICAL_PREVIEW_WARNING,
|
||||
options: {
|
||||
tags: ['oas-tag:Dashboards'],
|
||||
},
|
||||
});
|
||||
|
||||
listRoute.addVersion(
|
||||
{
|
||||
version: PUBLIC_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 1 }),
|
||||
perPage: schema.maybe(schema.number()),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: () =>
|
||||
schema.object({
|
||||
items: schema.arrayOf(dashboardSearchResultsSchema),
|
||||
total: schema.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const { page, perPage: limit } = req.query;
|
||||
const client = contentManagement.contentClient
|
||||
.getForRequest({ request: req, requestHandlerContext: ctx })
|
||||
.for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
|
||||
let result;
|
||||
try {
|
||||
// TODO add filtering
|
||||
({ result } = await client.search({ cursor: page.toString(), limit }));
|
||||
} catch (e) {
|
||||
if (e.isBoom && e.output.statusCode === 403) {
|
||||
return res.forbidden();
|
||||
}
|
||||
|
||||
return res.badRequest();
|
||||
}
|
||||
|
||||
const body = {
|
||||
items: result.hits,
|
||||
total: result.pagination.total,
|
||||
};
|
||||
return res.ok({ body });
|
||||
}
|
||||
);
|
||||
|
||||
// Get API route
|
||||
const getRoute = versionedRouter.get({
|
||||
path: `${PUBLIC_API_PATH}/{id}`,
|
||||
access: 'public',
|
||||
summary: `Get a dashboard.`,
|
||||
description: TECHNICAL_PREVIEW_WARNING,
|
||||
options: {
|
||||
tags: ['oas-tag:Dashboards'],
|
||||
},
|
||||
});
|
||||
|
||||
getRoute.addVersion(
|
||||
{
|
||||
version: PUBLIC_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: () => dashboardGetResultSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const client = contentManagement.contentClient
|
||||
.getForRequest({ request: req, requestHandlerContext: ctx })
|
||||
.for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
|
||||
let result;
|
||||
try {
|
||||
({ result } = await client.get(req.params.id));
|
||||
} catch (e) {
|
||||
if (e.isBoom && e.output.statusCode === 404) {
|
||||
return res.notFound({
|
||||
body: {
|
||||
message: `A dashboard with saved object ID ${req.params.id}] was not found.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (e.isBoom && e.output.statusCode === 403) {
|
||||
return res.forbidden();
|
||||
}
|
||||
|
||||
return res.badRequest(e.message);
|
||||
}
|
||||
|
||||
return res.ok({ body: result });
|
||||
}
|
||||
);
|
||||
|
||||
// Delete API route
|
||||
const deleteRoute = versionedRouter.delete({
|
||||
path: `${PUBLIC_API_PATH}/{id}`,
|
||||
access: 'public',
|
||||
summary: `Delete a dashboard.`,
|
||||
description: TECHNICAL_PREVIEW_WARNING,
|
||||
options: {
|
||||
tags: ['oas-tag:Dashboards'],
|
||||
},
|
||||
});
|
||||
|
||||
deleteRoute.addVersion(
|
||||
{
|
||||
version: PUBLIC_API_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
const client = contentManagement.contentClient
|
||||
.getForRequest({ request: req, requestHandlerContext: ctx })
|
||||
.for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
|
||||
try {
|
||||
await client.delete(req.params.id);
|
||||
} catch (e) {
|
||||
if (e.isBoom && e.output.statusCode === 404) {
|
||||
return res.notFound({
|
||||
body: {
|
||||
message: `A dashboard with saved object ID ${req.params.id} was not found.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (e.isBoom && e.output.statusCode === 403) {
|
||||
return res.forbidden();
|
||||
}
|
||||
return res.badRequest();
|
||||
}
|
||||
|
||||
return res.ok();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -17,8 +17,10 @@ import type {
|
|||
|
||||
import { serviceDefinition as v1 } from './v1';
|
||||
import { serviceDefinition as v2 } from './v2';
|
||||
import { serviceDefinition as v3 } from './v3';
|
||||
|
||||
export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = {
|
||||
1: v1,
|
||||
2: v2,
|
||||
3: v3,
|
||||
};
|
|
@ -7,23 +7,40 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import Boom from '@hapi/boom';
|
||||
import { tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import {
|
||||
SavedObjectsFindOptions,
|
||||
SavedObjectsFindResult,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import { cmServicesDefinition } from './schema/cm_services';
|
||||
import type { DashboardCrudTypes } from '../../common/content_management';
|
||||
import { CreateResult, DeleteResult, SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import { StorageContext } from '@kbn/content-management-plugin/server';
|
||||
import { DASHBOARD_SAVED_OBJECT_TYPE } from '../dashboard_saved_object';
|
||||
import { cmServicesDefinition } from './cm_services';
|
||||
import { DashboardSavedObjectAttributes } from '../dashboard_saved_object';
|
||||
import { itemAttrsToSavedObjectAttrs, savedObjectToItem } from './latest';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardItem,
|
||||
DashboardCreateOut,
|
||||
DashboardCreateOptions,
|
||||
DashboardGetOut,
|
||||
DashboardSearchOut,
|
||||
DashboardUpdateOptions,
|
||||
DashboardUpdateOut,
|
||||
DashboardSearchOptions,
|
||||
} from './latest';
|
||||
|
||||
const searchArgsToSOFindOptions = (
|
||||
args: DashboardCrudTypes['SearchIn']
|
||||
query: SearchQuery,
|
||||
options: DashboardSearchOptions
|
||||
): SavedObjectsFindOptions => {
|
||||
const { query, contentTypeId, options } = args;
|
||||
|
||||
return {
|
||||
type: contentTypeId,
|
||||
type: DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
searchFields: options?.onlyTitle ? ['title'] : ['title^3', 'description'],
|
||||
fields: ['description', 'title', 'timeRestore'],
|
||||
fields: options?.fields ?? ['title', 'description', 'timeRestore'],
|
||||
search: query.text,
|
||||
perPage: query.limit,
|
||||
page: query.cursor ? +query.cursor : undefined,
|
||||
|
@ -32,7 +49,16 @@ const searchArgsToSOFindOptions = (
|
|||
};
|
||||
};
|
||||
|
||||
export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
||||
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
|
||||
if (!ctx.requestHandlerContext) {
|
||||
throw new Error('Storage context.requestHandlerContext missing.');
|
||||
}
|
||||
|
||||
const { savedObjects } = await ctx.requestHandlerContext.core;
|
||||
return savedObjects.client;
|
||||
};
|
||||
|
||||
export class DashboardStorage {
|
||||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
|
@ -40,26 +66,316 @@ export class DashboardStorage extends SOContentStorage<DashboardCrudTypes> {
|
|||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}) {
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
searchArgsToSOFindOptions,
|
||||
enableMSearch: true,
|
||||
allowedSavedObjectAttributes: [
|
||||
'kibanaSavedObjectMeta',
|
||||
'controlGroupInput',
|
||||
'refreshInterval',
|
||||
'description',
|
||||
'timeRestore',
|
||||
'optionsJSON',
|
||||
'panelsJSON',
|
||||
'timeFrom',
|
||||
'version',
|
||||
'timeTo',
|
||||
'title',
|
||||
],
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
});
|
||||
this.logger = logger;
|
||||
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
|
||||
this.mSearch = {
|
||||
savedObjectType: DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
additionalSearchFields: [],
|
||||
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): DashboardItem => {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(
|
||||
savedObject as SavedObjectsFindResult<DashboardSavedObjectAttributes>,
|
||||
false
|
||||
);
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
||||
const validationError = transforms.mSearch.out.result.validate(item);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.mSearch.out.result.down<
|
||||
DashboardItem,
|
||||
DashboardItem
|
||||
>(
|
||||
item,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private logger: Logger;
|
||||
private throwOnResultValidationError: boolean;
|
||||
|
||||
mSearch: {
|
||||
savedObjectType: string;
|
||||
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult) => DashboardItem;
|
||||
additionalSearchFields?: string[];
|
||||
};
|
||||
|
||||
async get(ctx: StorageContext, id: string): Promise<DashboardGetOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Save data in DB
|
||||
const {
|
||||
saved_object: savedObject,
|
||||
alias_purpose: aliasPurpose,
|
||||
alias_target_id: aliasTargetId,
|
||||
outcome,
|
||||
} = await soClient.resolve<DashboardSavedObjectAttributes>(DASHBOARD_SAVED_OBJECT_TYPE, id);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(savedObject, false);
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
||||
const response = { item, meta: { aliasPurpose, aliasTargetId, outcome } };
|
||||
|
||||
const validationError = transforms.get.out.result.validate(response);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.get.out.result.down<
|
||||
DashboardGetOut,
|
||||
DashboardGetOut
|
||||
>(
|
||||
response,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async bulkGet(): Promise<never> {
|
||||
// Not implemented
|
||||
throw new Error(`[bulkGet] has not been implemented. See DashboardStorage class.`);
|
||||
}
|
||||
|
||||
async create(
|
||||
ctx: StorageContext,
|
||||
data: DashboardAttributes,
|
||||
options: DashboardCreateOptions
|
||||
): Promise<DashboardCreateOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
|
||||
DashboardAttributes,
|
||||
DashboardAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
|
||||
DashboardCreateOptions,
|
||||
DashboardCreateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const { attributes: soAttributes, error: attributesError } =
|
||||
itemAttrsToSavedObjectAttrs(dataToLatest);
|
||||
if (attributesError) {
|
||||
throw Boom.badRequest(`Invalid data. ${attributesError.message}`);
|
||||
}
|
||||
|
||||
// Save data in DB
|
||||
const savedObject = await soClient.create<DashboardSavedObjectAttributes>(
|
||||
DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
soAttributes,
|
||||
optionsToLatest
|
||||
);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(savedObject, false);
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
||||
const validationError = transforms.create.out.result.validate({ item });
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.create.out.result.down<
|
||||
CreateResult<DashboardItem>
|
||||
>(
|
||||
{ item },
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async update(
|
||||
ctx: StorageContext,
|
||||
id: string,
|
||||
data: DashboardAttributes,
|
||||
options: DashboardUpdateOptions
|
||||
): Promise<DashboardUpdateOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
|
||||
DashboardAttributes,
|
||||
DashboardAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
|
||||
DashboardUpdateOptions,
|
||||
DashboardUpdateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const { attributes: soAttributes, error: attributesError } =
|
||||
itemAttrsToSavedObjectAttrs(dataToLatest);
|
||||
if (attributesError) {
|
||||
throw Boom.badRequest(`Invalid data. ${attributesError.message}`);
|
||||
}
|
||||
|
||||
// Save data in DB
|
||||
const partialSavedObject = await soClient.update<DashboardSavedObjectAttributes>(
|
||||
DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
id,
|
||||
soAttributes,
|
||||
optionsToLatest
|
||||
);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(partialSavedObject, true);
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
||||
const validationError = transforms.update.out.result.validate({ item });
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.update.out.result.down<
|
||||
DashboardUpdateOut,
|
||||
DashboardUpdateOut
|
||||
>(
|
||||
{ item },
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async delete(
|
||||
ctx: StorageContext,
|
||||
id: string,
|
||||
// force is necessary to delete saved objects that exist in multiple namespaces
|
||||
options?: { force: boolean }
|
||||
): Promise<DeleteResult> {
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
await soClient.delete(DASHBOARD_SAVED_OBJECT_TYPE, id, { force: options?.force ?? false });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async search(
|
||||
ctx: StorageContext,
|
||||
query: SearchQuery,
|
||||
options: DashboardSearchOptions
|
||||
): Promise<DashboardSearchOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate and UP transform the options
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
|
||||
DashboardSearchOptions,
|
||||
DashboardSearchOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const soQuery = searchArgsToSOFindOptions(query, optionsToLatest);
|
||||
// Execute the query in the DB
|
||||
const soResponse = await soClient.find<DashboardSavedObjectAttributes>(soQuery);
|
||||
const hits = soResponse.saved_objects
|
||||
.map((so) => {
|
||||
const { item } = savedObjectToItem(so, false, soQuery.fields);
|
||||
return item;
|
||||
})
|
||||
// Ignore any saved objects that failed to convert to items.
|
||||
.filter((item) => item !== null);
|
||||
const response = {
|
||||
hits,
|
||||
pagination: {
|
||||
total: soResponse.total,
|
||||
},
|
||||
};
|
||||
|
||||
const validationError = transforms.search.out.result.validate(response);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.search.out.result.down<
|
||||
DashboardSearchOut,
|
||||
DashboardSearchOut
|
||||
>(
|
||||
response,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,24 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type {
|
||||
ControlGroupAttributes,
|
||||
GridData,
|
||||
DashboardPanel,
|
||||
DashboardAttributes,
|
||||
DashboardItem,
|
||||
DashboardGetIn,
|
||||
DashboardGetOut,
|
||||
DashboardCreateIn,
|
||||
DashboardCreateOut,
|
||||
DashboardCreateOptions,
|
||||
DashboardSearchIn,
|
||||
DashboardSearchOut,
|
||||
DashboardSearchOptions,
|
||||
DashboardUpdateIn,
|
||||
DashboardUpdateOut,
|
||||
DashboardUpdateOptions,
|
||||
DashboardOptions,
|
||||
} from './latest';
|
||||
|
||||
export { DashboardStorage } from './dashboard_storage';
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
// Latest version is 2
|
||||
export * from './v2';
|
||||
// Latest version is 3
|
||||
export * from './v3';
|
|
@ -16,51 +16,7 @@ import {
|
|||
updateOptionsSchema,
|
||||
createResultSchema,
|
||||
} from '@kbn/content-management-utils';
|
||||
|
||||
export const controlGroupInputSchema = schema
|
||||
.object({
|
||||
panelsJSON: schema.maybe(schema.string()),
|
||||
controlStyle: schema.maybe(schema.string()),
|
||||
chainingSystem: schema.maybe(schema.string()),
|
||||
ignoreParentSettingsJSON: schema.maybe(schema.string()),
|
||||
})
|
||||
.extends({}, { unknowns: 'ignore' });
|
||||
|
||||
export const dashboardAttributesSchema = schema.object(
|
||||
{
|
||||
// General
|
||||
title: schema.string(),
|
||||
description: schema.string({ defaultValue: '' }),
|
||||
|
||||
// Search
|
||||
kibanaSavedObjectMeta: schema.object({
|
||||
searchSourceJSON: schema.maybe(schema.string()),
|
||||
}),
|
||||
|
||||
// Time
|
||||
timeRestore: schema.maybe(schema.boolean()),
|
||||
timeFrom: schema.maybe(schema.string()),
|
||||
timeTo: schema.maybe(schema.string()),
|
||||
refreshInterval: schema.maybe(
|
||||
schema.object({
|
||||
pause: schema.boolean(),
|
||||
value: schema.number(),
|
||||
display: schema.maybe(schema.string()),
|
||||
section: schema.maybe(schema.number()),
|
||||
})
|
||||
),
|
||||
|
||||
// Dashboard Content
|
||||
controlGroupInput: schema.maybe(controlGroupInputSchema),
|
||||
panelsJSON: schema.string({ defaultValue: '[]' }),
|
||||
optionsJSON: schema.string({ defaultValue: '{}' }),
|
||||
|
||||
// Legacy
|
||||
hits: schema.maybe(schema.number()),
|
||||
version: schema.maybe(schema.number()),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
);
|
||||
import { dashboardAttributesSchema } from '../../dashboard_saved_object/schema/v1';
|
||||
|
||||
export const dashboardSavedObjectSchema = savedObjectSchema(dashboardAttributesSchema);
|
||||
|
||||
|
@ -84,8 +40,10 @@ const dashboardUpdateOptionsSchema = schema.object({
|
|||
mergeAttributes: schema.maybe(updateOptionsSchema.mergeAttributes),
|
||||
});
|
||||
|
||||
// Content management service definition.
|
||||
// We need it for BWC support between different versions of the content
|
||||
/**
|
||||
* Content management service definition v1.
|
||||
* Dashboard attributes in content management version v1 are tightly coupled with the v1 model version saved object schema.
|
||||
*/
|
||||
export const serviceDefinition: ServicesDefinition = {
|
||||
get: {
|
||||
out: {
|
|
@ -7,9 +7,4 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export {
|
||||
serviceDefinition,
|
||||
dashboardSavedObjectSchema,
|
||||
controlGroupInputSchema,
|
||||
dashboardAttributesSchema,
|
||||
} from './cm_services';
|
||||
export { serviceDefinition } from './cm_services';
|
|
@ -7,36 +7,23 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
createResultSchema,
|
||||
objectTypeToGetResultSchema,
|
||||
savedObjectSchema,
|
||||
} from '@kbn/content-management-utils';
|
||||
import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
|
||||
import {
|
||||
controlGroupInputSchema as controlGroupInputSchemaV1,
|
||||
dashboardAttributesSchema as dashboardAttributesSchemaV1,
|
||||
serviceDefinition as serviceDefinitionV1,
|
||||
} from '../v1';
|
||||
|
||||
export const dashboardAttributesSchema = dashboardAttributesSchemaV1.extends(
|
||||
{
|
||||
controlGroupInput: schema.maybe(
|
||||
controlGroupInputSchemaV1.extends(
|
||||
{
|
||||
showApplySelections: schema.maybe(schema.boolean()),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
)
|
||||
),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
import type { DashboardCrudTypes } from '../../../common/content_management/v2';
|
||||
import { serviceDefinition as serviceDefinitionV1 } from '../v1';
|
||||
import { dashboardAttributesOut as attributesTov3 } from '../v3';
|
||||
import { dashboardAttributesSchema } from '../../dashboard_saved_object/schema/v2';
|
||||
|
||||
export const dashboardSavedObjectSchema = savedObjectSchema(dashboardAttributesSchema);
|
||||
|
||||
// Content management service definition.
|
||||
/**
|
||||
* Content management service definition v2.
|
||||
* Dashboard attributes in content management version v2 are tightly coupled with the v2 model version saved object schema.
|
||||
*/
|
||||
export const serviceDefinition: ServicesDefinition = {
|
||||
get: {
|
||||
out: {
|
||||
|
@ -50,6 +37,7 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
...serviceDefinitionV1?.create?.in,
|
||||
data: {
|
||||
schema: dashboardAttributesSchema,
|
||||
up: (data: DashboardCrudTypes['CreateIn']['data']) => attributesTov3(data),
|
||||
},
|
||||
},
|
||||
out: {
|
||||
|
@ -63,6 +51,7 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
...serviceDefinitionV1.update?.in,
|
||||
data: {
|
||||
schema: dashboardAttributesSchema,
|
||||
up: (data: DashboardCrudTypes['UpdateIn']['data']) => attributesTov3(data),
|
||||
},
|
||||
},
|
||||
},
|
10
src/plugins/dashboard/server/content_management/v2/index.ts
Normal file
10
src/plugins/dashboard/server/content_management/v2/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { serviceDefinition } from './cm_services';
|
|
@ -0,0 +1,539 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { schema, Type } from '@kbn/config-schema';
|
||||
import { createOptionsSchemas, updateOptionsSchema } from '@kbn/content-management-utils';
|
||||
import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning';
|
||||
import {
|
||||
type ControlGroupChainingSystem,
|
||||
type ControlLabelPosition,
|
||||
type ControlWidth,
|
||||
CONTROL_CHAINING_OPTIONS,
|
||||
CONTROL_LABEL_POSITION_OPTIONS,
|
||||
CONTROL_WIDTH_OPTIONS,
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_GROW,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { SortDirection } from '@kbn/data-plugin/common/search';
|
||||
import {
|
||||
DASHBOARD_GRID_COLUMN_COUNT,
|
||||
DEFAULT_PANEL_HEIGHT,
|
||||
DEFAULT_PANEL_WIDTH,
|
||||
DEFAULT_DASHBOARD_OPTIONS,
|
||||
} from '../../../common/content_management';
|
||||
import { getResultV3ToV2 } from './transform_utils';
|
||||
|
||||
const apiError = schema.object({
|
||||
error: schema.string(),
|
||||
message: schema.string(),
|
||||
statusCode: schema.number(),
|
||||
metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
|
||||
});
|
||||
|
||||
// This schema should be provided by the controls plugin. Perhaps we can resolve this with the embeddable registry.
|
||||
// See https://github.com/elastic/kibana/issues/192622
|
||||
export const controlGroupInputSchema = schema.object({
|
||||
controls: schema.arrayOf(
|
||||
schema.object(
|
||||
{
|
||||
type: schema.string({ meta: { description: 'The type of the control panel.' } }),
|
||||
controlConfig: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
id: schema.string({
|
||||
defaultValue: uuidv4(),
|
||||
meta: { description: 'The unique ID of the control.' },
|
||||
}),
|
||||
order: schema.number({
|
||||
meta: {
|
||||
description: 'The order of the control panel in the control group.',
|
||||
},
|
||||
}),
|
||||
width: schema.oneOf(
|
||||
Object.values(CONTROL_WIDTH_OPTIONS).map((value) => schema.literal(value)) as [
|
||||
Type<ControlWidth>
|
||||
],
|
||||
{
|
||||
defaultValue: DEFAULT_CONTROL_WIDTH,
|
||||
meta: { description: 'Minimum width of the control panel in the control group.' },
|
||||
}
|
||||
),
|
||||
grow: schema.boolean({
|
||||
defaultValue: DEFAULT_CONTROL_GROW,
|
||||
meta: { description: 'Expand width of the control panel to fit available space.' },
|
||||
}),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
{
|
||||
defaultValue: [],
|
||||
meta: { description: 'An array of control panels and their state in the control group.' },
|
||||
}
|
||||
),
|
||||
labelPosition: schema.oneOf(
|
||||
Object.values(CONTROL_LABEL_POSITION_OPTIONS).map((value) => schema.literal(value)) as [
|
||||
Type<ControlLabelPosition>
|
||||
],
|
||||
{
|
||||
defaultValue: DEFAULT_CONTROL_LABEL_POSITION,
|
||||
meta: {
|
||||
description: 'Position of the labels for controls. For example, "oneLine", "twoLine".',
|
||||
},
|
||||
}
|
||||
),
|
||||
chainingSystem: schema.oneOf(
|
||||
Object.values(CONTROL_CHAINING_OPTIONS).map((value) => schema.literal(value)) as [
|
||||
Type<ControlGroupChainingSystem>
|
||||
],
|
||||
{
|
||||
defaultValue: DEFAULT_CONTROL_CHAINING,
|
||||
meta: {
|
||||
description:
|
||||
'The chaining strategy for multiple controls. For example, "HIERARCHICAL" or "NONE".',
|
||||
},
|
||||
}
|
||||
),
|
||||
enhancements: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
ignoreParentSettings: schema.object({
|
||||
ignoreFilters: schema.boolean({
|
||||
meta: { description: 'Ignore global filters in controls.' },
|
||||
defaultValue: DEFAULT_IGNORE_PARENT_SETTINGS.ignoreFilters,
|
||||
}),
|
||||
ignoreQuery: schema.boolean({
|
||||
meta: { description: 'Ignore the global query bar in controls.' },
|
||||
defaultValue: DEFAULT_IGNORE_PARENT_SETTINGS.ignoreQuery,
|
||||
}),
|
||||
ignoreTimerange: schema.boolean({
|
||||
meta: { description: 'Ignore the global time range in controls.' },
|
||||
defaultValue: DEFAULT_IGNORE_PARENT_SETTINGS.ignoreTimerange,
|
||||
}),
|
||||
ignoreValidations: schema.boolean({
|
||||
meta: { description: 'Ignore validations in controls.' },
|
||||
defaultValue: DEFAULT_IGNORE_PARENT_SETTINGS.ignoreValidations,
|
||||
}),
|
||||
}),
|
||||
autoApplySelections: schema.boolean({
|
||||
meta: { description: 'Show apply selections button in controls.' },
|
||||
defaultValue: DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
}),
|
||||
});
|
||||
|
||||
const searchSourceSchema = schema.object(
|
||||
{
|
||||
type: schema.maybe(schema.string()),
|
||||
query: schema.maybe(
|
||||
schema.object({
|
||||
query: schema.oneOf([
|
||||
schema.string({
|
||||
meta: {
|
||||
description:
|
||||
'A text-based query such as Kibana Query Language (KQL) or Lucene query language.',
|
||||
},
|
||||
}),
|
||||
schema.recordOf(schema.string(), schema.any()),
|
||||
]),
|
||||
language: schema.string({
|
||||
meta: { description: 'The query language such as KQL or Lucene.' },
|
||||
}),
|
||||
})
|
||||
),
|
||||
filter: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object(
|
||||
{
|
||||
meta: schema.object(
|
||||
{
|
||||
alias: schema.maybe(schema.nullable(schema.string())),
|
||||
disabled: schema.maybe(schema.boolean()),
|
||||
negate: schema.maybe(schema.boolean()),
|
||||
controlledBy: schema.maybe(schema.string()),
|
||||
group: schema.maybe(schema.string()),
|
||||
index: schema.maybe(schema.string()),
|
||||
isMultiIndex: schema.maybe(schema.boolean()),
|
||||
type: schema.maybe(schema.string()),
|
||||
key: schema.maybe(schema.string()),
|
||||
params: schema.maybe(schema.any()),
|
||||
value: schema.maybe(schema.string()),
|
||||
field: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
$state: schema.maybe(
|
||||
schema.object({
|
||||
store: schema.oneOf(
|
||||
[
|
||||
schema.literal(FilterStateStore.APP_STATE),
|
||||
schema.literal(FilterStateStore.GLOBAL_STATE),
|
||||
],
|
||||
{
|
||||
meta: {
|
||||
description:
|
||||
"Denote whether a filter is specific to an application's context (e.g. 'appState') or whether it should be applied globally (e.g. 'globalState').",
|
||||
},
|
||||
}
|
||||
),
|
||||
})
|
||||
),
|
||||
},
|
||||
{ meta: { description: 'A filter for the search source.' } }
|
||||
)
|
||||
)
|
||||
),
|
||||
sort: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.oneOf([
|
||||
schema.oneOf([schema.literal(SortDirection.asc), schema.literal(SortDirection.desc)]),
|
||||
schema.object({
|
||||
order: schema.oneOf([
|
||||
schema.literal(SortDirection.asc),
|
||||
schema.literal(SortDirection.desc),
|
||||
]),
|
||||
format: schema.maybe(schema.string()),
|
||||
}),
|
||||
schema.object({
|
||||
order: schema.oneOf([
|
||||
schema.literal(SortDirection.asc),
|
||||
schema.literal(SortDirection.desc),
|
||||
]),
|
||||
numeric_type: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal('double'),
|
||||
schema.literal('long'),
|
||||
schema.literal('date'),
|
||||
schema.literal('date_nanos'),
|
||||
])
|
||||
),
|
||||
}),
|
||||
])
|
||||
)
|
||||
)
|
||||
),
|
||||
},
|
||||
/**
|
||||
The Dashboard _should_ only ever uses the query and filters fields on the search
|
||||
source. But we should be liberal in what we accept, so we allow unknowns.
|
||||
*/
|
||||
{ defaultValue: {}, unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const gridDataSchema = schema.object({
|
||||
x: schema.number({ meta: { description: 'The x coordinate of the panel in grid units' } }),
|
||||
y: schema.number({ meta: { description: 'The y coordinate of the panel in grid units' } }),
|
||||
w: schema.number({
|
||||
defaultValue: DEFAULT_PANEL_WIDTH,
|
||||
min: 1,
|
||||
max: DASHBOARD_GRID_COLUMN_COUNT,
|
||||
meta: { description: 'The width of the panel in grid units' },
|
||||
}),
|
||||
h: schema.number({
|
||||
defaultValue: DEFAULT_PANEL_HEIGHT,
|
||||
min: 1,
|
||||
meta: { description: 'The height of the panel in grid units' },
|
||||
}),
|
||||
i: schema.string({
|
||||
meta: { description: 'The unique identifier of the panel' },
|
||||
defaultValue: uuidv4(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const panelSchema = schema.object({
|
||||
panelConfig: schema.object(
|
||||
{
|
||||
version: schema.maybe(
|
||||
schema.string({
|
||||
meta: { description: 'The version of the embeddable in the panel.' },
|
||||
})
|
||||
),
|
||||
title: schema.maybe(schema.string({ meta: { description: 'The title of the panel' } })),
|
||||
description: schema.maybe(
|
||||
schema.string({ meta: { description: 'The description of the panel' } })
|
||||
),
|
||||
savedObjectId: schema.maybe(
|
||||
schema.string({
|
||||
meta: { description: 'The unique id of the library item to construct the embeddable.' },
|
||||
})
|
||||
),
|
||||
hidePanelTitles: schema.maybe(
|
||||
schema.boolean({
|
||||
defaultValue: false,
|
||||
meta: { description: 'Set to true to hide the panel title in its container.' },
|
||||
})
|
||||
),
|
||||
enhancements: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
},
|
||||
{
|
||||
unknowns: 'allow',
|
||||
}
|
||||
),
|
||||
id: schema.maybe(
|
||||
schema.string({ meta: { description: 'The saved object id for by reference panels' } })
|
||||
),
|
||||
type: schema.string({ meta: { description: 'The embeddable type' } }),
|
||||
panelRefName: schema.maybe(schema.string()),
|
||||
gridData: gridDataSchema,
|
||||
panelIndex: schema.string({
|
||||
meta: { description: 'The unique ID of the panel.' },
|
||||
defaultValue: schema.siblingRef('gridData.i'),
|
||||
}),
|
||||
title: schema.maybe(schema.string({ meta: { description: 'The title of the panel' } })),
|
||||
version: schema.maybe(
|
||||
schema.string({
|
||||
meta: {
|
||||
description:
|
||||
"The version was used to store Kibana version information from versions 7.3.0 -> 8.11.0. As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the embeddable's input. (panelConfig in this type).",
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const optionsSchema = schema.object({
|
||||
hidePanelTitles: schema.boolean({
|
||||
defaultValue: DEFAULT_DASHBOARD_OPTIONS.hidePanelTitles,
|
||||
meta: { description: 'Hide the panel titles in the dashboard.' },
|
||||
}),
|
||||
useMargins: schema.boolean({
|
||||
defaultValue: DEFAULT_DASHBOARD_OPTIONS.useMargins,
|
||||
meta: { description: 'Show margins between panels in the dashboard layout.' },
|
||||
}),
|
||||
syncColors: schema.boolean({
|
||||
defaultValue: DEFAULT_DASHBOARD_OPTIONS.syncColors,
|
||||
meta: { description: 'Synchronize colors between related panels in the dashboard.' },
|
||||
}),
|
||||
syncTooltips: schema.boolean({
|
||||
defaultValue: DEFAULT_DASHBOARD_OPTIONS.syncTooltips,
|
||||
meta: { description: 'Synchronize tooltips between related panels in the dashboard.' },
|
||||
}),
|
||||
syncCursor: schema.boolean({
|
||||
defaultValue: DEFAULT_DASHBOARD_OPTIONS.syncCursor,
|
||||
meta: { description: 'Synchronize cursor position between related panels in the dashboard.' },
|
||||
}),
|
||||
});
|
||||
|
||||
// These are the attributes that are returned in search results
|
||||
export const searchResultsAttributesSchema = schema.object({
|
||||
title: schema.string({ meta: { description: 'A human-readable title for the dashboard' } }),
|
||||
description: schema.string({ defaultValue: '', meta: { description: 'A short description.' } }),
|
||||
timeRestore: schema.boolean({
|
||||
defaultValue: false,
|
||||
meta: { description: 'Whether to restore time upon viewing this dashboard' },
|
||||
}),
|
||||
});
|
||||
|
||||
export const dashboardAttributesSchema = searchResultsAttributesSchema.extends({
|
||||
// Search
|
||||
kibanaSavedObjectMeta: schema.object(
|
||||
{
|
||||
searchSource: schema.maybe(searchSourceSchema),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
description: 'A container for various metadata',
|
||||
},
|
||||
defaultValue: {},
|
||||
}
|
||||
),
|
||||
// Time
|
||||
timeFrom: schema.maybe(
|
||||
schema.string({ meta: { description: 'An ISO string indicating when to restore time from' } })
|
||||
),
|
||||
timeTo: schema.maybe(
|
||||
schema.string({ meta: { description: 'An ISO string indicating when to restore time from' } })
|
||||
),
|
||||
refreshInterval: schema.maybe(
|
||||
schema.object(
|
||||
{
|
||||
pause: schema.boolean({
|
||||
meta: {
|
||||
description:
|
||||
'Whether the refresh interval is set to be paused while viewing the dashboard.',
|
||||
},
|
||||
}),
|
||||
value: schema.number({
|
||||
meta: {
|
||||
description: 'A numeric value indicating refresh frequency in milliseconds.',
|
||||
},
|
||||
}),
|
||||
display: schema.maybe(
|
||||
schema.string({
|
||||
meta: {
|
||||
description:
|
||||
'A human-readable string indicating the refresh frequency. No longer used.',
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
),
|
||||
section: schema.maybe(
|
||||
schema.number({
|
||||
meta: {
|
||||
description: 'No longer used.', // TODO what is this legacy property?
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
description: 'A container for various refresh interval settings',
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
// Dashboard Content
|
||||
controlGroupInput: schema.maybe(controlGroupInputSchema),
|
||||
panels: schema.arrayOf(panelSchema, { defaultValue: [] }),
|
||||
options: optionsSchema,
|
||||
version: schema.maybe(schema.number({ meta: { deprecated: true } })),
|
||||
});
|
||||
|
||||
export const referenceSchema = schema.object(
|
||||
{
|
||||
name: schema.string(),
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
);
|
||||
|
||||
export const dashboardItemSchema = schema.object(
|
||||
{
|
||||
id: schema.string(),
|
||||
type: schema.string(),
|
||||
version: schema.maybe(schema.string()),
|
||||
createdAt: schema.maybe(schema.string()),
|
||||
updatedAt: schema.maybe(schema.string()),
|
||||
createdBy: schema.maybe(schema.string()),
|
||||
updatedBy: schema.maybe(schema.string()),
|
||||
managed: schema.maybe(schema.boolean()),
|
||||
error: schema.maybe(apiError),
|
||||
attributes: dashboardAttributesSchema,
|
||||
references: schema.arrayOf(referenceSchema),
|
||||
namespaces: schema.maybe(schema.arrayOf(schema.string())),
|
||||
originId: schema.maybe(schema.string()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
||||
export const dashboardSearchResultsSchema = dashboardItemSchema.extends({
|
||||
attributes: searchResultsAttributesSchema,
|
||||
});
|
||||
|
||||
export const dashboardSearchOptionsSchema = schema.maybe(
|
||||
schema.object(
|
||||
{
|
||||
onlyTitle: schema.maybe(schema.boolean()),
|
||||
fields: schema.maybe(schema.arrayOf(schema.string())),
|
||||
kuery: schema.maybe(schema.string()),
|
||||
cursor: schema.maybe(schema.number()),
|
||||
limit: schema.maybe(schema.number()),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
)
|
||||
);
|
||||
|
||||
export const dashboardCreateOptionsSchema = schema.object({
|
||||
id: schema.maybe(createOptionsSchemas.id),
|
||||
overwrite: schema.maybe(createOptionsSchemas.overwrite),
|
||||
references: schema.maybe(schema.arrayOf(referenceSchema)),
|
||||
initialNamespaces: schema.maybe(createOptionsSchemas.initialNamespaces),
|
||||
});
|
||||
|
||||
export const dashboardUpdateOptionsSchema = schema.object({
|
||||
references: schema.maybe(schema.arrayOf(referenceSchema)),
|
||||
mergeAttributes: schema.maybe(updateOptionsSchema.mergeAttributes),
|
||||
});
|
||||
|
||||
export const dashboardGetResultSchema = schema.object(
|
||||
{
|
||||
item: dashboardItemSchema,
|
||||
meta: schema.object(
|
||||
{
|
||||
outcome: schema.oneOf([
|
||||
schema.literal('exactMatch'),
|
||||
schema.literal('aliasMatch'),
|
||||
schema.literal('conflict'),
|
||||
]),
|
||||
aliasTargetId: schema.maybe(schema.string()),
|
||||
aliasPurpose: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal('savedObjectConversion'),
|
||||
schema.literal('savedObjectImport'),
|
||||
])
|
||||
),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
);
|
||||
|
||||
export const dashboardCreateResultSchema = schema.object(
|
||||
{
|
||||
item: dashboardItemSchema,
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
);
|
||||
|
||||
export const serviceDefinition: ServicesDefinition = {
|
||||
get: {
|
||||
out: {
|
||||
result: {
|
||||
schema: dashboardGetResultSchema,
|
||||
down: getResultV3ToV2,
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
in: {
|
||||
options: {
|
||||
schema: dashboardCreateOptionsSchema,
|
||||
},
|
||||
data: {
|
||||
schema: dashboardAttributesSchema,
|
||||
},
|
||||
},
|
||||
out: {
|
||||
result: {
|
||||
schema: dashboardCreateResultSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
in: {
|
||||
options: {
|
||||
schema: dashboardUpdateOptionsSchema,
|
||||
},
|
||||
data: {
|
||||
schema: dashboardAttributesSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
search: {
|
||||
in: {
|
||||
options: {
|
||||
schema: dashboardSearchOptionsSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
mSearch: {
|
||||
out: {
|
||||
result: {
|
||||
schema: dashboardItemSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
42
src/plugins/dashboard/server/content_management/v3/index.ts
Normal file
42
src/plugins/dashboard/server/content_management/v3/index.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type {
|
||||
ControlGroupAttributes,
|
||||
GridData,
|
||||
DashboardPanel,
|
||||
DashboardAttributes,
|
||||
DashboardItem,
|
||||
DashboardGetIn,
|
||||
DashboardGetOut,
|
||||
DashboardCreateIn,
|
||||
DashboardCreateOut,
|
||||
DashboardCreateOptions,
|
||||
DashboardSearchIn,
|
||||
DashboardSearchOut,
|
||||
DashboardSearchOptions,
|
||||
DashboardUpdateIn,
|
||||
DashboardUpdateOut,
|
||||
DashboardUpdateOptions,
|
||||
DashboardOptions,
|
||||
} from './types';
|
||||
export {
|
||||
serviceDefinition,
|
||||
dashboardAttributesSchema,
|
||||
dashboardGetResultSchema,
|
||||
dashboardCreateResultSchema,
|
||||
dashboardItemSchema,
|
||||
dashboardSearchResultsSchema,
|
||||
referenceSchema,
|
||||
} from './cm_services';
|
||||
export {
|
||||
dashboardAttributesOut,
|
||||
itemAttrsToSavedObjectAttrs,
|
||||
savedObjectToItem,
|
||||
} from './transform_utils';
|
|
@ -0,0 +1,551 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { SavedObject } from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
DashboardSavedObjectAttributes,
|
||||
SavedDashboardPanel,
|
||||
} from '../../dashboard_saved_object';
|
||||
import type { DashboardAttributes, DashboardItem } from './types';
|
||||
import {
|
||||
dashboardAttributesOut,
|
||||
getResultV3ToV2,
|
||||
itemAttrsToSavedObjectAttrs,
|
||||
savedObjectToItem,
|
||||
} from './transform_utils';
|
||||
import {
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_GROW,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
ControlLabelPosition,
|
||||
ControlGroupChainingSystem,
|
||||
ControlWidth,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { DEFAULT_DASHBOARD_OPTIONS } from '../../../common/content_management';
|
||||
|
||||
describe('dashboardAttributesOut', () => {
|
||||
const controlGroupInputControlsSo = {
|
||||
explicitInput: { anyKey: 'some value' },
|
||||
type: 'type1',
|
||||
order: 0,
|
||||
};
|
||||
|
||||
const panelsSo: SavedDashboardPanel[] = [
|
||||
{
|
||||
embeddableConfig: { enhancements: {} },
|
||||
gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' },
|
||||
id: '1',
|
||||
panelIndex: '1',
|
||||
panelRefName: 'ref1',
|
||||
title: 'title1',
|
||||
type: 'type1',
|
||||
version: '2',
|
||||
},
|
||||
];
|
||||
|
||||
it('should set default values if not provided', () => {
|
||||
const input: DashboardSavedObjectAttributes = {
|
||||
controlGroupInput: {
|
||||
panelsJSON: JSON.stringify({ foo: controlGroupInputControlsSo }),
|
||||
},
|
||||
panelsJSON: JSON.stringify(panelsSo),
|
||||
optionsJSON: JSON.stringify({
|
||||
hidePanelTitles: false,
|
||||
}),
|
||||
kibanaSavedObjectMeta: {},
|
||||
title: 'my title',
|
||||
description: 'my description',
|
||||
};
|
||||
expect(dashboardAttributesOut(input)).toEqual<DashboardAttributes>({
|
||||
controlGroupInput: {
|
||||
chainingSystem: DEFAULT_CONTROL_CHAINING,
|
||||
labelPosition: DEFAULT_CONTROL_LABEL_POSITION,
|
||||
ignoreParentSettings: DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
autoApplySelections: DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
controls: [
|
||||
{
|
||||
controlConfig: { anyKey: 'some value' },
|
||||
grow: DEFAULT_CONTROL_GROW,
|
||||
id: 'foo',
|
||||
order: 0,
|
||||
type: 'type1',
|
||||
width: DEFAULT_CONTROL_WIDTH,
|
||||
},
|
||||
],
|
||||
},
|
||||
description: 'my description',
|
||||
kibanaSavedObjectMeta: {},
|
||||
options: {
|
||||
...DEFAULT_DASHBOARD_OPTIONS,
|
||||
hidePanelTitles: false,
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
panelConfig: { enhancements: {} },
|
||||
gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' },
|
||||
id: '1',
|
||||
panelIndex: '1',
|
||||
panelRefName: 'ref1',
|
||||
title: 'title1',
|
||||
type: 'type1',
|
||||
version: '2',
|
||||
},
|
||||
],
|
||||
timeRestore: false,
|
||||
title: 'my title',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform full attributes correctly', () => {
|
||||
const input: DashboardSavedObjectAttributes = {
|
||||
controlGroupInput: {
|
||||
panelsJSON: JSON.stringify({
|
||||
foo: {
|
||||
...controlGroupInputControlsSo,
|
||||
grow: false,
|
||||
width: 'small',
|
||||
},
|
||||
}),
|
||||
ignoreParentSettingsJSON: JSON.stringify({ ignoreFilters: true }),
|
||||
controlStyle: 'twoLine',
|
||||
chainingSystem: 'NONE',
|
||||
showApplySelections: true,
|
||||
},
|
||||
description: 'description',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: JSON.stringify({ query: { query: 'test', language: 'KQL' } }),
|
||||
},
|
||||
optionsJSON: JSON.stringify({
|
||||
hidePanelTitles: true,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: false,
|
||||
}),
|
||||
panelsJSON: JSON.stringify(panelsSo),
|
||||
refreshInterval: { pause: true, value: 1000 },
|
||||
timeFrom: 'now-15m',
|
||||
timeRestore: true,
|
||||
timeTo: 'now',
|
||||
title: 'title',
|
||||
};
|
||||
expect(dashboardAttributesOut(input)).toEqual<DashboardAttributes>({
|
||||
controlGroupInput: {
|
||||
chainingSystem: 'NONE',
|
||||
labelPosition: 'twoLine',
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: true,
|
||||
ignoreQuery: false,
|
||||
ignoreTimerange: false,
|
||||
ignoreValidations: false,
|
||||
},
|
||||
autoApplySelections: false,
|
||||
controls: [
|
||||
{
|
||||
controlConfig: {
|
||||
anyKey: 'some value',
|
||||
},
|
||||
id: 'foo',
|
||||
grow: false,
|
||||
width: 'small',
|
||||
order: 0,
|
||||
type: 'type1',
|
||||
},
|
||||
],
|
||||
},
|
||||
description: 'description',
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSource: { query: { query: 'test', language: 'KQL' } },
|
||||
},
|
||||
options: {
|
||||
hidePanelTitles: true,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: false,
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
panelConfig: {
|
||||
enhancements: {},
|
||||
},
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 10,
|
||||
h: 10,
|
||||
i: '1',
|
||||
},
|
||||
id: '1',
|
||||
panelIndex: '1',
|
||||
panelRefName: 'ref1',
|
||||
title: 'title1',
|
||||
type: 'type1',
|
||||
version: '2',
|
||||
},
|
||||
],
|
||||
refreshInterval: {
|
||||
pause: true,
|
||||
value: 1000,
|
||||
},
|
||||
timeFrom: 'now-15m',
|
||||
timeRestore: true,
|
||||
timeTo: 'now',
|
||||
title: 'title',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemAttrsToSavedObjectAttrs', () => {
|
||||
it('should transform item attributes to saved object attributes correctly', () => {
|
||||
const input: DashboardAttributes = {
|
||||
controlGroupInput: {
|
||||
chainingSystem: 'NONE',
|
||||
labelPosition: 'twoLine',
|
||||
controls: [
|
||||
{
|
||||
controlConfig: { anyKey: 'some value' },
|
||||
grow: false,
|
||||
id: 'foo',
|
||||
order: 0,
|
||||
type: 'type1',
|
||||
width: 'small',
|
||||
},
|
||||
],
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: true,
|
||||
ignoreQuery: true,
|
||||
ignoreTimerange: true,
|
||||
ignoreValidations: true,
|
||||
},
|
||||
autoApplySelections: false,
|
||||
},
|
||||
description: 'description',
|
||||
kibanaSavedObjectMeta: { searchSource: { query: { query: 'test', language: 'KQL' } } },
|
||||
options: {
|
||||
hidePanelTitles: true,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: false,
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' },
|
||||
id: '1',
|
||||
panelConfig: { enhancements: {} },
|
||||
panelIndex: '1',
|
||||
panelRefName: 'ref1',
|
||||
title: 'title1',
|
||||
type: 'type1',
|
||||
version: '2',
|
||||
},
|
||||
],
|
||||
timeRestore: true,
|
||||
title: 'title',
|
||||
refreshInterval: { pause: true, value: 1000 },
|
||||
timeFrom: 'now-15m',
|
||||
timeTo: 'now',
|
||||
};
|
||||
|
||||
const output = itemAttrsToSavedObjectAttrs(input);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"controlGroupInput": Object {
|
||||
"chainingSystem": "NONE",
|
||||
"controlStyle": "twoLine",
|
||||
"ignoreParentSettingsJSON": "{\\"ignoreFilters\\":true,\\"ignoreQuery\\":true,\\"ignoreTimerange\\":true,\\"ignoreValidations\\":true}",
|
||||
"panelsJSON": "{\\"foo\\":{\\"grow\\":false,\\"order\\":0,\\"type\\":\\"type1\\",\\"width\\":\\"small\\",\\"explicitInput\\":{\\"anyKey\\":\\"some value\\",\\"id\\":\\"foo\\"}}}",
|
||||
"showApplySelections": true,
|
||||
},
|
||||
"description": "description",
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{\\"query\\":{\\"query\\":\\"test\\",\\"language\\":\\"KQL\\"}}",
|
||||
},
|
||||
"optionsJSON": "{\\"hidePanelTitles\\":true,\\"useMargins\\":false,\\"syncColors\\":false,\\"syncTooltips\\":false,\\"syncCursor\\":false}",
|
||||
"panelsJSON": "[{\\"id\\":\\"1\\",\\"panelRefName\\":\\"ref1\\",\\"title\\":\\"title1\\",\\"type\\":\\"type1\\",\\"version\\":\\"2\\",\\"embeddableConfig\\":{\\"enhancements\\":{}},\\"panelIndex\\":\\"1\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":10,\\"h\\":10,\\"i\\":\\"1\\"}}]",
|
||||
"refreshInterval": Object {
|
||||
"pause": true,
|
||||
"value": 1000,
|
||||
},
|
||||
"timeFrom": "now-15m",
|
||||
"timeRestore": true,
|
||||
"timeTo": "now",
|
||||
"title": "title",
|
||||
},
|
||||
"error": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle missing optional attributes', () => {
|
||||
const input: DashboardAttributes = {
|
||||
title: 'title',
|
||||
description: 'my description',
|
||||
timeRestore: false,
|
||||
panels: [],
|
||||
options: DEFAULT_DASHBOARD_OPTIONS,
|
||||
kibanaSavedObjectMeta: {},
|
||||
};
|
||||
|
||||
const output = itemAttrsToSavedObjectAttrs(input);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"description": "my description",
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{}",
|
||||
},
|
||||
"optionsJSON": "{\\"hidePanelTitles\\":false,\\"useMargins\\":true,\\"syncColors\\":true,\\"syncCursor\\":true,\\"syncTooltips\\":true}",
|
||||
"panelsJSON": "[]",
|
||||
"timeRestore": false,
|
||||
"title": "title",
|
||||
},
|
||||
"error": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('savedObjectToItem', () => {
|
||||
const commonSavedObject: SavedObject = {
|
||||
references: [],
|
||||
id: '3d8459d9-0f1a-403d-aa82-6d93713a54b5',
|
||||
type: 'dashboard',
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
const getSavedObjectForAttributes = (
|
||||
attributes: DashboardSavedObjectAttributes
|
||||
): SavedObject<DashboardSavedObjectAttributes> => {
|
||||
return {
|
||||
...commonSavedObject,
|
||||
attributes,
|
||||
};
|
||||
};
|
||||
it('should convert saved object to item with all attributes', () => {
|
||||
const input = getSavedObjectForAttributes({
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
timeRestore: true,
|
||||
panelsJSON: JSON.stringify([
|
||||
{
|
||||
embeddableConfig: { enhancements: {} },
|
||||
gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' },
|
||||
id: '1',
|
||||
panelIndex: '1',
|
||||
panelRefName: 'ref1',
|
||||
title: 'title1',
|
||||
type: 'type1',
|
||||
version: '2',
|
||||
},
|
||||
]),
|
||||
optionsJSON: JSON.stringify({
|
||||
hidePanelTitles: true,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: false,
|
||||
}),
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{"query":{"query":"test","language":"KQL"}}',
|
||||
},
|
||||
});
|
||||
|
||||
const { item, error } = savedObjectToItem(input, false);
|
||||
expect(error).toBeNull();
|
||||
expect(item).toEqual<DashboardItem>({
|
||||
...commonSavedObject,
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
timeRestore: true,
|
||||
panels: [
|
||||
{
|
||||
panelConfig: { enhancements: {} },
|
||||
gridData: { x: 0, y: 0, w: 10, h: 10, i: '1' },
|
||||
id: '1',
|
||||
panelIndex: '1',
|
||||
panelRefName: 'ref1',
|
||||
title: 'title1',
|
||||
type: 'type1',
|
||||
version: '2',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
hidePanelTitles: true,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
syncTooltips: false,
|
||||
syncCursor: false,
|
||||
},
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSource: { query: { query: 'test', language: 'KQL' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing optional attributes', () => {
|
||||
const input = getSavedObjectForAttributes({
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
timeRestore: false,
|
||||
panelsJSON: '[]',
|
||||
optionsJSON: '{}',
|
||||
kibanaSavedObjectMeta: {},
|
||||
});
|
||||
|
||||
const { item, error } = savedObjectToItem(input, false);
|
||||
expect(error).toBeNull();
|
||||
expect(item).toEqual<DashboardItem>({
|
||||
...commonSavedObject,
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
timeRestore: false,
|
||||
panels: [],
|
||||
options: DEFAULT_DASHBOARD_OPTIONS,
|
||||
kibanaSavedObjectMeta: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle partial saved object', () => {
|
||||
const input = {
|
||||
...commonSavedObject,
|
||||
references: undefined,
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'my description',
|
||||
timeRestore: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { item, error } = savedObjectToItem(input, true, ['title', 'description']);
|
||||
expect(error).toBeNull();
|
||||
expect(item).toEqual({
|
||||
...commonSavedObject,
|
||||
references: undefined,
|
||||
attributes: {
|
||||
title: 'title',
|
||||
description: 'my description',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if attributes can not be parsed', () => {
|
||||
const input = {
|
||||
...commonSavedObject,
|
||||
references: undefined,
|
||||
attributes: {
|
||||
title: 'title',
|
||||
panelsJSON: 'not stringified json',
|
||||
},
|
||||
};
|
||||
const { item, error } = savedObjectToItem(input, true);
|
||||
expect(item).toBeNull();
|
||||
expect(error).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResultV3ToV2', () => {
|
||||
const commonAttributes = {
|
||||
description: 'description',
|
||||
refreshInterval: { pause: true, value: 1000 },
|
||||
timeFrom: 'now-15m',
|
||||
timeRestore: true,
|
||||
timeTo: 'now',
|
||||
title: 'title',
|
||||
};
|
||||
it('should transform a v3 result to a v2 result with all attributes', () => {
|
||||
const v3Result = {
|
||||
meta: { outcome: 'exactMatch' as const },
|
||||
item: {
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
attributes: {
|
||||
...commonAttributes,
|
||||
controlGroupInput: {
|
||||
chainingSystem: 'NONE' as ControlGroupChainingSystem,
|
||||
labelPosition: 'twoLine' as ControlLabelPosition,
|
||||
controls: [
|
||||
{
|
||||
controlConfig: { bizz: 'buzz' },
|
||||
grow: false,
|
||||
order: 0,
|
||||
id: 'foo',
|
||||
type: 'type1',
|
||||
width: 'small' as ControlWidth,
|
||||
},
|
||||
],
|
||||
ignoreParentSettings: {
|
||||
ignoreFilters: true,
|
||||
ignoreQuery: true,
|
||||
ignoreTimerange: true,
|
||||
ignoreValidations: true,
|
||||
},
|
||||
autoApplySelections: false,
|
||||
},
|
||||
kibanaSavedObjectMeta: { searchSource: { query: { query: 'test', language: 'KQL' } } },
|
||||
options: {
|
||||
hidePanelTitles: true,
|
||||
useMargins: false,
|
||||
syncColors: false,
|
||||
syncCursor: false,
|
||||
syncTooltips: false,
|
||||
},
|
||||
panels: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
panelConfig: { title: 'my panel' },
|
||||
gridData: { x: 0, y: 0, w: 15, h: 15, i: 'foo' },
|
||||
panelIndex: 'foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
};
|
||||
|
||||
const output = getResultV3ToV2(v3Result);
|
||||
|
||||
// Common attributes should match between v2 and v3
|
||||
expect(output.item.attributes).toMatchObject(commonAttributes);
|
||||
expect(output.item.attributes.controlGroupInput).toMatchObject({
|
||||
chainingSystem: 'NONE',
|
||||
controlStyle: 'twoLine',
|
||||
showApplySelections: true,
|
||||
});
|
||||
|
||||
// Check transformed attributes
|
||||
expect(output.item.attributes.controlGroupInput!.panelsJSON).toMatchInlineSnapshot(
|
||||
`"{\\"foo\\":{\\"grow\\":false,\\"order\\":0,\\"type\\":\\"type1\\",\\"width\\":\\"small\\",\\"explicitInput\\":{\\"bizz\\":\\"buzz\\",\\"id\\":\\"foo\\"}}}"`
|
||||
);
|
||||
expect(
|
||||
output.item.attributes.controlGroupInput!.ignoreParentSettingsJSON
|
||||
).toMatchInlineSnapshot(
|
||||
`"{\\"ignoreFilters\\":true,\\"ignoreQuery\\":true,\\"ignoreTimerange\\":true,\\"ignoreValidations\\":true}"`
|
||||
);
|
||||
expect(output.item.attributes.kibanaSavedObjectMeta.searchSourceJSON).toMatchInlineSnapshot(
|
||||
`"{\\"query\\":{\\"query\\":\\"test\\",\\"language\\":\\"KQL\\"}}"`
|
||||
);
|
||||
expect(output.item.attributes.optionsJSON).toMatchInlineSnapshot(
|
||||
`"{\\"hidePanelTitles\\":true,\\"useMargins\\":false,\\"syncColors\\":false,\\"syncCursor\\":false,\\"syncTooltips\\":false}"`
|
||||
);
|
||||
expect(output.item.attributes.panelsJSON).toMatchInlineSnapshot(
|
||||
`"[{\\"id\\":\\"1\\",\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{\\"title\\":\\"my panel\\"},\\"panelIndex\\":\\"foo\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":15,\\"h\\":15,\\"i\\":\\"foo\\"}}]"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import {
|
||||
type ControlGroupChainingSystem,
|
||||
type ControlLabelPosition,
|
||||
type ControlPanelsState,
|
||||
type SerializedControlState,
|
||||
DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
DEFAULT_CONTROL_CHAINING,
|
||||
DEFAULT_CONTROL_GROW,
|
||||
DEFAULT_CONTROL_LABEL_POSITION,
|
||||
DEFAULT_CONTROL_WIDTH,
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
import { SerializedSearchSourceFields, parseSearchSourceJSON } from '@kbn/data-plugin/common';
|
||||
|
||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
ControlGroupAttributes,
|
||||
DashboardAttributes,
|
||||
DashboardGetOut,
|
||||
DashboardItem,
|
||||
DashboardOptions,
|
||||
ItemAttrsToSavedObjectAttrsReturn,
|
||||
PartialDashboardItem,
|
||||
SavedObjectToItemReturn,
|
||||
} from './types';
|
||||
import type {
|
||||
DashboardSavedObjectAttributes,
|
||||
SavedDashboardPanel,
|
||||
} from '../../dashboard_saved_object';
|
||||
import type {
|
||||
ControlGroupAttributes as ControlGroupAttributesV2,
|
||||
DashboardCrudTypes as DashboardCrudTypesV2,
|
||||
} from '../../../common/content_management/v2';
|
||||
import { DEFAULT_DASHBOARD_OPTIONS } from '../../../common/content_management';
|
||||
|
||||
function controlGroupInputOut(
|
||||
controlGroupInput?: DashboardSavedObjectAttributes['controlGroupInput']
|
||||
): ControlGroupAttributes | undefined {
|
||||
if (!controlGroupInput) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
panelsJSON,
|
||||
ignoreParentSettingsJSON,
|
||||
controlStyle = DEFAULT_CONTROL_LABEL_POSITION,
|
||||
chainingSystem = DEFAULT_CONTROL_CHAINING,
|
||||
showApplySelections = !DEFAULT_AUTO_APPLY_SELECTIONS,
|
||||
} = controlGroupInput;
|
||||
const controls = panelsJSON
|
||||
? Object.entries(JSON.parse(panelsJSON) as ControlPanelsState<SerializedControlState>).map(
|
||||
([
|
||||
id,
|
||||
{
|
||||
explicitInput,
|
||||
type,
|
||||
grow = DEFAULT_CONTROL_GROW,
|
||||
width = DEFAULT_CONTROL_WIDTH,
|
||||
order,
|
||||
},
|
||||
]) => ({
|
||||
controlConfig: explicitInput,
|
||||
id,
|
||||
grow,
|
||||
order,
|
||||
type,
|
||||
width,
|
||||
})
|
||||
)
|
||||
: [];
|
||||
|
||||
const {
|
||||
ignoreFilters = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreFilters,
|
||||
ignoreQuery = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreQuery,
|
||||
ignoreTimerange = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreTimerange,
|
||||
ignoreValidations = DEFAULT_IGNORE_PARENT_SETTINGS.ignoreValidations,
|
||||
} = ignoreParentSettingsJSON ? JSON.parse(ignoreParentSettingsJSON) : {};
|
||||
|
||||
// try to maintain a consistent (alphabetical) order of keys
|
||||
return {
|
||||
autoApplySelections: !showApplySelections,
|
||||
chainingSystem: chainingSystem as ControlGroupChainingSystem,
|
||||
controls,
|
||||
labelPosition: controlStyle as ControlLabelPosition,
|
||||
ignoreParentSettings: { ignoreFilters, ignoreQuery, ignoreTimerange, ignoreValidations },
|
||||
};
|
||||
}
|
||||
|
||||
function kibanaSavedObjectMetaOut(
|
||||
kibanaSavedObjectMeta: DashboardSavedObjectAttributes['kibanaSavedObjectMeta']
|
||||
): DashboardAttributes['kibanaSavedObjectMeta'] {
|
||||
const { searchSourceJSON } = kibanaSavedObjectMeta;
|
||||
if (!searchSourceJSON) {
|
||||
return {};
|
||||
}
|
||||
// Dashboards do not yet support ES|QL (AggregateQuery) in the search source
|
||||
return {
|
||||
searchSource: parseSearchSourceJSON(searchSourceJSON) as Omit<
|
||||
SerializedSearchSourceFields,
|
||||
'query'
|
||||
> & { query?: Query },
|
||||
};
|
||||
}
|
||||
|
||||
function optionsOut(optionsJSON: string): DashboardAttributes['options'] {
|
||||
const {
|
||||
hidePanelTitles = DEFAULT_DASHBOARD_OPTIONS.hidePanelTitles,
|
||||
useMargins = DEFAULT_DASHBOARD_OPTIONS.useMargins,
|
||||
syncColors = DEFAULT_DASHBOARD_OPTIONS.syncColors,
|
||||
syncCursor = DEFAULT_DASHBOARD_OPTIONS.syncCursor,
|
||||
syncTooltips = DEFAULT_DASHBOARD_OPTIONS.syncTooltips,
|
||||
} = JSON.parse(optionsJSON) as DashboardOptions;
|
||||
return {
|
||||
hidePanelTitles,
|
||||
useMargins,
|
||||
syncColors,
|
||||
syncCursor,
|
||||
syncTooltips,
|
||||
};
|
||||
}
|
||||
|
||||
function panelsOut(panelsJSON: string): DashboardAttributes['panels'] {
|
||||
const panels = JSON.parse(panelsJSON) as SavedDashboardPanel[];
|
||||
return panels.map(
|
||||
({ embeddableConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({
|
||||
gridData,
|
||||
id,
|
||||
panelConfig: embeddableConfig,
|
||||
panelIndex,
|
||||
panelRefName,
|
||||
title,
|
||||
type,
|
||||
version,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function dashboardAttributesOut(
|
||||
attributes: DashboardSavedObjectAttributes | Partial<DashboardSavedObjectAttributes>
|
||||
): DashboardAttributes | Partial<DashboardAttributes> {
|
||||
const {
|
||||
controlGroupInput,
|
||||
description,
|
||||
kibanaSavedObjectMeta,
|
||||
optionsJSON,
|
||||
panelsJSON,
|
||||
refreshInterval,
|
||||
timeFrom,
|
||||
timeRestore,
|
||||
timeTo,
|
||||
title,
|
||||
version,
|
||||
} = attributes;
|
||||
// try to maintain a consistent (alphabetical) order of keys
|
||||
return {
|
||||
...(controlGroupInput && { controlGroupInput: controlGroupInputOut(controlGroupInput) }),
|
||||
...(description && { description }),
|
||||
...(kibanaSavedObjectMeta && {
|
||||
kibanaSavedObjectMeta: kibanaSavedObjectMetaOut(kibanaSavedObjectMeta),
|
||||
}),
|
||||
...(optionsJSON && { options: optionsOut(optionsJSON) }),
|
||||
...(panelsJSON && { panels: panelsOut(panelsJSON) }),
|
||||
...(refreshInterval && {
|
||||
refreshInterval: { pause: refreshInterval.pause, value: refreshInterval.value },
|
||||
}),
|
||||
...(timeFrom && { timeFrom }),
|
||||
timeRestore: timeRestore ?? false,
|
||||
...(timeTo && { timeTo }),
|
||||
title,
|
||||
...(version && { version }),
|
||||
};
|
||||
}
|
||||
|
||||
function controlGroupInputIn(
|
||||
controlGroupInput?: ControlGroupAttributes
|
||||
): DashboardSavedObjectAttributes['controlGroupInput'] | undefined {
|
||||
if (!controlGroupInput) {
|
||||
return;
|
||||
}
|
||||
const { controls, ignoreParentSettings, labelPosition, chainingSystem, autoApplySelections } =
|
||||
controlGroupInput;
|
||||
const updatedControls = Object.fromEntries(
|
||||
controls.map(({ controlConfig, id = uuidv4(), ...restOfControl }) => {
|
||||
return [id, { ...restOfControl, explicitInput: { ...controlConfig, id } }];
|
||||
})
|
||||
);
|
||||
return {
|
||||
chainingSystem,
|
||||
controlStyle: labelPosition,
|
||||
ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings),
|
||||
panelsJSON: JSON.stringify(updatedControls),
|
||||
showApplySelections: !autoApplySelections,
|
||||
};
|
||||
}
|
||||
|
||||
function panelsIn(
|
||||
panels: DashboardAttributes['panels']
|
||||
): DashboardSavedObjectAttributes['panelsJSON'] {
|
||||
const updatedPanels = panels.map(({ panelIndex, gridData, panelConfig, ...restPanel }) => {
|
||||
const idx = panelIndex ?? uuidv4();
|
||||
return {
|
||||
...restPanel,
|
||||
embeddableConfig: panelConfig,
|
||||
panelIndex: idx,
|
||||
gridData: {
|
||||
...gridData,
|
||||
i: idx,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return JSON.stringify(updatedPanels);
|
||||
}
|
||||
|
||||
function kibanaSavedObjectMetaIn(
|
||||
kibanaSavedObjectMeta: DashboardAttributes['kibanaSavedObjectMeta']
|
||||
) {
|
||||
const { searchSource } = kibanaSavedObjectMeta;
|
||||
return { searchSourceJSON: JSON.stringify(searchSource ?? {}) };
|
||||
}
|
||||
|
||||
export const getResultV3ToV2 = (result: DashboardGetOut): DashboardCrudTypesV2['GetOut'] => {
|
||||
const { meta, item } = result;
|
||||
const { attributes, ...rest } = item;
|
||||
const {
|
||||
controlGroupInput,
|
||||
description,
|
||||
kibanaSavedObjectMeta,
|
||||
options,
|
||||
panels,
|
||||
refreshInterval,
|
||||
timeFrom,
|
||||
timeRestore,
|
||||
timeTo,
|
||||
title,
|
||||
version,
|
||||
} = attributes;
|
||||
|
||||
const v2Attributes = {
|
||||
...(controlGroupInput && {
|
||||
controlGroupInput: controlGroupInputIn(controlGroupInput) as ControlGroupAttributesV2,
|
||||
}),
|
||||
description,
|
||||
...(kibanaSavedObjectMeta && {
|
||||
kibanaSavedObjectMeta: kibanaSavedObjectMetaIn(kibanaSavedObjectMeta),
|
||||
}),
|
||||
...(options && { optionsJSON: JSON.stringify(options) }),
|
||||
panelsJSON: panels ? panelsIn(panels) : '[]',
|
||||
refreshInterval,
|
||||
...(timeFrom && { timeFrom }),
|
||||
timeRestore,
|
||||
...(timeTo && { timeTo }),
|
||||
title,
|
||||
...(version && { version }),
|
||||
};
|
||||
return {
|
||||
meta,
|
||||
item: {
|
||||
...rest,
|
||||
attributes: v2Attributes,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const itemAttrsToSavedObjectAttrs = (
|
||||
attributes: DashboardAttributes
|
||||
): ItemAttrsToSavedObjectAttrsReturn => {
|
||||
try {
|
||||
const { controlGroupInput, kibanaSavedObjectMeta, options, panels, ...rest } = attributes;
|
||||
const soAttributes = {
|
||||
...rest,
|
||||
...(controlGroupInput && {
|
||||
controlGroupInput: controlGroupInputIn(controlGroupInput),
|
||||
}),
|
||||
...(options && {
|
||||
optionsJSON: JSON.stringify(options),
|
||||
}),
|
||||
...(panels && {
|
||||
panelsJSON: panelsIn(panels),
|
||||
}),
|
||||
...(kibanaSavedObjectMeta && {
|
||||
kibanaSavedObjectMeta: kibanaSavedObjectMetaIn(kibanaSavedObjectMeta),
|
||||
}),
|
||||
};
|
||||
return { attributes: soAttributes, error: null };
|
||||
} catch (e) {
|
||||
return { attributes: null, error: e };
|
||||
}
|
||||
};
|
||||
|
||||
type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
|
||||
references: SavedObjectReference[] | undefined;
|
||||
};
|
||||
|
||||
export function savedObjectToItem(
|
||||
savedObject: SavedObject<DashboardSavedObjectAttributes>,
|
||||
partial: false,
|
||||
allowedAttributes?: string[]
|
||||
): SavedObjectToItemReturn<DashboardItem>;
|
||||
|
||||
export function savedObjectToItem(
|
||||
savedObject: PartialSavedObject<DashboardSavedObjectAttributes>,
|
||||
partial: true,
|
||||
allowedAttributes?: string[]
|
||||
): SavedObjectToItemReturn<PartialDashboardItem>;
|
||||
|
||||
export function savedObjectToItem(
|
||||
savedObject:
|
||||
| SavedObject<DashboardSavedObjectAttributes>
|
||||
| PartialSavedObject<DashboardSavedObjectAttributes>,
|
||||
partial: boolean,
|
||||
allowedAttributes?: string[]
|
||||
): SavedObjectToItemReturn<DashboardItem | PartialDashboardItem> {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
updated_at: updatedAt,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
created_by: createdBy,
|
||||
attributes,
|
||||
error,
|
||||
namespaces,
|
||||
references,
|
||||
version,
|
||||
managed,
|
||||
} = savedObject;
|
||||
|
||||
try {
|
||||
const attributesOut = allowedAttributes
|
||||
? pick(dashboardAttributesOut(attributes), allowedAttributes)
|
||||
: dashboardAttributesOut(attributes);
|
||||
return {
|
||||
item: {
|
||||
id,
|
||||
type,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
createdBy,
|
||||
attributes: attributesOut,
|
||||
error,
|
||||
namespaces,
|
||||
references,
|
||||
version,
|
||||
managed,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
} catch (e) {
|
||||
return { item: null, error: e };
|
||||
}
|
||||
}
|
90
src/plugins/dashboard/server/content_management/v3/types.ts
Normal file
90
src/plugins/dashboard/server/content_management/v3/types.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
CreateIn,
|
||||
GetIn,
|
||||
SearchIn,
|
||||
SearchResult,
|
||||
UpdateIn,
|
||||
} from '@kbn/content-management-plugin/common';
|
||||
import { SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||
import {
|
||||
dashboardItemSchema,
|
||||
controlGroupInputSchema,
|
||||
gridDataSchema,
|
||||
panelSchema,
|
||||
dashboardAttributesSchema,
|
||||
dashboardCreateOptionsSchema,
|
||||
dashboardCreateResultSchema,
|
||||
dashboardGetResultSchema,
|
||||
dashboardSearchOptionsSchema,
|
||||
dashboardSearchResultsSchema,
|
||||
dashboardUpdateOptionsSchema,
|
||||
optionsSchema,
|
||||
} from './cm_services';
|
||||
import { CONTENT_ID } from '../../../common/content_management';
|
||||
import { DashboardSavedObjectAttributes } from '../../dashboard_saved_object';
|
||||
|
||||
export type DashboardOptions = TypeOf<typeof optionsSchema>;
|
||||
|
||||
// Panel config has some defined types but also allows for custom keys added by embeddables
|
||||
// The schema uses "unknowns: 'allow'" to permit any other keys, but the TypeOf helper does not
|
||||
// recognize this, so we need to manually extend the type here.
|
||||
export type DashboardPanel = Omit<TypeOf<typeof panelSchema>, 'panelConfig'> & {
|
||||
panelConfig: TypeOf<typeof panelSchema>['panelConfig'] & { [key: string]: any };
|
||||
};
|
||||
export type DashboardAttributes = Omit<TypeOf<typeof dashboardAttributesSchema>, 'panels'> & {
|
||||
panels: DashboardPanel[];
|
||||
};
|
||||
|
||||
export type DashboardItem = TypeOf<typeof dashboardItemSchema>;
|
||||
export type PartialDashboardItem = Omit<DashboardItem, 'attributes' | 'references'> & {
|
||||
attributes: Partial<DashboardAttributes>;
|
||||
references: SavedObjectReference[] | undefined;
|
||||
};
|
||||
|
||||
export type ControlGroupAttributes = TypeOf<typeof controlGroupInputSchema>;
|
||||
export type GridData = TypeOf<typeof gridDataSchema>;
|
||||
|
||||
export type DashboardGetIn = GetIn<typeof CONTENT_ID>;
|
||||
export type DashboardGetOut = TypeOf<typeof dashboardGetResultSchema>;
|
||||
|
||||
export type DashboardCreateIn = CreateIn<typeof CONTENT_ID, DashboardAttributes>;
|
||||
export type DashboardCreateOut = TypeOf<typeof dashboardCreateResultSchema>;
|
||||
export type DashboardCreateOptions = TypeOf<typeof dashboardCreateOptionsSchema>;
|
||||
|
||||
export type DashboardUpdateIn = UpdateIn<typeof CONTENT_ID, Partial<DashboardAttributes>>;
|
||||
export type DashboardUpdateOut = TypeOf<typeof dashboardCreateResultSchema>;
|
||||
export type DashboardUpdateOptions = TypeOf<typeof dashboardUpdateOptionsSchema>;
|
||||
|
||||
export type DashboardSearchIn = SearchIn<typeof CONTENT_ID>;
|
||||
export type DashboardSearchOptions = TypeOf<typeof dashboardSearchOptionsSchema>;
|
||||
export type DashboardSearchOut = SearchResult<TypeOf<typeof dashboardSearchResultsSchema>>;
|
||||
|
||||
export type SavedObjectToItemReturn<T> =
|
||||
| {
|
||||
item: T;
|
||||
error: null;
|
||||
}
|
||||
| {
|
||||
item: null;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type ItemAttrsToSavedObjectAttrsReturn =
|
||||
| {
|
||||
attributes: DashboardSavedObjectAttributes;
|
||||
error: null;
|
||||
}
|
||||
| {
|
||||
attributes: null;
|
||||
error: Error;
|
||||
};
|
|
@ -10,19 +10,21 @@
|
|||
import { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
|
||||
import { SavedObjectsType } from '@kbn/core/server';
|
||||
|
||||
import { dashboardAttributesSchema as dashboardAttributesSchemaV1 } from '../content_management/schema/v1';
|
||||
import { dashboardAttributesSchema as dashboardAttributesSchemaV2 } from '../content_management/schema/v2';
|
||||
import { dashboardAttributesSchema as dashboardAttributesSchemaV1 } from './schema/v1';
|
||||
import { dashboardAttributesSchema as dashboardAttributesSchemaV2 } from './schema/v2';
|
||||
import {
|
||||
createDashboardSavedObjectTypeMigrations,
|
||||
DashboardSavedObjectTypeMigrationsDeps,
|
||||
} from './migrations/dashboard_saved_object_migrations';
|
||||
|
||||
export const DASHBOARD_SAVED_OBJECT_TYPE = 'dashboard';
|
||||
|
||||
export const createDashboardSavedObjectType = ({
|
||||
migrationDeps,
|
||||
}: {
|
||||
migrationDeps: DashboardSavedObjectTypeMigrationsDeps;
|
||||
}): SavedObjectsType => ({
|
||||
name: 'dashboard',
|
||||
name: DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
indexPattern: ANALYTICS_SAVED_OBJECT_INDEX,
|
||||
hidden: false,
|
||||
namespaceType: 'multiple-isolated',
|
||||
|
|
|
@ -7,4 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { createDashboardSavedObjectType } from './dashboard_saved_object';
|
||||
export {
|
||||
createDashboardSavedObjectType,
|
||||
DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
} from './dashboard_saved_object';
|
||||
export type { DashboardSavedObjectAttributes, GridData, SavedDashboardPanel } from './schema';
|
||||
|
|
|
@ -613,12 +613,11 @@ describe('dashboard', () => {
|
|||
expect(newDoc).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"description": "",
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{\\"query\\":{\\"language\\":\\"kuery\\",\\"query\\":\\"\\"},\\"filter\\":[{\\"query\\":{\\"match_phrase\\":{\\"machine.os.keyword\\":\\"osx\\"}},\\"$state\\":{\\"store\\":\\"appState\\"},\\"meta\\":{\\"type\\":\\"phrase\\",\\"key\\":\\"machine.os.keyword\\",\\"params\\":{\\"query\\":\\"osx\\"},\\"disabled\\":false,\\"negate\\":false,\\"alias\\":null,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}",
|
||||
},
|
||||
"optionsJSON": "{\\"useMargins\\":true,\\"hidePanelTitles\\":false}",
|
||||
"panelsJSON": "[{\\"version\\":\\"7.9.3\\",\\"type\\":\\"visualization\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\"},\\"panelIndex\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\",\\"embeddableConfig\\":{\\"enhancements\\":{\\"dynamicActions\\":{\\"events\\":[]}}},\\"panelRefName\\":\\"panel_82fa0882-9f9e-476a-bbb9-03555e5ced91\\"}]",
|
||||
"optionsJSON": "{\\"hidePanelTitles\\":false,\\"useMargins\\":true,\\"syncColors\\":true,\\"syncCursor\\":true,\\"syncTooltips\\":true}",
|
||||
"panelsJSON": "[{\\"version\\":\\"7.9.3\\",\\"type\\":\\"visualization\\",\\"panelRefName\\":\\"panel_82fa0882-9f9e-476a-bbb9-03555e5ced91\\",\\"embeddableConfig\\":{\\"enhancements\\":{\\"dynamicActions\\":{\\"events\\":[]}}},\\"panelIndex\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"82fa0882-9f9e-476a-bbb9-03555e5ced91\\"}}]",
|
||||
"timeRestore": false,
|
||||
"title": "Dashboard A",
|
||||
"version": 1,
|
||||
|
@ -710,7 +709,7 @@ describe('dashboard', () => {
|
|||
contextMock
|
||||
);
|
||||
expect(migratedDoc.attributes.panelsJSON).toMatchInlineSnapshot(
|
||||
`"[{\\"version\\":\\"7.9.3\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"0\\"},\\"panelIndex\\":\\"0\\",\\"embeddableConfig\\":{}},{\\"version\\":\\"7.13.0\\",\\"gridData\\":{\\"x\\":24,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"1\\"},\\"panelIndex\\":\\"1\\",\\"embeddableConfig\\":{\\"attributes\\":{\\"byValueThing\\":\\"ThisIsByValue\\"},\\"superCoolKey\\":\\"ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH\\"}}]"`
|
||||
`"[{\\"version\\":\\"7.9.3\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"0\\"},\\"panelIndex\\":\\"0\\",\\"embeddableConfig\\":{}},{\\"gridData\\":{\\"x\\":24,\\"y\\":0,\\"w\\":24,\\"h\\":15,\\"i\\":\\"1\\"},\\"panelIndex\\":\\"1\\",\\"embeddableConfig\\":{\\"attributes\\":{\\"byValueThing\\":\\"ThisIsByValue\\"},\\"superCoolKey\\":\\"ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH\\"},\\"version\\":\\"7.13.0\\"}]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
|
||||
import {
|
||||
controlGroupSerializedStateToSerializableRuntimeState,
|
||||
serializableRuntimeStateToControlGroupSerializedState,
|
||||
controlGroupSavedObjectStateToSerializableRuntimeState,
|
||||
serializableRuntimeStateToControlGroupSavedObjectState,
|
||||
} from '@kbn/controls-plugin/server';
|
||||
import { Serializable, SerializableRecord } from '@kbn/utility-types';
|
||||
import { SavedObjectMigrationFn } from '@kbn/core/server';
|
||||
|
@ -20,8 +20,8 @@ import { SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
|
|||
import {
|
||||
convertPanelStateToSavedDashboardPanel,
|
||||
convertSavedDashboardPanelToPanelState,
|
||||
} from '../../../common';
|
||||
import { SavedDashboardPanel } from '../../../common/content_management';
|
||||
} from './utils';
|
||||
import type { SavedDashboardPanel } from '..';
|
||||
|
||||
type ValueOrReferenceInput = SavedObjectEmbeddableInput & {
|
||||
attributes?: Serializable;
|
||||
|
@ -35,7 +35,7 @@ export const migrateByValueDashboardPanels =
|
|||
const { attributes } = doc;
|
||||
|
||||
if (attributes?.controlGroupInput) {
|
||||
const controlGroupState = controlGroupSerializedStateToSerializableRuntimeState(
|
||||
const controlGroupState = controlGroupSavedObjectStateToSerializableRuntimeState(
|
||||
attributes.controlGroupInput
|
||||
);
|
||||
const migratedControlGroupInput = migrate({
|
||||
|
@ -43,7 +43,7 @@ export const migrateByValueDashboardPanels =
|
|||
type: CONTROL_GROUP_TYPE,
|
||||
} as SerializableRecord);
|
||||
attributes.controlGroupInput =
|
||||
serializableRuntimeStateToControlGroupSerializedState(migratedControlGroupInput);
|
||||
serializableRuntimeStateToControlGroupSavedObjectState(migratedControlGroupInput);
|
||||
}
|
||||
|
||||
// Skip if panelsJSON is missing otherwise this will cause saved object import to fail when
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SavedObjectMigrationFn } from '@kbn/core/server';
|
||||
import { SavedObject, SavedObjectMigrationFn } from '@kbn/core/server';
|
||||
|
||||
import { extractReferences, injectReferences } from '../../../common';
|
||||
import { DashboardAttributes } from '../../../common/content_management';
|
||||
import { DashboardSavedObjectTypeMigrationsDeps } from './dashboard_saved_object_migrations';
|
||||
import type { DashboardSavedObjectTypeMigrationsDeps } from './dashboard_saved_object_migrations';
|
||||
import type { DashboardSavedObjectAttributes } from '../schema';
|
||||
import { itemAttrsToSavedObjectAttrs, savedObjectToItem } from '../../content_management/latest';
|
||||
|
||||
/**
|
||||
* In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state
|
||||
|
@ -26,7 +27,7 @@ import { DashboardSavedObjectTypeMigrationsDeps } from './dashboard_saved_object
|
|||
*/
|
||||
export function createExtractPanelReferencesMigration(
|
||||
deps: DashboardSavedObjectTypeMigrationsDeps
|
||||
): SavedObjectMigrationFn<DashboardAttributes> {
|
||||
): SavedObjectMigrationFn<DashboardSavedObjectAttributes> {
|
||||
return (doc) => {
|
||||
const references = doc.references ?? [];
|
||||
|
||||
|
@ -36,19 +37,32 @@ export function createExtractPanelReferencesMigration(
|
|||
*/
|
||||
const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_'));
|
||||
|
||||
// Use Content Management to convert the saved object to the DashboardAttributes
|
||||
// expected by injectReferences
|
||||
const { item, error: itemError } = savedObjectToItem(
|
||||
doc as unknown as SavedObject<DashboardSavedObjectAttributes>,
|
||||
false
|
||||
);
|
||||
|
||||
if (itemError) throw itemError;
|
||||
|
||||
const parsedAttributes = item.attributes;
|
||||
const injectedAttributes = injectReferences(
|
||||
{
|
||||
attributes: doc.attributes,
|
||||
attributes: parsedAttributes,
|
||||
references,
|
||||
},
|
||||
{ embeddablePersistableStateService: deps.embeddable }
|
||||
);
|
||||
|
||||
const { attributes, references: newPanelReferences } = extractReferences(
|
||||
const { attributes: extractedAttributes, references: newPanelReferences } = extractReferences(
|
||||
{ attributes: injectedAttributes, references: [] },
|
||||
{ embeddablePersistableStateService: deps.embeddable }
|
||||
);
|
||||
|
||||
const { attributes, error: attributesError } = itemAttrsToSavedObjectAttrs(extractedAttributes);
|
||||
if (attributesError) throw attributesError;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
references: [...oldNonPanelReferences, ...newPanelReferences],
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SavedObjectMigrationFn } from '@kbn/core/server';
|
||||
import { EmbeddableInput } from '@kbn/embeddable-plugin/common';
|
||||
import type { SavedObjectMigrationFn } from '@kbn/core/server';
|
||||
import type { EmbeddableInput } from '@kbn/embeddable-plugin/common';
|
||||
import type { SavedDashboardPanel } from '../schema';
|
||||
|
||||
import {
|
||||
convertSavedDashboardPanelToPanelState,
|
||||
convertPanelStateToSavedDashboardPanel,
|
||||
} from '../../../common';
|
||||
import { SavedDashboardPanel } from '../../../common/content_management';
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* Before 7.10, hidden panel titles were stored as a blank string on the title attribute. In 7.10, this was replaced
|
||||
|
|
|
@ -13,7 +13,7 @@ import semverSatisfies from 'semver/functions/satisfies';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import {
|
||||
import type {
|
||||
SavedDashboardPanel620,
|
||||
SavedDashboardPanel630,
|
||||
SavedDashboardPanel610,
|
||||
|
@ -25,7 +25,7 @@ import {
|
|||
RawSavedDashboardPanel640To720,
|
||||
RawSavedDashboardPanel730ToLatest,
|
||||
} from './types';
|
||||
import { GridData } from '../../../../common/content_management';
|
||||
import type { GridData } from '../../../content_management';
|
||||
|
||||
const PANEL_HEIGHT_SCALE_FACTOR = 5;
|
||||
const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4;
|
||||
|
|
|
@ -49,7 +49,7 @@ export const migrations730 = (doc: DashboardDoc700To720, { log }: SavedObjectMig
|
|||
}
|
||||
|
||||
try {
|
||||
const searchSource = JSON.parse(doc.attributes.kibanaSavedObjectMeta.searchSourceJSON);
|
||||
const searchSource = JSON.parse(doc.attributes.kibanaSavedObjectMeta.searchSourceJSON!);
|
||||
doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(
|
||||
moveFiltersToQuery(searchSource)
|
||||
);
|
||||
|
|
|
@ -10,14 +10,12 @@
|
|||
import type { Serializable } from '@kbn/utility-types';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
|
||||
import type {
|
||||
GridData,
|
||||
DashboardAttributes as CurrentDashboardAttributes, // Dashboard attributes from common are the source of truth for the current version.
|
||||
} from '../../../../common/content_management';
|
||||
import type { GridData } from '../../../content_management';
|
||||
import type { DashboardSavedObjectAttributes } from '../../schema';
|
||||
|
||||
interface KibanaAttributes {
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: string;
|
||||
searchSourceJSON?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -45,7 +43,7 @@ interface DashboardAttributesTo720 extends KibanaAttributes {
|
|||
optionsJSON?: string;
|
||||
}
|
||||
|
||||
export type DashboardDoc730ToLatest = Doc<CurrentDashboardAttributes>;
|
||||
export type DashboardDoc730ToLatest = Doc<DashboardSavedObjectAttributes>;
|
||||
|
||||
export type DashboardDoc700To720 = Doc<DashboardAttributesTo720>;
|
||||
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { EmbeddableInput } from '@kbn/embeddable-plugin/common/types';
|
||||
import type { SavedDashboardPanel } from '../schema';
|
||||
import type { DashboardPanelState } from '../../../common';
|
||||
|
||||
import {
|
||||
convertSavedDashboardPanelToPanelState,
|
||||
convertPanelStateToSavedDashboardPanel,
|
||||
} from './dashboard_panel_converters';
|
||||
import { SavedDashboardPanel } from '../content_management';
|
||||
import { DashboardPanelState } from '../dashboard_container/types';
|
||||
import { EmbeddableInput } from '@kbn/embeddable-plugin/common/types';
|
||||
} from './utils';
|
||||
|
||||
test('convertSavedDashboardPanelToPanelState', () => {
|
||||
const savedDashboardPanel: SavedDashboardPanel = {
|
||||
|
@ -148,7 +149,7 @@ test('convertPanelStateToSavedDashboardPanel will not leave title as part of emb
|
|||
expect(converted.title).toBe('title');
|
||||
});
|
||||
|
||||
test('convertPanelStateToSavedDashboardPanel retains legacy version info when not passed removeLegacyVersion', () => {
|
||||
test('convertPanelStateToSavedDashboardPanel retains legacy version info', () => {
|
||||
const dashboardPanel: DashboardPanelState = {
|
||||
gridData: {
|
||||
x: 0,
|
||||
|
@ -168,24 +169,3 @@ test('convertPanelStateToSavedDashboardPanel retains legacy version info when no
|
|||
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel);
|
||||
expect(converted.version).toBe('8.10.0');
|
||||
});
|
||||
|
||||
test('convertPanelStateToSavedDashboardPanel removes legacy version info when passed removeLegacyVersion', () => {
|
||||
const dashboardPanel: DashboardPanelState = {
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
h: 15,
|
||||
w: 15,
|
||||
i: '123',
|
||||
},
|
||||
explicitInput: {
|
||||
id: '123',
|
||||
title: 'title',
|
||||
} as EmbeddableInput,
|
||||
type: 'search',
|
||||
version: '8.10.0',
|
||||
};
|
||||
|
||||
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, true);
|
||||
expect(converted.version).not.toBeDefined();
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import type { EmbeddableInput, SavedObjectEmbeddableInput } from '@kbn/embeddable-plugin/common';
|
||||
import type { SavedDashboardPanel } from '../schema';
|
||||
import type { DashboardPanelState } from '../../../common';
|
||||
|
||||
export function convertSavedDashboardPanelToPanelState<
|
||||
TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput
|
||||
>(savedDashboardPanel: SavedDashboardPanel): DashboardPanelState<TEmbeddableInput> {
|
||||
return {
|
||||
type: savedDashboardPanel.type,
|
||||
gridData: savedDashboardPanel.gridData,
|
||||
panelRefName: savedDashboardPanel.panelRefName,
|
||||
explicitInput: {
|
||||
id: savedDashboardPanel.panelIndex,
|
||||
...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }),
|
||||
...(savedDashboardPanel.title !== undefined && { title: savedDashboardPanel.title }),
|
||||
...savedDashboardPanel.embeddableConfig,
|
||||
} as TEmbeddableInput,
|
||||
version: savedDashboardPanel.version,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertPanelStateToSavedDashboardPanel(
|
||||
panelState: DashboardPanelState
|
||||
): SavedDashboardPanel {
|
||||
const savedObjectId = (panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId;
|
||||
const panelIndex = panelState.explicitInput.id;
|
||||
return {
|
||||
type: panelState.type,
|
||||
gridData: {
|
||||
...panelState.gridData,
|
||||
i: panelIndex,
|
||||
},
|
||||
panelIndex,
|
||||
embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']),
|
||||
...(panelState.explicitInput.title !== undefined && { title: panelState.explicitInput.title }),
|
||||
...(savedObjectId !== undefined && { id: savedObjectId }),
|
||||
...(panelState.panelRefName !== undefined && { panelRefName: panelState.panelRefName }),
|
||||
...(panelState.version !== undefined && { version: panelState.version }),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { DashboardSavedObjectAttributes, GridData, SavedDashboardPanel } from './latest';
|
||||
export { dashboardSavedObjectSchema } from './latest';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
// Latest model version for dashboard saved objects is v2
|
||||
export {
|
||||
dashboardAttributesSchema as dashboardSavedObjectSchema,
|
||||
type DashboardAttributes as DashboardSavedObjectAttributes,
|
||||
type GridData,
|
||||
type SavedDashboardPanel,
|
||||
} from './v2';
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { DashboardAttributes } from './types';
|
||||
export { controlGroupInputSchema, dashboardAttributesSchema } from './v1';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { dashboardAttributesSchema } from './v1';
|
||||
|
||||
export type DashboardAttributes = TypeOf<typeof dashboardAttributesSchema>;
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const controlGroupInputSchema = schema
|
||||
.object({
|
||||
panelsJSON: schema.maybe(schema.string()),
|
||||
controlStyle: schema.maybe(schema.string()),
|
||||
chainingSystem: schema.maybe(schema.string()),
|
||||
ignoreParentSettingsJSON: schema.maybe(schema.string()),
|
||||
})
|
||||
.extends({}, { unknowns: 'ignore' });
|
||||
|
||||
export const dashboardAttributesSchema = schema.object(
|
||||
{
|
||||
// General
|
||||
title: schema.string(),
|
||||
description: schema.string({ defaultValue: '' }),
|
||||
|
||||
// Search
|
||||
kibanaSavedObjectMeta: schema.object({
|
||||
searchSourceJSON: schema.maybe(schema.string()),
|
||||
}),
|
||||
|
||||
// Time
|
||||
timeRestore: schema.maybe(schema.boolean()),
|
||||
timeFrom: schema.maybe(schema.string()),
|
||||
timeTo: schema.maybe(schema.string()),
|
||||
refreshInterval: schema.maybe(
|
||||
schema.object({
|
||||
pause: schema.boolean(),
|
||||
value: schema.number(),
|
||||
display: schema.maybe(schema.string()),
|
||||
section: schema.maybe(schema.number()),
|
||||
})
|
||||
),
|
||||
|
||||
// Dashboard Content
|
||||
controlGroupInput: schema.maybe(controlGroupInputSchema),
|
||||
panelsJSON: schema.string({ defaultValue: '[]' }),
|
||||
optionsJSON: schema.maybe(schema.string()),
|
||||
|
||||
// Legacy
|
||||
hits: schema.maybe(schema.number()),
|
||||
version: schema.maybe(schema.number()),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
);
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { DashboardAttributes, GridData, SavedDashboardPanel } from './types';
|
||||
export { controlGroupInputSchema, dashboardAttributesSchema } from './v2';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { Serializable } from '@kbn/utility-types';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { dashboardAttributesSchema, gridDataSchema } from './v2';
|
||||
|
||||
export type DashboardAttributes = TypeOf<typeof dashboardAttributesSchema>;
|
||||
export type GridData = TypeOf<typeof gridDataSchema>;
|
||||
|
||||
/**
|
||||
* A saved dashboard panel parsed directly from the Dashboard Attributes panels JSON
|
||||
*/
|
||||
export interface SavedDashboardPanel {
|
||||
embeddableConfig: { [key: string]: Serializable }; // parsed into the panel's explicitInput
|
||||
id?: string; // the saved object id for by reference panels
|
||||
type: string; // the embeddable type
|
||||
panelRefName?: string;
|
||||
gridData: GridData;
|
||||
panelIndex: string;
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* This version key was used to store Kibana version information from versions 7.3.0 -> 8.11.0.
|
||||
* As of version 8.11.0, the versioning information is now per-embeddable-type and is stored on the
|
||||
* embeddable's input. (embeddableConfig in this type).
|
||||
*/
|
||||
version?: string;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
controlGroupInputSchema as controlGroupInputSchemaV1,
|
||||
dashboardAttributesSchema as dashboardAttributesSchemaV1,
|
||||
} from '../v1';
|
||||
|
||||
export const controlGroupInputSchema = controlGroupInputSchemaV1.extends(
|
||||
{
|
||||
showApplySelections: schema.maybe(schema.boolean()),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export const dashboardAttributesSchema = dashboardAttributesSchemaV1.extends(
|
||||
{
|
||||
controlGroupInput: schema.maybe(controlGroupInputSchema),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export const gridDataSchema = schema.object({
|
||||
x: schema.number(),
|
||||
y: schema.number(),
|
||||
w: schema.number(),
|
||||
h: schema.number(),
|
||||
i: schema.string(),
|
||||
});
|
|
@ -26,3 +26,7 @@ export async function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export type { DashboardPluginSetup, DashboardPluginStart } from './types';
|
||||
export type { DashboardAttributes } from './content_management';
|
||||
export type { DashboardSavedObjectAttributes } from './dashboard_saved_object';
|
||||
|
||||
export { PUBLIC_API_PATH } from './api/constants';
|
||||
|
|
|
@ -30,6 +30,7 @@ import { createDashboardSavedObjectType } from './dashboard_saved_object';
|
|||
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
|
||||
import { registerDashboardUsageCollector } from './usage/register_collector';
|
||||
import { dashboardPersistableStateServiceFactory } from './dashboard_container/dashboard_container_embeddable_factory';
|
||||
import { registerAPIRoutes } from './api';
|
||||
|
||||
interface SetupDeps {
|
||||
embeddable: EmbeddableSetup;
|
||||
|
@ -111,6 +112,12 @@ export class DashboardPlugin
|
|||
|
||||
core.uiSettings.register(getUISettings());
|
||||
|
||||
registerAPIRoutes({
|
||||
http: core.http,
|
||||
contentManagement: plugins.contentManagement,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { SavedDashboardPanel } from '../../common/content_management';
|
||||
import { SavedDashboardPanel } from '../dashboard_saved_object';
|
||||
import { getEmptyDashboardData, collectPanelsByType } from './dashboard_telemetry';
|
||||
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import { createEmbeddablePersistableStateServiceMock } from '@kbn/embeddable-plugin/common/mocks';
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { EmbeddablePersistableStateService } from '@kbn/embeddable-plugin/common';
|
||||
import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
|
||||
|
||||
import { DashboardAttributes, SavedDashboardPanel } from '../../common/content_management';
|
||||
import { DashboardSavedObjectAttributes, SavedDashboardPanel } from '../dashboard_saved_object';
|
||||
import { TASK_ID } from './dashboard_telemetry_collection_task';
|
||||
import { emptyState, type LatestTaskStateSchema } from './task_state';
|
||||
|
||||
|
@ -95,7 +95,7 @@ export const collectPanelsByType = (
|
|||
|
||||
export const controlsCollectorFactory =
|
||||
(embeddableService: EmbeddablePersistableStateService) =>
|
||||
(attributes: DashboardAttributes, collectorData: DashboardCollectorData) => {
|
||||
(attributes: DashboardSavedObjectAttributes, collectorData: DashboardCollectorData) => {
|
||||
if (!isEmpty(attributes.controlGroupInput)) {
|
||||
collectorData.controls = embeddableService.telemetry(
|
||||
{
|
||||
|
|
|
@ -23,9 +23,15 @@ import {
|
|||
collectPanelsByType,
|
||||
getEmptyDashboardData,
|
||||
} from './dashboard_telemetry';
|
||||
import { injectReferences } from '../../common';
|
||||
import { DashboardAttributesAndReferences } from '../../common/types';
|
||||
import { DashboardAttributes, SavedDashboardPanel } from '../../common/content_management';
|
||||
import type {
|
||||
DashboardSavedObjectAttributes,
|
||||
SavedDashboardPanel,
|
||||
} from '../dashboard_saved_object';
|
||||
|
||||
interface DashboardSavedObjectAttributesAndReferences {
|
||||
attributes: DashboardSavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
// This task is responsible for running daily and aggregating all the Dashboard telemerty data
|
||||
// into a single document. This is an effort to make sure the load of fetching/parsing all of the
|
||||
|
@ -88,17 +94,18 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
|
|||
async run() {
|
||||
let dashboardData = getEmptyDashboardData();
|
||||
const controlsCollector = controlsCollectorFactory(embeddable);
|
||||
const processDashboards = (dashboards: DashboardAttributesAndReferences[]) => {
|
||||
const processDashboards = (dashboards: DashboardSavedObjectAttributesAndReferences[]) => {
|
||||
for (const dashboard of dashboards) {
|
||||
const attributes = injectReferences(dashboard, {
|
||||
embeddablePersistableStateService: embeddable,
|
||||
});
|
||||
// TODO is this injecting references really necessary?
|
||||
// const attributes = injectReferences(dashboard, {
|
||||
// embeddablePersistableStateService: embeddable,
|
||||
// });
|
||||
|
||||
dashboardData = controlsCollector(attributes, dashboardData);
|
||||
dashboardData = controlsCollector(dashboard.attributes, dashboardData);
|
||||
|
||||
try {
|
||||
const panels = JSON.parse(
|
||||
attributes.panelsJSON as string
|
||||
dashboard.attributes.panelsJSON as string
|
||||
) as unknown as SavedDashboardPanel[];
|
||||
|
||||
collectPanelsByType(panels, dashboardData, embeddable);
|
||||
|
@ -129,7 +136,7 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
|
|||
const esClient = await getEsClient();
|
||||
|
||||
let result = await esClient.search<{
|
||||
dashboard: DashboardAttributes;
|
||||
dashboard: DashboardSavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
}>(searchParams);
|
||||
|
||||
|
@ -144,8 +151,8 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
|
|||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter<DashboardAttributesAndReferences>(
|
||||
(s): s is DashboardAttributesAndReferences => s !== undefined
|
||||
.filter<DashboardSavedObjectAttributesAndReferences>(
|
||||
(s): s is DashboardSavedObjectAttributesAndReferences => s !== undefined
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -163,8 +170,8 @@ export function dashboardTaskRunner(logger: Logger, core: CoreSetup, embeddable:
|
|||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter<DashboardAttributesAndReferences>(
|
||||
(s): s is DashboardAttributesAndReferences => s !== undefined
|
||||
.filter<DashboardSavedObjectAttributesAndReferences>(
|
||||
(s): s is DashboardSavedObjectAttributesAndReferences => s !== undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { DynamicActionsSerializedState } from '@kbn/embeddable-enhanced-plugin/p
|
|||
import { HasSerializedChildState, PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { LocatorPublic } from '@kbn/share-plugin/common';
|
||||
import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE } from '@kbn/dashboard-plugin/public';
|
||||
import { DashboardAttributes } from '@kbn/dashboard-plugin/common';
|
||||
import type { DashboardAttributes } from '@kbn/dashboard-plugin/server';
|
||||
|
||||
import { CONTENT_ID } from '../common';
|
||||
import { Link, LinksAttributes, LinksLayoutType } from '../common/content_management';
|
||||
|
@ -73,5 +73,5 @@ export type ResolvedLink = Link & {
|
|||
|
||||
export interface DashboardItem {
|
||||
id: string;
|
||||
attributes: DashboardAttributes;
|
||||
attributes: Pick<DashboardAttributes, 'title' | 'description' | 'timeRestore'>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
describe('dashboards - create', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.importExport.unload(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
loadTestFile(require.resolve('./validation'));
|
||||
});
|
||||
}
|
216
test/api_integration/apis/dashboards/create_dashboard/main.ts
Normal file
216
test/api_integration/apis/dashboards/create_dashboard/main.ts
Normal file
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server';
|
||||
import { DEFAULT_IGNORE_PARENT_SETTINGS } from '@kbn/controls-plugin/common';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
describe('main', () => {
|
||||
it('sets top level default values', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.kibanaSavedObjectMeta.searchSource).to.eql({});
|
||||
expect(response.body.item.attributes.panels).to.eql([]);
|
||||
expect(response.body.item.attributes.timeRestore).to.be(false);
|
||||
expect(response.body.item.attributes.options).to.eql({
|
||||
hidePanelTitles: false,
|
||||
useMargins: true,
|
||||
syncColors: true,
|
||||
syncTooltips: true,
|
||||
syncCursor: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets panels default values', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title,
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 24,
|
||||
h: 15,
|
||||
},
|
||||
panelConfig: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.panels).to.be.an('array');
|
||||
// panel index is a random uuid when not provided
|
||||
expect(response.body.item.attributes.panels[0].panelIndex).match(/^[0-9a-f-]{36}$/);
|
||||
expect(response.body.item.attributes.panels[0].panelIndex).to.eql(
|
||||
response.body.item.attributes.panels[0].gridData.i
|
||||
);
|
||||
});
|
||||
|
||||
it('sets controls default values', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title,
|
||||
controlGroupInput: {
|
||||
controls: [
|
||||
{
|
||||
type: 'optionsListControl',
|
||||
order: 0,
|
||||
width: 'medium',
|
||||
grow: true,
|
||||
controlConfig: {
|
||||
title: 'Origin City',
|
||||
fieldName: 'OriginCityName',
|
||||
dataViewId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
|
||||
selectedOptions: [],
|
||||
enhancements: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
// generates a random saved object id
|
||||
expect(response.body.item.id).match(/^[0-9a-f-]{36}$/);
|
||||
// saved object stores controls panels as an object, but the API should return as an array
|
||||
expect(response.body.item.attributes.controlGroupInput.controls).to.be.an('array');
|
||||
|
||||
expect(response.body.item.attributes.controlGroupInput.ignoreParentSettings).to.eql(
|
||||
DEFAULT_IGNORE_PARENT_SETTINGS
|
||||
);
|
||||
});
|
||||
|
||||
it('can create a dashboard with a specific id', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
const id = `bar-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(`${PUBLIC_API_PATH}/${id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: { title },
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.id).to.be(id);
|
||||
});
|
||||
|
||||
it('creates a dashboard with references', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title,
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
gridData: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 24,
|
||||
h: 15,
|
||||
i: 'bizz',
|
||||
},
|
||||
panelConfig: {},
|
||||
panelIndex: 'bizz',
|
||||
panelRefName: 'panel_bizz',
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'bizz:panel_bizz',
|
||||
type: 'visualization',
|
||||
id: 'my-saved-object',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.panels).to.be.an('array');
|
||||
});
|
||||
|
||||
// TODO Maybe move this test to x-pack/test/api_integration/dashboards
|
||||
it('can create a dashboard in a defined space', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const spaceId = 'space-1';
|
||||
|
||||
const response = await supertest
|
||||
.post(`/s/${spaceId}${PUBLIC_API_PATH}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title,
|
||||
},
|
||||
spaces: [spaceId],
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.namespaces).to.eql([spaceId]);
|
||||
});
|
||||
|
||||
it('return error if provided id already exists', async () => {
|
||||
const title = `foo-${Date.now()}-${Math.random()}`;
|
||||
// id is a saved object loaded by the kbn_archiver
|
||||
const id = 'be3733a0-9efe-11e7-acb3-3dab96693fab';
|
||||
|
||||
const response = await supertest
|
||||
.post(`${PUBLIC_API_PATH}/${id}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(409);
|
||||
expect(response.body.message).to.be(
|
||||
'A dashboard with saved object ID be3733a0-9efe-11e7-acb3-3dab96693fab already exists.'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
describe('validation', () => {
|
||||
it('returns error when attributes object is not provided', async () => {
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({});
|
||||
expect(response.status).to.be(400);
|
||||
expect(response.body.statusCode).to.be(400);
|
||||
expect(response.body.message).to.be(
|
||||
'[request body.attributes.title]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error when title is not provided', async () => {
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {},
|
||||
});
|
||||
expect(response.status).to.be(400);
|
||||
expect(response.body.statusCode).to.be(400);
|
||||
expect(response.body.message).to.be(
|
||||
'[request body.attributes.title]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error if panels is not an array', async () => {
|
||||
const response = await supertest
|
||||
.post(PUBLIC_API_PATH)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'foo',
|
||||
panels: {},
|
||||
},
|
||||
});
|
||||
expect(response.status).to.be(400);
|
||||
expect(response.body.statusCode).to.be(400);
|
||||
expect(response.body.message).to.be(
|
||||
'[request body.attributes.panels]: expected value of type [array] but got [Object]'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
describe('dashboards - delete', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.importExport.unload(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
describe('main', () => {
|
||||
it('should return 404 for a non-existent dashboard', async () => {
|
||||
const response = await supertest
|
||||
.delete(`${PUBLIC_API_PATH}/non-existent-dashboard`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send();
|
||||
|
||||
expect(response.status).to.be(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'A dashboard with saved object ID non-existent-dashboard was not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 if the dashboard is deleted', async () => {
|
||||
const response = await supertest
|
||||
.delete(`${PUBLIC_API_PATH}/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send();
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
});
|
||||
}
|
29
test/api_integration/apis/dashboards/get_dashboard/index.ts
Normal file
29
test/api_integration/apis/dashboards/get_dashboard/index.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
describe('dashboards - get', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.importExport.unload(
|
||||
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue