diff --git a/core/gui/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts b/core/gui/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts index bdb1784d48c..c05121bf57a 100644 --- a/core/gui/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts +++ b/core/gui/src/app/workspace/component/workflow-editor/context-menu/context-menu/context-menu.component.spec.ts @@ -3,6 +3,7 @@ import { OperatorMetadataService } from "src/app/workspace/service/operator-meta import { StubOperatorMetadataService } from "src/app/workspace/service/operator-metadata/stub-operator-metadata.service"; import { ContextMenuComponent } from "./context-menu.component"; +import { HttpClientModule } from "@angular/common/http"; describe("ContextMenuComponent", () => { let component: ContextMenuComponent; @@ -12,6 +13,7 @@ describe("ContextMenuComponent", () => { await TestBed.configureTestingModule({ declarations: [ContextMenuComponent], providers: [{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService }], + imports: [HttpClientModule], }).compileComponents(); }); diff --git a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts index 4176537ae05..e7360b1a447 100644 --- a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts +++ b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { from, Observable, Subject } from "rxjs"; import { WorkflowActionService } from "../workflow-graph/model/workflow-action.service"; import { WorkflowGraphReadonly } from "../workflow-graph/model/workflow-graph"; @@ -27,6 +27,8 @@ import { WorkflowStatusService } from "../workflow-status/workflow-status.servic import { isDefined } from "../../../common/util/predicate"; import { intersection } from "../../../common/util/set"; import { Workflow, WorkflowContent, WorkflowSettings } from "../../../common/type/workflow"; +import { GmailService } from "src/app/common/service/gmail/gmail.service"; +import { DOCUMENT } from "@angular/common"; // TODO: change this declaration export const FORM_DEBOUNCE_TIME_MS = 150; @@ -74,7 +76,9 @@ export class ExecuteWorkflowService { private workflowActionService: WorkflowActionService, private workflowWebsocketService: WorkflowWebsocketService, private workflowStatusService: WorkflowStatusService, - private notificationService: NotificationService + private notificationService: NotificationService, + private gmailService: GmailService, + @Inject(DOCUMENT) private document: Document ) { workflowWebsocketService.websocketEvent().subscribe(event => { switch (event.type) { @@ -298,6 +302,15 @@ export class ExecuteWorkflowService { return; } this.updateWorkflowActionLock(stateInfo); + const isTransitionFromRunningToNonRunning = + this.currentState.state === ExecutionState.Running && + [ExecutionState.Completed, ExecutionState.Failed, ExecutionState.Killed, ExecutionState.Paused].includes( + stateInfo.state + ); + + if (isTransitionFromRunningToNonRunning) { + this.sendWorkflowStatusEmail(stateInfo); + } const previousState = this.currentState; // update current state this.currentState = stateInfo; @@ -332,6 +345,51 @@ export class ExecuteWorkflowService { } } + /** + * Sends an email notification about the change in workflow state. + * This method constructs the email content with details such as the workflow ID, name, + * new state, and a timestamp, then sends it to the user's email address. + * The email is sent only if the current user is defined. + * + * @param stateInfo - The new execution state information containing the updated state of the workflow. + */ + private sendWorkflowStatusEmail(stateInfo: ExecutionStateInfo): void { + const workflow = this.workflowActionService.getWorkflow(); + const timestamp = + new Date().toLocaleString("en-US", { + timeZone: "UTC", + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: true, + }) + " (UTC)"; + + const baseUrl = this.document.location.origin; + const dashboardUrl = `${baseUrl}/dashboard/workspace/${workflow.wid}`; + + const subject = `Workflow ${workflow.name} (${workflow.wid}) Status: ${stateInfo.state}`; + const content = ` + Hello, + + The workflow with the following details has changed its state: + + - Workflow ID: ${workflow.wid} + - Workflow Name: ${workflow.name} + - State: ${stateInfo.state} + - Timestamp: ${timestamp} + + You can view more details by visiting: ${dashboardUrl} + + Regards, + Texera Team + `; + + this.gmailService.sendEmail(subject, content); + } + /** * Transform a workflowGraph object to the HTTP request body according to the backend API. * diff --git a/core/gui/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts b/core/gui/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts index c85397f2ca7..5dffb371d4d 100644 --- a/core/gui/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts +++ b/core/gui/src/app/workspace/service/operator-menu/operator-menu.service.spec.ts @@ -3,6 +3,7 @@ import { OperatorMetadataService } from "../operator-metadata/operator-metadata. import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { OperatorMenuService } from "./operator-menu.service"; +import { HttpClientModule } from "@angular/common/http"; describe("OperatorMenuService", () => { let service: OperatorMenuService; @@ -10,6 +11,7 @@ describe("OperatorMenuService", () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService }], + imports: [HttpClientModule], }); service = TestBed.inject(OperatorMenuService); }); diff --git a/core/gui/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.spec.ts b/core/gui/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.spec.ts index 01db1b89929..0e465b5d5e5 100644 --- a/core/gui/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.spec.ts +++ b/core/gui/src/app/workspace/service/workflow-status/operator-reuse-cache-status.service.spec.ts @@ -3,6 +3,7 @@ import { OperatorMetadataService } from "../operator-metadata/operator-metadata. import { StubOperatorMetadataService } from "../operator-metadata/stub-operator-metadata.service"; import { OperatorReuseCacheStatusService } from "./operator-reuse-cache-status.service"; +import { HttpClientModule } from "@angular/common/http"; describe("OperatorCacheStatusService", () => { let service: OperatorReuseCacheStatusService; @@ -15,6 +16,7 @@ describe("OperatorCacheStatusService", () => { useClass: StubOperatorMetadataService, }, ], + imports: [HttpClientModule], }); service = TestBed.inject(OperatorReuseCacheStatusService); });