mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Dashboard] Inject / extract tag references (#214788)
Fixes #210619 ## Summary Provides a tags array on the request and response bodies of dashboards. This allows consumers of the Dashboards HTTP API and internal RPC API to add an array of tag names to the attributes in the body of create and update endpoints. The dashboard server will be responsible for converting the tag names into references in the saved object. If, during creation or update, a tag name does not have a matching tag saved object, a new tag saved object will be created. If the user lacks permissions to manage tags, then an error will be logged in the server and the tag will not be added to the dashboard. The server also injects the tag references as an array of tag names in the attributes of the response body of get and search endpoints of the HTTP and RPC APIs. For backwards compatibility in create and update endpoints, tags can alternatively be specified in the `references` array in the options instead of (or in addition to) the `attributes.tags` in the request body. Similarly, for backwards compatibility, tag references are returned in the `references` of the response body of get and search endpoints. Client-side tag handling is out of scope for this PR. Dashboards listing page and dashboard settings continue to use the tag references and do not use the `tags` attribute in the response. For example: Here's how we currently create a dashboard with tag references. ``` ## Creating a dashboard with tag references POST kbn:/api/dashboards/dashboard { "attributes": { "title": "my tagged dashboard" }, "references": [ { "type": "tag", "id": "37aab5de-a34d-47cb-9aa5-9375d5db595f", "name": "tag-ref-37aab5de-a34d-47cb-9aa5-9375d5db595f" }, { "type": "tag", "id": "5ed29bba-c14d-4302-9a8c-9602e40dbc2a", "name": "tag-ref-5ed29bba-c14d-4302-9a8c-9602e40dbc2a" }, { "type": "tag", "id": "fc7890e8-c00f-44a1-88a2-250e4d27e61d", "name": "tag-ref-fc7890e8-c00f-44a1-88a2-250e4d27e61d" } ] } ``` With this PR, creating a dashboard with tags is much simpler. ``` ## Creating a dashboard with tag names POST kbn:/api/dashboards/dashboard { "attributes": { "title": "my tagged dashboard", "tags": [ "boo", "foo", "bingo", "bongo" ] } } ``` ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] If there are more than one tag saved objects with the same name, only one of the tag references will be added to the saved object when creating a dashboard. Creating tags with duplicate names are not permitted via the UI. But there is no such restrictions when creating tags from imported saved objects. Having multiple tags with the same name is an edge case that Kibana guards against with reasonable restrictions, so I think we should not be too concerned about it. - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8728a23282
commit
c4f7c649b1
26 changed files with 2893 additions and 135 deletions
|
@ -6738,6 +6738,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",
|
||||
|
@ -7384,6 +7391,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"
|
||||
|
@ -8038,6 +8052,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"
|
||||
|
@ -8576,6 +8597,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"
|
||||
|
@ -9202,6 +9230,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"
|
||||
|
@ -9734,6 +9769,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"
|
||||
|
|
|
@ -6738,6 +6738,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",
|
||||
|
@ -7384,6 +7391,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"
|
||||
|
@ -8038,6 +8052,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"
|
||||
|
@ -8576,6 +8597,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"
|
||||
|
@ -9202,6 +9230,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"
|
||||
|
@ -9734,6 +9769,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"
|
||||
|
|
|
@ -6168,6 +6168,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
|
||||
|
@ -6628,6 +6633,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
|
||||
|
@ -7093,6 +7103,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
|
||||
|
@ -7481,6 +7496,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
|
||||
|
@ -7926,6 +7946,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
|
||||
|
@ -8310,6 +8335,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
|
||||
|
|
|
@ -7703,6 +7703,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
|
||||
|
@ -8163,6 +8168,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
|
||||
|
@ -8628,6 +8638,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
|
||||
|
@ -9016,6 +9031,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
|
||||
|
@ -9461,6 +9481,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
|
||||
|
@ -9845,6 +9870,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