Skip to content

Commit

Permalink
Add Email Notification Logic (#2837)
Browse files Browse the repository at this point in the history
This PR introduces functionality to send an email notification whenever
a workflow transitions from a running state to a non-running state
(e.g., `Completed`, `Failed`, `Killed`, or `Paused`). This feature was
requested by the bioinformatics team at Cornell University to facilitate
their long-running workflows. They wanted to be notified once the
execution of their workflow is finished.

### Implementation Details

A new `sendWorkflowStatusEmail` method has been added to handle the
construction of the email notification, which includes details such as
the workflow ID, name, state, timestamp, and a link to the dashboard.
This method is triggered on the frontend whenever it detects a status
change from the backend.

### Setup
To enable this functionality, update the following parameters in the
core/amber/src/main/resources/application.conf file:
1. Set `user-sys.enabled` to true.
2. Set `user-sys.google.clientId` to your Google API client ID.
3. Set `user-sys.google.smtp.gmail` to your Gmail ID (ensure that IMAP
is enabled for this account).
4. Set `user-sys.google.smtp.password` to your Gmail password.



https://github.com/user-attachments/assets/dd72af90-b8a8-45ef-9bcc-1ba628e55e9a

---------

Co-authored-by: Kunwoo Park <[email protected]>
Co-authored-by: linxinyuan <[email protected]>
  • Loading branch information
3 people authored and PurelyBlank committed Dec 4, 2024
1 parent dbf3366 commit 4bc7c61
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +13,7 @@ describe("ContextMenuComponent", () => {
await TestBed.configureTestingModule({
declarations: [ContextMenuComponent],
providers: [{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService }],
imports: [HttpClientModule],
}).compileComponents();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ 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;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [{ provide: OperatorMetadataService, useClass: StubOperatorMetadataService }],
imports: [HttpClientModule],
});
service = TestBed.inject(OperatorMenuService);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,6 +16,7 @@ describe("OperatorCacheStatusService", () => {
useClass: StubOperatorMetadataService,
},
],
imports: [HttpClientModule],
});
service = TestBed.inject(OperatorReuseCacheStatusService);
});
Expand Down

0 comments on commit 4bc7c61

Please sign in to comment.