From 889e61a816dbaa948bff504277e2f781cadbeb3b Mon Sep 17 00:00:00 2001 From: Kunwoo Park Date: Mon, 16 Sep 2024 09:51:58 -0700 Subject: [PATCH 1/7] Add email notification after workflow status changes --- .../execute-workflow.service.ts | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) 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..082ad97666f 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 @@ -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 { UserService } from "src/app/common/service/user/user.service"; +import { GmailService } from "src/app/common/service/gmail/gmail.service"; // 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 userService: UserService, + private gmailService: GmailService ) { workflowWebsocketService.websocketEvent().subscribe(event => { switch (event.type) { @@ -298,6 +302,56 @@ export class ExecuteWorkflowService { return; } this.updateWorkflowActionLock(stateInfo); + if ( + stateInfo.state === ExecutionState.Completed || + stateInfo.state === ExecutionState.Failed || + stateInfo.state === ExecutionState.Killed || + stateInfo.state === ExecutionState.Paused + ) { + // Check if current user is defined + const currentUser = this.userService.getCurrentUser(); + if (currentUser) { + const userEmail = currentUser.email; + const workflowId = this.workflowActionService.getWorkflow().wid; + const workflowName = this.workflowActionService.getWorkflow().name; + const state = stateInfo.state; + 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 dashboardUrl = `https://texera.ics.uci.edu/dashboard/workspace/${workflowId}`; + + // Construct email subject and content + const subject = `Workflow ${workflowName} (${workflowId}) Status: ${state}`; + const content = ` + Hello, + + The workflow with the following details has changed its state: + + - Workflow ID: ${workflowId} + - Workflow Name: ${workflowName} + - State: ${state} + - Timestamp: ${timestamp} + + You can view more details by visiting: ${dashboardUrl} + + Regards, + Texera Team + `; + + // Send the email + this.gmailService.sendEmail(subject, content, userEmail); + } else { + console.log("Current user is undefined. Cannot send email."); + } + } const previousState = this.currentState; // update current state this.currentState = stateInfo; From 7b0207c6d9d2103b1c5084df9064dc8fb0f55939 Mon Sep 17 00:00:00 2001 From: Kunwoo Park Date: Mon, 16 Sep 2024 10:13:44 -0700 Subject: [PATCH 2/7] refactor the code and make the url dynamic --- .../execute-workflow.service.ts | 115 ++++++++++-------- 1 file changed, 64 insertions(+), 51 deletions(-) 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 082ad97666f..47295e13588 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"; @@ -29,6 +29,7 @@ import { intersection } from "../../../common/util/set"; import { Workflow, WorkflowContent, WorkflowSettings } from "../../../common/type/workflow"; import { UserService } from "src/app/common/service/user/user.service"; 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; @@ -78,7 +79,8 @@ export class ExecuteWorkflowService { private workflowStatusService: WorkflowStatusService, private notificationService: NotificationService, private userService: UserService, - private gmailService: GmailService + private gmailService: GmailService, + @Inject(DOCUMENT) private document: Document ) { workflowWebsocketService.websocketEvent().subscribe(event => { switch (event.type) { @@ -302,55 +304,14 @@ export class ExecuteWorkflowService { return; } this.updateWorkflowActionLock(stateInfo); - if ( - stateInfo.state === ExecutionState.Completed || - stateInfo.state === ExecutionState.Failed || - stateInfo.state === ExecutionState.Killed || - stateInfo.state === ExecutionState.Paused - ) { - // Check if current user is defined - const currentUser = this.userService.getCurrentUser(); - if (currentUser) { - const userEmail = currentUser.email; - const workflowId = this.workflowActionService.getWorkflow().wid; - const workflowName = this.workflowActionService.getWorkflow().name; - const state = stateInfo.state; - 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 dashboardUrl = `https://texera.ics.uci.edu/dashboard/workspace/${workflowId}`; - - // Construct email subject and content - const subject = `Workflow ${workflowName} (${workflowId}) Status: ${state}`; - const content = ` - Hello, - - The workflow with the following details has changed its state: - - - Workflow ID: ${workflowId} - - Workflow Name: ${workflowName} - - State: ${state} - - Timestamp: ${timestamp} - - You can view more details by visiting: ${dashboardUrl} - - Regards, - Texera Team - `; - - // Send the email - this.gmailService.sendEmail(subject, content, userEmail); - } else { - console.log("Current user is undefined. Cannot send email."); - } + 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 @@ -386,6 +347,58 @@ 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 currentUser = this.userService.getCurrentUser(); + if (!currentUser) { + console.log("Current user is undefined. Cannot send email."); + return; + } + + const userEmail = currentUser.email; + 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, userEmail); + } + /** * Transform a workflowGraph object to the HTTP request body according to the backend API. * From 246e857cb3cef43b10690b342a61482ebadcd0b3 Mon Sep 17 00:00:00 2001 From: linxinyuan Date: Mon, 16 Sep 2024 15:51:32 -0700 Subject: [PATCH 3/7] fix test --- .../service/execute-workflow/execute-workflow.service.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts index 8b71687a943..367fa898503 100644 --- a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts +++ b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts @@ -14,6 +14,8 @@ import { mockLogicalPlan_scan_result, mockWorkflowPlan_scan_result } from "./moc import { HttpClient } from "@angular/common/http"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { WorkflowSnapshotService } from "../../../dashboard/service/user/workflow-snapshot/workflow-snapshot.service"; +import { UserService } from "../../../common/service/user/user.service"; +import { StubUserService } from "../../../common/service/user/stub-user.service"; class StubHttpClient { public post(): Observable { @@ -38,6 +40,7 @@ describe("ExecuteWorkflowService", () => { useClass: StubOperatorMetadataService, }, { provide: HttpClient, useClass: StubHttpClient }, + { provide: UserService, useClass: StubUserService }, ], }); From a2cd64791f9639b1af570e5b1c9e66f9dcf17dee Mon Sep 17 00:00:00 2001 From: linxinyuan Date: Mon, 16 Sep 2024 15:54:50 -0700 Subject: [PATCH 4/7] fix test --- .../service/execute-workflow/execute-workflow.service.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts index 367fa898503..8b71687a943 100644 --- a/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts +++ b/core/gui/src/app/workspace/service/execute-workflow/execute-workflow.service.spec.ts @@ -14,8 +14,6 @@ import { mockLogicalPlan_scan_result, mockWorkflowPlan_scan_result } from "./moc import { HttpClient } from "@angular/common/http"; import { WorkflowUtilService } from "../workflow-graph/util/workflow-util.service"; import { WorkflowSnapshotService } from "../../../dashboard/service/user/workflow-snapshot/workflow-snapshot.service"; -import { UserService } from "../../../common/service/user/user.service"; -import { StubUserService } from "../../../common/service/user/stub-user.service"; class StubHttpClient { public post(): Observable { @@ -40,7 +38,6 @@ describe("ExecuteWorkflowService", () => { useClass: StubOperatorMetadataService, }, { provide: HttpClient, useClass: StubHttpClient }, - { provide: UserService, useClass: StubUserService }, ], }); From 2ab1402481e97c457adc0b0f308f4cc956b936ec Mon Sep 17 00:00:00 2001 From: Kunwoo Park Date: Mon, 16 Sep 2024 23:04:18 -0700 Subject: [PATCH 5/7] Remove UserService from constructor --- .../execute-workflow/execute-workflow.service.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) 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 47295e13588..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 @@ -27,7 +27,6 @@ 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 { UserService } from "src/app/common/service/user/user.service"; import { GmailService } from "src/app/common/service/gmail/gmail.service"; import { DOCUMENT } from "@angular/common"; @@ -78,7 +77,6 @@ export class ExecuteWorkflowService { private workflowWebsocketService: WorkflowWebsocketService, private workflowStatusService: WorkflowStatusService, private notificationService: NotificationService, - private userService: UserService, private gmailService: GmailService, @Inject(DOCUMENT) private document: Document ) { @@ -356,13 +354,6 @@ export class ExecuteWorkflowService { * @param stateInfo - The new execution state information containing the updated state of the workflow. */ private sendWorkflowStatusEmail(stateInfo: ExecutionStateInfo): void { - const currentUser = this.userService.getCurrentUser(); - if (!currentUser) { - console.log("Current user is undefined. Cannot send email."); - return; - } - - const userEmail = currentUser.email; const workflow = this.workflowActionService.getWorkflow(); const timestamp = new Date().toLocaleString("en-US", { @@ -396,7 +387,7 @@ export class ExecuteWorkflowService { Texera Team `; - this.gmailService.sendEmail(subject, content, userEmail); + this.gmailService.sendEmail(subject, content); } /** From c77cdd96cfcc992012c98a500e6b8a468278fcde Mon Sep 17 00:00:00 2001 From: Kunwoo Park Date: Mon, 16 Sep 2024 23:15:21 -0700 Subject: [PATCH 6/7] Fix Frontend Unit test --- .../context-menu/context-menu/context-menu.component.spec.ts | 5 +++++ .../service/operator-menu/operator-menu.service.spec.ts | 2 ++ .../operator-reuse-cache-status.service.spec.ts | 2 ++ 3 files changed, 9 insertions(+) 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..0f41ee25fbe 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,10 @@ 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 { GmailService } from "src/app/common/service/gmail/gmail.service"; +import { ExecuteWorkflowService } from "src/app/workspace/service/execute-workflow/execute-workflow.service"; +import { OperatorMenuService } from "src/app/workspace/service/operator-menu/operator-menu.service"; +import { HttpClientModule } from "@angular/common/http"; describe("ContextMenuComponent", () => { let component: ContextMenuComponent; @@ -12,6 +16,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/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); }); From 56575f928bbae38422d73bf018ef9d72a531f171 Mon Sep 17 00:00:00 2001 From: Kunwoo Park Date: Mon, 16 Sep 2024 23:18:53 -0700 Subject: [PATCH 7/7] Remove unnecessary import --- .../context-menu/context-menu/context-menu.component.spec.ts | 3 --- 1 file changed, 3 deletions(-) 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 0f41ee25fbe..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,9 +3,6 @@ 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 { GmailService } from "src/app/common/service/gmail/gmail.service"; -import { ExecuteWorkflowService } from "src/app/workspace/service/execute-workflow/execute-workflow.service"; -import { OperatorMenuService } from "src/app/workspace/service/operator-menu/operator-menu.service"; import { HttpClientModule } from "@angular/common/http"; describe("ContextMenuComponent", () => {