Add workflow standard comparison controls
This commit is contained in:
@@ -45,11 +45,11 @@ import {
|
||||
import {
|
||||
activateWorkflowDefinition,
|
||||
archiveWorkflowDefinition,
|
||||
compareWorkflowDefinitionToStandard,
|
||||
compileWorkflowBpmn,
|
||||
createWorkflowDefinition,
|
||||
deleteWorkflowDefinition,
|
||||
deriveWorkflowDefinition,
|
||||
getWorkflowRevision,
|
||||
listWorkflowDefinitions,
|
||||
listWorkflowNodeTypes,
|
||||
listWorkflowRevisions,
|
||||
@@ -63,7 +63,8 @@ import {
|
||||
type WorkflowDiagnostic,
|
||||
type WorkflowGraphEdge,
|
||||
type WorkflowNodeType,
|
||||
type WorkflowRevision
|
||||
type WorkflowRevision,
|
||||
type WorkflowStandardDiff
|
||||
} from "../../api/workflow";
|
||||
import WorkflowCanvas, {
|
||||
updateWorkflowGraphNode
|
||||
@@ -151,10 +152,6 @@ export default function WorkflowPage({
|
||||
() => definitions.find((item) => item.id === draft?.id) ?? null,
|
||||
[definitions, draft?.id]
|
||||
);
|
||||
const baselineDefinition = useMemo(() => {
|
||||
const baselineId = selectedDefinition?.standard?.baseline_definition_id;
|
||||
return definitions.find((item) => item.id === baselineId) ?? null;
|
||||
}, [definitions, selectedDefinition]);
|
||||
const dirty = Boolean(draft)
|
||||
&& workflowFingerprint(draft) !== workflowFingerprint(savedDraft);
|
||||
const displayedGraph = historicalRevision?.graph ?? draft?.graph ?? null;
|
||||
@@ -987,7 +984,6 @@ export default function WorkflowPage({
|
||||
open={compareOpen}
|
||||
settings={settings}
|
||||
override={selectedDefinition}
|
||||
baseline={baselineDefinition}
|
||||
onClose={() => setCompareOpen(false)}
|
||||
/>
|
||||
<WorkflowDefinitionSettingsDialog
|
||||
@@ -1041,45 +1037,31 @@ function WorkflowStandardComparisonDialog({
|
||||
open,
|
||||
settings,
|
||||
override,
|
||||
baseline,
|
||||
onClose
|
||||
}: {
|
||||
open: boolean;
|
||||
settings: ApiSettings;
|
||||
override: WorkflowDefinition | null;
|
||||
baseline: WorkflowDefinition | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [pinned, setPinned] = useState<WorkflowRevision | null>(null);
|
||||
const [latest, setLatest] = useState<WorkflowRevision | null>(null);
|
||||
const [comparison, setComparison] = useState<WorkflowStandardDiff | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const standard = override?.standard;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !baseline || !standard) return;
|
||||
if (!open || !override) return;
|
||||
let cancelled = false;
|
||||
setError("");
|
||||
const pinnedRevision = standard.pinned_baseline_revision
|
||||
?? standard.active_baseline_revision
|
||||
?? 1;
|
||||
void Promise.all([
|
||||
getWorkflowRevision(settings, baseline.id, pinnedRevision),
|
||||
getWorkflowRevision(
|
||||
settings,
|
||||
baseline.id,
|
||||
standard.latest_baseline_revision
|
||||
)
|
||||
]).then(([pinnedItem, latestItem]) => {
|
||||
setComparison(null);
|
||||
void compareWorkflowDefinitionToStandard(settings, override.id).then((result) => {
|
||||
if (cancelled) return;
|
||||
setPinned(pinnedItem);
|
||||
setLatest(latestItem);
|
||||
setComparison(result);
|
||||
}).catch((loadError) => {
|
||||
if (!cancelled) setError(apiErrorMessage(loadError));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [baseline, open, settings, standard]);
|
||||
}, [open, override, settings]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -1094,44 +1076,79 @@ function WorkflowStandardComparisonDialog({
|
||||
{error}
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
<div className="workflow-standard-comparison">
|
||||
<StandardRevisionColumn
|
||||
label="Pinned standard"
|
||||
revision={pinned}
|
||||
/>
|
||||
<StandardRevisionColumn
|
||||
label="Local override"
|
||||
revision={override?.revision ?? null}
|
||||
/>
|
||||
<StandardRevisionColumn
|
||||
label="Latest standard"
|
||||
revision={latest}
|
||||
/>
|
||||
</div>
|
||||
{!error && !comparison ? <LoadingFrame label="Comparing workflow revisions..." /> : null}
|
||||
{comparison ? (
|
||||
<div className="workflow-standard-comparison">
|
||||
<div className="workflow-standard-comparison-summary">
|
||||
<span>
|
||||
Pinned revision <strong>{comparison.pinned_baseline_revision}</strong>
|
||||
</span>
|
||||
<span>
|
||||
Local revision <strong>{comparison.local_revision}</strong>
|
||||
</span>
|
||||
<span>
|
||||
Latest standard <strong>{comparison.latest_baseline_revision}</strong>
|
||||
</span>
|
||||
<StatusBadge
|
||||
status={comparison.auto_mergeable ? "ready" : "warning"}
|
||||
label={comparison.auto_mergeable
|
||||
? "No semantic conflicts"
|
||||
: `${comparison.conflict_count} conflicts`}
|
||||
/>
|
||||
</div>
|
||||
<div className="workflow-standard-diff-list">
|
||||
{comparison.items.map((item) => (
|
||||
<section
|
||||
className={`workflow-standard-diff-item state-${item.state}`}
|
||||
key={`${item.resource_type}:${item.resource_id}`}
|
||||
>
|
||||
<header>
|
||||
<span>
|
||||
<strong>{item.resource_id}</strong>
|
||||
<small>{item.resource_type} · {item.changed_fields.join(", ")}</small>
|
||||
</span>
|
||||
<StatusBadge
|
||||
status={item.state === "conflict"
|
||||
? "warning"
|
||||
: item.state === "upstream_only"
|
||||
? "queued"
|
||||
: "active"}
|
||||
label={standardDiffLabel(item.state)}
|
||||
/>
|
||||
</header>
|
||||
<p>{standardDiffAction(item.recommended_action)}</p>
|
||||
</section>
|
||||
))}
|
||||
{!comparison.items.length ? (
|
||||
<p className="workflow-standard-diff-empty">
|
||||
The local override and latest standard contain no semantic changes.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function StandardRevisionColumn({
|
||||
label,
|
||||
revision
|
||||
}: {
|
||||
label: string;
|
||||
revision: WorkflowRevision | null;
|
||||
}) {
|
||||
return (
|
||||
<section className="workflow-standard-revision">
|
||||
<header>
|
||||
<strong>{label}</strong>
|
||||
<small>
|
||||
{revision
|
||||
? `Revision ${revision.revision} · ${revision.content_hash.slice(0, 12)}`
|
||||
: "Loading..."}
|
||||
</small>
|
||||
</header>
|
||||
<pre>{revision ? JSON.stringify(revision.graph, null, 2) : ""}</pre>
|
||||
</section>
|
||||
);
|
||||
function standardDiffLabel(state: string): string {
|
||||
return ({
|
||||
local_only: "Local change",
|
||||
upstream_only: "Standard update",
|
||||
same_change: "Same change",
|
||||
conflict: "Conflict",
|
||||
unchanged: "Unchanged"
|
||||
} as Record<string, string>)[state] ?? state;
|
||||
}
|
||||
|
||||
function standardDiffAction(action: string): string {
|
||||
return ({
|
||||
keep_local: "The local change can be retained automatically.",
|
||||
adopt_upstream: "The standard update can be adopted automatically.",
|
||||
either: "Both revisions made the same semantic change.",
|
||||
manual_resolution: "Local and upstream changes overlap and need a decision.",
|
||||
none: "No action is required."
|
||||
} as Record<string, string>)[action] ?? action;
|
||||
}
|
||||
|
||||
function WorkflowDefinitionSettingsDialog({
|
||||
|
||||
Reference in New Issue
Block a user