Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Job Viewer visual element to display job details #2051

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions frontend/taipy/src/JobDetailsViewer.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not reuse the JobViewer component ?

Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/*
* Copyright 2021-2024 Avaiga Private Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

import React, { useEffect, useCallback } from "react";
import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid2";
import ListItemText from "@mui/material/ListItemText";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";

import {
createRequestUpdateAction,
createSendActionNameAction,
getUpdateVar,
useDispatch,
useDispatchRequestUpdateOnFirstRender,
useModule,
} from "taipy-gui";

import { useClassNames, EllipsisSx, SecondaryEllipsisProps, CoreProps, calculateTimeDifference } from "./utils";
import StatusChip from "./StatusChip";

interface JobDetailsViewerProps extends CoreProps {
job: JobDetail;
showSubmitId?: boolean,
showTaskLabel?: boolean,
showSubmittedLabel?: boolean,
showExecutionDuration?: boolean,
showPendingDuration?: boolean,
showBlockedDuration?: boolean,
showSubmissionDuration?: boolean,
showCancel?: boolean;
showDelete?: boolean;
showStackTrace?: boolean,
onJobAction?: string;
updateJbVars?: string;
height?: string,
width?: string;
}

// job id, creation date, status, submit id, job name, entity id, entity name, pending duration, blocked duration, finished at, not cancellable, not deletable, execution time, logs,
export type JobDetail = [string, string, number, string, string, string, string, string, string, string, string, string, string, string[]];
const invalidJob: JobDetail = ["", "", 0, "", "", "", "", "", "" , "", "", "", "", []];

const JobDetailsViewer = (props: JobDetailsViewerProps) => {
const {
updateVarName = "",
id = "",
updateJbVars = "",
height = "50vh",
width = "50vw",
coreChanged
} = props;

const [
jobId,
creationDate,
status,
submitId,
jobName,
entityId,
entityName,
pendingDuration,
blockedDuration,
finishedAt,
notCancellable,
notDeletable,
executionDuration,
stacktrace,
] = props.job || invalidJob;

const dispatch = useDispatch();
const module = useModule();

const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);

useDispatchRequestUpdateOnFirstRender(dispatch, id, module, undefined, updateVarName);

const handleDeleteJob = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
try {
dispatch(
createSendActionNameAction(props.id, module, props.onJobAction, {
id: jobId,
action: "delete",
error_id: getUpdateVar(updateJbVars, "error_id"),
})
);
} catch (e) {
console.warn("Error parsing id for delete.", e);
}
},
[jobId, dispatch, module, props.id, props.onJobAction, updateJbVars]
);

const handleCancelJob = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
try {
dispatch(
createSendActionNameAction(props.id, module, props.onJobAction, {
id: jobId,
action: "cancel",
error_id: getUpdateVar(updateJbVars, "error_id"),
})
);
} catch (e) {
console.warn("Error parsing id for cancel.", e);
}
},
[jobId, dispatch, module, props.id, props.onJobAction, updateJbVars]
)

useEffect(() => {
if (coreChanged?.job == jobId) {
updateVarName && dispatch(createRequestUpdateAction(id, module, [updateVarName], true));
}
}, [coreChanged, updateVarName, jobId, module, dispatch, id]);

return (
<Grid container className={className} sx={{ maxWidth: width, maxHeight: height }}>
<Grid size={4}>
<Typography>Job Id</Typography>
</Grid>
<Grid size={8}>
<Tooltip title={jobId}>
<Typography sx={EllipsisSx}>{jobId}</Typography>
</Tooltip>
</Grid>
<Grid size={4}>
<Typography>Creation date</Typography>
</Grid>
<Grid size={8}>
<Typography>{creationDate ? new Date(creationDate).toLocaleString() : ""}</Typography>
</Grid>
<Grid size={4}>
<Typography>Status</Typography>
</Grid>
<Grid size={8}>
<StatusChip status={status} />
</Grid>
{props.showSubmitId && (
<Grid>
<Grid size={4}>
<Typography>Submit Id</Typography>
</Grid>
<Grid size={8}>
<Tooltip title={submitId}>
<Typography sx={EllipsisSx}>{submitId}</Typography>
</Tooltip>
</Grid>
</Grid>

)}
{props.showTaskLabel && (
<Grid>
<Grid size={4}>
<Typography>Task label</Typography>
</Grid>
<Grid size={8}>
<Tooltip title={jobName}>
<Typography sx={EllipsisSx}>{jobName}</Typography>
</Tooltip>
</Grid>
</Grid>
)}
{props.showSubmittedLabel && (
<Grid>
<Grid size={4}>
<Typography>Submitted entity</Typography>
</Grid>
<Grid size={8}>
<Tooltip title={entityId}>
<ListItemText
primary={entityName}
secondary={entityId}
secondaryTypographyProps={SecondaryEllipsisProps}
/>
</Tooltip>
</Grid>
</Grid>
)}
{props.showExecutionDuration && (
<Grid>
<Grid size={4}>
<Typography>Execution Duration</Typography>
</Grid>
<Grid size={8}>
<Typography>{executionDuration}</Typography>
</Grid>
</Grid>
)}
{props.showPendingDuration && (
<Grid>
<Grid size={4}>
<Typography>Pending time</Typography>
</Grid>
<Grid size={8}>
<Typography>{pendingDuration}</Typography>
</Grid>
</Grid>
)}
{props.showBlockedDuration && (
<Grid>
<Grid size={4}>
<Typography>Blocked time</Typography>
</Grid>
<Grid size={8}>
<Typography>{blockedDuration}</Typography>
</Grid>
</Grid>
)}
{props.showSubmissionDuration && (
<Grid>
<Grid size={4}>
<Typography>Submission duration</Typography>
</Grid>
<Grid size={8}>
<Typography>{(creationDate && finishedAt) ? calculateTimeDifference(new Date(creationDate), new Date(finishedAt)) : ""}</Typography>
</Grid>
</Grid>
)}
<Divider />
{props.showStackTrace && (
<Grid>
<Grid size={12}>
<Typography>Stack Trace</Typography>
</Grid>
<Grid size={12}>
<Typography variant="caption" component="pre" overflow="auto" maxHeight="50vh">
{stacktrace.join("<br/>")}
</Typography>
</Grid>
</Grid>
)}
{props.showCancel ? (
<>
<Divider />
<Grid size={6}>
<Tooltip title={notCancellable}>
<span>
<Button variant="outlined" onClick={handleCancelJob} disabled={!!notCancellable}>
Cancel
</Button>
</span>
</Tooltip>
</Grid>
</>
) : null}
{props.showDelete ? (
<>
<Divider />
<Grid size={6}>
<Tooltip title={notDeletable}>
<span>
<Button variant="outlined" onClick={handleDeleteJob} disabled={!!notDeletable}>
Delete
</Button>
</span>
</Tooltip>
</Grid>
</>
) : null}
</Grid>
);
};

export default JobDetailsViewer;
3 changes: 2 additions & 1 deletion frontend/taipy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ScenarioViewer from "./ScenarioViewer";
import ScenarioDag from "./ScenarioDag";
import NodeSelector from "./NodeSelector";
import JobSelector from "./JobSelector";
import JobDetailsViewer from "./JobDetailsViewer";
import DataNodeViewer from "./DataNodeViewer";

export { ScenarioSelector, ScenarioDag, ScenarioViewer as Scenario, NodeSelector as DataNodeSelector, JobSelector, DataNodeViewer as DataNode };
export { ScenarioSelector, ScenarioDag, ScenarioViewer as Scenario, NodeSelector as DataNodeSelector, JobSelector, DataNodeViewer as DataNode, JobDetailsViewer as JobViewer };
16 changes: 16 additions & 0 deletions frontend/taipy/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,19 @@ export const getUpdateVarNames = (updateVars: string, ...vars: string[]) =>

export const EllipsisSx = { textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" };
export const SecondaryEllipsisProps = { sx: EllipsisSx };

export const calculateTimeDifference = (date1: Date, date2: Date) => {
const milliseconds1 = date1.getTime();
const milliseconds2 = date2.getTime();

const differenceInMilliseconds = milliseconds2 - milliseconds1;
const seconds = Math.floor(differenceInMilliseconds / 1000);

const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secondsRemaining = seconds % 60;

const formattedTime = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secondsRemaining.toString().padStart(2, '0')}`;

return formattedTime;
}
10 changes: 10 additions & 0 deletions taipy/core/job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,16 @@
from ... import core as tp

return tp.is_deletable(self)

def is_cancellable(self) -> bool:
"""Indicate if the job can be cancelled.

Returns:
A Boolean value, which is True if the job can be cancelled. False otherwise.
"""

can_be_cancelled = self.is_pending or self.is_blocked or self.is_submitted

Check failure on line 471 in taipy/core/job/job.py

View workflow job for this annotation

GitHub Actions / partial-tests / linter

Function "is_pending" could always be true in boolean context [truthy-function]
return bool(can_be_cancelled)

def get_event_context(self):
return {"task_config_id": self._task.config_id}
Expand Down
35 changes: 35 additions & 0 deletions taipy/gui_core/_GuiCoreLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,41 @@ class _GuiCore(ElementLibrary):
),
},
),
"job_viewer": Element(
"value",
{
"id": ElementProperty(PropertyType.string),
"class_name": ElementProperty(PropertyType.dynamic_string),
"value": ElementProperty(PropertyType.lov_value),
"show_submit_id": ElementProperty(PropertyType.boolean, False),
"show_task_label": ElementProperty(PropertyType.boolean, True),
"show_submitted_label": ElementProperty(PropertyType.boolean, True),
"show_execution_duration": ElementProperty(PropertyType.boolean, True),
"show_pending_duration": ElementProperty(PropertyType.boolean, False),
"show_blocked_duration": ElementProperty(PropertyType.boolean, False),
"show_submission_duration": ElementProperty(PropertyType.boolean, True),
"show_cancel": ElementProperty(PropertyType.boolean, True),
"show_delete": ElementProperty(PropertyType.boolean, True),
"show_stack_trace": ElementProperty(PropertyType.boolean, True),
"height": ElementProperty(PropertyType.string, "50vh"),
"width": ElementProperty(PropertyType.string, "50vw"),
},
inner_properties={
"job": ElementProperty(
PropertyType.react,
f"{{{__CTX_VAR_NAME}.get_complete_job_details(" + f"{__JOB_DETAIL_ID_VAR}<tp:uniq:jb>)}}",
),
"core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
"type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
"on_job_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.act_on_jobs}}"),
"error": ElementProperty(PropertyType.dynamic_string, f"{{{__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>}}"),
"update_jb_vars": ElementProperty(
PropertyType.string,
f"error_id={__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>;"
+ f"detail_id={__JOB_DETAIL_ID_VAR}<tp:uniq:jb>;",
),
},
),
}

def get_name(self) -> str:
Expand Down
Loading
Loading