From babc32ad41bf9077394000fcc81113b1e3ae05bb Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 29 Aug 2025 14:53:25 -0600 Subject: [PATCH 1/7] [dashboard] tag ids --- .../content_management/dashboard_storage.ts | 139 +----------------- .../content_management/v1/cm_services.ts | 2 +- .../v1/transform_utils.test.ts | 47 ------ .../content_management/v1/transform_utils.ts | 9 +- .../in/transform_dashboard_in.test.ts | 8 +- .../transforms/in/transform_dashboard_in.ts | 33 ++--- .../transforms/in/transform_tags_in.test.ts | 50 +++++++ .../v1/transforms/in/transform_tags_in.ts | 34 +++++ .../out/transform_dashboard_out.test.ts | 20 ++- .../transforms/out/transform_dashboard_out.ts | 13 +- .../plugins/shared/dashboard/server/plugin.ts | 23 ++- 11 files changed, 144 insertions(+), 234 deletions(-) create mode 100644 src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts create mode 100644 src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.ts diff --git a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts index 58752f212f698..f30d1ba78f49e 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts @@ -18,9 +18,6 @@ import type { SearchQuery, } from '@kbn/content-management-plugin/common'; import type { 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 type { DashboardSavedObjectAttributes } from '../dashboard_saved_object'; @@ -37,10 +34,6 @@ import type { DashboardSearchOptions, } from './latest'; -const getRandomColor = (): string => { - return '#' + String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0'); -}; - const searchArgsToSOFindOptions = ( query: SearchQuery, options: DashboardSearchOptions @@ -71,117 +64,20 @@ 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, - allTags: Tag[], - tagsClient?: ITagsClient - ): Promise { - const combinedTagNames = await this.createTagsIfNeeded(tagNames, allTags, tagsClient); - - return Array.from(combinedTagNames).flatMap( - (tagName) => this.savedObjectsTagging?.convertTagNameToId(tagName, allTags) ?? [] - ); - } - - private async createTagsIfNeeded( - tagNames: Set, - 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 => 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 { 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, @@ -190,10 +86,7 @@ export class DashboardStorage { outcome, } = await soClient.resolve(DASHBOARD_SAVED_OBJECT_TYPE, id); - const { item, error: itemError } = savedObjectToItem(savedObject, false, { - getTagNamesFromReferences: (references: SavedObjectReference[]) => - this.getTagNamesFromReferences(references, allTags), - }); + const { item, error: itemError } = savedObjectToItem(savedObject, false); if (itemError) { throw Boom.badRequest(`Invalid response. ${itemError.message}`); } @@ -239,8 +132,6 @@ export class DashboardStorage { ): Promise { 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< @@ -263,11 +154,9 @@ export class DashboardStorage { attributes: soAttributes, references: soReferences, error: transformDashboardError, - } = await transformDashboardIn({ + } = transformDashboardIn({ dashboardState: dataToLatest, - replaceTagReferencesByName: ({ references, newTagNames }) => - this.replaceTagReferencesByName(references, newTagNames, allTags, tagsClient), - incomingReferences: options.references, + references: options.references, }); if (transformDashboardError) { throw Boom.badRequest(`Invalid data. ${transformDashboardError.message}`); @@ -280,10 +169,7 @@ export class DashboardStorage { { ...optionsToLatest, references: soReferences } ); - const { item, error: itemError } = savedObjectToItem(savedObject, false, { - getTagNamesFromReferences: (references: SavedObjectReference[]) => - this.getTagNamesFromReferences(references, allTags), - }); + const { item, error: itemError } = savedObjectToItem(savedObject, false); if (itemError) { throw Boom.badRequest(`Invalid response. ${itemError.message}`); } @@ -322,8 +208,6 @@ export class DashboardStorage { ): Promise { 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< @@ -346,11 +230,9 @@ export class DashboardStorage { attributes: soAttributes, references: soReferences, error: transformDashboardError, - } = await transformDashboardIn({ + } = transformDashboardIn({ dashboardState: dataToLatest, - replaceTagReferencesByName: ({ references, newTagNames }) => - this.replaceTagReferencesByName(references, newTagNames, allTags, tagsClient), - incomingReferences: options.references, + references: options.references, }); if (transformDashboardError) { throw Boom.badRequest(`Invalid data. ${transformDashboardError.message}`); @@ -364,10 +246,7 @@ export class DashboardStorage { { ...optionsToLatest, references: soReferences } ); - const { item, error: itemError } = savedObjectToItem(partialSavedObject, true, { - getTagNamesFromReferences: (references: SavedObjectReference[]) => - this.getTagNamesFromReferences(references, allTags), - }); + const { item, error: itemError } = savedObjectToItem(partialSavedObject, true); if (itemError) { throw Boom.badRequest(`Invalid response. ${itemError.message}`); } @@ -417,8 +296,6 @@ export class DashboardStorage { ): Promise { 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< @@ -437,8 +314,6 @@ export class DashboardStorage { const { item } = savedObjectToItem(so, false, { allowedAttributes: soQuery.fields, allowedReferences: optionsToLatest?.includeReferences, - getTagNamesFromReferences: (references: SavedObjectReference[]) => - this.getTagNamesFromReferences(references, allTags), }); return item; }) diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts index 677c41258e6f4..4b0f42b12b23a 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts @@ -231,7 +231,7 @@ export const searchResultsAttributesSchema = schema.object({ }), tags: schema.maybe( schema.arrayOf( - schema.string({ meta: { description: 'An array of tags applied to this dashboard' } }) + schema.string({ meta: { description: 'An array of tag ids applied to this dashboard' } }) ) ), }); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts index b28caed78d5d9..056e801a7f44a 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts @@ -32,8 +32,6 @@ describe('savedObjectToItem', () => { }; }; - const getTagNamesFromReferences = jest.fn(); - beforeEach(() => { jest.resetAllMocks(); }); @@ -101,51 +99,6 @@ 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', diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.ts index 05ced98c112f7..4beb3e2b2ee74 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.ts @@ -36,7 +36,6 @@ export function savedObjectToItem( { allowedAttributes, allowedReferences, - getTagNamesFromReferences, }: { /** * attributes to include in the output item @@ -46,7 +45,6 @@ export function savedObjectToItem( * references to include in the output item */ allowedReferences?: string[]; - getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[]; } = {} ): SavedObjectToItemReturn { const { @@ -63,11 +61,8 @@ export function savedObjectToItem( managed, } = savedObject; try { - const dashboardState = transformDashboardOut( - attributes, - savedObject.references, - getTagNamesFromReferences - ); + const dashboardState = transformDashboardOut(attributes, savedObject.references); + const references = transformReferencesOut(savedObject.references ?? []); return { diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts index 83ade34871a8d..32c23ebcbb3e3 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts @@ -12,7 +12,7 @@ import type { DashboardAttributes } from '../../types'; import { transformDashboardIn } from './transform_dashboard_in'; describe('transformDashboardIn', () => { - test('should transform dashboard state to saved object', async () => { + test('should transform dashboard state to saved object', () => { const dashboardState: DashboardAttributes = { controlGroupInput: { chainingSystem: 'NONE', @@ -65,7 +65,7 @@ describe('transformDashboardIn', () => { timeTo: 'now', }; - const output = await transformDashboardIn({ dashboardState }); + const output = transformDashboardIn({ dashboardState }); expect(output).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -97,7 +97,7 @@ describe('transformDashboardIn', () => { `); }); - it('should handle missing optional state keys', async () => { + it('should handle missing optional state keys', () => { const dashboardState: DashboardAttributes = { title: 'title', description: 'my description', @@ -107,7 +107,7 @@ describe('transformDashboardIn', () => { kibanaSavedObjectMeta: {}, }; - const output = await transformDashboardIn({ dashboardState }); + const output = transformDashboardIn({ dashboardState }); expect(output).toMatchInlineSnapshot(` Object { "attributes": Object { diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts index 6ff48690b7e48..7f233e93f2a0c 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts @@ -13,22 +13,15 @@ import type { DashboardSavedObjectAttributes } from '../../../../dashboard_saved import { transformPanelsIn } from './transform_panels_in'; import { transformControlGroupIn } from './transform_control_group_in'; import { transformSearchSourceIn } from './transform_search_source_in'; +import { transformTagsIn } from './transform_tags_in'; -export const transformDashboardIn = async ({ +export const transformDashboardIn = ({ dashboardState, - replaceTagReferencesByName, - incomingReferences = [], + references = [], }: { dashboardState: DashboardAttributes; - incomingReferences?: SavedObjectReference[]; - replaceTagReferencesByName?: ({ - references, - newTagNames, - }: { - references: SavedObjectReference[]; - newTagNames: string[]; - }) => Promise; -}): Promise< + references?: SavedObjectReference[]; +}): | { attributes: DashboardSavedObjectAttributes; references: SavedObjectReference[]; @@ -38,19 +31,15 @@ export const transformDashboardIn = async ({ attributes: null; references: null; error: Error; - } -> => { + } => { try { - const tagReferences = - replaceTagReferencesByName && dashboardState.tags && dashboardState.tags.length - ? await replaceTagReferencesByName({ - references: incomingReferences, - newTagNames: dashboardState.tags, - }) - : incomingReferences; - const { controlGroupInput, kibanaSavedObjectMeta, options, panels, tags, ...rest } = dashboardState; + + const tagReferences = transformTagsIn({ + tags, + references, + }); const { panelsJSON, sections, references: panelReferences } = transformPanelsIn(panels); const { searchSourceJSON, references: searchSourceReferences } = diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts new file mode 100644 index 0000000000000..bb9ffd069168b --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { transformTagsIn } from './transform_tags_in'; + +describe('transformTagsIn', () => { + test('Should merge tags from attributes and references', () => { + const tagRefs = transformTagsIn({ + tags: ['tag1', 'tag2'], + references: [ + { + id: 'tag2', + type: 'tag', + name: 'tag-ref-tag2', + }, + { + id: 'tag3', + type: 'tag', + name: 'tag-ref-tag3', + }, + ], + }); + + expect(tagRefs).toMatchInlineSnapshot(` + Array [ + Object { + "id": "tag2", + "name": "tag-ref-tag2", + "type": "tag", + }, + Object { + "id": "tag3", + "name": "tag-ref-tag3", + "type": "tag", + }, + Object { + "id": "tag1", + "name": "tag-ref-tag1", + "type": "tag", + }, + ] + `); + }); +}); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.ts new file mode 100644 index 0000000000000..ab133e8bef18e --- /dev/null +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; +import { tagSavedObjectTypeName } from '@kbn/saved-objects-tagging-plugin/common'; +import type { DashboardAttributes } from '../../types'; + +export function transformTagsIn({ + tags, + references, +}: { + tags: DashboardAttributes['tags']; + references?: SavedObjectReference[]; +}) { + const uniqueTagIds = new Set([]); + (references ?? []).forEach(({ type, id }) => { + if (type === tagSavedObjectTypeName) uniqueTagIds.add(id); + }); + (tags ?? []).forEach((tagId) => { + uniqueTagIds.add(tagId); + }); + + return Array.from(uniqueTagIds).map((tagId) => ({ + type: tagSavedObjectTypeName, + id: tagId, + name: `tag-ref-${tagId}`, + })); +} diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts index 08407d27cd37e..cc5cdae3c16db 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts @@ -129,7 +129,24 @@ describe('transformDashboardOut', () => { timeTo: 'now', title: 'title', }; - expect(transformDashboardOut(input)).toEqual({ + const 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', + }, + ]; + expect(transformDashboardOut(input, references)).toEqual({ controlGroupInput: { chainingSystem: 'NONE', labelPosition: 'twoLine', @@ -187,6 +204,7 @@ describe('transformDashboardOut', () => { pause: true, value: 1000, }, + tags: ['tag1', 'tag2'], timeFrom: 'now-15m', timeRestore: true, timeTo: 'now', diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.ts index 80015c15b9495..4e467bb92acd7 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.ts @@ -8,6 +8,7 @@ */ import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; +import { tagSavedObjectTypeName } from '@kbn/saved-objects-tagging-plugin/common'; import type { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object'; import type { DashboardAttributes } from '../../types'; import { transformControlGroupOut } from './transform_control_group_out'; @@ -17,8 +18,7 @@ import { transformPanelsOut } from './transform_panels_out'; export function transformDashboardOut( attributes: DashboardSavedObjectAttributes | Partial, - references?: SavedObjectReference[], - getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[] + references?: SavedObjectReference[] ): DashboardAttributes | Partial { const { controlGroupInput, @@ -34,11 +34,10 @@ export function transformDashboardOut( title, version, } = attributes; - // Inject any tag names from references into the attributes - let tags: string[] | undefined; - if (getTagNamesFromReferences && references && references.length) { - tags = getTagNamesFromReferences(references); - } + // Extract tag references + const tags: string[] = references + ? references.filter(({ type }) => type === tagSavedObjectTypeName).map(({ id }) => id) + : []; // try to maintain a consistent (alphabetical) order of keys return { diff --git a/src/platform/plugins/shared/dashboard/server/plugin.ts b/src/platform/plugins/shared/dashboard/server/plugin.ts index dd43b19cabe76..a69ee6f70114e 100644 --- a/src/platform/plugins/shared/dashboard/server/plugin.ts +++ b/src/platform/plugins/shared/dashboard/server/plugin.ts @@ -81,20 +81,17 @@ export class DashboardPlugin }) ); - 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; + 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, + }, }); + this.contentClient = contentClient; plugins.contentManagement.favorites.registerFavoriteType('dashboard'); From ccf2e4b4f1ba0bc732f929580a7f8eb58585aeec Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 29 Aug 2025 14:59:42 -0600 Subject: [PATCH 2/7] add more test cases --- .../transforms/in/transform_dashboard_in.ts | 2 +- .../transforms/in/transform_tags_in.test.ts | 53 +++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts index 7f233e93f2a0c..c9d7c861d9122 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts @@ -21,7 +21,7 @@ export const transformDashboardIn = ({ }: { dashboardState: DashboardAttributes; references?: SavedObjectReference[]; -}): +}): | { attributes: DashboardSavedObjectAttributes; references: SavedObjectReference[]; diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts index bb9ffd069168b..6dc913b48b34a 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_tags_in.test.ts @@ -12,18 +12,13 @@ import { transformTagsIn } from './transform_tags_in'; describe('transformTagsIn', () => { test('Should merge tags from attributes and references', () => { const tagRefs = transformTagsIn({ - tags: ['tag1', 'tag2'], + tags: ['tag1'], references: [ { id: 'tag2', type: 'tag', name: 'tag-ref-tag2', }, - { - id: 'tag3', - type: 'tag', - name: 'tag-ref-tag3', - }, ], }); @@ -35,16 +30,54 @@ describe('transformTagsIn', () => { "type": "tag", }, Object { - "id": "tag3", - "name": "tag-ref-tag3", + "id": "tag1", + "name": "tag-ref-tag1", "type": "tag", }, + ] + `); + }); + + test('Should exclude duplicate tags', () => { + const tagRefs = transformTagsIn({ + tags: ['tag2', 'tag2'], + references: [ + { + id: 'tag2', + type: 'tag', + name: 'tag-ref-tag2', + }, + { + id: 'tag2', + type: 'tag', + name: 'tag-ref-tag2', + }, + ], + }); + + expect(tagRefs).toMatchInlineSnapshot(` + Array [ Object { - "id": "tag1", - "name": "tag-ref-tag1", + "id": "tag2", + "name": "tag-ref-tag2", "type": "tag", }, ] `); }); + + test('Should exclude non-tag references', () => { + const tagRefs = transformTagsIn({ + tags: [], + references: [ + { + id: 'dataView1', + type: 'index-pattern', + name: 'ref-dataView1', + }, + ], + }); + + expect(tagRefs.length).toBe(0); + }); }); From 0e0a5ed5933f66a0b8fa10f7cf00cd8fc2d7ee8d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 1 Sep 2025 08:44:46 -0600 Subject: [PATCH 3/7] remove tags integration test --- .../apis/dashboards/create_dashboard/main.ts | 148 ------------------ 1 file changed, 148 deletions(-) diff --git a/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts b/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts index 02fca25efb467..2d90cb3c89334 100644 --- a/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts +++ b/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts @@ -8,7 +8,6 @@ */ 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-constants'; import type { FtrProviderContext } from '../../../ftr_provider_context'; @@ -171,153 +170,6 @@ 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()}`; From 2df093818549c76dfdca1e693f0deccecb5689ac Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 2 Sep 2025 10:27:20 -0600 Subject: [PATCH 4/7] avoid dropping references --- .../content_management/dashboard_storage.ts | 4 ++-- .../transforms/in/transform_dashboard_in.ts | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts index f30d1ba78f49e..9924a54b78260 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts @@ -156,7 +156,7 @@ export class DashboardStorage { error: transformDashboardError, } = transformDashboardIn({ dashboardState: dataToLatest, - references: options.references, + incomingReferences: options.references, }); if (transformDashboardError) { throw Boom.badRequest(`Invalid data. ${transformDashboardError.message}`); @@ -232,7 +232,7 @@ export class DashboardStorage { error: transformDashboardError, } = transformDashboardIn({ dashboardState: dataToLatest, - references: options.references, + incomingReferences: options.references, }); if (transformDashboardError) { throw Boom.badRequest(`Invalid data. ${transformDashboardError.message}`); diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts index c9d7c861d9122..d5e7b881b2f45 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.ts @@ -8,6 +8,7 @@ */ import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; +import { tagSavedObjectTypeName } from '@kbn/saved-objects-tagging-plugin/common'; import type { DashboardAttributes } from '../../types'; import type { DashboardSavedObjectAttributes } from '../../../../dashboard_saved_object'; import { transformPanelsIn } from './transform_panels_in'; @@ -17,10 +18,10 @@ import { transformTagsIn } from './transform_tags_in'; export const transformDashboardIn = ({ dashboardState, - references = [], + incomingReferences = [], }: { dashboardState: DashboardAttributes; - references?: SavedObjectReference[]; + incomingReferences?: SavedObjectReference[]; }): | { attributes: DashboardSavedObjectAttributes; @@ -38,8 +39,14 @@ export const transformDashboardIn = ({ const tagReferences = transformTagsIn({ tags, - references, + references: incomingReferences, }); + + // TODO - remove once all references are provided server side + const nonTagIncomingReferences = incomingReferences.filter( + ({ type }) => type !== tagSavedObjectTypeName + ); + const { panelsJSON, sections, references: panelReferences } = transformPanelsIn(panels); const { searchSourceJSON, references: searchSourceReferences } = @@ -63,7 +70,12 @@ export const transformDashboardIn = ({ }; return { attributes, - references: [...tagReferences, ...panelReferences, ...searchSourceReferences], + references: [ + ...tagReferences, + ...nonTagIncomingReferences, + ...panelReferences, + ...searchSourceReferences, + ], error: null, }; } catch (e) { From 359131acaf98bb3700afab49fb4c1480b2b059fc Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 2 Sep 2025 11:56:52 -0600 Subject: [PATCH 5/7] remove inject tag names test --- .../apis/dashboards/get_dashboard/main.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/platform/test/api_integration/apis/dashboards/get_dashboard/main.ts b/src/platform/test/api_integration/apis/dashboards/get_dashboard/main.ts index 0de3c07328701..57909e776446a 100644 --- a/src/platform/test/api_integration/apis/dashboards/get_dashboard/main.ts +++ b/src/platform/test/api_integration/apis/dashboards/get_dashboard/main.ts @@ -8,7 +8,6 @@ */ import expect from '@kbn/expect'; -import { type SavedObjectReference } from '@kbn/core/server'; import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server'; import type { FtrProviderContext } from '../../../ftr_provider_context'; @@ -40,22 +39,5 @@ export default function ({ getService }: FtrProviderContext) { 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); - }); }); } From 36c049e8e7cc2331f288ef1fd3e8667dee7236bb Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 2 Sep 2025 13:42:57 -0600 Subject: [PATCH 6/7] remove update tags test --- .../apis/dashboards/update_dashboard/main.ts | 103 ------------------ 1 file changed, 103 deletions(-) diff --git a/src/platform/test/api_integration/apis/dashboards/update_dashboard/main.ts b/src/platform/test/api_integration/apis/dashboards/update_dashboard/main.ts index 07db310024fbe..217ef5d91cf2a 100644 --- a/src/platform/test/api_integration/apis/dashboards/update_dashboard/main.ts +++ b/src/platform/test/api_integration/apis/dashboards/update_dashboard/main.ts @@ -8,7 +8,6 @@ */ import expect from '@kbn/expect'; -import { type SavedObjectReference } from '@kbn/core/server'; import { PUBLIC_API_PATH } from '@kbn/dashboard-plugin/server'; import type { FtrProviderContext } from '../../../ftr_provider_context'; @@ -72,107 +71,5 @@ 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(200); - 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(200); - 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(200); - 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(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.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); - }); - }); }); } From b4c45441004dac2338fc7484a35c837b839fdf6f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:43:23 +0000 Subject: [PATCH 7/7] [CI] Auto-commit changed files from 'node scripts/eslint_all_files --no-cache --fix' --- .../dashboard/server/content_management/v1/cm_services.ts | 2 +- src/platform/plugins/shared/dashboard/server/plugin.ts | 2 +- .../api_integration/apis/dashboards/create_dashboard/main.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts index b80201b79ca28..62769f75ee4a3 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts @@ -58,4 +58,4 @@ export const serviceDefinition: ServicesDefinition = { }, }, }, -}; \ No newline at end of file +}; diff --git a/src/platform/plugins/shared/dashboard/server/plugin.ts b/src/platform/plugins/shared/dashboard/server/plugin.ts index 5cf85923a4d2d..6e42d6471da06 100644 --- a/src/platform/plugins/shared/dashboard/server/plugin.ts +++ b/src/platform/plugins/shared/dashboard/server/plugin.ts @@ -179,4 +179,4 @@ export class DashboardPlugin } public stop() {} -} \ No newline at end of file +} diff --git a/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts b/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts index f0103de064b35..127ac7b68eecc 100644 --- a/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts +++ b/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts @@ -207,4 +207,4 @@ export default function ({ getService }: FtrProviderContext) { ); }); }); -} \ No newline at end of file +}