UNPKG

@gooddata/gooddata-js

Version:
387 lines (352 loc) • 14.5 kB
// (C) 2020 GoodData Corporation import { MetadataModule } from "./metadata"; import { XhrModule } from "./xhr"; import { UserModule } from "./user"; import cloneDeepWith from "lodash/cloneDeepWith"; import isEmpty from "lodash/isEmpty"; import compact from "lodash/compact"; import omit from "lodash/omit"; import { IKPI, IAnalyticalDashboardContent, DashboardExport, IVisualizationWidget, IAnalyticalDashboard, IObjectMeta, } from "@gooddata/typings"; /** * Modify how and what should be copied to the cloned dashboard */ export interface ICopyDashboardOptions { /** copy new kpi and reference it in the cloned dashboard */ copyKpi?: boolean; /** copy new visualization object and reference it in the cloned widget */ copyVisObj?: boolean; /** optional, default value of name is "Copy of (current dashboard title)" */ name?: string; /** optional, default value of summary is (current dashboard summary) */ summary?: string; /** optional, if true, the isLocked flag will be cleared for the newly created dashboard, defaults to false */ clearLockedFlag?: boolean; } type UriTranslator = (oldUri: string) => string; export function createTranslator( kpiMap: Map<string, string>, visWidgetMap: Map<string, string>, ): UriTranslator { return (oldUri: string): string => { const kpiMatch = kpiMap.get(oldUri); const visWidgetMatch = visWidgetMap.get(oldUri); if (kpiMatch) { return kpiMatch; } else if (visWidgetMatch) { return visWidgetMatch; } else { return oldUri; } }; } /** * Updates content of the dashboard * * @param {string} dashboardUri uri of dashboard * @param {UriTranslator} uriTranslator gets updated widgets and kpis uri * @param {string} filterContext updated filter context uri * @experimental */ export function updateContent( analyticalDashboard: any, uriTranslator: UriTranslator, filterContext: string, ): IAnalyticalDashboardContent { return cloneDeepWith( { ...analyticalDashboard.content, filterContext, widgets: analyticalDashboard.content.widgets.map((uri: string) => { return uriTranslator(uri); }), }, value => { const uri = value.uri; if (!uri) { return; } return { ...value, uri: uriTranslator(uri), }; }, ); } export class MetadataModuleExt { private metadataModule: MetadataModule; private userModule: UserModule; private xhr: XhrModule; constructor(xhr: XhrModule) { this.xhr = xhr; this.metadataModule = new MetadataModule(xhr); this.userModule = new UserModule(xhr); } /** * @param {string} projectId id of the project * @param {string} dashboardUri uri of the dashboard * @param {ICopyDashboardOptions} options object with options: * - default {} dashboard is cloned with new kpi reference and visualization widget is cloned with new * visualization object reference * - copyKpi {boolean} choose whether dashboard is cloned with new Kpi reference * - copyVisObj {boolean} choose whether visualization widget is cloned with new visualization object reference * - name {string} optional - choose name, default value is "Copy of (old title of the dashboard)" * @returns {string} uri of cloned dashboard * @experimental */ public async saveDashboardAs( projectId: string, dashboardUri: string, options: ICopyDashboardOptions, ): Promise<string> { const objectsFromDashboard = await this.getObjectsFromDashboard(projectId, dashboardUri); const dashboardDetails = await this.metadataModule.getObjectDetails(dashboardUri); const { analyticalDashboard }: { analyticalDashboard: IAnalyticalDashboard } = dashboardDetails; const allCreatedObjUris: string[] = []; const visWidgetUris: string[] = []; try { const filterContext = await this.duplicateFilterContext(projectId, objectsFromDashboard, options); allCreatedObjUris.push(filterContext); const kpiMap = await this.duplicateOrKeepKpis(projectId, objectsFromDashboard, options); if (this.shouldCopyKpi(options)) { allCreatedObjUris.push(...Array.from(kpiMap.values())); } const visWidgetMap = await this.duplicateWidgets(projectId, objectsFromDashboard, options); visWidgetUris.push(...Array.from(visWidgetMap.values())); const translator = createTranslator(kpiMap, visWidgetMap); const updatedContent = updateContent(analyticalDashboard, translator, filterContext); const dashboardTitle = this.getDashboardName(analyticalDashboard.meta.title, options.name); const dashboardSummary = this.getDashboardSummary( analyticalDashboard.meta.summary, options.summary, ); const duplicateDashboard = { ...dashboardDetails, analyticalDashboard: { ...dashboardDetails.analyticalDashboard, content: this.getDashboardDetailObject(updatedContent, filterContext), meta: { ...this.getSanitizedMeta(dashboardDetails.analyticalDashboard.meta, options), title: dashboardTitle, summary: dashboardSummary, }, }, }; const duplicateDashboardUri: string = ( await this.metadataModule.createObject(projectId, duplicateDashboard) ).analyticalDashboard.meta.uri; return duplicateDashboardUri; } catch (err) { if (this.shouldCopyVisObj(options)) { await Promise.all(visWidgetUris.map(uri => this.cascadingDelete(projectId, uri))); } else { await Promise.all(visWidgetUris.map(uri => this.metadataModule.deleteObject(uri))); } await Promise.all(allCreatedObjUris.map(uri => this.cascadingDelete(projectId, uri))); return dashboardUri; } } /** * Deletes dashboard and its objects * (only the author of the dashboard can delete the dashboard and its objects) * * @method deleteAllObjects * @param {string} projectId Project identifier * @param {string} dashboardUri Uri of a dashboard to be deleted * @experimental */ public async cascadingDelete(projectID: string, dashboardUri: string): Promise<any> { const objects: any[] = await this.metadataModule.getObjectUsing(projectID, dashboardUri); const currentUser: string = (await this.userModule.getAccountInfo()).profileUri; const objectsToBeDeleted = objects .filter((object: any) => object.author === currentUser) .map((object: any) => { return object.link; }); return this.xhr.post(`/gdc/md/${projectID}/objects/delete`, { body: { delete: { items: [dashboardUri].concat(objectsToBeDeleted), mode: "cascade", }, }, }); } private getDashboardDetailObject( updatedContent: IAnalyticalDashboardContent, filterContext: string, ): IAnalyticalDashboardContent { const { layout } = updatedContent; return { ...updatedContent, filterContext, widgets: [...updatedContent.widgets], ...(isEmpty(layout) ? {} : { layout }), }; } private getDashboardName(originalName: string, newName?: string): string { if (newName !== undefined) { return newName; } return `Copy of ${originalName}`; } private getDashboardSummary(originalSummary?: string, newSummary?: string): string { if (newSummary !== undefined) { return newSummary; } else if (originalSummary !== undefined) { return originalSummary; } return ""; } private async duplicateOrKeepKpis( projectId: string, objsFromDashboard: any[], options: ICopyDashboardOptions, ): Promise<Map<string, string>> { const uriMap: Map<string, string> = new Map(); if (this.shouldCopyKpi(options)) { await Promise.all( objsFromDashboard .filter((obj: any) => this.unwrapObj(obj).meta.category === "kpi") .map(async (kpiWidget: any) => { const { kpi }: { kpi: IKPI } = kpiWidget; const toSave = { kpi: { meta: this.getSanitizedMeta(kpi.meta as IObjectMeta, options), content: { ...kpi.content }, }, }; const newUriKpiObj: string = ( await this.metadataModule.createObject(projectId, toSave) ).kpi.meta.uri; uriMap.set(kpi.meta.uri as string, newUriKpiObj); }), ); } return uriMap; } private async duplicateWidgets( projectId: string, objsFromDashboard: any[], options: ICopyDashboardOptions, ): Promise<Map<string, string>> { const uriMap: Map<string, string> = new Map(); await Promise.all( objsFromDashboard .filter((obj: any) => this.unwrapObj(obj).meta.category === "visualizationWidget") .map(async (visWidget: any) => { return this.createAndUpdateWidgets(projectId, visWidget, options, uriMap); }), ); return uriMap; } private async createAndUpdateWidgets( projectId: string, visWidget: any, options: ICopyDashboardOptions, uriMap: Map<string, string>, ): Promise<void> { const { visualizationWidget } = visWidget; if (this.shouldCopyVisObj(options)) { const visObj = await this.metadataModule.getObjectDetails( visualizationWidget.content.visualization, ); const toSave = { visualizationObject: { meta: this.getSanitizedMeta(visObj.visualizationObject.meta, options), content: { ...visObj.visualizationObject.content }, }, }; const newUriVisObj = (await this.metadataModule.createObject(projectId, toSave)) .visualizationObject.meta.uri; const updatedVisWidget = { ...visWidget, visualizationWidget: { meta: this.getSanitizedMeta(visWidget.visualizationWidget.meta, options), content: { ...visWidget.visualizationWidget.content, visualization: newUriVisObj, }, }, }; const visUri = (await this.metadataModule.createObject(projectId, updatedVisWidget)) .visualizationWidget.meta.uri; uriMap.set(visualizationWidget.meta.uri, visUri); } else { const updatedVisWidget = { ...visWidget, visualizationWidget: { meta: this.getSanitizedMeta(visWidget.visualizationWidget.meta, options), content: { ...visWidget.visualizationWidget.content }, }, }; const { visualizationWidget } = await this.metadataModule.createObject( projectId, updatedVisWidget, ); uriMap.set(visWidget.visualizationWidget.meta.uri, visualizationWidget.meta.uri); } } private async duplicateFilterContext( projectId: string, objsFromDashboard: any, options: ICopyDashboardOptions, ): Promise<string> { const originalFilterContext = objsFromDashboard.filter( (obj: any) => this.unwrapObj(obj).meta.category === "filterContext", )[0]; const toSave = { filterContext: { meta: this.getSanitizedMeta(originalFilterContext.filterContext.meta, options), content: { ...originalFilterContext.filterContext.content }, }, }; const { filterContext } = await this.metadataModule.createObject(projectId, toSave); return filterContext.meta.uri; } private getSanitizedMeta(originalMeta: IObjectMeta, options: ICopyDashboardOptions): IObjectMeta { return omit( originalMeta, compact([ "identifier", "uri", "author", "created", "updated", "contributor", options && options.clearLockedFlag && "locked", ]), ) as IObjectMeta; } private async getObjectsFromDashboard( projectId: string, dashboardUri: string, ): Promise<Array<IKPI | DashboardExport.IFilterContext | IVisualizationWidget>> { const uris = await this.getObjectsUrisInDashboard(projectId, dashboardUri); return this.metadataModule.getObjects(projectId, uris); } private async getObjectsUrisInDashboard(projectId: string, dashboardUri: string): Promise<string[]> { return ( await this.metadataModule.getObjectUsing(projectId, dashboardUri, { types: ["kpi", "visualizationWidget", "filterContext"], }) ).map((obj: any) => { return obj.link; }); } private unwrapObj(obj: any): any { return obj[Object.keys(obj)[0]]; } private shouldCopyVisObj(options: ICopyDashboardOptions): boolean { return !!(options.copyVisObj || typeof options.copyVisObj === "undefined"); } private shouldCopyKpi(options: ICopyDashboardOptions): boolean { return !!(options.copyKpi || typeof options.copyKpi === "undefined"); } }