refactor: route console lifecycle workspace
This commit is contained in:
@@ -23,7 +23,6 @@ const createMockController = (
|
||||
selectedRunId: null,
|
||||
runDetail: null,
|
||||
trace: null,
|
||||
rawEvidence: [],
|
||||
errors: [],
|
||||
...overrides,
|
||||
},
|
||||
@@ -37,6 +36,18 @@ const createMockController = (
|
||||
});
|
||||
|
||||
describe("LifecycleExplorer", () => {
|
||||
it("marks the route collection and omits the explorer-local raw evidence mode", () => {
|
||||
const { container } = render(
|
||||
<LifecycleExplorer controller={createMockController()} primaryKind="deployment" />,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".lifecycle-explorer")).toHaveAttribute(
|
||||
"data-primary-lifecycle-kind",
|
||||
"deployment",
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "Raw" })).toBeNull();
|
||||
});
|
||||
|
||||
it("renders artifact buttons when loaded", () => {
|
||||
const controller = createMockController({
|
||||
artifactList: {
|
||||
@@ -61,7 +72,7 @@ describe("LifecycleExplorer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<LifecycleExplorer controller={controller} />);
|
||||
render(<LifecycleExplorer controller={controller} primaryKind="artifact" />);
|
||||
expect(screen.getByRole("option", { name: /Report version 1/i })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -83,7 +94,7 @@ describe("LifecycleExplorer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<LifecycleExplorer controller={controller} />);
|
||||
render(<LifecycleExplorer controller={controller} primaryKind="deployment" />);
|
||||
expect(screen.getByRole("option", { name: /report.default/i })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -109,7 +120,7 @@ describe("LifecycleExplorer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<LifecycleExplorer controller={controller} />);
|
||||
render(<LifecycleExplorer controller={controller} primaryKind="run" />);
|
||||
expect(screen.getByRole("option", { name: /run_1 interrupted/i })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -137,7 +148,7 @@ describe("LifecycleExplorer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<LifecycleExplorer controller={controller} />);
|
||||
render(<LifecycleExplorer controller={controller} primaryKind="artifact" />);
|
||||
fireEvent.click(screen.getAllByRole("option", { name: /Report version 1/i })[0]!);
|
||||
expect(controller.selectArtifact).toHaveBeenCalledWith("report@1");
|
||||
});
|
||||
@@ -147,7 +158,7 @@ describe("LifecycleExplorer", () => {
|
||||
artifactList: { phase: "loaded", value: { items: [], total: 0, nextCursor: null } },
|
||||
});
|
||||
|
||||
render(<LifecycleExplorer controller={controller} />);
|
||||
render(<LifecycleExplorer controller={controller} primaryKind="artifact" />);
|
||||
expect(screen.getAllByText(/no artifacts/i)[0]).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -194,7 +205,7 @@ describe("LifecycleExplorer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(<LifecycleExplorer controller={controller} />);
|
||||
render(<LifecycleExplorer controller={controller} primaryKind="run" />);
|
||||
|
||||
expect(screen.getByText(/loading artifacts/i)).toBeVisible();
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(/could not load runs: network down/i);
|
||||
|
||||
@@ -10,9 +10,10 @@ import type { LoadState } from "./state.js";
|
||||
|
||||
type LifecycleExplorerProps = {
|
||||
readonly controller: LifecycleExplorerController;
|
||||
readonly primaryKind: "artifact" | "deployment" | "run";
|
||||
};
|
||||
|
||||
type FocusMode = "lifecycle" | "graph" | "execution" | "raw";
|
||||
type FocusMode = "lifecycle" | "graph" | "execution";
|
||||
|
||||
const loadedItems = <T,>(
|
||||
state: LoadState<{ readonly items: ReadonlyArray<T> }>,
|
||||
@@ -25,7 +26,7 @@ const listStatus = <T,>(label: string, state: LoadState<T>) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const LifecycleExplorer = ({ controller }: LifecycleExplorerProps) => {
|
||||
export const LifecycleExplorer = ({ controller, primaryKind }: LifecycleExplorerProps) => {
|
||||
const { state } = controller;
|
||||
const [focusMode, setFocusMode] = useState<FocusMode>("lifecycle");
|
||||
|
||||
@@ -49,7 +50,7 @@ export const LifecycleExplorer = ({ controller }: LifecycleExplorerProps) => {
|
||||
}, [state.trace]);
|
||||
|
||||
return (
|
||||
<div className="lifecycle-explorer">
|
||||
<div className="lifecycle-explorer" data-primary-lifecycle-kind={primaryKind}>
|
||||
<nav className="focus-nav" aria-label="Focus modes">
|
||||
<button
|
||||
onClick={() => setFocusMode("lifecycle")}
|
||||
@@ -71,12 +72,6 @@ export const LifecycleExplorer = ({ controller }: LifecycleExplorerProps) => {
|
||||
>
|
||||
Execution
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFocusMode("raw")}
|
||||
className={focusMode === "raw" ? "active" : ""}
|
||||
>
|
||||
Raw
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{focusMode === "lifecycle" && (
|
||||
@@ -98,6 +93,7 @@ export const LifecycleExplorer = ({ controller }: LifecycleExplorerProps) => {
|
||||
selectedArtifactId={state.selectedArtifactId}
|
||||
selectedDeploymentId={state.selectedDeploymentId}
|
||||
selectedRunId={state.selectedRunId}
|
||||
primaryKind={primaryKind}
|
||||
onSelectArtifact={controller.selectArtifact}
|
||||
onSelectDeployment={controller.selectDeployment}
|
||||
onSelectRun={controller.selectRun}
|
||||
@@ -136,36 +132,6 @@ export const LifecycleExplorer = ({ controller }: LifecycleExplorerProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{focusMode === "raw" && (
|
||||
<div className="raw-content">
|
||||
<h3>Protocol Evidence</h3>
|
||||
{state.rawEvidence.length === 0 ? (
|
||||
<p className="empty-state">No evidence recorded yet.</p>
|
||||
) : (
|
||||
<ul className="evidence-list">
|
||||
{state.rawEvidence.map((record) => (
|
||||
<li key={record.id}>
|
||||
<span className="evidence-op">{record.operation}</span>
|
||||
<span className="evidence-label">{record.label}</span>
|
||||
<span className="evidence-duration">{record.durationMs}ms</span>
|
||||
<details>
|
||||
<summary>Equivalent CLI</summary>
|
||||
<pre><code>{record.equivalentCli}</code></pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Request</summary>
|
||||
<pre><code>{JSON.stringify(record.request, null, 2)}</code></pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Response</summary>
|
||||
<pre><code>{JSON.stringify(record.response, null, 2)}</code></pre>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ type RecordColumnsProps = {
|
||||
readonly selectedArtifactId: string | null;
|
||||
readonly selectedDeploymentId: string | null;
|
||||
readonly selectedRunId: string | null;
|
||||
readonly primaryKind: "artifact" | "deployment" | "run";
|
||||
readonly onSelectArtifact: (artifactId: string | null) => void;
|
||||
readonly onSelectDeployment: (deploymentId: string | null) => void;
|
||||
readonly onSelectRun: (runId: string | null) => void;
|
||||
@@ -23,6 +24,7 @@ export const RecordColumns = ({
|
||||
selectedArtifactId,
|
||||
selectedDeploymentId,
|
||||
selectedRunId,
|
||||
primaryKind,
|
||||
onSelectArtifact,
|
||||
onSelectDeployment,
|
||||
onSelectRun,
|
||||
@@ -32,7 +34,10 @@ export const RecordColumns = ({
|
||||
hasMoreRuns,
|
||||
}: RecordColumnsProps) => (
|
||||
<div className="lifecycle-columns">
|
||||
<div className="lifecycle-column">
|
||||
<div
|
||||
className={`lifecycle-column lifecycle-column--artifact${primaryKind === "artifact" ? " lifecycle-column--primary" : ""}`}
|
||||
data-lifecycle-kind="artifact"
|
||||
>
|
||||
<h3>Artifacts</h3>
|
||||
{artifacts.length === 0 ? (
|
||||
<p className="empty-state">No artifacts</p>
|
||||
@@ -58,7 +63,10 @@ export const RecordColumns = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="lifecycle-column">
|
||||
<div
|
||||
className={`lifecycle-column lifecycle-column--deployment${primaryKind === "deployment" ? " lifecycle-column--primary" : ""}`}
|
||||
data-lifecycle-kind="deployment"
|
||||
>
|
||||
<h3>Deployments</h3>
|
||||
{deployments.length === 0 ? (
|
||||
<p className="empty-state">No deployments</p>
|
||||
@@ -79,7 +87,10 @@ export const RecordColumns = ({
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="lifecycle-column">
|
||||
<div
|
||||
className={`lifecycle-column lifecycle-column--run${primaryKind === "run" ? " lifecycle-column--primary" : ""}`}
|
||||
data-lifecycle-kind="run"
|
||||
>
|
||||
<h3>Runs</h3>
|
||||
{runs.length === 0 ? (
|
||||
<p className="empty-state">No runs</p>
|
||||
|
||||
@@ -15,16 +15,6 @@ export type LoadState<T> =
|
||||
| { readonly phase: "loaded"; readonly value: T }
|
||||
| { readonly phase: "error"; readonly message: string; readonly previous: T | null };
|
||||
|
||||
export type EvidenceRecord = {
|
||||
readonly id: string;
|
||||
readonly operation: string;
|
||||
readonly label: string;
|
||||
readonly equivalentCli: string;
|
||||
readonly request: unknown;
|
||||
readonly response: unknown;
|
||||
readonly durationMs: number;
|
||||
};
|
||||
|
||||
export type LifecycleError = {
|
||||
readonly operation: string;
|
||||
readonly message: string;
|
||||
@@ -43,7 +33,6 @@ export type LifecycleState = {
|
||||
readonly selectedRunId: string | null;
|
||||
readonly runDetail: RunDetail | null;
|
||||
readonly trace: TracePage | null;
|
||||
readonly rawEvidence: ReadonlyArray<EvidenceRecord>;
|
||||
readonly errors: ReadonlyArray<LifecycleError>;
|
||||
};
|
||||
|
||||
@@ -59,7 +48,6 @@ export const initialLifecycleState: LifecycleState = {
|
||||
selectedRunId: null,
|
||||
runDetail: null,
|
||||
trace: null,
|
||||
rawEvidence: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
@@ -81,7 +69,6 @@ export type LifecycleAction =
|
||||
| { readonly type: "setDeploymentValidation"; readonly validation: DeploymentValidation | null }
|
||||
| { readonly type: "setRunDetail"; readonly detail: RunDetail | null }
|
||||
| { readonly type: "setTrace"; readonly trace: TracePage | null }
|
||||
| { readonly type: "setRawEvidence"; readonly evidence: ReadonlyArray<EvidenceRecord> }
|
||||
| { readonly type: "pushError"; readonly error: LifecycleError };
|
||||
|
||||
const setLoadPhase = <T>(
|
||||
@@ -224,12 +211,6 @@ export const lifecycleReducer = (
|
||||
trace: action.trace,
|
||||
};
|
||||
|
||||
case "setRawEvidence":
|
||||
return {
|
||||
...state,
|
||||
rawEvidence: action.evidence,
|
||||
};
|
||||
|
||||
case "pushError":
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,316 +1,231 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type {
|
||||
ArtifactClient,
|
||||
DeploymentClient,
|
||||
LifecycleClients,
|
||||
RunClient,
|
||||
} from "../workspace/domain/lifecycle-clients.js";
|
||||
import { useLifecycleExplorer } from "./useLifecycleExplorer.js";
|
||||
import type {
|
||||
ArtifactDetail,
|
||||
ArtifactList,
|
||||
DeploymentDetail,
|
||||
DeploymentList,
|
||||
DeploymentValidation,
|
||||
RunDetail,
|
||||
RunList,
|
||||
} from "./models.js";
|
||||
|
||||
const mockCallOperation = vi.fn();
|
||||
vi.mock("../connection/api.js", () => ({
|
||||
callOperation: (...args: unknown[]) => mockCallOperation(...args),
|
||||
}));
|
||||
const artifactList: ArtifactList = { items: [], total: 0, nextCursor: null };
|
||||
const deploymentList: DeploymentList = { items: [] };
|
||||
const runList: RunList = { items: [], total: 0, nextCursor: null };
|
||||
const artifactDetail = {
|
||||
artifactId: "report",
|
||||
version: 2,
|
||||
title: "Report",
|
||||
kind: "workflow",
|
||||
description: null,
|
||||
outcomes: ["ok"],
|
||||
plan: { nodes: [], edges: [] },
|
||||
requiredCapabilities: [],
|
||||
workflowDependencies: {},
|
||||
createdFromCatalogVersion: null,
|
||||
} satisfies ArtifactDetail;
|
||||
const deploymentDetail = {
|
||||
id: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 2,
|
||||
bindings: [],
|
||||
driftPolicy: "block",
|
||||
} satisfies DeploymentDetail;
|
||||
const deploymentValidation = {
|
||||
deploymentId: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 2,
|
||||
status: "runnable",
|
||||
diagnostics: [],
|
||||
nextActions: {
|
||||
canContinue: true,
|
||||
canSaveNow: null,
|
||||
recommendedNextTool: null,
|
||||
reason: "ready",
|
||||
patchExamples: [],
|
||||
warnings: [],
|
||||
},
|
||||
} satisfies DeploymentValidation;
|
||||
const runDetail = {
|
||||
runId: "run_123",
|
||||
deploymentId: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 2,
|
||||
status: "completed",
|
||||
resumeReadiness: "not_applicable",
|
||||
interrupt: null,
|
||||
outcome: "ok",
|
||||
error: null,
|
||||
output: {},
|
||||
diagnostics: [],
|
||||
traceCount: 0,
|
||||
nextActions: {
|
||||
canContinue: false,
|
||||
canSaveNow: null,
|
||||
recommendedNextTool: null,
|
||||
reason: "done",
|
||||
patchExamples: [],
|
||||
warnings: [],
|
||||
},
|
||||
} satisfies RunDetail;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCallOperation.mockReset();
|
||||
const makeClients = (overrides: Partial<{
|
||||
artifacts: Partial<ArtifactClient>;
|
||||
deployments: Partial<DeploymentClient>;
|
||||
runs: Partial<RunClient>;
|
||||
}> = {}): LifecycleClients => ({
|
||||
artifacts: {
|
||||
list: vi.fn().mockResolvedValue(artifactList),
|
||||
inspect: vi.fn().mockResolvedValue(artifactDetail),
|
||||
...overrides.artifacts,
|
||||
},
|
||||
deployments: {
|
||||
list: vi.fn().mockResolvedValue(deploymentList),
|
||||
inspect: vi.fn().mockResolvedValue(deploymentDetail),
|
||||
validate: vi.fn().mockResolvedValue(deploymentValidation),
|
||||
...overrides.deployments,
|
||||
},
|
||||
runs: {
|
||||
list: vi.fn().mockResolvedValue(runList),
|
||||
inspect: vi.fn().mockResolvedValue(runDetail),
|
||||
trace: vi.fn().mockResolvedValue({
|
||||
frames: [],
|
||||
traceStart: 0,
|
||||
traceLimit: 50,
|
||||
traceTruncated: false,
|
||||
}),
|
||||
...overrides.runs,
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("useLifecycleExplorer", () => {
|
||||
it("loads artifact, deployment, and run lists on target change", async () => {
|
||||
mockCallOperation.mockResolvedValue({
|
||||
ok: true,
|
||||
operation: "workflow.artifacts.list",
|
||||
interpreted: { items: [], total: 0, nextCursor: null },
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf artifact list",
|
||||
durationMs: 5,
|
||||
it("loads all lifecycle collections through the Task 3 clients", async () => {
|
||||
const clients = makeClients();
|
||||
renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clients.artifacts.list).toHaveBeenCalledWith({ limit: 50 });
|
||||
expect(clients.deployments.list).toHaveBeenCalledWith();
|
||||
expect(clients.runs.list).toHaveBeenCalledWith({ limit: 50 });
|
||||
});
|
||||
|
||||
const recordEvidence = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useLifecycleExplorer("http://127.0.0.1:8000/rpc", recordEvidence),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
expect(mockCallOperation).toHaveBeenCalledWith(
|
||||
"workflow.artifacts.list",
|
||||
"http://127.0.0.1:8000/rpc",
|
||||
expect.objectContaining({ limit: 50 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("selects an artifact and requests inspect", async () => {
|
||||
mockCallOperation.mockImplementation(async (operation: string) => {
|
||||
if (operation === "workflow.artifacts.inspect") {
|
||||
return {
|
||||
ok: true,
|
||||
operation: "workflow.artifacts.inspect",
|
||||
interpreted: {
|
||||
artifactId: "report",
|
||||
version: 1,
|
||||
title: "Report",
|
||||
kind: "workflow",
|
||||
description: null,
|
||||
outcomes: ["ok"],
|
||||
plan: { nodes: [], edges: [] },
|
||||
requiredCapabilities: [],
|
||||
workflowDependencies: {},
|
||||
createdFromCatalogVersion: null,
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf artifact inspect report --version 1",
|
||||
durationMs: 5,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted: {},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "",
|
||||
durationMs: 5,
|
||||
};
|
||||
});
|
||||
it("uses domain methods for artifact, deployment, and run inspection", async () => {
|
||||
const clients = makeClients();
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
const recordEvidence = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useLifecycleExplorer("http://127.0.0.1:8000/rpc", recordEvidence),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.selectArtifact("report@1");
|
||||
});
|
||||
|
||||
expect(mockCallOperation).toHaveBeenCalledWith(
|
||||
"workflow.artifacts.inspect",
|
||||
"http://127.0.0.1:8000/rpc",
|
||||
expect.objectContaining({ artifact_id: "report", version: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps deployment inspect and validation results from the same selection", async () => {
|
||||
mockCallOperation.mockImplementation(async (operation: string) => {
|
||||
if (operation === "workflow.deployments.inspect") {
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted: {
|
||||
id: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 1,
|
||||
bindings: [],
|
||||
driftPolicy: "block",
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf deploy inspect report.default",
|
||||
durationMs: 5,
|
||||
};
|
||||
}
|
||||
if (operation === "workflow.deployments.validate") {
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted: {
|
||||
deploymentId: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 1,
|
||||
status: "runnable",
|
||||
diagnostics: [],
|
||||
nextActions: {
|
||||
canContinue: true,
|
||||
canSaveNow: null,
|
||||
recommendedNextTool: null,
|
||||
reason: "deployment is runnable",
|
||||
patchExamples: [],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf deploy validate report.default",
|
||||
durationMs: 5,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted:
|
||||
operation === "workflow.deployments.list"
|
||||
? { items: [] }
|
||||
: { items: [], total: 0, nextCursor: null },
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "",
|
||||
durationMs: 5,
|
||||
};
|
||||
});
|
||||
|
||||
const recordEvidence = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useLifecycleExplorer("http://127.0.0.1:8000/rpc", recordEvidence),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
act(() => {
|
||||
result.current.selectArtifact("report@2");
|
||||
result.current.selectDeployment("report.default");
|
||||
result.current.selectRun("run_123");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.deploymentDetail?.id).toBe("report.default");
|
||||
expect(result.current.state.deploymentValidation?.status).toBe("runnable");
|
||||
expect(clients.artifacts.inspect).toHaveBeenCalledWith("report", 2);
|
||||
expect(clients.deployments.inspect).toHaveBeenCalledWith("report.default");
|
||||
expect(clients.deployments.validate).toHaveBeenCalledWith("report.default");
|
||||
expect(clients.runs.inspect).toHaveBeenCalledWith("run_123");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps deployment validation when a run selection happens before validation returns", async () => {
|
||||
let releaseValidation: (() => void) | null = null;
|
||||
const validationGate = new Promise<void>((resolve) => {
|
||||
releaseValidation = resolve;
|
||||
it("keeps a late response from the previous client generation out of state", async () => {
|
||||
let releaseFirst!: (value: ArtifactList) => void;
|
||||
const firstList = new Promise<ArtifactList>((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
|
||||
mockCallOperation.mockImplementation(async (operation: string) => {
|
||||
if (operation === "workflow.deployments.validate") {
|
||||
await validationGate;
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted: {
|
||||
deploymentId: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 1,
|
||||
status: "runnable",
|
||||
diagnostics: [],
|
||||
nextActions: {
|
||||
canContinue: true,
|
||||
canSaveNow: null,
|
||||
recommendedNextTool: null,
|
||||
reason: "deployment is runnable",
|
||||
patchExamples: [],
|
||||
warnings: [],
|
||||
const first = makeClients({ artifacts: { list: vi.fn().mockReturnValue(firstList) } });
|
||||
const second = makeClients({
|
||||
artifacts: {
|
||||
list: vi.fn().mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
key: "new@1",
|
||||
artifactId: "new",
|
||||
version: 1,
|
||||
kind: "workflow",
|
||||
displayName: "New",
|
||||
description: null,
|
||||
outcomes: [],
|
||||
requiredSources: [],
|
||||
diagnosticCount: 0,
|
||||
},
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf deploy validate report.default",
|
||||
durationMs: 5,
|
||||
};
|
||||
}
|
||||
if (operation === "workflow.deployments.inspect") {
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted: {
|
||||
id: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 1,
|
||||
bindings: [],
|
||||
driftPolicy: "block",
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf deploy inspect report.default",
|
||||
durationMs: 5,
|
||||
};
|
||||
}
|
||||
if (operation === "workflow.runs.inspect") {
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted: {
|
||||
runId: "run_1",
|
||||
deploymentId: "report.default",
|
||||
artifactId: "report",
|
||||
artifactVersion: 1,
|
||||
status: "completed",
|
||||
resumeReadiness: "not_applicable",
|
||||
interrupt: null,
|
||||
outcome: "ok",
|
||||
error: null,
|
||||
output: {},
|
||||
diagnostics: [],
|
||||
traceCount: 0,
|
||||
nextActions: {
|
||||
canContinue: false,
|
||||
canSaveNow: null,
|
||||
recommendedNextTool: null,
|
||||
reason: "done",
|
||||
patchExamples: [],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf run inspect run_1",
|
||||
durationMs: 5,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
operation,
|
||||
interpreted:
|
||||
operation === "workflow.deployments.list"
|
||||
? { items: [] }
|
||||
: { items: [], total: 0, nextCursor: null },
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "",
|
||||
durationMs: 5,
|
||||
};
|
||||
],
|
||||
total: 1,
|
||||
nextCursor: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const recordEvidence = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useLifecycleExplorer("http://127.0.0.1:8000/rpc", recordEvidence),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.selectDeployment("report.default");
|
||||
result.current.selectRun("run_1");
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
releaseValidation?.();
|
||||
await validationGate;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.state.runDetail?.runId).toBe("run_1");
|
||||
expect(result.current.state.deploymentValidation?.status).toBe("runnable");
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores stale responses after target change", async () => {
|
||||
let callCount = 0;
|
||||
mockCallOperation.mockImplementation(async () => {
|
||||
callCount++;
|
||||
if (callCount === 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
operation: "workflow.artifacts.list",
|
||||
interpreted: { items: [], total: 0, nextCursor: null },
|
||||
exchange: { request: {}, response: {} },
|
||||
equivalentCli: "uv run wf artifact list",
|
||||
durationMs: 5,
|
||||
};
|
||||
});
|
||||
|
||||
const recordEvidence = vi.fn();
|
||||
const { result, rerender } = renderHook(
|
||||
({ target }) => useLifecycleExplorer(target, recordEvidence),
|
||||
{ initialProps: { target: "http://first-target/rpc" } },
|
||||
({ clients: currentClients }) => useLifecycleExplorer(currentClients),
|
||||
{ initialProps: { clients: first } },
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
rerender({ clients: second });
|
||||
await waitFor(() => expect(result.current.state.artifactList.phase).toBe("loaded"));
|
||||
releaseFirst(artifactList);
|
||||
await act(async () => await firstList);
|
||||
|
||||
expect(result.current.state.artifactList).toMatchObject({
|
||||
phase: "loaded",
|
||||
value: { items: [{ key: "new@1" }] },
|
||||
});
|
||||
});
|
||||
|
||||
rerender({ target: "http://second-target/rpc" });
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
it("rejects a late artifact inspect after the URL-owned id changes", async () => {
|
||||
let releaseFirst!: (value: ArtifactDetail) => void;
|
||||
const firstInspect = new Promise<ArtifactDetail>((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
|
||||
expect(mockCallOperation).toHaveBeenCalledWith(
|
||||
"workflow.artifacts.list",
|
||||
"http://second-target/rpc",
|
||||
expect.anything(),
|
||||
const inspect = vi.fn((_: string, version: number) =>
|
||||
version === 1 ? firstInspect : Promise.resolve(artifactDetail),
|
||||
);
|
||||
const clients = makeClients({ artifacts: { inspect } });
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectArtifact("report@1"));
|
||||
act(() => result.current.selectArtifact("report@2"));
|
||||
await waitFor(() => expect(result.current.state.artifactDetail?.version).toBe(2));
|
||||
|
||||
releaseFirst({ ...artifactDetail, version: 1 });
|
||||
await act(async () => await firstInspect);
|
||||
|
||||
expect(result.current.state.selectedArtifactId).toBe("report@2");
|
||||
expect(result.current.state.artifactDetail?.version).toBe(2);
|
||||
});
|
||||
|
||||
it("clears lifecycle state when the client bundle is disconnected", async () => {
|
||||
const clients = makeClients();
|
||||
const { result, rerender } = renderHook(
|
||||
({ currentClients }) => useLifecycleExplorer(currentClients),
|
||||
{ initialProps: { currentClients: clients as LifecycleClients | null } },
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.state.artifactList.phase).toBe("loaded"));
|
||||
rerender({ currentClients: null });
|
||||
|
||||
expect(result.current.state).toEqual({
|
||||
artifactList: { phase: "idle" },
|
||||
deploymentList: { phase: "idle" },
|
||||
runList: { phase: "idle" },
|
||||
selectedArtifactId: null,
|
||||
artifactDetail: null,
|
||||
selectedDeploymentId: null,
|
||||
deploymentDetail: null,
|
||||
deploymentValidation: null,
|
||||
selectedRunId: null,
|
||||
runDetail: null,
|
||||
trace: null,
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { useReducer, useEffect, useRef, useCallback, type MutableRefObject } from "react";
|
||||
import { callOperation } from "../connection/api.js";
|
||||
import type { OperationName } from "../connection/contracts.js";
|
||||
import {
|
||||
decodeArtifactList,
|
||||
decodeArtifactDetail,
|
||||
decodeDeploymentList,
|
||||
decodeDeploymentDetail,
|
||||
decodeDeploymentValidation,
|
||||
decodeRunList,
|
||||
decodeRunDetail,
|
||||
decodeTracePage,
|
||||
} from "./models.js";
|
||||
import { lifecycleReducer, initialLifecycleState, type LifecycleState, type EvidenceRecord } from "./state.js";
|
||||
import { useCallback, useEffect, useReducer, useRef, type MutableRefObject } from "react";
|
||||
import { ConsoleClientError } from "../workspace/domain/errors.js";
|
||||
import type { LifecycleClients } from "../workspace/domain/lifecycle-clients.js";
|
||||
import { lifecycleReducer, initialLifecycleState, type LifecycleState } from "./state.js";
|
||||
|
||||
export type LifecycleExplorerController = {
|
||||
readonly state: LifecycleState;
|
||||
@@ -24,85 +14,47 @@ export type LifecycleExplorerController = {
|
||||
readonly loadTrace: (start: number, limit: number) => void;
|
||||
};
|
||||
|
||||
type ReadFailure = (message: string, operation: string) => void;
|
||||
|
||||
const readErrorMessage = (error: unknown): string =>
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
const readErrorOperation = (error: unknown): string =>
|
||||
error instanceof ConsoleClientError ? error.operation : "lifecycle";
|
||||
|
||||
export const useLifecycleExplorer = (
|
||||
target: string | null,
|
||||
recordEvidence: (record: EvidenceRecord) => void,
|
||||
clients: LifecycleClients | null,
|
||||
): LifecycleExplorerController => {
|
||||
const [state, dispatch] = useReducer(lifecycleReducer, initialLifecycleState);
|
||||
const generationRef = useRef(0);
|
||||
const artifactGenerationRef = useRef(0);
|
||||
const deploymentGenerationRef = useRef(0);
|
||||
const runGenerationRef = useRef(0);
|
||||
const rawEvidenceRef = useRef<ReadonlyArray<EvidenceRecord>>([]);
|
||||
const evidenceSeqRef = useRef(0);
|
||||
|
||||
const executeOperation = useCallback(
|
||||
async (
|
||||
operation: OperationName,
|
||||
params: unknown,
|
||||
const executeRead = useCallback(
|
||||
async <T>(
|
||||
read: () => Promise<T>,
|
||||
generation: number,
|
||||
checkGenerationRef: MutableRefObject<number>,
|
||||
onSuccess: (interpreted: unknown) => void,
|
||||
onFailure?: (message: string) => void,
|
||||
) => {
|
||||
if (!target) return;
|
||||
targetGeneration: number,
|
||||
onSuccess: (value: T) => void,
|
||||
onFailure?: ReadFailure,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const result = await callOperation(operation, target, params);
|
||||
if (generation !== checkGenerationRef.current) return;
|
||||
if (result.ok) {
|
||||
const seq = evidenceSeqRef.current++;
|
||||
const record: EvidenceRecord = {
|
||||
id: `${result.operation}-${seq}`,
|
||||
operation: result.operation,
|
||||
label: result.operation,
|
||||
equivalentCli: result.equivalentCli,
|
||||
request: result.exchange.request,
|
||||
response: result.exchange.response,
|
||||
durationMs: result.durationMs,
|
||||
};
|
||||
recordEvidence(record);
|
||||
rawEvidenceRef.current = [...rawEvidenceRef.current, record];
|
||||
dispatch({ type: "setRawEvidence", evidence: rawEvidenceRef.current });
|
||||
try {
|
||||
onSuccess(result.interpreted);
|
||||
} catch (decodeError) {
|
||||
dispatch({
|
||||
type: "pushError",
|
||||
error: {
|
||||
operation: result.operation,
|
||||
message: decodeError instanceof Error ? decodeError.message : String(decodeError),
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onFailure?.(result.error.message);
|
||||
const seq = evidenceSeqRef.current++;
|
||||
const record: EvidenceRecord = {
|
||||
id: `${operation}-${seq}`,
|
||||
operation,
|
||||
label: `${operation} failed`,
|
||||
equivalentCli: "unavailable: operation failed before CLI metadata",
|
||||
request: result.exchange.request,
|
||||
response: result.exchange.response,
|
||||
durationMs: 0,
|
||||
};
|
||||
recordEvidence(record);
|
||||
rawEvidenceRef.current = [...rawEvidenceRef.current, record];
|
||||
dispatch({ type: "setRawEvidence", evidence: rawEvidenceRef.current });
|
||||
dispatch({
|
||||
type: "pushError",
|
||||
error: {
|
||||
operation,
|
||||
message: result.error.message,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (rpcError) {
|
||||
if (generation !== checkGenerationRef.current) return;
|
||||
const message = rpcError instanceof Error ? rpcError.message : String(rpcError);
|
||||
onFailure?.(message);
|
||||
const value = await read();
|
||||
if (
|
||||
targetGeneration !== generationRef.current ||
|
||||
generation !== checkGenerationRef.current
|
||||
) return;
|
||||
onSuccess(value);
|
||||
} catch (error) {
|
||||
if (
|
||||
targetGeneration !== generationRef.current ||
|
||||
generation !== checkGenerationRef.current
|
||||
) return;
|
||||
const message = readErrorMessage(error);
|
||||
const operation = readErrorOperation(error);
|
||||
onFailure?.(message, operation);
|
||||
dispatch({
|
||||
type: "pushError",
|
||||
error: {
|
||||
@@ -113,199 +65,198 @@ export const useLifecycleExplorer = (
|
||||
});
|
||||
}
|
||||
},
|
||||
[target, recordEvidence],
|
||||
[],
|
||||
);
|
||||
|
||||
const startCollectionReads = useCallback(
|
||||
(
|
||||
artifactGeneration: number,
|
||||
deploymentGeneration: number,
|
||||
runGeneration: number,
|
||||
targetGeneration: number,
|
||||
): void => {
|
||||
if (!clients) return;
|
||||
dispatch({ type: "setArtifactListPhase", phase: "loading" });
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "loading" });
|
||||
dispatch({ type: "setRunListPhase", phase: "loading" });
|
||||
|
||||
void executeRead(
|
||||
() => clients.artifacts.list({ limit: 50 }),
|
||||
artifactGeneration,
|
||||
artifactGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setArtifactListPhase", phase: "loaded", value }),
|
||||
(message) => dispatch({ type: "setArtifactListPhase", phase: "error", message }),
|
||||
);
|
||||
void executeRead(
|
||||
() => clients.deployments.list(),
|
||||
deploymentGeneration,
|
||||
deploymentGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setDeploymentListPhase", phase: "loaded", value }),
|
||||
(message) => dispatch({ type: "setDeploymentListPhase", phase: "error", message }),
|
||||
);
|
||||
void executeRead(
|
||||
() => clients.runs.list({ limit: 50 }),
|
||||
runGeneration,
|
||||
runGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setRunListPhase", phase: "loaded", value }),
|
||||
(message) => dispatch({ type: "setRunListPhase", phase: "error", message }),
|
||||
);
|
||||
},
|
||||
[clients, executeRead],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!target) return;
|
||||
generationRef.current++;
|
||||
artifactGenerationRef.current++;
|
||||
deploymentGenerationRef.current++;
|
||||
runGenerationRef.current++;
|
||||
const generation = generationRef.current;
|
||||
rawEvidenceRef.current = [];
|
||||
const targetGeneration = ++generationRef.current;
|
||||
const artifactGeneration = ++artifactGenerationRef.current;
|
||||
const deploymentGeneration = ++deploymentGenerationRef.current;
|
||||
const runGeneration = ++runGenerationRef.current;
|
||||
dispatch({ type: "targetChanged" });
|
||||
dispatch({ type: "setArtifactListPhase", phase: "loading" });
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "loading" });
|
||||
dispatch({ type: "setRunListPhase", phase: "loading" });
|
||||
|
||||
executeOperation("workflow.artifacts.list", { limit: 50 }, generation, generationRef, (interpreted) => {
|
||||
dispatch({ type: "setArtifactListPhase", phase: "loaded", value: decodeArtifactList(interpreted) });
|
||||
}, (message) => {
|
||||
dispatch({ type: "setArtifactListPhase", phase: "error", message });
|
||||
});
|
||||
|
||||
executeOperation("workflow.deployments.list", {}, generation, generationRef, (interpreted) => {
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "loaded", value: decodeDeploymentList(interpreted) });
|
||||
}, (message) => {
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "error", message });
|
||||
});
|
||||
|
||||
executeOperation("workflow.runs.list", { limit: 50 }, generation, generationRef, (interpreted) => {
|
||||
dispatch({ type: "setRunListPhase", phase: "loaded", value: decodeRunList(interpreted) });
|
||||
}, (message) => {
|
||||
dispatch({ type: "setRunListPhase", phase: "error", message });
|
||||
});
|
||||
}, [target, executeOperation]);
|
||||
startCollectionReads(
|
||||
artifactGeneration,
|
||||
deploymentGeneration,
|
||||
runGeneration,
|
||||
targetGeneration,
|
||||
);
|
||||
}, [startCollectionReads]);
|
||||
|
||||
const selectArtifact = useCallback(
|
||||
(artifactId: string | null) => {
|
||||
dispatch({ type: "selectArtifact", artifactId });
|
||||
if (!artifactId || !target) return;
|
||||
(artifactKey: string | null): void => {
|
||||
dispatch({ type: "selectArtifact", artifactId: artifactKey });
|
||||
if (!artifactKey || !clients) return;
|
||||
artifactGenerationRef.current++;
|
||||
const generation = artifactGenerationRef.current;
|
||||
const [id, version] = artifactId.split("@");
|
||||
executeOperation(
|
||||
"workflow.artifacts.inspect",
|
||||
{ artifact_id: id, version: Number(version) },
|
||||
const separator = artifactKey.lastIndexOf("@");
|
||||
const artifactId = artifactKey.slice(0, separator);
|
||||
const version = Number(artifactKey.slice(separator + 1));
|
||||
void executeRead(
|
||||
() => clients.artifacts.inspect(artifactId, version),
|
||||
generation,
|
||||
artifactGenerationRef,
|
||||
(interpreted) => {
|
||||
dispatch({ type: "setArtifactDetail", detail: decodeArtifactDetail(interpreted) });
|
||||
},
|
||||
generationRef.current,
|
||||
(value) => dispatch({ type: "setArtifactDetail", detail: value }),
|
||||
);
|
||||
},
|
||||
[target, executeOperation],
|
||||
[clients, executeRead],
|
||||
);
|
||||
|
||||
const selectDeployment = useCallback(
|
||||
(deploymentId: string | null) => {
|
||||
(deploymentId: string | null): void => {
|
||||
dispatch({ type: "selectDeployment", deploymentId });
|
||||
if (!deploymentId || !target) return;
|
||||
if (!deploymentId || !clients) return;
|
||||
deploymentGenerationRef.current++;
|
||||
const generation = deploymentGenerationRef.current;
|
||||
// Deployment selection fans out to inspect + validate. Both describe the
|
||||
// same selected deployment, so they must share one generation token.
|
||||
executeOperation(
|
||||
"workflow.deployments.inspect",
|
||||
{ deployment_id: deploymentId },
|
||||
const targetGeneration = generationRef.current;
|
||||
// Inspection and validation describe one URL-owned deployment selection.
|
||||
void executeRead(
|
||||
() => clients.deployments.inspect(deploymentId),
|
||||
generation,
|
||||
deploymentGenerationRef,
|
||||
(interpreted) => {
|
||||
dispatch({ type: "setDeploymentDetail", detail: decodeDeploymentDetail(interpreted) });
|
||||
},
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setDeploymentDetail", detail: value }),
|
||||
);
|
||||
executeOperation(
|
||||
"workflow.deployments.validate",
|
||||
{ deployment_id: deploymentId },
|
||||
void executeRead(
|
||||
() => clients.deployments.validate(deploymentId),
|
||||
generation,
|
||||
deploymentGenerationRef,
|
||||
(interpreted) => {
|
||||
dispatch({ type: "setDeploymentValidation", validation: decodeDeploymentValidation(interpreted) });
|
||||
},
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setDeploymentValidation", validation: value }),
|
||||
);
|
||||
},
|
||||
[target, executeOperation],
|
||||
[clients, executeRead],
|
||||
);
|
||||
|
||||
const selectRun = useCallback(
|
||||
(runId: string | null) => {
|
||||
(runId: string | null): void => {
|
||||
dispatch({ type: "selectRun", runId });
|
||||
if (!runId || !target) return;
|
||||
if (!runId || !clients) return;
|
||||
runGenerationRef.current++;
|
||||
const generation = runGenerationRef.current;
|
||||
executeOperation(
|
||||
"workflow.runs.inspect",
|
||||
{ run_id: runId },
|
||||
const targetGeneration = generationRef.current;
|
||||
void executeRead(
|
||||
() => clients.runs.inspect(runId),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
(interpreted) => {
|
||||
const detail = decodeRunDetail(interpreted);
|
||||
dispatch({ type: "setRunDetail", detail });
|
||||
if (detail.traceCount > 0) {
|
||||
executeOperation(
|
||||
"workflow.runs.trace",
|
||||
{ run_id: runId, trace_range: { start: 0, limit: 50 } },
|
||||
targetGeneration,
|
||||
(value) => {
|
||||
dispatch({ type: "setRunDetail", detail: value });
|
||||
if (value.traceCount > 0) {
|
||||
void executeRead(
|
||||
() => clients.runs.trace(runId, 0, 50),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
(traceInterpreted) => {
|
||||
dispatch({ type: "setTrace", trace: decodeTracePage(traceInterpreted) });
|
||||
},
|
||||
targetGeneration,
|
||||
(trace) => dispatch({ type: "setTrace", trace }),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
[target, executeOperation],
|
||||
[clients, executeRead],
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (!target) return;
|
||||
const refresh = useCallback((): void => {
|
||||
if (!clients) return;
|
||||
generationRef.current++;
|
||||
artifactGenerationRef.current++;
|
||||
deploymentGenerationRef.current++;
|
||||
runGenerationRef.current++;
|
||||
const artifactGeneration = artifactGenerationRef.current;
|
||||
const deploymentGeneration = deploymentGenerationRef.current;
|
||||
const runGeneration = runGenerationRef.current;
|
||||
dispatch({ type: "setArtifactListPhase", phase: "loading" });
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "loading" });
|
||||
dispatch({ type: "setRunListPhase", phase: "loading" });
|
||||
executeOperation("workflow.artifacts.list", { limit: 50 }, artifactGeneration, artifactGenerationRef, (interpreted) => {
|
||||
dispatch({ type: "setArtifactListPhase", phase: "loaded", value: decodeArtifactList(interpreted) });
|
||||
}, (message) => {
|
||||
dispatch({ type: "setArtifactListPhase", phase: "error", message });
|
||||
});
|
||||
executeOperation("workflow.deployments.list", {}, deploymentGeneration, deploymentGenerationRef, (interpreted) => {
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "loaded", value: decodeDeploymentList(interpreted) });
|
||||
}, (message) => {
|
||||
dispatch({ type: "setDeploymentListPhase", phase: "error", message });
|
||||
});
|
||||
executeOperation("workflow.runs.list", { limit: 50 }, runGeneration, runGenerationRef, (interpreted) => {
|
||||
dispatch({ type: "setRunListPhase", phase: "loaded", value: decodeRunList(interpreted) });
|
||||
}, (message) => {
|
||||
dispatch({ type: "setRunListPhase", phase: "error", message });
|
||||
});
|
||||
}, [target, executeOperation]);
|
||||
startCollectionReads(
|
||||
artifactGenerationRef.current,
|
||||
deploymentGenerationRef.current,
|
||||
runGenerationRef.current,
|
||||
generationRef.current,
|
||||
);
|
||||
}, [clients, startCollectionReads]);
|
||||
|
||||
const loadMoreArtifacts = useCallback(() => {
|
||||
const loadMoreArtifacts = useCallback((): void => {
|
||||
const current = state.artifactList;
|
||||
if (current.phase !== "loaded" || !current.value.nextCursor || !target) return;
|
||||
if (current.phase !== "loaded" || !current.value.nextCursor || !clients) return;
|
||||
const cursor = current.value.nextCursor;
|
||||
artifactGenerationRef.current++;
|
||||
const generation = artifactGenerationRef.current;
|
||||
executeOperation(
|
||||
"workflow.artifacts.list",
|
||||
{ cursor: current.value.nextCursor, limit: 50 },
|
||||
void executeRead(
|
||||
() => clients.artifacts.list({ cursor, limit: 50 }),
|
||||
generation,
|
||||
artifactGenerationRef,
|
||||
(interpreted) => {
|
||||
dispatch({ type: "appendArtifactList", value: decodeArtifactList(interpreted) });
|
||||
},
|
||||
generationRef.current,
|
||||
(value) => dispatch({ type: "appendArtifactList", value }),
|
||||
);
|
||||
}, [state.artifactList, target, executeOperation]);
|
||||
}, [clients, executeRead, state.artifactList]);
|
||||
|
||||
const loadMoreRuns = useCallback(() => {
|
||||
const loadMoreRuns = useCallback((): void => {
|
||||
const current = state.runList;
|
||||
if (current.phase !== "loaded" || !current.value.nextCursor || !target) return;
|
||||
if (current.phase !== "loaded" || !current.value.nextCursor || !clients) return;
|
||||
const cursor = current.value.nextCursor;
|
||||
runGenerationRef.current++;
|
||||
const generation = runGenerationRef.current;
|
||||
executeOperation(
|
||||
"workflow.runs.list",
|
||||
{ cursor: current.value.nextCursor, limit: 50 },
|
||||
void executeRead(
|
||||
() => clients.runs.list({ cursor, limit: 50 }),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
(interpreted) => {
|
||||
dispatch({ type: "appendRunList", value: decodeRunList(interpreted) });
|
||||
},
|
||||
generationRef.current,
|
||||
(value) => dispatch({ type: "appendRunList", value }),
|
||||
);
|
||||
}, [state.runList, target, executeOperation]);
|
||||
}, [clients, executeRead, state.runList]);
|
||||
|
||||
const loadTrace = useCallback(
|
||||
(start: number, limit: number) => {
|
||||
if (!state.selectedRunId || !target) return;
|
||||
(start: number, limit: number): void => {
|
||||
if (!state.selectedRunId || !clients) return;
|
||||
runGenerationRef.current++;
|
||||
const generation = runGenerationRef.current;
|
||||
executeOperation(
|
||||
"workflow.runs.trace",
|
||||
{ run_id: state.selectedRunId, trace_range: { start, limit } },
|
||||
const targetGeneration = generationRef.current;
|
||||
const runId = state.selectedRunId;
|
||||
void executeRead(
|
||||
() => clients.runs.trace(runId, start, limit),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
(interpreted) => {
|
||||
dispatch({ type: "setTrace", trace: decodeTracePage(interpreted) });
|
||||
},
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setTrace", trace: value }),
|
||||
);
|
||||
},
|
||||
[state.selectedRunId, target, executeOperation],
|
||||
[clients, executeRead, state.selectedRunId],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user