fix(ui): make all Jira and time panels read-only everywhere

WorklogTimeline: add readOnly prop — hides 'Log Time' button and form.
TestPhaseTimeline: remove 'Sync to Tempo' button from TempoSyncBadge;
  only displays the green 'Tempo' badge when already synced. Cleans up
  unused imports (useState, useMutation, useQueryClient, syncTestToTempo).
CampaignDetailPage: JiraLinkPanel and WorklogTimeline both now rendered
  with readOnly=true; JiraLinkPanel receives campaign name as label.

Jira tickets and time worklogs are created automatically by the system
(campaign activation, test workflow) — no manual editing from detail pages.
This commit is contained in:
kitos
2026-05-29 11:33:55 +02:00
parent 069728a010
commit 6a4a153d59
3 changed files with 36 additions and 98 deletions
+12 -78
View File
@@ -1,8 +1,7 @@
/**
* TestPhaseTimeline
*
* Read-only timeline of automated phase durations, with Tempo sync status and
* a manual "Sync to Tempo" button for the Red Team Execution phase.
* Read-only timeline of automated phase durations with Tempo sync status.
*
* 1. Red Team Execution (red_started_at → blue_started_at, minus paused)
* 2. Blue Queue (blue_started_at → blue_work_started_at)
@@ -11,14 +10,12 @@
* 5. Blue Lead Validation (blue_validated_at + status)
*/
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import {
Clock, Sword, Shield, CheckCircle, XCircle, Timer, RefreshCw, Loader2,
Clock, Sword, Shield, CheckCircle, XCircle, Timer,
} from "lucide-react";
import type { Test } from "../types/models";
import { listTestWorklogs, type Worklog } from "../api/worklogs";
import { syncTestToTempo } from "../api/tests";
// ── Helpers ──────────────────────────────────────────────────────────
@@ -118,71 +115,18 @@ function ValidationBadge({ status }: { status: string | null }) {
function TempoSyncBadge({
worklog,
testId,
onSynced,
}: {
worklog: Worklog | undefined;
testId: string;
onSynced: () => void;
}) {
const [toast, setToast] = useState<string | null>(null);
if (!worklog?.tempo_synced) return null;
const mutation = useMutation({
mutationFn: () => syncTestToTempo(testId),
onSuccess: (data) => {
const r = data.results[0];
if (r?.status === "synced") {
setToast("Synced to Tempo ✓");
onSynced();
} else if (r?.status === "already_synced") {
setToast("Already synced");
} else {
setToast(r?.detail ?? "Skipped — check Tempo settings");
}
setTimeout(() => setToast(null), 4000);
},
onError: (e: unknown) => {
const msg =
e && typeof e === "object" && "response" in e
? ((e as { response?: { data?: { detail?: string } } }).response?.data?.detail ?? "Sync failed")
: "Sync failed";
setToast(msg);
setTimeout(() => setToast(null), 5000);
},
});
// Already synced
if (worklog?.tempo_synced) {
return (
<span
className="flex items-center gap-0.5 rounded px-1.5 py-0.5 text-xs bg-green-900/30 text-green-400"
title={`Synced to Tempo${worklog.tempo_worklog_id ? ` (ID: ${worklog.tempo_worklog_id})` : ""}`}
>
<CheckCircle className="h-3 w-3" /> Tempo
</span>
);
}
// Not synced — show button
return (
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
className="flex items-center gap-1 rounded px-2 py-0.5 text-xs border border-blue-500/30 bg-blue-900/20 text-blue-400 hover:bg-blue-900/40 disabled:opacity-50 transition-colors"
title="Push this phase's time to Tempo"
>
{mutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<RefreshCw className="h-3 w-3" />
)}
Sync to Tempo
</button>
{toast && (
<span className="text-xs text-gray-400">{toast}</span>
)}
</div>
<span
className="flex items-center gap-0.5 rounded px-1.5 py-0.5 text-xs bg-green-900/30 text-green-400"
title={`Synced to Tempo${worklog.tempo_worklog_id ? ` (ID: ${worklog.tempo_worklog_id})` : ""}`}
>
<CheckCircle className="h-3 w-3" /> Tempo
</span>
);
}
@@ -194,8 +138,6 @@ interface TestPhaseTimelineProps {
}
export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelineProps) {
const queryClient = useQueryClient();
const {
red_started_at,
blue_started_at,
@@ -217,10 +159,6 @@ export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelinePro
const redWorklog = worklogs.find((w) => w.activity_type === "red_team_execution");
const refreshWorklogs = () => {
if (testId) queryClient.invalidateQueries({ queryKey: ["worklogs", "test", testId] });
};
// Compute per-phase durations
const redExecSecs =
red_started_at && blue_started_at
@@ -270,12 +208,8 @@ export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelinePro
duration={redExecSecs}
isLast={++phaseIdx === lastIdx}
extra={
testId ? (
<TempoSyncBadge
worklog={redWorklog}
testId={testId}
onSynced={refreshWorklogs}
/>
redWorklog ? (
<TempoSyncBadge worklog={redWorklog} />
) : undefined
}
/>