Rapport_React/src/views/workflow/Monitor.jsx

204 lines
7.2 KiB
JavaScript

import { useEffect, useMemo, useState } from 'react';
import { Link, useSearchParams } from 'react-router';
import { Background, Controls, ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Button, Card, Col, Container, Row, Spinner, Table } from 'react-bootstrap';
import PageBreadcrumb from '@/components/PageBreadcrumb';
import { isSuccess, signedGet, signedPost } from '@/api/client';
import { getSession } from '@/auth/storage';
import { toFlow } from '@/views/workflow/graph';
export default function WorkflowMonitor() {
const [params] = useSearchParams();
const initialId = params.get('id') || '';
const definitionId = params.get('definition_id') || '';
const [items, setItems] = useState([]);
const [selectedId, setSelectedId] = useState(initialId);
const [instance, setInstance] = useState(null);
const [definition, setDefinition] = useState(null);
const [steps, setSteps] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [comment, setComment] = useState('');
const loadList = async () => {
const query = definitionId ? { definition_id: definitionId } : {};
const response = await signedGet('workflow', 'workflow_instance/get_all', query);
if (!isSuccess(response)) {
setError(response?.message || 'Unable to load runs');
return [];
}
const rows = response.data?.items || [];
setItems(rows);
return rows;
};
const loadRun = async (id) => {
if (!id) {
setInstance(null);
setDefinition(null);
setSteps([]);
return;
}
const [one, log] = await Promise.all([
signedGet('workflow', 'workflow_instance/get_one', { id }),
signedGet('workflow', 'workflow_step/list', { instance_id: id }),
]);
if (!isSuccess(one)) {
setError(one?.message || 'Unable to load run');
return;
}
const row = one.data.item;
setInstance(row);
setSteps(isSuccess(log) ? log.data?.items || [] : []);
const versionId = `${row.definition_id}#v${row.definition_version}`;
const graph = await signedGet('workflow', 'workflow_definition/get_one', { id: versionId });
setDefinition(isSuccess(graph) ? graph.data.item : null);
};
useEffect(() => {
const boot = async () => {
setLoading(true);
const rows = await loadList();
const next = initialId || rows[0]?.id || '';
setSelectedId(next);
await loadRun(next);
setLoading(false);
};
boot();
}, [definitionId, initialId]);
const flow = useMemo(() => {
if (!definition) return { nodes: [], edges: [] };
const mapped = toFlow(definition);
return {
...mapped,
nodes: mapped.nodes.map((node) => ({
...node,
style: node.id === instance?.current_node_id ? { border: '2px solid #0d6efd', borderRadius: 8 } : undefined,
})),
};
}, [definition, instance]);
const session = getSession() || {};
const user = session.user || {};
const canAct = (instance?.waiting_for || []).some(
(token) => token === user.id || (session.roles || user.roles || []).includes(token)
);
const signal = async (action) => {
setError('');
const response = await signedPost('workflow', 'workflow_instance/signal', {
instance_id: instance.id,
action,
comment,
});
if (!isSuccess(response)) {
setError(response?.message || 'Unable to update the run');
return;
}
await loadList();
await loadRun(instance.id);
};
if (loading) {
return (
<Container className="py-4">
<Spinner animation="border" size="sm" /> Loading runs
</Container>
);
}
return (
<Container fluid className="py-3">
<PageBreadcrumb title="Workflow monitor" subtitle="Workflows" />
{error ? <div className="alert alert-danger">{error}</div> : null}
<Row>
<Col md={4}>
<Card>
<Card.Header>Runs</Card.Header>
<Table responsive hover className="mb-0">
<thead>
<tr>
<th>Definition</th>
<th>Status</th>
<th>Waiting</th>
</tr>
</thead>
<tbody>
{items.map((row) => (
<tr key={row.id} onClick={() => { setSelectedId(row.id); loadRun(row.id); }} role="button">
<td className={row.id === selectedId ? 'fw-bold' : ''}>{row.definition_name || row.definition_id}</td>
<td>{row.status}</td>
<td>{(row.waiting_for || []).join(', ')}</td>
</tr>
))}
{!items.length ? (
<tr>
<td colSpan={3} className="text-muted">No runs yet.</td>
</tr>
) : null}
</tbody>
</Table>
</Card>
</Col>
<Col md={8}>
{instance ? (
<>
<div className="d-flex justify-content-between mb-2">
<div>
<strong>{instance.definition_name}</strong> v{instance.definition_version} · {instance.status}
{instance.outcome ? ` · ${instance.outcome}` : ''}
</div>
<Button as={Link} to={`/workflow/editor?id=${instance.definition_id}`} variant="light" size="sm">
Open definition
</Button>
</div>
<div style={{ height: 360, border: '1px solid #dee2e6', borderRadius: 8 }} className="mb-3">
<ReactFlow nodes={flow.nodes} edges={flow.edges} nodesDraggable={false} nodesConnectable={false} elementsSelectable={false} fitView>
<Controls showInteractive={false} />
<Background />
</ReactFlow>
</div>
{canAct && instance.status === 'waiting' ? (
<div className="d-flex gap-2 mb-3">
<input className="form-control" placeholder="Comment" value={comment} onChange={(event) => setComment(event.target.value)} />
<Button onClick={() => signal('approve')}>Approve</Button>
<Button variant="danger" onClick={() => signal('reject')}>Reject</Button>
</div>
) : null}
<Card>
<Card.Header>Step log</Card.Header>
<Table responsive className="mb-0">
<thead>
<tr>
<th>#</th>
<th>Event</th>
<th>Node</th>
<th>Actor</th>
<th>When</th>
</tr>
</thead>
<tbody>
{steps.map((step) => (
<tr key={step.id}>
<td>{step.seq}</td>
<td>{step.event}</td>
<td>{step.node_id}</td>
<td>{step.actor}</td>
<td>{step.at}</td>
</tr>
))}
</tbody>
</Table>
</Card>
</>
) : (
<p className="text-muted">Select a run to see its graph and log.</p>
)}
</Col>
</Row>
</Container>
);
}