Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,10 +32,6 @@ import type {
} from './latest';
import type { DashboardCreateOut, DashboardSearchOut, DashboardUpdateOut } from './v1/types';

const getRandomColor = (): string => {
return '#' + String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0');
};

const searchArgsToSOFindOptions = (
query: SearchQuery,
options: DashboardSearchOptions
Expand Down Expand Up @@ -69,117 +62,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<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,
Expand All @@ -188,10 +84,7 @@ export class DashboardStorage {
outcome,
} = await soClient.resolve<DashboardSavedObjectAttributes>(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}`);
}
Expand Down Expand Up @@ -237,8 +130,6 @@ 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<
Expand All @@ -261,10 +152,8 @@ export class DashboardStorage {
attributes: soAttributes,
references: soReferences,
error: transformDashboardError,
} = await transformDashboardIn({
} = transformDashboardIn({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully we can keep this synchronous from now on!

dashboardState: dataToLatest,
replaceTagReferencesByName: ({ references, newTagNames }) =>
this.replaceTagReferencesByName(references, newTagNames, allTags, tagsClient),
incomingReferences: options.references,
});
if (transformDashboardError) {
Expand All @@ -278,10 +167,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}`);
Expand Down Expand Up @@ -320,8 +206,6 @@ 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<
Expand All @@ -344,10 +228,8 @@ 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,
});
if (transformDashboardError) {
Expand All @@ -362,10 +244,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}`);
}
Expand Down Expand Up @@ -415,8 +294,6 @@ 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<
Expand All @@ -435,8 +312,6 @@ export class DashboardStorage {
const { item } = savedObjectToItem(so, false, {
allowedAttributes: soQuery.fields,
allowedReferences: optionsToLatest?.includeReferences,
getTagNamesFromReferences: (references: SavedObjectReference[]) =>
this.getTagNamesFromReferences(references, allTags),
});
return item;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export const searchResultsAttributes = {
}),
tags: schema.maybe(
schema.arrayOf(
schema.string({ meta: { description: 'An array of tags applied to this dashboard' } })
schema.string({ meta: { description: 'An array of tags ids applied to this dashboard' } })
)
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ describe('savedObjectToItem', () => {
};
};

const getTagNamesFromReferences = jest.fn();

beforeEach(() => {
jest.resetAllMocks();
});
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export function savedObjectToItem(
{
allowedAttributes,
allowedReferences,
getTagNamesFromReferences,
}: {
/**
* attributes to include in the output item
Expand All @@ -46,7 +45,6 @@ export function savedObjectToItem(
* references to include in the output item
*/
allowedReferences?: string[];
getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[];
} = {}
): SavedObjectToItemReturn<DashboardItem | PartialDashboardItem> {
const {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -65,7 +65,7 @@ describe('transformDashboardIn', () => {
timeTo: 'now',
};

const output = await transformDashboardIn({ dashboardState });
const output = transformDashboardIn({ dashboardState });
expect(output).toMatchInlineSnapshot(`
Object {
"attributes": Object {
Expand Down Expand Up @@ -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',
Expand All @@ -107,7 +107,7 @@ describe('transformDashboardIn', () => {
kibanaSavedObjectMeta: {},
};

const output = await transformDashboardIn({ dashboardState });
const output = transformDashboardIn({ dashboardState });
expect(output).toMatchInlineSnapshot(`
Object {
"attributes": Object {
Expand Down
Loading