Skip to content
Merged
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
10 changes: 10 additions & 0 deletions frontend/src/components/common/icon/icons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MdComment,
MdFileDownload,
} from "react-icons/md";
import { FaRegStopCircle } from "react-icons/fa";

// These function are needed in IconButton because it expects Icon as a function

Expand Down Expand Up @@ -53,3 +54,12 @@ export function downloadReportIcon() {
export function SpinnerIcon() {
return <Spinner type="border" size="sm" className="text-darker" />;
}

export function killJobIcon() {
return (
<span>
<FaRegStopCircle className="me-1" />
Kill job
</span>
);
}
62 changes: 62 additions & 0 deletions frontend/src/components/jobs/result/CustomJobPipelineNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react";
import PropTypes from "prop-types";
import { Handle, Position } from "reactflow";
import "reactflow/dist/style.css";
import { StatusIcon } from "../../common/icon/StatusIcon";

function CustomJobPipelineNode({ data }) {
let statusIcon = "pending";
if (data.completed) statusIcon = "success";
else if (data.running) statusIcon = "running";

return (
<>
<div
className="react-flow__node-default d-flex align-items-center"
id={`jobPipeline-${data.id}`}
style={{
background: "#5593ab",
color: "#D6D5E6",
border: "1px solid #2f515e",
minWidth: "350px",
opacity: !(data.running || data.completed) && "60%",
}}
>
<StatusIcon
size="15%"
status={statusIcon}
className={`${!data.completed && "text-dark"} m-2`}
/>
<div className="d-flex-start-start flex-column ms-2">
<h6 className="mt-2 mb-1 fw-bold text-darker">
{data?.label} {data.running && "RUNNING"}
{data.completed && "COMPLETED"}{" "}
</h6>
<strong className="fs-6">Reported {data.report}</strong>
</div>
</div>
<Handle
type="source"
position={Position.Right}
id={`jobPipelineHandle-${data.id}`}
isConnectable
hidden={data?.id === "step-4"}
style={{ opacity: "0" }}
/>
<Handle
type="target"
position={Position.Left}
id={`jobPipelineHandle-${data.id}`}
isConnectable
hidden={data?.id === "step-1"}
style={{ opacity: "0" }}
/>
</>
);
}

CustomJobPipelineNode.propTypes = {
data: PropTypes.object.isRequired,
};

export default React.memo(CustomJobPipelineNode);
176 changes: 121 additions & 55 deletions frontend/src/components/jobs/result/JobIsRunningAlert.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
/* eslint-disable id-length */
import React from "react";
import PropTypes from "prop-types";
import { Fade } from "reactstrap";
import { MdPauseCircleOutline } from "react-icons/md";
import ReactFlow, { MarkerType } from "reactflow";
import "reactflow/dist/style.css";
import { IconButton } from "@certego/certego-ui";

import { IconAlert, IconButton } from "@certego/certego-ui";

import { killJob } from "./jobApi";
import CustomJobPipelineNode from "./CustomJobPipelineNode";
import { JobStatuses } from "../../../constants/jobConst";
import { areYouSureConfirmDialog } from "../../common/areYouSureConfirmDialog";

import {
reportedPluginNumber,
reportedVisualizerNumber,
} from "./utils/reportedPlugins";
import { killJob } from "./jobApi";
import { killJobIcon } from "../../common/icon/icons";

// Important! This must be defined outside of the component
const nodeTypes = {
jobPipelineNode: CustomJobPipelineNode,
};

const defaultEdgeOptions = {
style: { strokeWidth: 3 },
type: "step",
markerEnd: {
type: MarkerType.ArrowClosed,
},
};

export function JobIsRunningAlert({ job }) {
// number of analyzers/connectors/visualizers reported (status: killed/succes/failed)
Expand Down Expand Up @@ -41,75 +57,125 @@ export function JobIsRunningAlert({ job }) {
.slice(9)
.includes(job.status);

const alertElements = [
const nodes = [
{
id: `isRunningJob-analyzers`,
position: { x: 0, y: 0 },
data: {
id: "step-1",
label: "ANALYZERS",
running: job.status === JobStatuses.ANALYZERS_RUNNING,
completed:
analizersReported === job.analyzers_to_execute.length &&
analyzersCompleted,
report: `${analizersReported}/${job.analyzers_to_execute.length}`,
},
type: "jobPipelineNode",
draggable: false,
},
{
id: `isRunningJob-connectors`,
position: { x: 450, y: 0 },
data: {
id: "step-2",
label: "CONNECTORS",
running: job.status === JobStatuses.CONNECTORS_RUNNING,
completed:
connectorsReported === job.connectors_to_execute.length &&
connectorsCompleted,
report: `${connectorsReported}/${job.connectors_to_execute.length}`,
},
type: "jobPipelineNode",
draggable: false,
},
{
id: `isRunningJob-pivots`,
position: { x: 900, y: 0 },
data: {
id: "step-3",
label: "PIVOTS",
running: job.status === JobStatuses.PIVOTS_RUNNING,
completed:
pivotsReported === job.pivots_to_execute.length && pivotsCompleted,
report: `${pivotsReported}/${job.pivots_to_execute.length}`,
},
type: "jobPipelineNode",
draggable: false,
},
{
step: 1,
type: "ANALYZERS",
completed:
analizersReported === job.analyzers_to_execute.length &&
analyzersCompleted,
report: `${analizersReported}/${job.analyzers_to_execute.length}`,
id: `isRunningJob-visualizers`,
position: { x: 1350, y: 0 },
data: {
id: "step-4",
label: "VISUALIZERS",
running: job.status === JobStatuses.VISUALIZERS_RUNNING,
completed:
visualizersReported === job.visualizers_to_execute.length &&
visualizersCompleted,
report: `${visualizersReported}/${job.visualizers_to_execute.length}`,
},
type: "jobPipelineNode",
draggable: false,
},
];

const edges = [
{
step: 2,
type: "CONNECTORS",
completed:
connectorsReported === job.connectors_to_execute.length &&
connectorsCompleted,
report: `${connectorsReported}/${job.connectors_to_execute.length}`,
id: `edge-analyzers-connectors`,
source: `isRunningJob-analyzers`,
target: `isRunningJob-connectors`,
},
{
step: 3,
type: "PIVOTS",
completed:
pivotsReported === job.pivots_to_execute.length && pivotsCompleted,
report: `${pivotsReported}/${job.pivots_to_execute.length}`,
id: `edge-connectors-pivots`,
source: `isRunningJob-connectors`,
target: `isRunningJob-pivots`,
},
{
step: 4,
type: "VISUALIZERS",
completed:
visualizersReported === job.visualizers_to_execute.length &&
visualizersCompleted,
report: `${visualizersReported}/${job.visualizers_to_execute.length}`,
id: `edge-pivots-visualizers`,
source: `isRunningJob-pivots`,
target: `isRunningJob-visualizers`,
},
];

const onKillJobBtnClick = async () => {
const sure = await areYouSureConfirmDialog(`Kill Job #${job.id}`);
if (!sure) return null;
await killJob(job.id);
return null;
};

return (
<Fade className="d-flex-center mx-auto">
<IconAlert
id="jobisrunningalert-iconalert"
color="info"
className="text-info text-center"
>
<h6>
This job is currently <strong className="text-accent">running</strong>
.
</h6>
{alertElements.map((element) => (
<div className="text-white">
STEP {element.step}: {element.type} RUNNING -
<strong
className={`text-${element.completed ? "success" : "info"}`}
>
&nbsp;reported {element.report}
</strong>
</div>
))}
<>
<div className="bg-body" style={{ width: "100vw", height: "15vh" }}>
<ReactFlow
nodes={nodes}
edges={edges}
defaultEdgeOptions={defaultEdgeOptions}
defaultViewport={{ x: 0, y: 0, zoom: 1.2 }}
nodeTypes={nodeTypes}
deleteKeyCode={null}
preventScrolling={false}
zoomOnDoubleClick={false}
panOnDrag={false}
proOptions={{ hideAttribution: true }}
fitView
/>
</div>
<div className="d-flex-center">
{job.permissions?.kill && (
<IconButton
id="jobisrunningalert-iconbutton"
Icon={MdPauseCircleOutline}
id="killjob-iconbutton"
Icon={killJobIcon}
size="xs"
title="Stop Job Process"
color="danger"
titlePlacement="top"
onClick={() => killJob(job.id)}
className="mt-2"
onClick={onKillJobBtnClick}
className="mt-0"
/>
)}
</IconAlert>
</Fade>
</div>
</>
);
}

Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/jobs/result/jobApi.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export async function downloadJobSample(jobId) {
}

export async function killJob(jobId) {
const sure = await areYouSureConfirmDialog(`kill job #${jobId}`);
if (!sure) return Promise.reject();
let success = false;
try {
const response = await axios.patch(`${JOB_BASE_URI}/${jobId}/kill`);
Expand Down
Loading