mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Dashboard] Inject / extract tag references (#214788)](https://github.com/elastic/kibana/pull/214788) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Nick Peihl","email":"nick.peihl@elastic.co"},"sourceCommit":{"committedDate":"2025-03-31T14:52:24Z","message":"[Dashboard] Inject / extract tag references (#214788)\n\nFixes #210619\n\n## Summary\n\nProvides a tags array on the request and response bodies of dashboards.\n\nThis allows consumers of the Dashboards HTTP API and internal RPC API to\nadd an array of tag names to the attributes in the body of create and\nupdate endpoints. The dashboard server will be responsible for\nconverting the tag names into references in the saved object.\n\nIf, during creation or update, a tag name does not have a matching tag\nsaved object, a new tag saved object will be created. If the user lacks\npermissions to manage tags, then an error will be logged in the server\nand the tag will not be added to the dashboard.\n\nThe server also injects the tag references as an array of tag names in\nthe attributes of the response body of get and search endpoints of the\nHTTP and RPC APIs.\n\nFor backwards compatibility in create and update endpoints, tags can\nalternatively be specified in the `references` array in the options\ninstead of (or in addition to) the `attributes.tags` in the request\nbody. Similarly, for backwards compatibility, tag references are\nreturned in the `references` of the response body of get and search\nendpoints.\n\nClient-side tag handling is out of scope for this PR. Dashboards listing\npage and dashboard settings continue to use the tag references and do\nnot use the `tags` attribute in the response.\n\nFor example:\n\nHere's how we currently create a dashboard with tag references.\n```\n## Creating a dashboard with tag references\nPOST kbn:/api/dashboards/dashboard\n{\n \"attributes\": {\n \"title\": \"my tagged dashboard\"\n },\n \"references\": [\n {\n \"type\": \"tag\",\n \"id\": \"37aab5de-a34d-47cb-9aa5-9375d5db595f\",\n \"name\": \"tag-ref-37aab5de-a34d-47cb-9aa5-9375d5db595f\"\n },\n {\n \"type\": \"tag\",\n \"id\": \"5ed29bba-c14d-4302-9a8c-9602e40dbc2a\",\n \"name\": \"tag-ref-5ed29bba-c14d-4302-9a8c-9602e40dbc2a\"\n },\n {\n \"type\": \"tag\",\n \"id\": \"fc7890e8-c00f-44a1-88a2-250e4d27e61d\",\n \"name\": \"tag-ref-fc7890e8-c00f-44a1-88a2-250e4d27e61d\"\n }\n ]\n}\n```\n\nWith this PR, creating a dashboard with tags is much simpler.\n\n```\n## Creating a dashboard with tag names\nPOST kbn:/api/dashboards/dashboard\n{\n \"attributes\": {\n \"title\": \"my tagged dashboard\",\n \"tags\": [\n \"boo\",\n \"foo\",\n \"bingo\",\n \"bongo\"\n ]\n }\n}\n\n```\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n### Identify risks\n\nDoes this PR introduce any risks? For example, consider risks like hard\nto test bugs, performance regression, potential of data loss.\n\nDescribe the risk, its severity, and mitigation for each identified\nrisk. Invite stakeholders and evaluate how to proceed before merging.\n\n- [ ] If there are more than one tag saved objects with the same name,\nonly one of the tag references will be added to the saved object when\ncreating a dashboard. Creating tags with duplicate names are not\npermitted via the UI. But there is no such restrictions when creating\ntags from imported saved objects. Having multiple tags with the same\nname is an edge case that Kibana guards against with reasonable\nrestrictions, so I think we should not be too concerned about it.\n- [ ] ...\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"c4f7c649b12b8189f1b7a00d4857c1372acc8755","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Presentation","backport:version","v9.1.0","v8.19.0"],"title":"[Dashboard] Inject / extract tag references","number":214788,"url":"https://github.com/elastic/kibana/pull/214788","mergeCommit":{"message":"[Dashboard] Inject / extract tag references (#214788)\n\nFixes #210619\n\n## Summary\n\nProvides a tags array on the request and response bodies of dashboards.\n\nThis allows consumers of the Dashboards HTTP API and internal RPC API to\nadd an array of tag names to the attributes in the body of create and\nupdate endpoints. The dashboard server will be responsible for\nconverting the tag names into references in the saved object.\n\nIf, during creation or update, a tag name does not have a matching tag\nsaved object, a new tag saved object will be created. If the user lacks\npermissions to manage tags, then an error will be logged in the server\nand the tag will not be added to the dashboard.\n\nThe server also injects the tag references as an array of tag names in\nthe attributes of the response body of get and search endpoints of the\nHTTP and RPC APIs.\n\nFor backwards compatibility in create and update endpoints, tags can\nalternatively be specified in the `references` array in the options\ninstead of (or in addition to) the `attributes.tags` in the request\nbody. Similarly, for backwards compatibility, tag references are\nreturned in the `references` of the response body of get and search\nendpoints.\n\nClient-side tag handling is out of scope for this PR. Dashboards listing\npage and dashboard settings continue to use the tag references and do\nnot use the `tags` attribute in the response.\n\nFor example:\n\nHere's how we currently create a dashboard with tag references.\n```\n## Creating a dashboard with tag references\nPOST kbn:/api/dashboards/dashboard\n{\n \"attributes\": {\n \"title\": \"my tagged dashboard\"\n },\n \"references\": [\n {\n \"type\": \"tag\",\n \"id\": \"37aab5de-a34d-47cb-9aa5-9375d5db595f\",\n \"name\": \"tag-ref-37aab5de-a34d-47cb-9aa5-9375d5db595f\"\n },\n {\n \"type\": \"tag\",\n \"id\": \"5ed29bba-c14d-4302-9a8c-9602e40dbc2a\",\n \"name\": \"tag-ref-5ed29bba-c14d-4302-9a8c-9602e40dbc2a\"\n },\n {\n \"type\": \"tag\",\n \"id\": \"fc7890e8-c00f-44a1-88a2-250e4d27e61d\",\n \"name\": \"tag-ref-fc7890e8-c00f-44a1-88a2-250e4d27e61d\"\n }\n ]\n}\n```\n\nWith this PR, creating a dashboard with tags is much simpler.\n\n```\n## Creating a dashboard with tag names\nPOST kbn:/api/dashboards/dashboard\n{\n \"attributes\": {\n \"title\": \"my tagged dashboard\",\n \"tags\": [\n \"boo\",\n \"foo\",\n \"bingo\",\n \"bongo\"\n ]\n }\n}\n\n```\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n### Identify risks\n\nDoes this PR introduce any risks? For example, consider risks like hard\nto test bugs, performance regression, potential of data loss.\n\nDescribe the risk, its severity, and mitigation for each identified\nrisk. Invite stakeholders and evaluate how to proceed before merging.\n\n- [ ] If there are more than one tag saved objects with the same name,\nonly one of the tag references will be added to the saved object when\ncreating a dashboard. Creating tags with duplicate names are not\npermitted via the UI. But there is no such restrictions when creating\ntags from imported saved objects. Having multiple tags with the same\nname is an edge case that Kibana guards against with reasonable\nrestrictions, so I think we should not be too concerned about it.\n- [ ] ...\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"c4f7c649b12b8189f1b7a00d4857c1372acc8755"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/214788","number":214788,"mergeCommit":{"message":"[Dashboard] Inject / extract tag references (#214788)\n\nFixes #210619\n\n## Summary\n\nProvides a tags array on the request and response bodies of dashboards.\n\nThis allows consumers of the Dashboards HTTP API and internal RPC API to\nadd an array of tag names to the attributes in the body of create and\nupdate endpoints. The dashboard server will be responsible for\nconverting the tag names into references in the saved object.\n\nIf, during creation or update, a tag name does not have a matching tag\nsaved object, a new tag saved object will be created. If the user lacks\npermissions to manage tags, then an error will be logged in the server\nand the tag will not be added to the dashboard.\n\nThe server also injects the tag references as an array of tag names in\nthe attributes of the response body of get and search endpoints of the\nHTTP and RPC APIs.\n\nFor backwards compatibility in create and update endpoints, tags can\nalternatively be specified in the `references` array in the options\ninstead of (or in addition to) the `attributes.tags` in the request\nbody. Similarly, for backwards compatibility, tag references are\nreturned in the `references` of the response body of get and search\nendpoints.\n\nClient-side tag handling is out of scope for this PR. Dashboards listing\npage and dashboard settings continue to use the tag references and do\nnot use the `tags` attribute in the response.\n\nFor example:\n\nHere's how we currently create a dashboard with tag references.\n```\n## Creating a dashboard with tag references\nPOST kbn:/api/dashboards/dashboard\n{\n \"attributes\": {\n \"title\": \"my tagged dashboard\"\n },\n \"references\": [\n {\n \"type\": \"tag\",\n \"id\": \"37aab5de-a34d-47cb-9aa5-9375d5db595f\",\n \"name\": \"tag-ref-37aab5de-a34d-47cb-9aa5-9375d5db595f\"\n },\n {\n \"type\": \"tag\",\n \"id\": \"5ed29bba-c14d-4302-9a8c-9602e40dbc2a\",\n \"name\": \"tag-ref-5ed29bba-c14d-4302-9a8c-9602e40dbc2a\"\n },\n {\n \"type\": \"tag\",\n \"id\": \"fc7890e8-c00f-44a1-88a2-250e4d27e61d\",\n \"name\": \"tag-ref-fc7890e8-c00f-44a1-88a2-250e4d27e61d\"\n }\n ]\n}\n```\n\nWith this PR, creating a dashboard with tags is much simpler.\n\n```\n## Creating a dashboard with tag names\nPOST kbn:/api/dashboards/dashboard\n{\n \"attributes\": {\n \"title\": \"my tagged dashboard\",\n \"tags\": [\n \"boo\",\n \"foo\",\n \"bingo\",\n \"bongo\"\n ]\n }\n}\n\n```\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- [x] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n### Identify risks\n\nDoes this PR introduce any risks? For example, consider risks like hard\nto test bugs, performance regression, potential of data loss.\n\nDescribe the risk, its severity, and mitigation for each identified\nrisk. Invite stakeholders and evaluate how to proceed before merging.\n\n- [ ] If there are more than one tag saved objects with the same name,\nonly one of the tag references will be added to the saved object when\ncreating a dashboard. Creating tags with duplicate names are not\npermitted via the UI. But there is no such restrictions when creating\ntags from imported saved objects. Having multiple tags with the same\nname is an edge case that Kibana guards against with reasonable\nrestrictions, so I think we should not be too concerned about it.\n- [ ] ...\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"c4f7c649b12b8189f1b7a00d4857c1372acc8755"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
56b0868f48
commit
b078ff2f99
26 changed files with 2893 additions and 135 deletions
|
@ -6293,6 +6293,13 @@
|
|||
"description": "A short description.",
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeRestore": {
|
||||
"default": false,
|
||||
"description": "Whether to restore time upon viewing this dashboard",
|
||||
|
@ -6939,6 +6946,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -7593,6 +7607,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -8131,6 +8152,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -8757,6 +8785,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -9289,6 +9324,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
|
|
@ -5830,6 +5830,13 @@
|
|||
"description": "A short description.",
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeRestore": {
|
||||
"default": false,
|
||||
"description": "Whether to restore time upon viewing this dashboard",
|
||||
|
@ -6476,6 +6483,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -7130,6 +7144,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -7668,6 +7689,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -8294,6 +8322,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
@ -8826,6 +8861,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"description": "An array of tags applied to this dashboard",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeFrom": {
|
||||
"description": "An ISO string indicating when to restore time from",
|
||||
"type": "string"
|
||||
|
|
|
@ -5540,6 +5540,11 @@ paths:
|
|||
default: ''
|
||||
description: A short description.
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeRestore:
|
||||
default: false
|
||||
description: Whether to restore time upon viewing this dashboard
|
||||
|
@ -6000,6 +6005,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -6465,6 +6475,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -6853,6 +6868,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -7298,6 +7318,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -7682,6 +7707,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
|
|
@ -8056,6 +8056,11 @@ paths:
|
|||
default: ''
|
||||
description: A short description.
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeRestore:
|
||||
default: false
|
||||
description: Whether to restore time upon viewing this dashboard
|
||||
|
@ -8516,6 +8521,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -8981,6 +8991,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -9369,6 +9384,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -9814,6 +9834,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
@ -10198,6 +10223,11 @@ paths:
|
|||
required:
|
||||
- pause
|
||||
- value
|
||||
tags:
|
||||
items:
|
||||
description: An array of tags applied to this dashboard
|
||||
type: string
|
||||
type: array
|
||||
timeFrom:
|
||||
description: An ISO string indicating when to restore time from
|
||||
type: string
|
||||
|
|
|
@ -138,6 +138,8 @@ export const getSerializedState = ({
|
|||
{ embeddablePersistableStateService: embeddableService }
|
||||
);
|
||||
|
||||
// TODO Provide tags as an array of tag names in the attribute. In that case, tag references
|
||||
// will be extracted by the server.
|
||||
const savedObjectsTaggingApi = savedObjectsTaggingService?.getTaggingApi();
|
||||
const references = savedObjectsTaggingApi?.ui.updateTagsReferences
|
||||
? savedObjectsTaggingApi?.ui.updateTagsReferences(dashboardReferences, tags)
|
||||
|
|
|
@ -14,10 +14,13 @@ import type { Logger } from '@kbn/logging';
|
|||
|
||||
import { CreateResult, DeleteResult, SearchQuery } from '@kbn/content-management-plugin/common';
|
||||
import { StorageContext } from '@kbn/content-management-plugin/server';
|
||||
import type { SavedObjectTaggingStart } from '@kbn/saved-objects-tagging-plugin/server';
|
||||
import type { SavedObjectReference } from '@kbn/core/server';
|
||||
import type { ITagsClient, Tag } from '@kbn/saved-objects-tagging-oss-plugin/common';
|
||||
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 { itemAttrsToSavedObjectWithTags, savedObjectToItem } from './latest';
|
||||
import type {
|
||||
DashboardAttributes,
|
||||
DashboardItem,
|
||||
|
@ -28,8 +31,13 @@ import type {
|
|||
DashboardUpdateOptions,
|
||||
DashboardUpdateOut,
|
||||
DashboardSearchOptions,
|
||||
ReplaceTagReferencesByNameParams,
|
||||
} from './latest';
|
||||
|
||||
const getRandomColor = (): string => {
|
||||
return '#' + String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0');
|
||||
};
|
||||
|
||||
const searchArgsToSOFindOptions = (
|
||||
query: SearchQuery,
|
||||
options: DashboardSearchOptions
|
||||
|
@ -60,21 +68,117 @@ export class DashboardStorage {
|
|||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
savedObjectsTagging,
|
||||
}: {
|
||||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
savedObjectsTagging?: SavedObjectTaggingStart;
|
||||
}) {
|
||||
this.savedObjectsTagging = savedObjectsTagging;
|
||||
this.logger = logger;
|
||||
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
|
||||
}
|
||||
|
||||
private logger: Logger;
|
||||
private savedObjectsTagging?: SavedObjectTaggingStart;
|
||||
private throwOnResultValidationError: boolean;
|
||||
|
||||
private getTagNamesFromReferences(references: SavedObjectReference[], allTags: Tag[]) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
this.savedObjectsTagging
|
||||
? this.savedObjectsTagging
|
||||
.getTagsFromReferences(references, allTags)
|
||||
.tags.map((tag) => tag.name)
|
||||
: []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private getUniqueTagNames(
|
||||
references: SavedObjectReference[],
|
||||
newTagNames: string[],
|
||||
allTags: Tag[]
|
||||
) {
|
||||
const referenceTagNames = this.getTagNamesFromReferences(references, allTags);
|
||||
return new Set([...referenceTagNames, ...newTagNames]);
|
||||
}
|
||||
|
||||
private async replaceTagReferencesByName(
|
||||
references: SavedObjectReference[],
|
||||
newTagNames: string[],
|
||||
allTags: Tag[],
|
||||
tagsClient?: ITagsClient
|
||||
) {
|
||||
const combinedTagNames = this.getUniqueTagNames(references, newTagNames, allTags);
|
||||
const newTagIds = await this.convertTagNamesToIds(combinedTagNames, allTags, tagsClient);
|
||||
return this.savedObjectsTagging?.replaceTagReferences(references, newTagIds) ?? references;
|
||||
}
|
||||
|
||||
private async convertTagNamesToIds(
|
||||
tagNames: Set<string>,
|
||||
allTags: Tag[],
|
||||
tagsClient?: ITagsClient
|
||||
): Promise<string[]> {
|
||||
const combinedTagNames = await this.createTagsIfNeeded(tagNames, allTags, tagsClient);
|
||||
|
||||
return Array.from(combinedTagNames).flatMap(
|
||||
(tagName) => this.savedObjectsTagging?.convertTagNameToId(tagName, allTags) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
private async createTagsIfNeeded(
|
||||
tagNames: Set<string>,
|
||||
allTags: Tag[],
|
||||
tagsClient?: ITagsClient
|
||||
) {
|
||||
const tagsToCreate = Array.from(tagNames).filter(
|
||||
(tagName) => !allTags.some((tag) => tag.name === tagName)
|
||||
);
|
||||
const tagCreationResults = await Promise.allSettled(
|
||||
tagsToCreate.flatMap(
|
||||
(tagName) =>
|
||||
tagsClient?.create({
|
||||
name: tagName,
|
||||
description: '',
|
||||
color: getRandomColor(),
|
||||
}) ?? []
|
||||
)
|
||||
);
|
||||
|
||||
for (const result of tagCreationResults) {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.error(`Error creating tag: ${result.reason}`);
|
||||
} else {
|
||||
this.logger.info(`Tag created: ${result.value.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const createdTags = tagCreationResults
|
||||
.filter((result): result is PromiseFulfilledResult<Tag> => result.status === 'fulfilled')
|
||||
.map((result) => result.value);
|
||||
|
||||
// Remove tags that were not created
|
||||
const invalidTagNames = tagsToCreate.filter(
|
||||
(tagName) => !createdTags.some((tag) => tag.name === tagName)
|
||||
);
|
||||
invalidTagNames.forEach((tagName) => tagNames.delete(tagName));
|
||||
|
||||
// Add newly created tags to allTags
|
||||
allTags.push(...createdTags);
|
||||
|
||||
const combinedTagNames = new Set([
|
||||
...tagNames,
|
||||
...createdTags.map((createdTag) => createdTag.name),
|
||||
]);
|
||||
return combinedTagNames;
|
||||
}
|
||||
|
||||
async get(ctx: StorageContext, id: string): Promise<DashboardGetOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
const tagsClient = this.savedObjectsTagging?.createTagClient({ client: soClient });
|
||||
const allTags = (await tagsClient?.getAll()) ?? [];
|
||||
// Save data in DB
|
||||
const {
|
||||
saved_object: savedObject,
|
||||
|
@ -83,7 +187,10 @@ export class DashboardStorage {
|
|||
outcome,
|
||||
} = await soClient.resolve<DashboardSavedObjectAttributes>(DASHBOARD_SAVED_OBJECT_TYPE, id);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(savedObject, false);
|
||||
const { item, error: itemError } = savedObjectToItem(savedObject, false, {
|
||||
getTagNamesFromReferences: (references: SavedObjectReference[]) =>
|
||||
this.getTagNamesFromReferences(references, allTags),
|
||||
});
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
@ -128,6 +235,8 @@ export class DashboardStorage {
|
|||
): Promise<DashboardCreateOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
const tagsClient = this.savedObjectsTagging?.createTagClient({ client: soClient });
|
||||
const allTags = tagsClient ? await tagsClient?.getAll() : [];
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
|
||||
|
@ -146,8 +255,16 @@ export class DashboardStorage {
|
|||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const { attributes: soAttributes, error: attributesError } =
|
||||
itemAttrsToSavedObjectAttrs(dataToLatest);
|
||||
const {
|
||||
attributes: soAttributes,
|
||||
references: soReferences,
|
||||
error: attributesError,
|
||||
} = await itemAttrsToSavedObjectWithTags({
|
||||
attributes: dataToLatest,
|
||||
replaceTagReferencesByName: ({ references, newTagNames }: ReplaceTagReferencesByNameParams) =>
|
||||
this.replaceTagReferencesByName(references, newTagNames, allTags, tagsClient),
|
||||
incomingReferences: options.references,
|
||||
});
|
||||
if (attributesError) {
|
||||
throw Boom.badRequest(`Invalid data. ${attributesError.message}`);
|
||||
}
|
||||
|
@ -156,10 +273,13 @@ export class DashboardStorage {
|
|||
const savedObject = await soClient.create<DashboardSavedObjectAttributes>(
|
||||
DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
soAttributes,
|
||||
optionsToLatest
|
||||
{ ...optionsToLatest, references: soReferences }
|
||||
);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(savedObject, false);
|
||||
const { item, error: itemError } = savedObjectToItem(savedObject, false, {
|
||||
getTagNamesFromReferences: (references: SavedObjectReference[]) =>
|
||||
this.getTagNamesFromReferences(references, allTags),
|
||||
});
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
@ -197,6 +317,8 @@ export class DashboardStorage {
|
|||
): Promise<DashboardUpdateOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
const tagsClient = this.savedObjectsTagging?.createTagClient({ client: soClient });
|
||||
const allTags = (await tagsClient?.getAll()) ?? [];
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
|
||||
|
@ -215,8 +337,16 @@ export class DashboardStorage {
|
|||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const { attributes: soAttributes, error: attributesError } =
|
||||
itemAttrsToSavedObjectAttrs(dataToLatest);
|
||||
const {
|
||||
attributes: soAttributes,
|
||||
references: soReferences,
|
||||
error: attributesError,
|
||||
} = await itemAttrsToSavedObjectWithTags({
|
||||
attributes: dataToLatest,
|
||||
replaceTagReferencesByName: ({ references, newTagNames }: ReplaceTagReferencesByNameParams) =>
|
||||
this.replaceTagReferencesByName(references, newTagNames, allTags, tagsClient),
|
||||
incomingReferences: options.references,
|
||||
});
|
||||
if (attributesError) {
|
||||
throw Boom.badRequest(`Invalid data. ${attributesError.message}`);
|
||||
}
|
||||
|
@ -226,10 +356,13 @@ export class DashboardStorage {
|
|||
DASHBOARD_SAVED_OBJECT_TYPE,
|
||||
id,
|
||||
soAttributes,
|
||||
optionsToLatest
|
||||
{ ...optionsToLatest, references: soReferences }
|
||||
);
|
||||
|
||||
const { item, error: itemError } = savedObjectToItem(partialSavedObject, true);
|
||||
const { item, error: itemError } = savedObjectToItem(partialSavedObject, true, {
|
||||
getTagNamesFromReferences: (references: SavedObjectReference[]) =>
|
||||
this.getTagNamesFromReferences(references, allTags),
|
||||
});
|
||||
if (itemError) {
|
||||
throw Boom.badRequest(`Invalid response. ${itemError.message}`);
|
||||
}
|
||||
|
@ -278,6 +411,8 @@ export class DashboardStorage {
|
|||
): Promise<DashboardSearchOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
const tagsClient = this.savedObjectsTagging?.createTagClient({ client: soClient });
|
||||
const allTags = (await tagsClient?.getAll()) ?? [];
|
||||
|
||||
// Validate and UP transform the options
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
|
||||
|
@ -291,16 +426,20 @@ export class DashboardStorage {
|
|||
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, {
|
||||
allowedAttributes: soQuery.fields,
|
||||
allowedReferences: optionsToLatest?.includeReferences,
|
||||
});
|
||||
return item;
|
||||
})
|
||||
// Ignore any saved objects that failed to convert to items.
|
||||
.filter((item) => item !== null);
|
||||
const hits = await Promise.all(
|
||||
soResponse.saved_objects
|
||||
.map(async (so) => {
|
||||
const { item } = savedObjectToItem(so, false, {
|
||||
allowedAttributes: soQuery.fields,
|
||||
allowedReferences: optionsToLatest?.includeReferences,
|
||||
getTagNamesFromReferences: (references: SavedObjectReference[]) =>
|
||||
this.getTagNamesFromReferences(references, allTags),
|
||||
});
|
||||
return item;
|
||||
})
|
||||
// Ignore any saved objects that failed to convert to items.
|
||||
.filter((item) => item !== null)
|
||||
);
|
||||
const response = {
|
||||
hits,
|
||||
pagination: {
|
||||
|
|
|
@ -84,11 +84,4 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
},
|
||||
},
|
||||
},
|
||||
mSearch: {
|
||||
out: {
|
||||
result: {
|
||||
schema: dashboardSavedObjectSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -51,18 +51,11 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
...serviceDefinitionV1.update?.in,
|
||||
data: {
|
||||
schema: dashboardAttributesSchema,
|
||||
up: (data: DashboardCrudTypes['UpdateIn']['data']) => attributesTov3(data),
|
||||
up: (data: DashboardCrudTypes['UpdateIn']['data']) => attributesTov3(data, [], () => []),
|
||||
},
|
||||
},
|
||||
},
|
||||
search: {
|
||||
in: serviceDefinitionV1.search?.in,
|
||||
},
|
||||
mSearch: {
|
||||
out: {
|
||||
result: {
|
||||
schema: dashboardSavedObjectSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -333,6 +333,11 @@ export const searchResultsAttributesSchema = schema.object({
|
|||
defaultValue: false,
|
||||
meta: { description: 'Whether to restore time upon viewing this dashboard' },
|
||||
}),
|
||||
tags: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.string({ meta: { description: 'An array of tags applied to this dashboard' } })
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const dashboardAttributesSchema = searchResultsAttributesSchema.extends({
|
||||
|
@ -553,11 +558,4 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
},
|
||||
},
|
||||
},
|
||||
mSearch: {
|
||||
out: {
|
||||
result: {
|
||||
schema: dashboardItemSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ export type {
|
|||
DashboardUpdateOut,
|
||||
DashboardUpdateOptions,
|
||||
DashboardOptions,
|
||||
ReplaceTagReferencesByNameParams,
|
||||
} from './types';
|
||||
export {
|
||||
serviceDefinition,
|
||||
|
@ -37,6 +38,7 @@ export {
|
|||
} from './cm_services';
|
||||
export {
|
||||
dashboardAttributesOut,
|
||||
itemAttrsToSavedObjectAttrs,
|
||||
itemAttrsToSavedObject,
|
||||
itemAttrsToSavedObjectWithTags,
|
||||
savedObjectToItem,
|
||||
} from './transform_utils';
|
||||
|
|
|
@ -8,17 +8,6 @@
|
|||
*/
|
||||
|
||||
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,
|
||||
|
@ -30,6 +19,19 @@ import {
|
|||
ControlGroupChainingSystem,
|
||||
ControlWidth,
|
||||
} from '@kbn/controls-plugin/common';
|
||||
|
||||
import type {
|
||||
DashboardSavedObjectAttributes,
|
||||
SavedDashboardPanel,
|
||||
} from '../../dashboard_saved_object';
|
||||
import type { DashboardAttributes, DashboardItem } from './types';
|
||||
|
||||
import {
|
||||
dashboardAttributesOut,
|
||||
getResultV3ToV2,
|
||||
itemAttrsToSavedObject,
|
||||
savedObjectToItem,
|
||||
} from './transform_utils';
|
||||
import { DEFAULT_DASHBOARD_OPTIONS } from '../../../common/content_management';
|
||||
|
||||
describe('dashboardAttributesOut', () => {
|
||||
|
@ -205,8 +207,8 @@ describe('dashboardAttributesOut', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('itemAttrsToSavedObjectAttrs', () => {
|
||||
it('should transform item attributes to saved object attributes correctly', () => {
|
||||
describe('itemAttrsToSavedObject', () => {
|
||||
it('should transform item attributes to saved object correctly', () => {
|
||||
const input: DashboardAttributes = {
|
||||
controlGroupInput: {
|
||||
chainingSystem: 'NONE',
|
||||
|
@ -250,6 +252,7 @@ describe('itemAttrsToSavedObjectAttrs', () => {
|
|||
version: '2',
|
||||
},
|
||||
],
|
||||
tags: [],
|
||||
timeRestore: true,
|
||||
title: 'title',
|
||||
refreshInterval: { pause: true, value: 1000 },
|
||||
|
@ -257,7 +260,7 @@ describe('itemAttrsToSavedObjectAttrs', () => {
|
|||
timeTo: 'now',
|
||||
};
|
||||
|
||||
const output = itemAttrsToSavedObjectAttrs(input);
|
||||
const output = itemAttrsToSavedObject({ attributes: input });
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
|
@ -284,6 +287,7 @@ describe('itemAttrsToSavedObjectAttrs', () => {
|
|||
"title": "title",
|
||||
},
|
||||
"error": null,
|
||||
"references": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -298,7 +302,7 @@ describe('itemAttrsToSavedObjectAttrs', () => {
|
|||
kibanaSavedObjectMeta: {},
|
||||
};
|
||||
|
||||
const output = itemAttrsToSavedObjectAttrs(input);
|
||||
const output = itemAttrsToSavedObject({ attributes: input });
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
|
@ -312,6 +316,7 @@ describe('itemAttrsToSavedObjectAttrs', () => {
|
|||
"title": "title",
|
||||
},
|
||||
"error": null,
|
||||
"references": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -333,6 +338,13 @@ describe('savedObjectToItem', () => {
|
|||
attributes,
|
||||
};
|
||||
};
|
||||
|
||||
const getTagNamesFromReferences = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should convert saved object to item with all attributes', () => {
|
||||
const input = getSavedObjectForAttributes({
|
||||
title: 'title',
|
||||
|
@ -396,6 +408,51 @@ describe('savedObjectToItem', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should pass references to getTagNamesFromReferences', () => {
|
||||
getTagNamesFromReferences.mockReturnValue(['tag1', 'tag2']);
|
||||
const input = {
|
||||
...getSavedObjectForAttributes({
|
||||
title: 'dashboard with tags',
|
||||
description: 'I have some tags!',
|
||||
timeRestore: true,
|
||||
kibanaSavedObjectMeta: {},
|
||||
panelsJSON: JSON.stringify([]),
|
||||
}),
|
||||
references: [
|
||||
{
|
||||
type: 'tag',
|
||||
id: 'tag1',
|
||||
name: 'tag-ref-tag1',
|
||||
},
|
||||
{
|
||||
type: 'tag',
|
||||
id: 'tag2',
|
||||
name: 'tag-ref-tag2',
|
||||
},
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: 'index-pattern1',
|
||||
name: 'index-pattern-ref-index-pattern1',
|
||||
},
|
||||
],
|
||||
};
|
||||
const { item, error } = savedObjectToItem(input, false, { getTagNamesFromReferences });
|
||||
expect(getTagNamesFromReferences).toHaveBeenCalledWith(input.references);
|
||||
expect(error).toBeNull();
|
||||
expect(item).toEqual({
|
||||
...commonSavedObject,
|
||||
references: [...input.references],
|
||||
attributes: {
|
||||
title: 'dashboard with tags',
|
||||
description: 'I have some tags!',
|
||||
panels: [],
|
||||
timeRestore: true,
|
||||
kibanaSavedObjectMeta: {},
|
||||
tags: ['tag1', 'tag2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing optional attributes', () => {
|
||||
const input = getSavedObjectForAttributes({
|
||||
title: 'title',
|
||||
|
|
|
@ -14,7 +14,9 @@ import type {
|
|||
DashboardAttributes,
|
||||
DashboardGetOut,
|
||||
DashboardItem,
|
||||
ItemAttrsToSavedObjectAttrsReturn,
|
||||
ItemAttrsToSavedObjectParams,
|
||||
ItemAttrsToSavedObjectReturn,
|
||||
ItemAttrsToSavedObjectWithTagsParams,
|
||||
PartialDashboardItem,
|
||||
SavedObjectToItemReturn,
|
||||
} from './types';
|
||||
|
@ -34,7 +36,9 @@ import {
|
|||
} from './transforms';
|
||||
|
||||
export function dashboardAttributesOut(
|
||||
attributes: DashboardSavedObjectAttributes | Partial<DashboardSavedObjectAttributes>
|
||||
attributes: DashboardSavedObjectAttributes | Partial<DashboardSavedObjectAttributes>,
|
||||
references?: SavedObjectReference[],
|
||||
getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[]
|
||||
): DashboardAttributes | Partial<DashboardAttributes> {
|
||||
const {
|
||||
controlGroupInput,
|
||||
|
@ -49,6 +53,13 @@ export function dashboardAttributesOut(
|
|||
title,
|
||||
version,
|
||||
} = attributes;
|
||||
|
||||
// Inject any tag names from references into the attributes
|
||||
let tags: string[] | undefined;
|
||||
if (getTagNamesFromReferences && references && references.length) {
|
||||
tags = getTagNamesFromReferences(references);
|
||||
}
|
||||
|
||||
// try to maintain a consistent (alphabetical) order of keys
|
||||
return {
|
||||
...(controlGroupInput && { controlGroupInput: transformControlGroupOut(controlGroupInput) }),
|
||||
|
@ -61,6 +72,7 @@ export function dashboardAttributesOut(
|
|||
...(refreshInterval && {
|
||||
refreshInterval: { pause: refreshInterval.pause, value: refreshInterval.value },
|
||||
}),
|
||||
...(tags && tags.length && { tags }),
|
||||
...(timeFrom && { timeFrom }),
|
||||
timeRestore: timeRestore ?? false,
|
||||
...(timeTo && { timeTo }),
|
||||
|
@ -112,11 +124,12 @@ export const getResultV3ToV2 = (result: DashboardGetOut): DashboardCrudTypesV2['
|
|||
};
|
||||
};
|
||||
|
||||
export const itemAttrsToSavedObjectAttrs = (
|
||||
attributes: DashboardAttributes
|
||||
): ItemAttrsToSavedObjectAttrsReturn => {
|
||||
export const itemAttrsToSavedObject = ({
|
||||
attributes,
|
||||
incomingReferences = [],
|
||||
}: ItemAttrsToSavedObjectParams): ItemAttrsToSavedObjectReturn => {
|
||||
try {
|
||||
const { controlGroupInput, kibanaSavedObjectMeta, options, panels, ...rest } = attributes;
|
||||
const { controlGroupInput, kibanaSavedObjectMeta, options, panels, tags, ...rest } = attributes;
|
||||
const soAttributes = {
|
||||
...rest,
|
||||
...(controlGroupInput && {
|
||||
|
@ -132,17 +145,34 @@ export const itemAttrsToSavedObjectAttrs = (
|
|||
kibanaSavedObjectMeta: transformSearchSourceIn(kibanaSavedObjectMeta),
|
||||
}),
|
||||
};
|
||||
return { attributes: soAttributes, error: null };
|
||||
return { attributes: soAttributes, references: incomingReferences, error: null };
|
||||
} catch (e) {
|
||||
return { attributes: null, error: e };
|
||||
return { attributes: null, references: null, error: e };
|
||||
}
|
||||
};
|
||||
|
||||
export const itemAttrsToSavedObjectWithTags = async ({
|
||||
attributes,
|
||||
replaceTagReferencesByName,
|
||||
incomingReferences = [],
|
||||
}: ItemAttrsToSavedObjectWithTagsParams): Promise<ItemAttrsToSavedObjectReturn> => {
|
||||
const { tags, ...restAttributes } = attributes;
|
||||
// Tags can be specified as an attribute or in the incomingReferences.
|
||||
const soReferences =
|
||||
replaceTagReferencesByName && tags && tags.length
|
||||
? await replaceTagReferencesByName({ references: incomingReferences, newTagNames: tags })
|
||||
: incomingReferences;
|
||||
return itemAttrsToSavedObject({
|
||||
attributes: restAttributes,
|
||||
incomingReferences: soReferences,
|
||||
});
|
||||
};
|
||||
|
||||
type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
|
||||
references: SavedObjectReference[] | undefined;
|
||||
};
|
||||
|
||||
export interface SavedObjectToItemOptions {
|
||||
interface SavedObjectToItemOptions {
|
||||
/**
|
||||
* attributes to include in the output item
|
||||
*/
|
||||
|
@ -151,6 +181,7 @@ export interface SavedObjectToItemOptions {
|
|||
* references to include in the output item
|
||||
*/
|
||||
allowedReferences?: string[];
|
||||
getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[];
|
||||
}
|
||||
|
||||
export function savedObjectToItem(
|
||||
|
@ -170,7 +201,7 @@ export function savedObjectToItem(
|
|||
| SavedObject<DashboardSavedObjectAttributes>
|
||||
| PartialSavedObject<DashboardSavedObjectAttributes>,
|
||||
partial: boolean /* partial arg is used to enforce the correct savedObject type */,
|
||||
{ allowedAttributes, allowedReferences }: SavedObjectToItemOptions = {}
|
||||
{ allowedAttributes, allowedReferences, getTagNamesFromReferences }: SavedObjectToItemOptions = {}
|
||||
): SavedObjectToItemReturn<DashboardItem | PartialDashboardItem> {
|
||||
const {
|
||||
id,
|
||||
|
@ -189,8 +220,11 @@ export function savedObjectToItem(
|
|||
|
||||
try {
|
||||
const attributesOut = allowedAttributes
|
||||
? pick(dashboardAttributesOut(attributes), allowedAttributes)
|
||||
: dashboardAttributesOut(attributes);
|
||||
? pick(
|
||||
dashboardAttributesOut(attributes, references, getTagNamesFromReferences),
|
||||
allowedAttributes
|
||||
)
|
||||
: dashboardAttributesOut(attributes, references, getTagNamesFromReferences);
|
||||
|
||||
// if includeReferences is provided, only include references of those types
|
||||
const referencesOut = allowedReferences
|
||||
|
|
|
@ -81,12 +81,30 @@ export type SavedObjectToItemReturn<T> =
|
|||
error: Error;
|
||||
};
|
||||
|
||||
export type ItemAttrsToSavedObjectAttrsReturn =
|
||||
export interface ItemAttrsToSavedObjectParams {
|
||||
attributes: DashboardAttributes;
|
||||
incomingReferences?: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export type ItemAttrsToSavedObjectReturn =
|
||||
| {
|
||||
attributes: DashboardSavedObjectAttributes;
|
||||
references: SavedObjectReference[];
|
||||
error: null;
|
||||
}
|
||||
| {
|
||||
attributes: null;
|
||||
references: null;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export interface ItemAttrsToSavedObjectWithTagsParams extends ItemAttrsToSavedObjectParams {
|
||||
replaceTagReferencesByName?: (
|
||||
params: ReplaceTagReferencesByNameParams
|
||||
) => Promise<SavedObjectReference[]>;
|
||||
}
|
||||
|
||||
export interface ReplaceTagReferencesByNameParams {
|
||||
references: SavedObjectReference[];
|
||||
newTagNames: string[];
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { SavedObject, SavedObjectMigrationFn } from '@kbn/core/server';
|
|||
import { extractReferences, injectReferences } from '../../../common';
|
||||
import type { DashboardSavedObjectTypeMigrationsDeps } from './dashboard_saved_object_migrations';
|
||||
import type { DashboardSavedObjectAttributes } from '../schema';
|
||||
import { itemAttrsToSavedObjectAttrs, savedObjectToItem } from '../../content_management/latest';
|
||||
import { itemAttrsToSavedObject, 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
|
||||
|
@ -60,7 +60,9 @@ export function createExtractPanelReferencesMigration(
|
|||
{ embeddablePersistableStateService: deps.embeddable }
|
||||
);
|
||||
|
||||
const { attributes, error: attributesError } = itemAttrsToSavedObjectAttrs(extractedAttributes);
|
||||
const { attributes, error: attributesError } = itemAttrsToSavedObject({
|
||||
attributes: extractedAttributes,
|
||||
});
|
||||
if (attributesError) throw attributesError;
|
||||
|
||||
return {
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ContentManagementServerSetup } from '@kbn/content-management-plugin/ser
|
|||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
|
||||
import { registerContentInsights } from '@kbn/content-management-content-insights-server';
|
||||
|
||||
import type { SavedObjectTaggingStart } from '@kbn/saved-objects-tagging-plugin/server';
|
||||
import {
|
||||
initializeDashboardTelemetryTask,
|
||||
scheduleDashboardTelemetry,
|
||||
|
@ -42,6 +43,7 @@ interface SetupDeps {
|
|||
interface StartDeps {
|
||||
taskManager: TaskManagerStartContract;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
savedObjectsTagging?: SavedObjectTaggingStart;
|
||||
}
|
||||
|
||||
export class DashboardPlugin
|
||||
|
@ -65,17 +67,20 @@ export class DashboardPlugin
|
|||
})
|
||||
);
|
||||
|
||||
const { contentClient } = plugins.contentManagement.register({
|
||||
id: CONTENT_ID,
|
||||
storage: new DashboardStorage({
|
||||
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||
logger: this.logger.get('storage'),
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
void core.getStartServices().then(([_, { savedObjectsTagging }]) => {
|
||||
const { contentClient } = plugins.contentManagement.register({
|
||||
id: CONTENT_ID,
|
||||
storage: new DashboardStorage({
|
||||
throwOnResultValidationError: this.initializerContext.env.mode.dev,
|
||||
logger: this.logger.get('storage'),
|
||||
savedObjectsTagging,
|
||||
}),
|
||||
version: {
|
||||
latest: LATEST_VERSION,
|
||||
},
|
||||
});
|
||||
this.contentClient = contentClient;
|
||||
});
|
||||
this.contentClient = contentClient;
|
||||
|
||||
plugins.contentManagement.favorites.registerFavoriteType('dashboard');
|
||||
|
||||
|
|
|
@ -83,7 +83,8 @@
|
|||
"@kbn/core-rendering-browser",
|
||||
"@kbn/grid-layout",
|
||||
"@kbn/ui-actions-browser",
|
||||
"@kbn/esql-types"
|
||||
"@kbn/esql-types",
|
||||
"@kbn/saved-objects-tagging-plugin"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/tags.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -23,6 +26,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/tags.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
loadTestFile(require.resolve('./validation'));
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { type SavedObjectReference } from '@kbn/core/server';
|
||||
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';
|
||||
|
@ -171,6 +172,153 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response.body.item.attributes.panels).to.be.an('array');
|
||||
});
|
||||
|
||||
describe('create a dashboard with tags', () => {
|
||||
it('with tags specified as an array of names', 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,
|
||||
tags: ['foo'],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'bizz:panel_bizz',
|
||||
type: 'visualization',
|
||||
id: 'my-saved-object',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.tags).to.contain('foo');
|
||||
expect(response.body.item.attributes.tags).to.have.length(1);
|
||||
// adds tag reference to existing references
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-1');
|
||||
expect(referenceIds).to.contain('my-saved-object');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
|
||||
it('creates tags if a saved object matching a tag name is not found', 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,
|
||||
tags: ['foo', 'not-found-tag'],
|
||||
},
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.tags).to.contain('foo', 'not-found-tag');
|
||||
expect(response.body.item.attributes.tags).to.have.length(2);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-1');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
|
||||
it('with tags specified as 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,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: 'tag',
|
||||
id: 'tag-3',
|
||||
name: 'tag-ref-tag-3',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.tags).to.contain('buzz');
|
||||
expect(response.body.item.attributes.tags).to.have.length(1);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-3');
|
||||
expect(response.body.item.references).to.have.length(1);
|
||||
});
|
||||
|
||||
it('with tags specified using both tags array and 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,
|
||||
tags: ['foo'],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: 'tag',
|
||||
id: 'tag-2',
|
||||
name: 'tag-ref-tag-2',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.tags).to.contain('foo');
|
||||
expect(response.body.item.attributes.tags).to.contain('bar');
|
||||
expect(response.body.item.attributes.tags).to.have.length(2);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-1');
|
||||
expect(referenceIds).to.contain('tag-2');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
|
||||
it('with the same tag specified as a reference and a tag name', 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,
|
||||
tags: ['foo', 'buzz'],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: 'tag',
|
||||
id: 'tag-1',
|
||||
name: 'tag-ref-tag-1',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.item.attributes.tags).to.contain('foo');
|
||||
expect(response.body.item.attributes.tags).to.contain('buzz');
|
||||
expect(response.body.item.attributes.tags).to.have.length(2);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-1');
|
||||
expect(referenceIds).to.contain('tag-3');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
});
|
||||
|
||||
// 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()}`;
|
||||
|
|
|
@ -16,6 +16,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/tags.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -23,6 +26,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/tags.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { type SavedObjectReference } from '@kbn/core/server';
|
||||
import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
|
@ -30,5 +31,31 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(response.body.item.attributes.options).to.not.have.keys(['darkTheme']);
|
||||
expect(response.body.item.attributes.refreshInterval).to.not.have.keys(['display']);
|
||||
});
|
||||
|
||||
it('should return 404 with a non-existing dashboard', async () => {
|
||||
const response = await supertest
|
||||
.get(`${PUBLIC_API_PATH}/does-not-exist`)
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send();
|
||||
|
||||
expect(response.status).to.be(404);
|
||||
});
|
||||
|
||||
it('should inject tag names into attributes', async () => {
|
||||
const response = await supertest
|
||||
.get(`${PUBLIC_API_PATH}/8d66658a-f5b7-4482-84dc-f41d317473b8`)
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send();
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expect(response.body.item.attributes.tags).to.contain('bar');
|
||||
expect(response.body.item.attributes.tags).to.contain('buzz');
|
||||
expect(response.body.item.attributes.tags).to.have.length(2);
|
||||
const referenceIds = response.body.item.references.map((ref: SavedObjectReference) => ref.id);
|
||||
expect(referenceIds).to.contain('tag-2');
|
||||
expect(referenceIds).to.contain('tag-3');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,36 +11,18 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
|
|||
|
||||
export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const supertest = getService('supertest');
|
||||
describe('dashboards - list', () => {
|
||||
const createManyDashboards = async (count: number) => {
|
||||
const fileChunks: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = `test-dashboard-${i}`;
|
||||
fileChunks.push(
|
||||
JSON.stringify({
|
||||
type: 'dashboard',
|
||||
id,
|
||||
attributes: {
|
||||
title: `My dashboard (${i})`,
|
||||
kibanaSavedObjectMeta: { searchSourceJSON: '{}' },
|
||||
},
|
||||
references: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await supertest
|
||||
.post(`/api/saved_objects/_import`)
|
||||
.attach('file', Buffer.from(fileChunks.join('\n'), 'utf8'), 'export.ndjson')
|
||||
.expect(200);
|
||||
};
|
||||
before(async () => {
|
||||
await createManyDashboards(100);
|
||||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/many-dashboards.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/many-dashboards.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.total).to.be(100);
|
||||
expect(response.body.items[0].id).to.be('test-dashboard-0');
|
||||
expect(response.body.items[0].id).to.be('test-dashboard-00');
|
||||
expect(response.body.items.length).to.be(20);
|
||||
});
|
||||
|
||||
|
|
|
@ -16,12 +16,18 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
await kibanaServer.importExport.load(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/tags.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/tags.json'
|
||||
);
|
||||
});
|
||||
loadTestFile(require.resolve('./main'));
|
||||
loadTestFile(require.resolve('./validation'));
|
||||
|
|
|
@ -8,9 +8,36 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { type SavedObjectReference } from '@kbn/core/server';
|
||||
import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const updatedDashboard = {
|
||||
attributes: {
|
||||
title: 'Refresh Requests (Updated)',
|
||||
options: { useMargins: false },
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
gridData: { x: 0, y: 0, w: 48, h: 60, i: '1' },
|
||||
panelIndex: '1',
|
||||
panelRefName: 'panel_1',
|
||||
version: '7.3.0',
|
||||
},
|
||||
],
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: '1:panel_1',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
describe('main', () => {
|
||||
|
@ -19,31 +46,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.put(`${PUBLIC_API_PATH}/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
attributes: {
|
||||
title: 'Refresh Requests (Updated)',
|
||||
options: { useMargins: false },
|
||||
panels: [
|
||||
{
|
||||
type: 'visualization',
|
||||
gridData: { x: 0, y: 0, w: 48, h: 60, i: '1' },
|
||||
panelIndex: '1',
|
||||
panelRefName: 'panel_1',
|
||||
version: '7.3.0',
|
||||
},
|
||||
],
|
||||
timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700',
|
||||
timeRestore: true,
|
||||
timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
name: '1:panel_1',
|
||||
type: 'visualization',
|
||||
},
|
||||
],
|
||||
});
|
||||
.send(updatedDashboard);
|
||||
|
||||
expect(response.status).to.be(201);
|
||||
|
||||
|
@ -70,5 +73,107 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
message: 'A dashboard with saved object ID not-an-id was not found.',
|
||||
});
|
||||
});
|
||||
|
||||
describe('update a dashboard with tags', () => {
|
||||
it('adds a tag to the dashboard', async () => {
|
||||
const response = await supertest
|
||||
.put(`${PUBLIC_API_PATH}/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
...updatedDashboard,
|
||||
attributes: {
|
||||
...updatedDashboard.attributes,
|
||||
tags: ['bar'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(201);
|
||||
expect(response.body.item.attributes.tags).to.contain('bar');
|
||||
expect(response.body.item.attributes.tags).to.have.length(1);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-2');
|
||||
expect(referenceIds).to.contain('dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
|
||||
it('replaces the tags on the dashboard', async () => {
|
||||
const response = await supertest
|
||||
.put(`${PUBLIC_API_PATH}/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
...updatedDashboard,
|
||||
attributes: {
|
||||
...updatedDashboard.attributes,
|
||||
tags: ['foo'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(201);
|
||||
expect(response.body.item.attributes.tags).to.contain('foo');
|
||||
expect(response.body.item.attributes.tags).to.have.length(1);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-1');
|
||||
expect(referenceIds).to.contain('dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(response.body.item.references).to.have.length(2);
|
||||
});
|
||||
|
||||
it('empty tags array removes all tags', async () => {
|
||||
const response = await supertest
|
||||
.put(`${PUBLIC_API_PATH}/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
...updatedDashboard,
|
||||
attributes: {
|
||||
...updatedDashboard.attributes,
|
||||
tags: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(201);
|
||||
expect(response.body.item.attributes).not.to.have.property('tags');
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(response.body.item.references).to.have.length(1);
|
||||
});
|
||||
|
||||
it('creates tag if a saved object matching a tag name is not found', async () => {
|
||||
const randomTagName = `tag-${Math.random() * 1000}`;
|
||||
const response = await supertest
|
||||
.put(`${PUBLIC_API_PATH}/be3733a0-9efe-11e7-acb3-3dab96693fab`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.set('ELASTIC_HTTP_VERSION_HEADER', '2023-10-31')
|
||||
.send({
|
||||
...updatedDashboard,
|
||||
attributes: {
|
||||
...updatedDashboard.attributes,
|
||||
tags: ['foo', 'bar', 'buzz', randomTagName],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(201);
|
||||
expect(response.body.item.attributes.tags).to.contain('foo');
|
||||
expect(response.body.item.attributes.tags).to.contain('bar');
|
||||
expect(response.body.item.attributes.tags).to.contain('buzz');
|
||||
expect(response.body.item.attributes.tags).to.contain(randomTagName);
|
||||
expect(response.body.item.attributes.tags).to.have.length(4);
|
||||
const referenceIds = response.body.item.references.map(
|
||||
(ref: SavedObjectReference) => ref.id
|
||||
);
|
||||
expect(referenceIds).to.contain('tag-1');
|
||||
expect(referenceIds).to.contain('tag-2');
|
||||
expect(referenceIds).to.contain('tag-3');
|
||||
expect(referenceIds).to.contain('dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(response.body.item.references).to.have.length(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
"attributes": {
|
||||
"color": "#123456",
|
||||
"description": "Another awesome tag",
|
||||
"name": "bar"
|
||||
},
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"created_at": "2025-03-19T18:17:49.747Z",
|
||||
"id": "tag-2",
|
||||
"managed": false,
|
||||
"references": [],
|
||||
"type": "tag",
|
||||
"typeMigrationVersion": "8.0.0",
|
||||
"updated_at": "2025-03-19T18:17:49.747Z",
|
||||
"version": "WzYxMywxXQ=="
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"color": "#000000",
|
||||
"description": "Last but not least",
|
||||
"name": "buzz"
|
||||
},
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"created_at": "2025-03-19T18:17:49.747Z",
|
||||
"id": "tag-3",
|
||||
"managed": false,
|
||||
"references": [],
|
||||
"type": "tag",
|
||||
"typeMigrationVersion": "8.0.0",
|
||||
"updated_at": "2025-03-19T18:17:49.747Z",
|
||||
"version": "WzYxNCwxXQ=="
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"controlGroupInput": {
|
||||
"chainingSystem": "HIERARCHICAL",
|
||||
"controlStyle": "oneLine",
|
||||
"ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}",
|
||||
"panelsJSON": "{}",
|
||||
"showApplySelections": false
|
||||
},
|
||||
"description": "I have some tags!",
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}"
|
||||
},
|
||||
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
|
||||
"panelsJSON": "[{\"type\":\"visualization\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[]}},\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"# Hello world\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}}},\"panelIndex\":\"b9e32168-d39e-4289-ab07-30cfda9e00d4\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"b9e32168-d39e-4289-ab07-30cfda9e00d4\"}}]",
|
||||
"timeRestore": false,
|
||||
"title": "tagged dashboard",
|
||||
"version": 3
|
||||
},
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"created_at": "2025-03-19T18:18:29.671Z",
|
||||
"id": "8d66658a-f5b7-4482-84dc-f41d317473b8",
|
||||
"managed": false,
|
||||
"references": [
|
||||
{
|
||||
"id": "tag-2",
|
||||
"name": "tag-ref-tag-2",
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"id": "tag-3",
|
||||
"name": "tag-ref-tag-3",
|
||||
"type": "tag"
|
||||
}
|
||||
],
|
||||
"type": "dashboard",
|
||||
"typeMigrationVersion": "10.2.0",
|
||||
"updated_at": "2025-03-19T18:18:58.406Z",
|
||||
"version": "WzQ1MTQxLDFd"
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"color": "#FF00FF",
|
||||
"description": "My first tag!",
|
||||
"name": "foo"
|
||||
},
|
||||
"coreMigrationVersion": "8.8.0",
|
||||
"created_at": "2025-03-19T18:17:49.747Z",
|
||||
"id": "tag-1",
|
||||
"managed": false,
|
||||
"references": [],
|
||||
"type": "tag",
|
||||
"typeMigrationVersion": "8.0.0",
|
||||
"updated_at": "2025-03-19T18:17:49.747Z",
|
||||
"version": "WzYxMiwxXQ=="
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue