diff --git a/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx b/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx index 4c51b9eb..9913b37e 100644 --- a/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx +++ b/frontend/src/features/myWorkflows/components/WorkflowDetail/WorkflowRunsTable.tsx @@ -12,6 +12,7 @@ import React, { useState, } from "react"; import { useNavigate } from "react-router-dom"; +import { secondsToHMS } from "utils"; import { States } from "./States"; import { WorkflowRunTableFooter } from "./WorkflowRunTableFooter"; @@ -78,13 +79,13 @@ export const WorkflowRunsTable = forwardRef( valueFormatter: ({ value }) => new Date(value).toLocaleString(), }, { - field: "execution_date", - headerName: "Execution Date", + field: "duration_in_seconds", + headerName: "Duration", headerAlign: "center", align: "center", minWidth: 150, flex: 1, - valueFormatter: ({ value }) => new Date(value).toLocaleString(), + valueFormatter: ({ value }) => (value ? secondsToHMS(value) : "N/A"), }, { field: "state", diff --git a/frontend/src/features/myWorkflows/components/WorkflowsList/index.tsx b/frontend/src/features/myWorkflows/components/WorkflowsList/index.tsx index 9c8c441b..e5e7b6c6 100644 --- a/frontend/src/features/myWorkflows/components/WorkflowsList/index.tsx +++ b/frontend/src/features/myWorkflows/components/WorkflowsList/index.tsx @@ -90,8 +90,9 @@ export const WorkflowList: React.FC = () => { align: "center", headerAlign: "center", sortable: false, + minWidth: 100, }, - { field: "name", headerName: "Workflow Name", flex: 2 }, + { field: "name", headerName: "Workflow Name", flex: 2, minWidth: 220 }, { field: "start_date", headerName: ( @@ -104,15 +105,35 @@ export const WorkflowList: React.FC = () => { ) as any, flex: 1, align: "center", + minWidth: 220, valueFormatter: ({ value }) => new Date(value).toLocaleString(), headerAlign: "center", }, + { + field: "end_date", + headerName: ( + + + End Date{" "} + + + + ) as any, + headerAlign: "center", + align: "center", + type: "string", + flex: 1, + minWidth: 220, + valueFormatter: ({ value }) => + value ? new Date(value).toLocaleString() : "None", + }, { field: "created_at", headerName: "Created At", flex: 1, align: "center", + minWidth: 220, valueFormatter: ({ value }) => new Date(value).toLocaleString(), headerAlign: "center", @@ -124,6 +145,7 @@ export const WorkflowList: React.FC = () => { align: "center", valueFormatter: ({ value }) => new Date(value).toLocaleString(), headerAlign: "center", + minWidth: 220, }, { field: "schedule", @@ -132,6 +154,7 @@ export const WorkflowList: React.FC = () => { align: "center", headerAlign: "center", sortable: false, + minWidth: 100, }, { field: "next_dagrun", @@ -139,6 +162,7 @@ export const WorkflowList: React.FC = () => { flex: 1, align: "center", headerAlign: "center", + minWidth: 220, sortable: false, valueFormatter: ({ value }) => value ? new Date(value).toLocaleString() : "none", @@ -168,6 +192,7 @@ export const WorkflowList: React.FC = () => { headerAlign: "center", align: "center", sortable: false, + minWidth: 150, }, ], [], diff --git a/frontend/src/utils/datetime.ts b/frontend/src/utils/datetime.ts new file mode 100644 index 00000000..d0407b6d --- /dev/null +++ b/frontend/src/utils/datetime.ts @@ -0,0 +1,11 @@ +export function secondsToHMS(totalSeconds: number) { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = Math.floor(totalSeconds % 60); + + const formattedHours = hours < 10 ? "0" + hours : hours; + const formattedMinutes = minutes < 10 ? "0" + minutes : minutes; + const formattedSeconds = seconds < 10 ? "0" + seconds : seconds; + + return formattedHours + ":" + formattedMinutes + ":" + formattedSeconds; +} diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 881ba8c4..6f710cd8 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -8,3 +8,4 @@ export { useInterval } from "./useInterval"; export { useMouseProximity } from "./useMouseProximity"; export { exportToJson } from "./downloadJson"; export { isEmpty } from "./isEmpty"; +export { secondsToHMS } from "./datetime"; diff --git a/rest/schemas/responses/workflow.py b/rest/schemas/responses/workflow.py index a4195741..55d4a824 100644 --- a/rest/schemas/responses/workflow.py +++ b/rest/schemas/responses/workflow.py @@ -70,6 +70,7 @@ class GetWorkflowsResponseData(BaseModel): name: str created_at: datetime start_date: datetime + end_date: Optional[datetime] last_changed_at: datetime last_changed_by: int created_by: int @@ -153,6 +154,7 @@ class GetWorkflowRunsResponseData(BaseModel): start_date: Optional[datetime] = None end_date: Optional[datetime] = None execution_date: Optional[datetime] = None + duration_in_seconds: Optional[float] = None state: Optional[WorkflowRunState] = None @field_validator('state') diff --git a/rest/services/workflow_service.py b/rest/services/workflow_service.py index a352e6d2..9a8b9604 100644 --- a/rest/services/workflow_service.py +++ b/rest/services/workflow_service.py @@ -219,6 +219,7 @@ async def list_workflows( name=dag_data.name, created_at=dag_data.created_at, start_date=dag_data.start_date, + end_date=dag_data.end_date, last_changed_at=dag_data.last_changed_at, last_changed_by=dag_data.last_changed_by, created_by=dag_data.created_by, @@ -520,9 +521,12 @@ def run_workflow(self, workflow_id: int): raise ResourceNotFoundException("Workflow not found") # Check if start date is in the past - if workflow.start_date and workflow.start_date > datetime.utcnow().replace(tzinfo=timezone.utc): + if workflow.start_date and workflow.start_date > datetime.now(tz=timezone.utc): raise ForbiddenException('Workflow start date is in the future. Can not run it now.') + if workflow.end_date and workflow.end_date < datetime.now(tz=timezone.utc): + raise ForbiddenException('You cannot run workflows that have ended.') + airflow_workflow_id = workflow.uuid_name # Force unpause workflow @@ -641,9 +645,17 @@ def list_workflow_runs(self, workflow_id: int, page: int, page_size: int): else: dag_runs = response_data['dag_runs'] - data = [ - GetWorkflowRunsResponseData(**run) for run in dag_runs - ] + data = [] + for run in dag_runs: + #duration = run.get('end_date') - run.get('start_date') + end_date_dt = datetime.fromisoformat(run.get('end_date')) + start_date_dt = datetime.fromisoformat(run.get('start_date')) + duration = end_date_dt - start_date_dt + run['duration_in_seconds'] = duration.total_seconds() + data.append( + GetWorkflowRunsResponseData(**run) + ) + response = GetWorkflowRunsResponse( data=data, metadata=dict(