diff --git a/libs/sdk-backend-base/api/sdk-backend-base.api.md b/libs/sdk-backend-base/api/sdk-backend-base.api.md index 93ba56b4fc6..646043fdf71 100644 --- a/libs/sdk-backend-base/api/sdk-backend-base.api.md +++ b/libs/sdk-backend-base/api/sdk-backend-base.api.md @@ -99,6 +99,7 @@ import { IWidgetAlertCount } from '@gooddata/sdk-backend-spi'; import { IWidgetAlertDefinition } from '@gooddata/sdk-model'; import { IWidgetReferences } from '@gooddata/sdk-backend-spi'; import { IWorkspaceAttributesService } from '@gooddata/sdk-backend-spi'; +import { IWorkspaceAutomationService } from '@gooddata/sdk-backend-spi'; import { IWorkspaceCatalog } from '@gooddata/sdk-backend-spi'; import { IWorkspaceCatalogAvailableItemsFactory } from '@gooddata/sdk-backend-spi'; import { IWorkspaceCatalogFactory } from '@gooddata/sdk-backend-spi'; @@ -209,6 +210,9 @@ export class AuthProviderCallGuard implements IAuthProviderCallGuard { reset: () => void; } +// @alpha (undocumented) +export type AutomationsDecoratorFactory = (automations: IWorkspaceAutomationService, workspace: string) => IWorkspaceAutomationService; + // @beta export class Builder implements IBuilder { constructor(item: Partial, validator?: ((item: Partial) => void) | undefined); @@ -254,6 +258,7 @@ export type CachingConfiguration = { maxSecuritySettingsOrgUrls?: number; maxSecuritySettingsOrgUrlsAge?: number; maxAttributeWorkspaces?: number; + maxAutomationsWorkspaces?: number; maxAttributeDisplayFormsPerWorkspace?: number; maxAttributesPerWorkspace?: number; maxAttributeElementResultsPerWorkspace?: number; @@ -598,6 +603,7 @@ export type DecoratorFactories = { securitySettings?: SecuritySettingsDecoratorFactory; workspaceSettings?: WorkspaceSettingsDecoratorFactory; attributes?: AttributesDecoratorFactory; + automations?: AutomationsDecoratorFactory; dashboards?: DashboardsDecoratorFactory; }; diff --git a/libs/sdk-backend-base/src/cachingBackend/index.ts b/libs/sdk-backend-base/src/cachingBackend/index.ts index 652ab61c9e1..f3ecf020699 100644 --- a/libs/sdk-backend-base/src/cachingBackend/index.ts +++ b/libs/sdk-backend-base/src/cachingBackend/index.ts @@ -22,6 +22,12 @@ import { IWorkspaceSettings, IWorkspaceSettingsService, ValidationContext, + IWorkspaceAutomationService, + IGetAutomationsOptions, + IGetAutomationOptions, + IAutomationsQuery, + IAutomationsQueryResult, + AutomationType, } from "@gooddata/sdk-backend-spi"; import { AttributesDecoratorFactory, @@ -29,6 +35,7 @@ import { ExecutionDecoratorFactory, SecuritySettingsDecoratorFactory, WorkspaceSettingsDecoratorFactory, + AutomationsDecoratorFactory, } from "../decoratedBackend/types.js"; import { decoratedBackend } from "../decoratedBackend/index.js"; import { LRUCache } from "lru-cache"; @@ -67,9 +74,15 @@ import { IMeasureDefinitionType, IRelativeDateFilter, IAbsoluteDateFilter, + IAutomationMetadataObject, + IAutomationMetadataObjectDefinition, } from "@gooddata/sdk-model"; import { DecoratedWorkspaceAttributesService } from "../decoratedBackend/attributes.js"; import { DecoratedWorkspaceSettingsService } from "../decoratedBackend/workspaceSettings.js"; +import { + DecoratedWorkspaceAutomationsService, + DecoratedAutomationsQuery, +} from "../decoratedBackend/automations.js"; // // Supporting types @@ -93,6 +106,11 @@ type AttributeCacheEntry = { attributeElementResults?: LRUCache>; }; +type AutomationCacheEntry = { + automations: LRUCache>; + queries: LRUCache>; +}; + type WorkspaceSettingsCacheEntry = { userWorkspaceSettings: LRUCache>; workspaceSettings: LRUCache>; @@ -104,6 +122,7 @@ type CachingContext = { workspaceCatalogs?: LRUCache; securitySettings?: LRUCache; workspaceAttributes?: LRUCache; + workspaceAutomations?: LRUCache; workspaceSettings?: LRUCache; }; config: CachingConfiguration; @@ -737,6 +756,165 @@ class WithAttributesCaching extends DecoratedWorkspaceAttributesService { } } +//AUTOMATIONS CACHING + +function getOrCreateAutomationsCache(ctx: CachingContext, workspace: string): AutomationCacheEntry { + const cache = ctx.caches.workspaceAutomations!; + let cacheEntry = cache.get(workspace); + + if (!cacheEntry) { + cacheEntry = { + automations: new LRUCache>({ + max: ctx.config.maxAutomationsWorkspaces!, + }), + queries: new LRUCache>({ + max: ctx.config.maxAutomationsWorkspaces!, + }), + }; + cache.set(workspace, cacheEntry); + } + + return cacheEntry; +} + +class CachedAutomationsQueryFactory extends DecoratedAutomationsQuery { + private settings: { + size: number; + page: number; + filter: { title?: string }; + sort: NonNullable; + type: AutomationType | undefined; + totalCount: number | undefined; + } = { + size: 50, + page: 0, + filter: {}, + sort: {}, + type: undefined, + totalCount: undefined, + }; + + constructor( + decorated: IAutomationsQuery, + private readonly ctx: CachingContext, + private readonly workspace: string, + ) { + super(decorated); + } + + withSize(size: number): IAutomationsQuery { + this.settings.size = size; + super.withSize(size); + return this; + } + + withPage(page: number): IAutomationsQuery { + this.settings.page = page; + super.withPage(page); + return this; + } + + withFilter(filter: { title?: string }): IAutomationsQuery { + this.settings.filter = { ...filter }; + this.settings.totalCount = undefined; + super.withFilter(filter); + return this; + } + + withSorting(sort: string[]): IAutomationsQuery { + this.settings.sort = { sort }; + super.withSorting(sort); + return this; + } + + withType(type: AutomationType): IAutomationsQuery { + this.settings.type = type; + super.withType(type); + return this; + } + + public query(): Promise { + const cache = getOrCreateAutomationsCache(this.ctx, this.workspace); + const key = stringify(this.settings); + + const result = cache.queries.get(key); + + if (!result) { + const promise = super.query().catch((e) => { + cache.queries.delete(key); + throw e; + }); + + cache.queries.set(key, promise); + return promise; + } + + return result; + } +} + +class WithAutomationsCaching extends DecoratedWorkspaceAutomationsService { + constructor( + decorated: IWorkspaceAutomationService, + private readonly ctx: CachingContext, + private readonly workspace: string, + ) { + super(decorated); + } + + public getAutomations(options?: IGetAutomationsOptions): Promise { + const cache = getOrCreateAutomationsCache(this.ctx, this.workspace).automations; + const key = stringify(options); + + const result = cache.get(key); + + if (!result) { + const promise = super.getAutomations(options).catch((e) => { + cache.delete(key); + throw e; + }); + + cache.set(key, promise); + return promise; + } + + return result; + } + + public async createAutomation( + automation: IAutomationMetadataObjectDefinition, + options?: IGetAutomationOptions, + ): Promise { + const cache = getOrCreateAutomationsCache(this.ctx, this.workspace); + const result = await super.createAutomation(automation, options); + cache.automations.clear(); + cache.queries.clear(); + return result; + } + + public async updateAutomation( + automation: IAutomationMetadataObject, + options?: IGetAutomationOptions, + ): Promise { + const cache = getOrCreateAutomationsCache(this.ctx, this.workspace); + const result = await super.updateAutomation(automation, options); + cache.automations.clear(); + cache.queries.clear(); + return result; + } + + public async deleteAutomation(id: string): Promise { + const cache = getOrCreateAutomationsCache(this.ctx, this.workspace); + await super.deleteAutomation(id); + cache.automations.clear(); + cache.queries.clear(); + } + + getAutomationsQuery(): IAutomationsQuery { + return new CachedAutomationsQueryFactory(super.getAutomationsQuery(), this.ctx, this.workspace); + } +} + // // // @@ -762,6 +940,10 @@ function cachedAttributes(ctx: CachingContext): AttributesDecoratorFactory { return (original, workspace) => new WithAttributesCaching(original, ctx, workspace); } +function cachedAutomations(ctx: CachingContext): AutomationsDecoratorFactory { + return (original, workspace) => new WithAutomationsCaching(original, ctx, workspace); +} + function cachingEnabled(desiredSize: number | undefined): boolean { return desiredSize !== undefined && desiredSize > 0; } @@ -974,6 +1156,20 @@ export type CachingConfiguration = { */ maxAttributeWorkspaces?: number; + /** + * Maximum number of workspaces for which to cache selected {@link @gooddata/sdk-backend-spi#IWorkspaceAutomationsService} calls. + * The workspace identifier is used as cache key. + * + * When limit is reached, cache entries will be evicted using LRU policy. + * + * When no maximum number is specified, the cache will be unbounded and no evictions will happen. Unbounded + * cache may be OK in applications where number of workspaces is small - the cache will be limited + * naturally and will not grow uncontrollably. + * + * When non-positive number is specified, then no caching will be done. + */ + maxAutomationsWorkspaces?: number; + /** * Maximum number of attribute display forms to cache per workspace. * @@ -1060,6 +1256,7 @@ export const RecommendedCachingConfiguration: CachingConfiguration = { maxAttributesPerWorkspace: 100, maxAttributeElementResultsPerWorkspace: 100, maxWorkspaceSettings: 1, + maxAutomationsWorkspaces: 1, }; /** @@ -1084,6 +1281,7 @@ export function withCaching( const securitySettingsCaching = cachingEnabled(config.maxSecuritySettingsOrgs); const attributeCaching = cachingEnabled(config.maxAttributeWorkspaces); const workspaceSettingsCaching = cachingEnabled(config.maxWorkspaceSettings); + const automationsCaching = cachingEnabled(config.maxAutomationsWorkspaces); const ctx: CachingContext = { caches: { @@ -1098,6 +1296,9 @@ export function withCaching( workspaceSettings: workspaceSettingsCaching ? new LRUCache({ max: config.maxWorkspaceSettings! }) : undefined, + workspaceAutomations: automationsCaching + ? new LRUCache({ max: config.maxAutomationsWorkspaces! }) + : undefined, }, config, capabilities: realBackend.capabilities, @@ -1107,6 +1308,7 @@ export function withCaching( const catalog = catalogCaching ? cachedCatalog(ctx) : identity; const securitySettings = securitySettingsCaching ? cachedSecuritySettings(ctx) : identity; const attributes = attributeCaching ? cachedAttributes(ctx) : identity; + const automations = automationsCaching ? cachedAutomations(ctx) : identity; const workspaceSettings = workspaceSettingsCaching ? cachedWorkspaceSettings(ctx) : identity; if (config.onCacheReady) { @@ -1119,6 +1321,7 @@ export function withCaching( securitySettings, attributes, workspaceSettings, + automations, }); } diff --git a/libs/sdk-backend-base/src/cachingBackend/tests/withCaching.test.ts b/libs/sdk-backend-base/src/cachingBackend/tests/withCaching.test.ts index 8a659427389..eb2f5dfd30e 100644 --- a/libs/sdk-backend-base/src/cachingBackend/tests/withCaching.test.ts +++ b/libs/sdk-backend-base/src/cachingBackend/tests/withCaching.test.ts @@ -1,4 +1,4 @@ -// (C) 2007-2022 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import { describe, it, expect } from "vitest"; import { IAnalyticalBackend, IElementsQueryResult, IExecutionResult } from "@gooddata/sdk-backend-spi"; import { CacheControl, withCaching } from "../index.js"; @@ -36,6 +36,7 @@ function withCachingForTests( maxAttributeElementResultsPerWorkspace: 1, maxAttributeWorkspaces: 1, maxWorkspaceSettings: 1, + maxAutomationsWorkspaces: 1, onCacheReady, }); } @@ -237,6 +238,57 @@ describe("withCaching", () => { expect(second).not.toBe(first); }); + it("evicts workspace automations list", () => { + const backend = withCachingForTests(); + + const first = backend.workspace("test").automations().getAutomations(); + const second = backend.workspace("test").automations().getAutomations(); + + expect(second).toBe(first); + }); + + it("evicts workspace automations query with same settings", () => { + const backend = withCachingForTests(); + + const first = backend + .workspace("test") + .automations() + .getAutomationsQuery() + .withPage(2) + .withSize(5) + .query(); + const second = backend + .workspace("test") + .automations() + .getAutomationsQuery() + .withPage(2) + .withSize(5) + .query(); + + expect(second).toBe(first); + }); + + it("evicts workspace automations query with different settings", () => { + const backend = withCachingForTests(); + + const first = backend + .workspace("test") + .automations() + .getAutomationsQuery() + .withPage(2) + .withSize(5) + .query(); + const second = backend + .workspace("test") + .automations() + .getAutomationsQuery() + .withPage(2) + .withSize(3) + .query(); + + expect(second).not.toBe(first); + }); + it("calls onCacheReady during construction", () => { let cacheControl: CacheControl | undefined; diff --git a/libs/sdk-backend-base/src/decoratedBackend/analyticalWorkspace.ts b/libs/sdk-backend-base/src/decoratedBackend/analyticalWorkspace.ts index 48ed5348250..f1f2a42d2e8 100644 --- a/libs/sdk-backend-base/src/decoratedBackend/analyticalWorkspace.ts +++ b/libs/sdk-backend-base/src/decoratedBackend/analyticalWorkspace.ts @@ -157,6 +157,12 @@ export class AnalyticalWorkspaceDecorator implements IAnalyticalWorkspace { } public automations(): IWorkspaceAutomationService { + const { automations } = this.factories; + + if (automations) { + return automations(this.decorated.automations(), this.workspace); + } + return this.decorated.automations(); } diff --git a/libs/sdk-backend-base/src/decoratedBackend/automations.ts b/libs/sdk-backend-base/src/decoratedBackend/automations.ts new file mode 100644 index 00000000000..79b8f38b50b --- /dev/null +++ b/libs/sdk-backend-base/src/decoratedBackend/automations.ts @@ -0,0 +1,71 @@ +// (C) 2021-2024 GoodData Corporation + +import { + IWorkspaceAutomationService, + IGetAutomationOptions, + IGetAutomationsOptions, + IAutomationsQuery, + AutomationType, + IAutomationsQueryResult, +} from "@gooddata/sdk-backend-spi"; +import { IAutomationMetadataObjectDefinition, IAutomationMetadataObject } from "@gooddata/sdk-model"; + +export abstract class DecoratedAutomationsQuery implements IAutomationsQuery { + protected constructor(protected readonly decorated: IAutomationsQuery) {} + + withSize(size: number): IAutomationsQuery { + return this.decorated.withSize(size); + } + withPage(page: number): IAutomationsQuery { + return this.decorated.withPage(page); + } + withFilter(filter: { title?: string }): IAutomationsQuery { + return this.decorated.withFilter(filter); + } + withSorting(sort: string[]): IAutomationsQuery { + return this.decorated.withSorting(sort); + } + withType(type: AutomationType): IAutomationsQuery { + return this.decorated.withType(type); + } + query(): Promise { + return this.decorated.query(); + } +} + +/** + * @alpha + */ +export abstract class DecoratedWorkspaceAutomationsService implements IWorkspaceAutomationService { + protected constructor(protected readonly decorated: IWorkspaceAutomationService) {} + + createAutomation( + automation: IAutomationMetadataObjectDefinition, + options?: IGetAutomationOptions, + ): Promise { + return this.decorated.createAutomation(automation, options); + } + + deleteAutomation(id: string): Promise { + return this.decorated.deleteAutomation(id); + } + + getAutomation(id: string, options?: IGetAutomationOptions): Promise { + return this.decorated.getAutomation(id, options); + } + + getAutomations(options?: IGetAutomationsOptions): Promise { + return this.decorated.getAutomations(options); + } + + getAutomationsQuery(): IAutomationsQuery { + return this.decorated.getAutomationsQuery(); + } + + updateAutomation( + automation: IAutomationMetadataObject, + options?: IGetAutomationOptions, + ): Promise { + return this.decorated.updateAutomation(automation, options); + } +} diff --git a/libs/sdk-backend-base/src/decoratedBackend/index.ts b/libs/sdk-backend-base/src/decoratedBackend/index.ts index dcd7e5ef949..6fc465718fb 100644 --- a/libs/sdk-backend-base/src/decoratedBackend/index.ts +++ b/libs/sdk-backend-base/src/decoratedBackend/index.ts @@ -26,6 +26,7 @@ export { SecuritySettingsDecoratorFactory, WorkspaceSettingsDecoratorFactory, AttributesDecoratorFactory, + AutomationsDecoratorFactory, DashboardsDecoratorFactory, DecoratorFactories, } from "./types.js"; diff --git a/libs/sdk-backend-base/src/decoratedBackend/types.ts b/libs/sdk-backend-base/src/decoratedBackend/types.ts index 3ec256f3e00..172ff2c1f4d 100644 --- a/libs/sdk-backend-base/src/decoratedBackend/types.ts +++ b/libs/sdk-backend-base/src/decoratedBackend/types.ts @@ -1,4 +1,4 @@ -// (C) 2023 GoodData Corporation +// (C) 2023-2024 GoodData Corporation import { IExecutionFactory, @@ -7,6 +7,7 @@ import { IWorkspaceSettingsService, IWorkspaceDashboardsService, ISecuritySettingsService, + IWorkspaceAutomationService, } from "@gooddata/sdk-backend-spi"; /** @@ -42,6 +43,14 @@ export type AttributesDecoratorFactory = ( workspace: string, ) => IWorkspaceAttributesService; +/** + * @alpha + */ +export type AutomationsDecoratorFactory = ( + automations: IWorkspaceAutomationService, + workspace: string, +) => IWorkspaceAutomationService; + /** * @alpha */ @@ -63,5 +72,6 @@ export type DecoratorFactories = { securitySettings?: SecuritySettingsDecoratorFactory; workspaceSettings?: WorkspaceSettingsDecoratorFactory; attributes?: AttributesDecoratorFactory; + automations?: AutomationsDecoratorFactory; dashboards?: DashboardsDecoratorFactory; }; diff --git a/libs/sdk-backend-base/src/dummyBackend/index.ts b/libs/sdk-backend-base/src/dummyBackend/index.ts index c952793c5ed..2cd98fba3f7 100644 --- a/libs/sdk-backend-base/src/dummyBackend/index.ts +++ b/libs/sdk-backend-base/src/dummyBackend/index.ts @@ -73,6 +73,11 @@ import { IClusteringConfig, IWorkspaceAutomationService, IGenAIService, + IGetAutomationOptions, + IGetAutomationsOptions, + IAutomationsQuery, + IAutomationsQueryResult, + AutomationType, } from "@gooddata/sdk-backend-spi"; import { defFingerprint, @@ -117,6 +122,8 @@ import { IAbsoluteDateFilter, IWebhookMetadataObjectDefinition, IWebhookMetadataObject, + IAutomationMetadataObjectDefinition, + IAutomationMetadataObject, } from "@gooddata/sdk-model"; import isEqual from "lodash/isEqual.js"; import isEmpty from "lodash/isEmpty.js"; @@ -363,7 +370,7 @@ function dummyWorkspace(workspace: string, config: DummyBackendConfig): IAnalyti throw new NotSupported("not supported"); }, automations(): IWorkspaceAutomationService { - throw new NotSupported("automations are not supported"); + return new DummyWorkspaceAutomationService(workspace); }, genAI(): IGenAIService { throw new NotSupported("not supported"); @@ -1106,3 +1113,162 @@ class DummyWorkspaceMeasuresService implements IWorkspaceMeasuresService { return Promise.resolve({ ...measure }); } } + +class DummyWorkspaceAutomationService implements IWorkspaceAutomationService { + constructor(public readonly workspace: string) {} + + async computeKeyDrivers(): Promise { + throw new NotSupported("not supported"); + } + + createAutomation( + automation: IAutomationMetadataObjectDefinition, + _options?: IGetAutomationOptions, + ): Promise { + return Promise.resolve({ + ...automation, + id: automation.id ?? "automation_id", + uri: automation.id ?? "automation_id", + ref: idRef(automation.id ?? "automation_id", "automation"), + type: "automation", + } as IAutomationMetadataObject); + } + + deleteAutomation(_id: string): Promise { + return Promise.resolve(undefined); + } + + getAutomation(id: string, _options?: IGetAutomationOptions): Promise { + return Promise.resolve({ + id: id, + uri: id, + ref: idRef(id, "automation"), + type: "automation", + exportDefinitions: [], + deprecated: false, + production: true, + description: "", + title: "", + unlisted: false, + details: { + subject: "", + message: "", + }, + } as IAutomationMetadataObject); + } + + getAutomations(_options?: IGetAutomationsOptions): Promise { + return Promise.resolve([ + { + id: "automation_id", + uri: "automation_id", + ref: idRef("automation_id", "automation"), + type: "automation", + exportDefinitions: [], + deprecated: false, + production: true, + description: "", + title: "", + unlisted: false, + details: { + subject: "", + message: "", + }, + }, + ] as IAutomationMetadataObject[]); + } + + updateAutomation( + automation: IAutomationMetadataObject, + _options?: IGetAutomationOptions, + ): Promise { + return Promise.resolve({ + exportDefinitions: [], + details: { + subject: "", + message: "", + }, + ...automation, + } as IAutomationMetadataObject); + } + + getAutomationsQuery(): IAutomationsQuery { + return new DummyAutomationsQuery(); + } +} + +class DummyAutomationsQuery implements IAutomationsQuery { + private settings: { + size: number; + page: number; + filter: { title?: string }; + sort: NonNullable; + type: AutomationType | undefined; + totalCount: number | undefined; + } = { + size: 50, + page: 0, + filter: {}, + sort: {}, + type: undefined, + totalCount: undefined, + }; + + query(): Promise { + return Promise.resolve( + new DummyAutomationsQueryResult([], this.settings.size, this.settings.page, 0), + ); + } + + withFilter(filter: { title?: string }): IAutomationsQuery { + this.settings.filter = filter; + return this; + } + + withPage(page: number): IAutomationsQuery { + this.settings.page = page; + return this; + } + + withSize(size: number): IAutomationsQuery { + this.settings.size = size; + return this; + } + + withSorting(sort: string[]): IAutomationsQuery { + this.settings.sort = sort; + return this; + } + + withType(type: AutomationType): IAutomationsQuery { + this.settings.type = type; + return this; + } +} + +class DummyAutomationsQueryResult implements IAutomationsQueryResult { + constructor( + public items: IAutomationMetadataObject[], + public limit: number, + public offset: number, + public totalCount: number, + ) {} + + all(): Promise { + throw new NotSupported("not supported"); + } + + allSorted( + _compareFn: (a: IAutomationMetadataObject, b: IAutomationMetadataObject) => number, + ): Promise { + throw new NotSupported("not supported"); + } + + goTo(_pageIndex: number): Promise> { + throw new NotSupported("not supported"); + } + + next(): Promise> { + throw new NotSupported("not supported"); + } +} diff --git a/libs/sdk-backend-base/src/index.ts b/libs/sdk-backend-base/src/index.ts index 4bd872eae11..017aa149173 100644 --- a/libs/sdk-backend-base/src/index.ts +++ b/libs/sdk-backend-base/src/index.ts @@ -1,4 +1,4 @@ -// (C) 2019-2022 GoodData Corporation +// (C) 2019-2024 GoodData Corporation /** * This package provides foundational reusable code useful for building new or decorating existing Analytical Backend implementations. * @@ -23,6 +23,7 @@ export { ExecutionDecoratorFactory, SecuritySettingsDecoratorFactory, WorkspaceSettingsDecoratorFactory, + AutomationsDecoratorFactory, AttributesDecoratorFactory, DashboardsDecoratorFactory, } from "./decoratedBackend/index.js"; diff --git a/libs/sdk-ui-dashboard/api/sdk-ui-dashboard.api.md b/libs/sdk-ui-dashboard/api/sdk-ui-dashboard.api.md index 56f2ada7602..96d2de149d7 100644 --- a/libs/sdk-ui-dashboard/api/sdk-ui-dashboard.api.md +++ b/libs/sdk-ui-dashboard/api/sdk-ui-dashboard.api.md @@ -231,6 +231,8 @@ import { IRelativeDateFilter } from '@gooddata/sdk-model'; import { IRenderListItemProps } from '@gooddata/sdk-ui-kit'; import { IResultWarning } from '@gooddata/sdk-model'; import { IRichTextWidget } from '@gooddata/sdk-model'; +import { IScheduleEmailContext as IScheduleEmailContext_2 } from '../../../types.js'; +import { IScheduleEmailDialogContext as IScheduleEmailDialogContext_2 } from '../../types.js'; import { ISeparators } from '@gooddata/sdk-model'; import { ISettings } from '@gooddata/sdk-model'; import { IShareDialogInteractionData } from '@gooddata/sdk-ui-kit'; @@ -3368,10 +3370,13 @@ export function getDefaultInsightMenuItems(intl: IntlShape, config: { exportXLSXDisabled: boolean; exportCSVDisabled: boolean; scheduleExportDisabled: boolean; + scheduleExportManagementDisabled: boolean; onExportXLSX: () => void; onExportCSV: () => void; onScheduleExport: () => void; + onScheduleManagementExport: () => void; isScheduleExportVisible: boolean; + isScheduleExportManagementVisible: boolean; isDataError: boolean; }): IInsightMenuItem[]; @@ -4716,6 +4721,7 @@ export function isBrokenAlertDateFilterInfo(item: IBrokenAlertFilterBasicInfo): // @alpha (undocumented) export interface IScheduledEmailDialogProps { automations: IAutomationMetadataObject[]; + context?: IScheduledEmailDialogPropsContext; editSchedule?: IAutomationMetadataObject; isVisible?: boolean; onCancel?: () => void; @@ -4731,9 +4737,16 @@ export interface IScheduledEmailDialogProps { webhooks: IWebhookMetadataObject[]; } +// @internal (undocumented) +export interface IScheduledEmailDialogPropsContext { + // (undocumented) + insightRef?: ObjRef | undefined; +} + // @alpha (undocumented) export interface IScheduledEmailManagementDialogProps { automations: IAutomationMetadataObject[]; + context?: IScheduledEmailDialogPropsContext; isLoadingScheduleData: boolean; isVisible?: boolean; onAdd?: () => void; @@ -4745,6 +4758,17 @@ export interface IScheduledEmailManagementDialogProps { webhooks: IWebhookMetadataObject[]; } +// @internal (undocumented) +export interface IScheduleEmailContext { + widget?: IWidget; +} + +// @internal (undocumented) +export interface IScheduleEmailDialogContext { + // (undocumented) + insightRef?: ObjRef | undefined; +} + // @internal export const isCreateAttributeHierarchyRequested: (obj: unknown) => obj is CreateAttributeHierarchyRequested; @@ -5232,6 +5256,14 @@ export interface IUseWidgetSelectionResult { onSelected: (e?: MouseEvent_2) => void; } +// @internal (undocumented) +export interface IUseWorkspaceAutomationsConfig extends UseCancelablePromiseCallbacks { + backend?: IAnalyticalBackend; + enable?: boolean; + limit?: number; + workspace?: string; +} + // @internal (undocumented) export type IWrapCreatePanelItemWithDragComponent = React.ComponentType; @@ -7099,9 +7131,15 @@ export const selectIsSaveAsNewButtonHidden: DashboardSelector; // @internal (undocumented) export const selectIsSaveAsNewButtonVisible: DashboardSelector; +// @alpha (undocumented) +export const selectIsScheduleEmailDialogContext: DashboardSelector; + // @alpha (undocumented) export const selectIsScheduleEmailDialogOpen: DashboardSelector; +// @alpha (undocumented) +export const selectIsScheduleEmailManagementDialogContext: DashboardSelector; + // @alpha (undocumented) export const selectIsScheduleEmailManagementDialogOpen: DashboardSelector; @@ -7592,14 +7630,20 @@ export interface TriggerEventPayload { // @internal export const uiActions: CaseReducerActions< { -openScheduleEmailDialog: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; +openScheduleEmailDialog: (state: WritableDraft, action: { +payload: IScheduleEmailContext_2; +type: string; +}) => void | UiState_2 | WritableDraft; closeScheduleEmailDialog: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; setScheduleEmailDialogDefaultAttachment: (state: WritableDraft, action: { payload: ObjRef; type: string; }) => void | UiState_2 | WritableDraft; resetScheduleEmailDialogDefaultAttachment: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; -openScheduleEmailManagementDialog: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; +openScheduleEmailManagementDialog: (state: WritableDraft, action: { +payload: IScheduleEmailContext_2; +type: string; +}) => void | UiState_2 | WritableDraft; closeScheduleEmailManagementDialog: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; openSaveAsDialog: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; closeSaveAsDialog: (state: WritableDraft, action: AnyAction) => void | UiState_2 | WritableDraft; @@ -7771,10 +7815,12 @@ export interface UiState { scheduleEmailDialog: { open: boolean; defaultAttachmentRef: ObjRef | undefined; + context?: IScheduleEmailDialogContext; }; // (undocumented) scheduleEmailManagementDialog: { open: boolean; + context?: IScheduleEmailDialogContext; }; // (undocumented) selectedFilterIndex: number | undefined; @@ -8290,18 +8336,23 @@ export const useDashboardScheduledEmails: ({ onReload }?: { onReload?: (() => void) | undefined; }) => { isScheduledEmailingVisible: boolean; - defaultOnScheduleEmailing: () => void; + isScheduledManagementEmailingVisible: boolean; + defaultOnScheduleEmailing: (widget?: IWidget) => void; + defaultOnScheduleEmailingManagement: (widget?: IWidget) => void; isScheduleEmailingDialogOpen: boolean; isScheduleEmailingManagementDialogOpen: boolean; - onScheduleEmailingOpen: () => void; - onScheduleEmailingManagementEdit: (schedule: IAutomationMetadataObject) => void; + scheduleEmailingDialogContext: IScheduleEmailDialogContext_2; + scheduleEmailingManagementDialogContext: IScheduleEmailDialogContext_2; + onScheduleEmailingOpen: (widget?: IWidget) => void; + onScheduleEmailingManagementOpen: (widget?: IWidget) => void; + onScheduleEmailingManagementEdit: (schedule: IAutomationMetadataObject, widget?: IWidget) => void; scheduledEmailToEdit: IAutomationMetadataObject | undefined; - onScheduleEmailingCancel: () => void; + onScheduleEmailingCancel: (widget?: IWidget) => void; onScheduleEmailingCreateError: () => void; - onScheduleEmailingCreateSuccess: () => void; + onScheduleEmailingCreateSuccess: (widget?: IWidget) => void; onScheduleEmailingSaveError: () => void; - onScheduleEmailingSaveSuccess: () => void; - onScheduleEmailingManagementAdd: () => void; + onScheduleEmailingSaveSuccess: (widget?: IWidget) => void; + onScheduleEmailingManagementAdd: (widget?: IWidget) => void; onScheduleEmailingManagementClose: () => void; onScheduleEmailingManagementLoadingError: () => void; onScheduleEmailingManagementDeleteSuccess: () => void; @@ -8533,6 +8584,9 @@ export function useWidgetFilters(widget: ExtendedDashboardWidget | undefined, in // @internal (undocumented) export function useWidgetSelection(widgetRef?: ObjRef): IUseWidgetSelectionResult; +// @internal +export function useWorkspaceAutomations({ enable, workspace, backend, onCancel, onError, onLoading, onPending, onSuccess, limit, }: IUseWorkspaceAutomationsConfig, dependencies?: any[]): UseCancelablePromiseState; + // @internal (undocumented) export type ValuesLimitingItem = IDashboardAttributeFilterParentItem | ObjRef | IDashboardDependentDateFilter; diff --git a/libs/sdk-ui-dashboard/src/assets/list.svg b/libs/sdk-ui-dashboard/src/assets/list.svg new file mode 100644 index 00000000000..94028a0cefa --- /dev/null +++ b/libs/sdk-ui-dashboard/src/assets/list.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/libs/sdk-ui-dashboard/src/model/react/index.ts b/libs/sdk-ui-dashboard/src/model/react/index.ts index 4370a404a04..80ea2c2452d 100644 --- a/libs/sdk-ui-dashboard/src/model/react/index.ts +++ b/libs/sdk-ui-dashboard/src/model/react/index.ts @@ -27,3 +27,4 @@ export { useWidgetExecutionsHandler } from "./useWidgetExecutionsHandler.js"; export { useDashboardScheduledEmails } from "./useDashboardScheduledEmails.js"; export { useDashboardScheduledEmailsData } from "./useDashboardScheduledEmailsData.js"; export { useWidgetSelection, IUseWidgetSelectionResult } from "./useWidgetSelection.js"; +export { useWorkspaceAutomations, IUseWorkspaceAutomationsConfig } from "./useWorkspaceAutomations.js"; diff --git a/libs/sdk-ui-dashboard/src/model/react/useDashboardScheduledEmails.ts b/libs/sdk-ui-dashboard/src/model/react/useDashboardScheduledEmails.ts index ab4ac20fa55..f307f15a5f8 100644 --- a/libs/sdk-ui-dashboard/src/model/react/useDashboardScheduledEmails.ts +++ b/libs/sdk-ui-dashboard/src/model/react/useDashboardScheduledEmails.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import { useToastMessage } from "@gooddata/sdk-ui-kit"; -import { IAutomationMetadataObject } from "@gooddata/sdk-model"; +import { IAutomationMetadataObject, IWidget } from "@gooddata/sdk-model"; import { useDashboardDispatch, useDashboardSelector } from "./DashboardStoreProvider.js"; import { @@ -14,12 +14,15 @@ import { selectIsReadOnly, selectIsScheduleEmailDialogOpen, selectIsScheduleEmailManagementDialogOpen, + selectIsScheduleEmailDialogContext, + selectIsScheduleEmailManagementDialogContext, selectMenuButtonItemsVisibility, selectWebhooks, uiActions, } from "../store/index.js"; import { messages } from "../../locales.js"; +import { useWorkspaceAutomations } from "./useWorkspaceAutomations.js"; /** * Hook that handles schedule emailing dialogs. @@ -28,9 +31,15 @@ import { messages } from "../../locales.js"; */ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => void } = {}) => { const { addSuccess, addError } = useToastMessage(); - const isScheduleEmailingDialogOpen = useDashboardSelector(selectIsScheduleEmailDialogOpen); + + const isScheduleEmailingDialogOpen = useDashboardSelector(selectIsScheduleEmailDialogOpen) || false; + const scheduleEmailingDialogContext = useDashboardSelector(selectIsScheduleEmailDialogContext); const isScheduleEmailingManagementDialogOpen = useDashboardSelector(selectIsScheduleEmailManagementDialogOpen) || false; + const scheduleEmailingManagementDialogContext = useDashboardSelector( + selectIsScheduleEmailManagementDialogContext, + ); + const dispatch = useDashboardDispatch(); const dashboardRef = useDashboardSelector(selectDashboardRef); const isReadOnly = useDashboardSelector(selectIsReadOnly); @@ -46,8 +55,13 @@ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => voi */ const showDueToNumberOfAvailableWebhooks = numberOfAvailableWebhooks > 0 || isWorkspaceManager; + const { result } = useWorkspaceAutomations({ + enable: isScheduledEmailingEnabled, + }); + const openScheduleEmailingDialog = useCallback( - () => isScheduledEmailingEnabled && dispatch(uiActions.openScheduleEmailDialog()), + (widget?: IWidget) => + isScheduledEmailingEnabled && dispatch(uiActions.openScheduleEmailDialog({ widget })), [dispatch, isScheduledEmailingEnabled], ); const closeScheduleEmailingDialog = useCallback( @@ -55,7 +69,8 @@ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => voi [dispatch, isScheduledEmailingEnabled], ); const openScheduleEmailingManagementDialog = useCallback( - () => isScheduledEmailingEnabled && dispatch(uiActions.openScheduleEmailManagementDialog()), + (widget?: IWidget) => + isScheduledEmailingEnabled && dispatch(uiActions.openScheduleEmailManagementDialog({ widget })), [dispatch, isScheduledEmailingEnabled], ); const closeScheduleEmailingManagementDialog = useCallback( @@ -73,35 +88,64 @@ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => voi showDueToNumberOfAvailableWebhooks && (menuButtonItemsVisibility.scheduleEmailButton ?? true); + const isScheduledManagementEmailingVisible = isScheduledEmailingVisible && (result ?? []).length > 0; + /* * exports and scheduling are not available when rendering a dashboard that is not persisted. * this can happen when a new dashboard is created and is being edited. * * the setup of menu items available in the menu needs to reflect this. */ - const defaultOnScheduleEmailing = useCallback(() => { - if (!dashboardRef) { - return; - } + const defaultOnScheduleEmailingManagement = useCallback( + (widget?: IWidget) => { + if (!dashboardRef) { + return; + } - openScheduleEmailingManagementDialog(); - }, [dashboardRef, openScheduleEmailingManagementDialog]); + openScheduleEmailingManagementDialog(widget); + }, + [dashboardRef, openScheduleEmailingManagementDialog], + ); - const onScheduleEmailingOpen = useCallback(() => { - openScheduleEmailingDialog(); - }, [openScheduleEmailingDialog]); + const defaultOnScheduleEmailing = useCallback( + (widget?: IWidget) => { + if (!dashboardRef) { + return; + } + + openScheduleEmailingDialog(widget); + }, + [dashboardRef, openScheduleEmailingDialog], + ); + + const onScheduleEmailingOpen = useCallback( + (widget?: IWidget) => { + openScheduleEmailingDialog(widget); + }, + [openScheduleEmailingDialog], + ); + + const onScheduleEmailingManagementOpen = useCallback( + (widget?: IWidget) => { + openScheduleEmailingManagementDialog(widget); + }, + [openScheduleEmailingManagementDialog], + ); const onScheduleEmailingCreateError = useCallback(() => { closeScheduleEmailingDialog(); addError(messages.scheduleEmailSubmitError); }, [closeScheduleEmailingDialog, addError]); - const onScheduleEmailingCreateSuccess = useCallback(() => { - closeScheduleEmailingDialog(); - openScheduleEmailingManagementDialog(); - addSuccess(messages.scheduleEmailSubmitSuccess); - onReload?.(); - }, [closeScheduleEmailingDialog, openScheduleEmailingManagementDialog, addSuccess, onReload]); + const onScheduleEmailingCreateSuccess = useCallback( + (widget?: IWidget) => { + closeScheduleEmailingDialog(); + openScheduleEmailingManagementDialog(widget); + addSuccess(messages.scheduleEmailSubmitSuccess); + onReload?.(); + }, + [closeScheduleEmailingDialog, openScheduleEmailingManagementDialog, addSuccess, onReload], + ); const onScheduleEmailingSaveError = useCallback(() => { closeScheduleEmailingDialog(); @@ -109,25 +153,31 @@ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => voi setScheduledEmailToEdit(undefined); }, [closeScheduleEmailingDialog, addError, setScheduledEmailToEdit]); - const onScheduleEmailingSaveSuccess = useCallback(() => { - closeScheduleEmailingDialog(); - openScheduleEmailingManagementDialog(); - addSuccess(messages.scheduleEmailSaveSuccess); - setScheduledEmailToEdit(undefined); - onReload?.(); - }, [ - closeScheduleEmailingDialog, - openScheduleEmailingManagementDialog, - addSuccess, - setScheduledEmailToEdit, - onReload, - ]); - - const onScheduleEmailingCancel = useCallback(() => { - closeScheduleEmailingDialog(); - openScheduleEmailingManagementDialog(); - setScheduledEmailToEdit(undefined); - }, [closeScheduleEmailingDialog, openScheduleEmailingManagementDialog, setScheduledEmailToEdit]); + const onScheduleEmailingSaveSuccess = useCallback( + (widget?: IWidget) => { + closeScheduleEmailingDialog(); + openScheduleEmailingManagementDialog(widget); + addSuccess(messages.scheduleEmailSaveSuccess); + setScheduledEmailToEdit(undefined); + onReload?.(); + }, + [ + closeScheduleEmailingDialog, + openScheduleEmailingManagementDialog, + addSuccess, + setScheduledEmailToEdit, + onReload, + ], + ); + + const onScheduleEmailingCancel = useCallback( + (widget?: IWidget) => { + closeScheduleEmailingDialog(); + setScheduledEmailToEdit(undefined); + openScheduleEmailingManagementDialog(widget); + }, + [closeScheduleEmailingDialog, openScheduleEmailingManagementDialog, setScheduledEmailToEdit], + ); const onScheduleEmailingManagementDeleteSuccess = useCallback(() => { closeScheduleEmailingDialog(); @@ -135,16 +185,19 @@ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => voi onReload?.(); }, [addSuccess, closeScheduleEmailingDialog, onReload]); - const onScheduleEmailingManagementAdd = useCallback(() => { - closeScheduleEmailingManagementDialog(); - openScheduleEmailingDialog(); - }, [closeScheduleEmailingManagementDialog, openScheduleEmailingDialog]); + const onScheduleEmailingManagementAdd = useCallback( + (widget?: IWidget) => { + closeScheduleEmailingManagementDialog(); + openScheduleEmailingDialog(widget); + }, + [closeScheduleEmailingManagementDialog, openScheduleEmailingDialog], + ); const onScheduleEmailingManagementEdit = useCallback( - (schedule: IAutomationMetadataObject) => { + (schedule: IAutomationMetadataObject, widget?: IWidget) => { closeScheduleEmailingManagementDialog(); setScheduledEmailToEdit(schedule); - openScheduleEmailingDialog(); + openScheduleEmailingDialog(widget); }, [closeScheduleEmailingManagementDialog, openScheduleEmailingDialog, setScheduledEmailToEdit], ); @@ -166,10 +219,15 @@ export const useDashboardScheduledEmails = ({ onReload }: { onReload?: () => voi return { isScheduledEmailingVisible, + isScheduledManagementEmailingVisible, defaultOnScheduleEmailing, + defaultOnScheduleEmailingManagement, isScheduleEmailingDialogOpen, isScheduleEmailingManagementDialogOpen, + scheduleEmailingDialogContext, + scheduleEmailingManagementDialogContext, onScheduleEmailingOpen, + onScheduleEmailingManagementOpen, onScheduleEmailingManagementEdit, scheduledEmailToEdit, onScheduleEmailingCancel, diff --git a/libs/sdk-ui-dashboard/src/model/react/useWorkspaceAutomations.ts b/libs/sdk-ui-dashboard/src/model/react/useWorkspaceAutomations.ts index 1f0b6f94170..4f861db1676 100644 --- a/libs/sdk-ui-dashboard/src/model/react/useWorkspaceAutomations.ts +++ b/libs/sdk-ui-dashboard/src/model/react/useWorkspaceAutomations.ts @@ -10,7 +10,10 @@ import { useWorkspaceStrict, } from "@gooddata/sdk-ui"; -interface IUseWorkspaceAutomationsConfig +/** + * @internal + */ +export interface IUseWorkspaceAutomationsConfig extends UseCancelablePromiseCallbacks { /** * Enable or disable the hook. diff --git a/libs/sdk-ui-dashboard/src/model/store/index.ts b/libs/sdk-ui-dashboard/src/model/store/index.ts index 47948fc80cf..be350f94b67 100644 --- a/libs/sdk-ui-dashboard/src/model/store/index.ts +++ b/libs/sdk-ui-dashboard/src/model/store/index.ts @@ -310,6 +310,8 @@ export { UiState, InvalidCustomUrlDrillParameterInfo } from "./ui/uiState.js"; export { selectIsScheduleEmailDialogOpen, selectIsScheduleEmailManagementDialogOpen, + selectIsScheduleEmailDialogContext, + selectIsScheduleEmailManagementDialogContext, selectIsSaveAsDialogOpen, selectIsShareDialogOpen, selectFilterBarExpanded, diff --git a/libs/sdk-ui-dashboard/src/model/store/ui/uiReducers.ts b/libs/sdk-ui-dashboard/src/model/store/ui/uiReducers.ts index 892214962a6..ab701d82181 100644 --- a/libs/sdk-ui-dashboard/src/model/store/ui/uiReducers.ts +++ b/libs/sdk-ui-dashboard/src/model/store/ui/uiReducers.ts @@ -1,4 +1,4 @@ -// (C) 2021-2023 GoodData Corporation +// (C) 2021-2024 GoodData Corporation import { Action, AnyAction, CaseReducer, PayloadAction } from "@reduxjs/toolkit"; import { areObjRefsEqual, @@ -9,21 +9,28 @@ import { widgetId, widgetRef, widgetUri, + isInsightWidget, } from "@gooddata/sdk-model"; import { InvalidCustomUrlDrillParameterInfo, UiState } from "./uiState.js"; -import { ILayoutCoordinates, IMenuButtonItemsVisibility } from "../../../types.js"; +import { ILayoutCoordinates, IMenuButtonItemsVisibility, IScheduleEmailContext } from "../../../types.js"; import { DraggableLayoutItem } from "../../../presentation/dragAndDrop/types.js"; import { IDashboardWidgetOverlay } from "../../types/commonTypes.js"; import { getDrillOriginLocalIdentifier } from "../../../_staging/drills/drillingUtils.js"; type UiReducer = CaseReducer; -const openScheduleEmailDialog: UiReducer = (state) => { +const openScheduleEmailDialog: UiReducer> = (state, action) => { state.scheduleEmailDialog.open = true; + if (isInsightWidget(action.payload.widget)) { + state.scheduleEmailDialog.context = { + insightRef: action.payload.widget.insight, + }; + } }; const closeScheduleEmailDialog: UiReducer = (state) => { state.scheduleEmailDialog.open = false; + state.scheduleEmailDialog.context = undefined; }; const setScheduleEmailDialogDefaultAttachment: UiReducer> = (state, action) => { @@ -34,12 +41,21 @@ const resetScheduleEmailDialogDefaultAttachment: UiReducer = (state) => { state.scheduleEmailDialog.defaultAttachmentRef = undefined; }; -const openScheduleEmailManagementDialog: UiReducer = (state) => { +const openScheduleEmailManagementDialog: UiReducer> = ( + state, + action, +) => { state.scheduleEmailManagementDialog.open = true; + if (isInsightWidget(action.payload.widget)) { + state.scheduleEmailManagementDialog.context = { + insightRef: action.payload.widget.insight, + }; + } }; const closeScheduleEmailManagementDialog: UiReducer = (state) => { state.scheduleEmailManagementDialog.open = false; + state.scheduleEmailManagementDialog.context = undefined; }; const openSaveAsDialog: UiReducer = (state) => { diff --git a/libs/sdk-ui-dashboard/src/model/store/ui/uiSelectors.ts b/libs/sdk-ui-dashboard/src/model/store/ui/uiSelectors.ts index a061199c9f4..002ecc85ba2 100644 --- a/libs/sdk-ui-dashboard/src/model/store/ui/uiSelectors.ts +++ b/libs/sdk-ui-dashboard/src/model/store/ui/uiSelectors.ts @@ -1,4 +1,4 @@ -// (C) 2021-2023 GoodData Corporation +// (C) 2021-2024 GoodData Corporation import { createSelector } from "@reduxjs/toolkit"; import { areObjRefsEqual, ObjRef, objRefToString } from "@gooddata/sdk-model"; @@ -9,7 +9,11 @@ import { DashboardSelector, DashboardState } from "../types.js"; import { createMemoizedSelector } from "../_infra/selectors.js"; import { IDashboardWidgetOverlay } from "../../types/commonTypes.js"; import { ObjRefMap } from "../../../_staging/metadata/objRefMap.js"; -import { ILayoutCoordinates, IMenuButtonItemsVisibility } from "../../../types.js"; +import { + ILayoutCoordinates, + IMenuButtonItemsVisibility, + IScheduleEmailDialogContext, +} from "../../../types.js"; import { DraggableLayoutItem } from "../../../presentation/dragAndDrop/types.js"; import { InvalidCustomUrlDrillParameterInfo } from "./uiState.js"; @@ -26,6 +30,12 @@ export const selectIsScheduleEmailDialogOpen: DashboardSelector = creat (state) => state.scheduleEmailDialog.open, ); +/** + * @alpha + */ +export const selectIsScheduleEmailDialogContext: DashboardSelector = + createSelector(selectSelf, (state) => state.scheduleEmailDialog.context ?? {}); + /** * @alpha */ @@ -40,6 +50,12 @@ export const selectIsScheduleEmailManagementDialogOpen: DashboardSelector state.scheduleEmailManagementDialog.open, ); +/** + * @alpha + */ +export const selectIsScheduleEmailManagementDialogContext: DashboardSelector = + createSelector(selectSelf, (state) => state.scheduleEmailManagementDialog.context ?? {}); + /** * @alpha */ diff --git a/libs/sdk-ui-dashboard/src/model/store/ui/uiState.ts b/libs/sdk-ui-dashboard/src/model/store/ui/uiState.ts index bb8819ea4e9..91904ed30d1 100644 --- a/libs/sdk-ui-dashboard/src/model/store/ui/uiState.ts +++ b/libs/sdk-ui-dashboard/src/model/store/ui/uiState.ts @@ -1,7 +1,11 @@ -// (C) 2021-2023 GoodData Corporation +// (C) 2021-2024 GoodData Corporation import { ObjRef, Identifier, Uri } from "@gooddata/sdk-model"; -import { ILayoutCoordinates, IMenuButtonItemsVisibility } from "../../../types.js"; +import { + ILayoutCoordinates, + IMenuButtonItemsVisibility, + IScheduleEmailDialogContext, +} from "../../../types.js"; import { DraggableLayoutItem } from "../../../presentation/dragAndDrop/types.js"; import { IDashboardWidgetOverlay } from "../../types/commonTypes.js"; @@ -22,10 +26,12 @@ export interface InvalidCustomUrlDrillParameterInfo { export interface UiState { scheduleEmailManagementDialog: { open: boolean; + context?: IScheduleEmailDialogContext; }; scheduleEmailDialog: { open: boolean; defaultAttachmentRef: ObjRef | undefined; + context?: IScheduleEmailDialogContext; }; saveAsDialog: { open: boolean; diff --git a/libs/sdk-ui-dashboard/src/presentation/dashboard/DashboardHeader/ScheduledEmailDialogProvider.tsx b/libs/sdk-ui-dashboard/src/presentation/dashboard/DashboardHeader/ScheduledEmailDialogProvider.tsx index 48966bfe76c..8fa7c370b67 100644 --- a/libs/sdk-ui-dashboard/src/presentation/dashboard/DashboardHeader/ScheduledEmailDialogProvider.tsx +++ b/libs/sdk-ui-dashboard/src/presentation/dashboard/DashboardHeader/ScheduledEmailDialogProvider.tsx @@ -15,7 +15,9 @@ export const ScheduledEmailDialogProvider = () => { const { isScheduleEmailingDialogOpen, + scheduleEmailingDialogContext, isScheduleEmailingManagementDialogOpen, + scheduleEmailingManagementDialogContext, onScheduleEmailingCancel, onScheduleEmailingCreateError, onScheduleEmailingCreateSuccess, @@ -40,6 +42,7 @@ export const ScheduledEmailDialogProvider = () => { {isScheduleEmailingManagementDialogOpen ? ( { {isScheduleEmailingDialogOpen ? ( , ", "limit": 0 }, + "options.menu.schedule.email.edit": { + "value": "Show schedules", + "comment": "Translate as imperative. Schedule emailing edit whole KPI dashboard", + "limit": 0 + }, "options.menu.delete": { "value": "Delete", "comment": "Delete Dashboard", @@ -1180,6 +1185,11 @@ "comment": "", "limit": 0 }, + "widget.options.menu.scheduleExport.edit": { + "value": "Show schedules", + "comment": "", + "limit": 0 + }, "share.button.text": { "value": "Share", "comment": "Text of button that opens share dialog", diff --git a/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/DefaultScheduledEmailDialog/utils/automationFilters.ts b/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/DefaultScheduledEmailDialog/utils/automationFilters.ts index 3edee075f8c..0718bc54778 100644 --- a/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/DefaultScheduledEmailDialog/utils/automationFilters.ts +++ b/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/DefaultScheduledEmailDialog/utils/automationFilters.ts @@ -23,6 +23,18 @@ export const isDashboardAutomation = ( }); }; +export const isVisualisationAutomation = ( + automation: IAutomationMetadataObject | IAutomationMetadataObjectDefinition | undefined, +) => { + if (!automation) { + return false; + } + + return automation?.exportDefinitions?.some((exportDefinition) => { + return isExportDefinitionVisualizationObjectContent(exportDefinition.requestPayload.content); + }); +}; + export const getAutomationDashboardFilters = ( automation: IAutomationMetadataObject | IAutomationMetadataObjectDefinition | undefined, ): FilterContextItem[] | undefined => { diff --git a/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/index.ts b/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/index.ts index 8c6a655ab8f..edb520d2dfc 100644 --- a/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/index.ts +++ b/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/index.ts @@ -1,4 +1,4 @@ -// (C) 2020-2022 GoodData Corporation +// (C) 2020-2024 GoodData Corporation export { DefaultScheduledEmailDialog } from "./DefaultScheduledEmailDialog/index.js"; export { DefaultScheduledEmailManagementDialog } from "./DefaultScheduledEmailManagementDialog/index.js"; export { ScheduledEmailDialog } from "./ScheduledEmailDialog.js"; @@ -8,4 +8,5 @@ export { CustomScheduledEmailManagementDialogComponent, IScheduledEmailDialogProps, IScheduledEmailManagementDialogProps, + IScheduledEmailDialogPropsContext, } from "./types.js"; diff --git a/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/types.ts b/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/types.ts index 48e68222575..f2f4f3e2e6f 100644 --- a/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/types.ts +++ b/libs/sdk-ui-dashboard/src/presentation/scheduledEmail/types.ts @@ -5,6 +5,7 @@ import { IAutomationMetadataObjectDefinition, IWebhookMetadataObject, IWorkspaceUser, + ObjRef, } from "@gooddata/sdk-model"; import { GoodDataSdkError } from "@gooddata/sdk-ui"; @@ -12,6 +13,13 @@ import { GoodDataSdkError } from "@gooddata/sdk-ui"; /// Component props /// +/** + * @internal + */ +export interface IScheduledEmailDialogPropsContext { + insightRef?: ObjRef | undefined; +} + /** * @alpha */ @@ -21,6 +29,11 @@ export interface IScheduledEmailDialogProps { */ isVisible?: boolean; + /** + * Context for the scheduled e-mail dialog. + */ + context?: IScheduledEmailDialogPropsContext; + /** * Callback to be called, when user submits the scheduled email dialog. */ @@ -98,6 +111,11 @@ export interface IScheduledEmailManagementDialogProps { */ isVisible?: boolean; + /** + * Context for the scheduled e-mail dialog. + */ + context?: IScheduledEmailDialogPropsContext; + /** * Callback to be called, when user adds new scheduled email item. */ diff --git a/libs/sdk-ui-dashboard/src/presentation/topBar/menuButton/useDefaultMenuItems.tsx b/libs/sdk-ui-dashboard/src/presentation/topBar/menuButton/useDefaultMenuItems.tsx index 648b87525f6..71f5affcf10 100644 --- a/libs/sdk-ui-dashboard/src/presentation/topBar/menuButton/useDefaultMenuItems.tsx +++ b/libs/sdk-ui-dashboard/src/presentation/topBar/menuButton/useDefaultMenuItems.tsx @@ -36,8 +36,13 @@ export function useDefaultMenuItems(): IMenuButtonItem[] { const isNewDashboard = useDashboardSelector(selectIsNewDashboard); const isEmptyLayout = !useDashboardSelector(selectLayoutHasAnalyticalWidgets); // we need at least one non-custom widget there const { addSuccess, addError, addProgress, removeMessage } = useToastMessage(); - const { isScheduledEmailingVisible, defaultOnScheduleEmailing, numberOfAvailableWebhooks } = - useDashboardScheduledEmails(); + const { + isScheduledEmailingVisible, + isScheduledManagementEmailingVisible, + defaultOnScheduleEmailing, + defaultOnScheduleEmailingManagement, + numberOfAvailableWebhooks, + } = useDashboardScheduledEmails(); const dispatch = useDashboardDispatch(); const openSaveAsDialog = useCallback(() => dispatch(uiActions.openSaveAsDialog()), [dispatch]); @@ -147,8 +152,7 @@ export function useDefaultMenuItems(): IMenuButtonItem[] { type: "separator", itemId: "save-as-separator", // show the separator if at least one item of the two groups is visible as well - visible: - isSaveAsVisible && (isPdfExportVisible || isScheduledEmailingVisible || isDeleteVisible), + visible: isSaveAsVisible && isPdfExportVisible, }, { type: "button", @@ -157,6 +161,14 @@ export function useDefaultMenuItems(): IMenuButtonItem[] { onClick: defaultOnExportToPdf, visible: isPdfExportVisible, }, + { + type: "separator", + itemId: "schedule-separator", + // show the separator if at least one item of the two groups is visible as well + visible: + isPdfExportVisible && + (isScheduledEmailingVisible || isScheduledManagementEmailingVisible), + }, { type: "button", itemId: "schedule-email-item", // careful, this is also used as a selector in tests, do not change @@ -177,6 +189,20 @@ export function useDefaultMenuItems(): IMenuButtonItem[] { ) : undefined, }, + { + type: "button", + itemId: "schedule-email-edit-item", // careful, this is also used as a selector in tests, do not change + itemName: intl.formatMessage({ id: "options.menu.schedule.email.edit" }), + onClick: defaultOnScheduleEmailingManagement, + visible: isScheduledManagementEmailingVisible, + }, + { + type: "separator", + itemId: "delete-separator", + // show the separator if at least one item of the two groups is visible as well + visible: + (isScheduledEmailingVisible || isScheduledManagementEmailingVisible) && isDeleteVisible, + }, { type: "button", itemId: "delete_dashboard", // careful, also a s- class selector, do not change @@ -192,6 +218,7 @@ export function useDefaultMenuItems(): IMenuButtonItem[] { defaultOnExportToPdf, defaultOnSaveAs, defaultOnScheduleEmailing, + defaultOnScheduleEmailingManagement, intl, isEmptyLayout, isExportPdfEntitlementPresent, @@ -202,6 +229,7 @@ export function useDefaultMenuItems(): IMenuButtonItem[] { isReadOnly, isSaveAsNewHidden, isScheduledEmailingVisible, + isScheduledManagementEmailingVisible, isStandaloneSaveAsNewButtonVisible, menuButtonItemsVisibility.deleteButton, menuButtonItemsVisibility.pdfExportButton, diff --git a/libs/sdk-ui-dashboard/src/presentation/widget/insightMenu/DefaultDashboardInsightMenu/getDefaultInsightMenuItems.ts b/libs/sdk-ui-dashboard/src/presentation/widget/insightMenu/DefaultDashboardInsightMenu/getDefaultInsightMenuItems.ts index 39d22a1e98c..feb830c8741 100644 --- a/libs/sdk-ui-dashboard/src/presentation/widget/insightMenu/DefaultDashboardInsightMenu/getDefaultInsightMenuItems.ts +++ b/libs/sdk-ui-dashboard/src/presentation/widget/insightMenu/DefaultDashboardInsightMenu/getDefaultInsightMenuItems.ts @@ -1,4 +1,4 @@ -// (C) 2021-2022 GoodData Corporation +// (C) 2021-2024 GoodData Corporation import { IntlShape } from "react-intl"; import compact from "lodash/compact.js"; @@ -13,10 +13,13 @@ export function getDefaultInsightMenuItems( exportXLSXDisabled: boolean; exportCSVDisabled: boolean; scheduleExportDisabled: boolean; + scheduleExportManagementDisabled: boolean; onExportXLSX: () => void; onExportCSV: () => void; onScheduleExport: () => void; + onScheduleManagementExport: () => void; isScheduleExportVisible: boolean; + isScheduleExportManagementVisible: boolean; isDataError: boolean; }, ): IInsightMenuItem[] { @@ -24,10 +27,13 @@ export function getDefaultInsightMenuItems( exportCSVDisabled, exportXLSXDisabled, scheduleExportDisabled, + scheduleExportManagementDisabled, onExportCSV, onExportXLSX, onScheduleExport, + onScheduleManagementExport, isScheduleExportVisible, + isScheduleExportManagementVisible, isDataError, } = config; @@ -35,6 +41,11 @@ export function getDefaultInsightMenuItems( ? intl.formatMessage({ id: "options.menu.unsupported.error" }) : intl.formatMessage({ id: "options.menu.unsupported.loading" }); + const isSomeExportVisible = !exportXLSXDisabled || !exportCSVDisabled; + const isSomeScheduleVisible = + (isScheduleExportVisible && !scheduleExportDisabled) || + (isScheduleExportManagementVisible && !scheduleExportManagementDisabled); + return compact([ { type: "button", @@ -56,6 +67,11 @@ export function getDefaultInsightMenuItems( icon: "gd-icon-download", className: "s-options-menu-export-csv", }, + isSomeScheduleVisible && + isSomeExportVisible && { + itemId: "ScheduleExportSeparator", + type: "separator", + }, isScheduleExportVisible && { type: "button", itemId: "ScheduleExport", @@ -66,5 +82,15 @@ export function getDefaultInsightMenuItems( icon: "gd-icon-clock", className: "s-options-menu-schedule-export", }, + isScheduleExportManagementVisible && { + type: "button", + itemId: "ScheduleExportEdit", + itemName: intl.formatMessage({ id: "widget.options.menu.scheduleExport.edit" }), + onClick: onScheduleManagementExport, + disabled: scheduleExportManagementDisabled, + tooltip, + icon: "gd-icon-list", + className: "s-options-menu-schedule-export-edit", + }, ]); } diff --git a/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/DefaultDashboardInsightWidget.tsx b/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/DefaultDashboardInsightWidget.tsx index 1c381e652af..f0310ba5d6c 100644 --- a/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/DefaultDashboardInsightWidget.tsx +++ b/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/DefaultDashboardInsightWidget.tsx @@ -2,14 +2,18 @@ import React, { useMemo, useCallback } from "react"; import cx from "classnames"; import { useIntl } from "react-intl"; -import { IInsight, insightVisualizationType, widgetTitle } from "@gooddata/sdk-model"; +import { IInsight, widgetTitle, insightVisualizationType } from "@gooddata/sdk-model"; import { VisType } from "@gooddata/sdk-ui"; import { useDashboardSelector, isCustomWidget, useDashboardScheduledEmails, + useWorkspaceAutomations, selectSettings, + selectWebhooks, + selectCanManageWorkspace, + selectEnableScheduling, } from "../../../../model/index.js"; import { DashboardItem, @@ -38,6 +42,22 @@ const DefaultDashboardInsightWidgetCore: React.FC< IDefaultDashboardInsightWidgetProps & { insight: IInsight } > = ({ widget, insight, screen, onError, onExportReady, onLoadingChanged, dashboardItemClasses }) => { const intl = useIntl(); + + const isScheduledEmailingEnabled = useDashboardSelector(selectEnableScheduling); + const webhooks = useDashboardSelector(selectWebhooks); + const isWorkspaceManager = useDashboardSelector(selectCanManageWorkspace); + const numberOfAvailableWebhooks = webhooks.length; + + const { result: automations = [] } = useWorkspaceAutomations({ + enable: isScheduledEmailingEnabled, + }); + + /** + * We want to hide scheduling when there are no webhooks unless the user is admin. + */ + const isScheduleExportVisible = numberOfAvailableWebhooks > 0 || isWorkspaceManager; + const isScheduleExportManagementVisible = isScheduleExportVisible && automations.length > 0; + const visType = insightVisualizationType(insight) as VisType; const { ref: widgetRef } = widget; @@ -47,12 +67,18 @@ const DefaultDashboardInsightWidgetCore: React.FC< insight, }); - const { onScheduleEmailingOpen } = useDashboardScheduledEmails(); + const { onScheduleEmailingOpen, onScheduleEmailingManagementOpen } = useDashboardScheduledEmails(); const onScheduleExport = useCallback(() => { onScheduleEmailingOpen(); }, [onScheduleEmailingOpen]); + + const onScheduleManagementExport = useCallback(() => { + onScheduleEmailingManagementOpen(widget); + }, [onScheduleEmailingManagementOpen, widget]); + const scheduleExportEnabled = !isCustomWidget(widget); + const scheduleExportManagementEnabled = !isCustomWidget(widget); const { closeMenu, isMenuOpen, menuItems, openMenu } = useInsightMenu({ insight, @@ -60,10 +86,13 @@ const DefaultDashboardInsightWidgetCore: React.FC< exportCSVEnabled, exportXLSXEnabled, scheduleExportEnabled, + scheduleExportManagementEnabled, onExportCSV, onExportXLSX, onScheduleExport, - isScheduleExportVisible: false, // Exporting insights is not supported yet + onScheduleManagementExport, + isScheduleExportVisible, + isScheduleExportManagementVisible, }); const toggleMenu = useCallback(() => { if (isMenuOpen) { diff --git a/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/useInsightMenu.ts b/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/useInsightMenu.ts index bbbbc345adc..8484b356b47 100644 --- a/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/useInsightMenu.ts +++ b/libs/sdk-ui-dashboard/src/presentation/widget/widget/InsightWidget/useInsightMenu.ts @@ -1,4 +1,4 @@ -// (C) 2021-2022 GoodData Corporation +// (C) 2021-2024 GoodData Corporation import { useCallback, useMemo, useState, Dispatch, SetStateAction } from "react"; import { useIntl } from "react-intl"; @@ -23,10 +23,13 @@ type UseInsightMenuConfig = { exportCSVEnabled: boolean; exportXLSXEnabled: boolean; scheduleExportEnabled: boolean; + scheduleExportManagementEnabled: boolean; onExportCSV: () => void; onExportXLSX: () => void; onScheduleExport: () => void; + onScheduleManagementExport: () => void; isScheduleExportVisible: boolean; + isScheduleExportManagementVisible: boolean; }; export const useInsightMenu = ( @@ -59,10 +62,13 @@ function useDefaultMenuItems( exportCSVEnabled, exportXLSXEnabled, scheduleExportEnabled, + scheduleExportManagementEnabled, onExportCSV, onExportXLSX, onScheduleExport, + onScheduleManagementExport, isScheduleExportVisible, + isScheduleExportManagementVisible, widget, } = config; @@ -78,6 +84,7 @@ function useDefaultMenuItems( exportCSVDisabled: !exportCSVEnabled, exportXLSXDisabled: !exportXLSXEnabled, scheduleExportDisabled: !scheduleExportEnabled, + scheduleExportManagementDisabled: !scheduleExportManagementEnabled, onExportCSV: () => { setIsMenuOpen(false); onExportCSV(); @@ -90,7 +97,12 @@ function useDefaultMenuItems( setIsMenuOpen(false); onScheduleExport(); }, + onScheduleManagementExport: () => { + setIsMenuOpen(false); + onScheduleManagementExport(); + }, isScheduleExportVisible, + isScheduleExportManagementVisible, isDataError: isDataError(execution?.error), }); }, [ @@ -99,10 +111,13 @@ function useDefaultMenuItems( exportCSVEnabled, exportXLSXEnabled, scheduleExportEnabled, + scheduleExportManagementEnabled, onExportCSV, onExportXLSX, onScheduleExport, + onScheduleManagementExport, isScheduleExportVisible, + isScheduleExportManagementVisible, intl, setIsMenuOpen, ]); diff --git a/libs/sdk-ui-dashboard/src/types.ts b/libs/sdk-ui-dashboard/src/types.ts index 6cb0b14a402..8817788bca4 100644 --- a/libs/sdk-ui-dashboard/src/types.ts +++ b/libs/sdk-ui-dashboard/src/types.ts @@ -1,4 +1,4 @@ -// (C) 2007-2023 GoodData Corporation +// (C) 2007-2024 GoodData Corporation import isEmpty from "lodash/isEmpty.js"; import { IAbsoluteDateFilter, @@ -278,3 +278,20 @@ export interface IGlobalDrillDownAttributeHierarchyDefinition { */ target: ObjRef; } + +/** + * @internal + */ +export interface IScheduleEmailContext { + /** + * Widget to schedule email for. + */ + widget?: IWidget; +} + +/** + * @internal + */ +export interface IScheduleEmailDialogContext { + insightRef?: ObjRef | undefined; +} diff --git a/libs/sdk-ui-dashboard/styles/scss/icons.scss b/libs/sdk-ui-dashboard/styles/scss/icons.scss index a0fb986009f..f3308b98531 100644 --- a/libs/sdk-ui-dashboard/styles/scss/icons.scss +++ b/libs/sdk-ui-dashboard/styles/scss/icons.scss @@ -98,6 +98,18 @@ } } +.insight-configuration .gd-list-item { + .gd-icon-list { + &::before { + content: ""; + background: url("~@gooddata/sdk-ui-dashboard/esm/assets/list.svg") no-repeat center; + width: 16px; + height: 16px; + display: inline-block; + } + } +} + .dropdown-body .gd-list-item { &.type-growIsGood, &.type-growIsBad,