fix: harden routed lifecycle guards
This commit is contained in:
@@ -48,6 +48,18 @@ describe("LifecycleExplorer", () => {
|
||||
expect(screen.queryByRole("button", { name: "Raw" })).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the primary lifecycle collection first in DOM order", () => {
|
||||
const { container } = render(
|
||||
<LifecycleExplorer controller={createMockController()} primaryKind="run" />,
|
||||
);
|
||||
|
||||
expect(
|
||||
[...container.querySelectorAll<HTMLElement>("[data-lifecycle-kind]")].map(
|
||||
(column) => column.dataset.lifecycleKind,
|
||||
),
|
||||
).toEqual(["run", "artifact", "deployment"]);
|
||||
});
|
||||
|
||||
it("renders artifact buttons when loaded", () => {
|
||||
const controller = createMockController({
|
||||
artifactList: {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import type { ArtifactSummary, DeploymentSummary, RunSummary } from "./models.js";
|
||||
|
||||
type LifecycleKind = "artifact" | "deployment" | "run";
|
||||
|
||||
type RecordColumnsProps = {
|
||||
readonly artifacts: ReadonlyArray<ArtifactSummary>;
|
||||
readonly deployments: ReadonlyArray<DeploymentSummary>;
|
||||
@@ -7,7 +10,7 @@ type RecordColumnsProps = {
|
||||
readonly selectedArtifactId: string | null;
|
||||
readonly selectedDeploymentId: string | null;
|
||||
readonly selectedRunId: string | null;
|
||||
readonly primaryKind: "artifact" | "deployment" | "run";
|
||||
readonly primaryKind: LifecycleKind;
|
||||
readonly onSelectArtifact: (artifactId: string | null) => void;
|
||||
readonly onSelectDeployment: (deploymentId: string | null) => void;
|
||||
readonly onSelectRun: (runId: string | null) => void;
|
||||
@@ -17,6 +20,25 @@ type RecordColumnsProps = {
|
||||
readonly hasMoreRuns?: boolean;
|
||||
};
|
||||
|
||||
const allKinds: ReadonlyArray<LifecycleKind> = ["artifact", "deployment", "run"];
|
||||
|
||||
const LifecycleColumn = ({
|
||||
kind,
|
||||
primaryKind,
|
||||
children,
|
||||
}: {
|
||||
readonly kind: LifecycleKind;
|
||||
readonly primaryKind: LifecycleKind;
|
||||
readonly children: ReactNode;
|
||||
}) => (
|
||||
<div
|
||||
className={`lifecycle-column lifecycle-column--${kind}${primaryKind === kind ? " lifecycle-column--primary" : ""}`}
|
||||
data-lifecycle-kind={kind}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const RecordColumns = ({
|
||||
artifacts,
|
||||
deployments,
|
||||
@@ -32,89 +54,93 @@ export const RecordColumns = ({
|
||||
hasMoreArtifacts,
|
||||
onLoadMoreRuns,
|
||||
hasMoreRuns,
|
||||
}: RecordColumnsProps) => (
|
||||
<div className="lifecycle-columns">
|
||||
<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>
|
||||
) : (
|
||||
<ul role="listbox" aria-label="Artifacts">
|
||||
{artifacts.map((artifact) => (
|
||||
<li key={artifact.key}>
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedArtifactId === artifact.key}
|
||||
onClick={() => onSelectArtifact(artifact.key)}
|
||||
className={selectedArtifactId === artifact.key ? "selected" : ""}
|
||||
>
|
||||
{artifact.displayName} version {artifact.version}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{hasMoreArtifacts && onLoadMoreArtifacts && (
|
||||
<button type="button" onClick={onLoadMoreArtifacts} className="load-more">
|
||||
Load more artifacts
|
||||
</button>
|
||||
)}
|
||||
}: RecordColumnsProps) => {
|
||||
const columns: Record<LifecycleKind, ReactElement> = {
|
||||
artifact: (
|
||||
<LifecycleColumn key="artifact" kind="artifact" primaryKind={primaryKind}>
|
||||
<h3>Artifacts</h3>
|
||||
{artifacts.length === 0 ? (
|
||||
<p className="empty-state">No artifacts</p>
|
||||
) : (
|
||||
<ul role="listbox" aria-label="Artifacts">
|
||||
{artifacts.map((artifact) => (
|
||||
<li key={artifact.key}>
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedArtifactId === artifact.key}
|
||||
onClick={() => onSelectArtifact(artifact.key)}
|
||||
className={selectedArtifactId === artifact.key ? "selected" : ""}
|
||||
>
|
||||
{artifact.displayName} version {artifact.version}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{hasMoreArtifacts && onLoadMoreArtifacts && (
|
||||
<button type="button" onClick={onLoadMoreArtifacts} className="load-more">
|
||||
Load more artifacts
|
||||
</button>
|
||||
)}
|
||||
</LifecycleColumn>
|
||||
),
|
||||
deployment: (
|
||||
<LifecycleColumn key="deployment" kind="deployment" primaryKind={primaryKind}>
|
||||
<h3>Deployments</h3>
|
||||
{deployments.length === 0 ? (
|
||||
<p className="empty-state">No deployments</p>
|
||||
) : (
|
||||
<ul role="listbox" aria-label="Deployments">
|
||||
{deployments.map((deployment) => (
|
||||
<li key={deployment.id}>
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedDeploymentId === deployment.id}
|
||||
onClick={() => onSelectDeployment(deployment.id)}
|
||||
className={selectedDeploymentId === deployment.id ? "selected" : ""}
|
||||
>
|
||||
{deployment.id}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</LifecycleColumn>
|
||||
),
|
||||
run: (
|
||||
<LifecycleColumn key="run" kind="run" primaryKind={primaryKind}>
|
||||
<h3>Runs</h3>
|
||||
{runs.length === 0 ? (
|
||||
<p className="empty-state">No runs</p>
|
||||
) : (
|
||||
<ul role="listbox" aria-label="Runs">
|
||||
{runs.map((run) => (
|
||||
<li key={run.runId}>
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedRunId === run.runId}
|
||||
onClick={() => onSelectRun(run.runId)}
|
||||
className={selectedRunId === run.runId ? "selected" : ""}
|
||||
>
|
||||
{run.runId} {run.status}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{hasMoreRuns && onLoadMoreRuns && (
|
||||
<button type="button" onClick={onLoadMoreRuns} className="load-more">
|
||||
Load more runs
|
||||
</button>
|
||||
)}
|
||||
</LifecycleColumn>
|
||||
),
|
||||
};
|
||||
const orderedKinds = [primaryKind, ...allKinds.filter((kind) => kind !== primaryKind)];
|
||||
|
||||
return (
|
||||
<div className="lifecycle-columns">
|
||||
{orderedKinds.map((kind) => columns[kind])}
|
||||
</div>
|
||||
<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>
|
||||
) : (
|
||||
<ul role="listbox" aria-label="Deployments">
|
||||
{deployments.map((deployment) => (
|
||||
<li key={deployment.id}>
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedDeploymentId === deployment.id}
|
||||
onClick={() => onSelectDeployment(deployment.id)}
|
||||
className={selectedDeploymentId === deployment.id ? "selected" : ""}
|
||||
>
|
||||
{deployment.id}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
) : (
|
||||
<ul role="listbox" aria-label="Runs">
|
||||
{runs.map((run) => (
|
||||
<li key={run.runId}>
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedRunId === run.runId}
|
||||
onClick={() => onSelectRun(run.runId)}
|
||||
className={selectedRunId === run.runId ? "selected" : ""}
|
||||
>
|
||||
{run.runId} {run.status}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{hasMoreRuns && onLoadMoreRuns && (
|
||||
<button type="button" onClick={onLoadMoreRuns} className="load-more">
|
||||
Load more runs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
DeploymentValidation,
|
||||
RunDetail,
|
||||
RunList,
|
||||
TracePage,
|
||||
} from "./models.js";
|
||||
|
||||
const artifactList: ArtifactList = { items: [], total: 0, nextCursor: null };
|
||||
@@ -106,6 +107,14 @@ const makeClients = (overrides: Partial<{
|
||||
},
|
||||
});
|
||||
|
||||
const deferred = <T,>() => {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
});
|
||||
return { promise, resolve };
|
||||
};
|
||||
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("useLifecycleExplorer", () => {
|
||||
@@ -203,6 +212,153 @@ describe("useLifecycleExplorer", () => {
|
||||
expect(result.current.state.artifactDetail?.version).toBe(2);
|
||||
});
|
||||
|
||||
it("rejects a late artifact detail after a null selection", async () => {
|
||||
const detail = deferred<ArtifactDetail>();
|
||||
const clients = makeClients({
|
||||
artifacts: { inspect: vi.fn().mockReturnValue(detail.promise) },
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectArtifact("report@2"));
|
||||
act(() => result.current.selectArtifact(null));
|
||||
detail.resolve(artifactDetail);
|
||||
await act(async () => await detail.promise);
|
||||
|
||||
expect(result.current.state.selectedArtifactId).toBeNull();
|
||||
expect(result.current.state.artifactDetail).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a late artifact detail after a cross-kind selection", async () => {
|
||||
const detail = deferred<ArtifactDetail>();
|
||||
const clients = makeClients({
|
||||
artifacts: { inspect: vi.fn().mockReturnValue(detail.promise) },
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectArtifact("report@2"));
|
||||
act(() => result.current.selectDeployment("report.default"));
|
||||
detail.resolve(artifactDetail);
|
||||
await act(async () => await detail.promise);
|
||||
|
||||
expect(result.current.state.artifactDetail).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects late deployment validation after a cross-kind selection", async () => {
|
||||
const validation = deferred<DeploymentValidation>();
|
||||
const clients = makeClients({
|
||||
deployments: { validate: vi.fn().mockReturnValue(validation.promise) },
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectDeployment("report.default"));
|
||||
act(() => result.current.selectArtifact(null));
|
||||
validation.resolve(deploymentValidation);
|
||||
await act(async () => await validation.promise);
|
||||
|
||||
expect(result.current.state.deploymentValidation).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a late deployment detail after a null selection", async () => {
|
||||
const detail = deferred<DeploymentDetail>();
|
||||
const clients = makeClients({
|
||||
deployments: { inspect: vi.fn().mockReturnValue(detail.promise) },
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectDeployment("report.default"));
|
||||
act(() => result.current.selectDeployment(null));
|
||||
detail.resolve(deploymentDetail);
|
||||
await act(async () => await detail.promise);
|
||||
|
||||
expect(result.current.state.selectedDeploymentId).toBeNull();
|
||||
expect(result.current.state.deploymentDetail).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a late run detail after a cross-kind selection", async () => {
|
||||
const detail = deferred<RunDetail>();
|
||||
const clients = makeClients({
|
||||
runs: { inspect: vi.fn().mockReturnValue(detail.promise) },
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectRun("run_123"));
|
||||
act(() => result.current.selectArtifact(null));
|
||||
detail.resolve(runDetail);
|
||||
await act(async () => await detail.promise);
|
||||
|
||||
expect(result.current.state.selectedRunId).toBeNull();
|
||||
expect(result.current.state.runDetail).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a late run trace after a null selection", async () => {
|
||||
const trace = deferred<TracePage>();
|
||||
const clients = makeClients({
|
||||
runs: {
|
||||
inspect: vi.fn().mockResolvedValue({ ...runDetail, traceCount: 1 }),
|
||||
trace: vi.fn().mockReturnValue(trace.promise),
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectRun("run_123"));
|
||||
await waitFor(() => expect(clients.runs.trace).toHaveBeenCalledWith("run_123", 0, 50));
|
||||
act(() => result.current.selectDeployment(null));
|
||||
trace.resolve({ frames: [], traceStart: 0, traceLimit: 50, traceTruncated: false });
|
||||
await act(async () => await trace.promise);
|
||||
|
||||
expect(result.current.state.selectedRunId).toBeNull();
|
||||
expect(result.current.state.trace).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a late run trace after a cross-kind selection", async () => {
|
||||
const trace = deferred<TracePage>();
|
||||
const clients = makeClients({
|
||||
runs: {
|
||||
inspect: vi.fn().mockResolvedValue({ ...runDetail, traceCount: 1 }),
|
||||
trace: vi.fn().mockReturnValue(trace.promise),
|
||||
},
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => result.current.selectRun("run_123"));
|
||||
await waitFor(() => expect(clients.runs.trace).toHaveBeenCalledWith("run_123", 0, 50));
|
||||
act(() => result.current.selectArtifact(null));
|
||||
trace.resolve({ frames: [], traceStart: 0, traceLimit: 50, traceTruncated: false });
|
||||
await act(async () => await trace.promise);
|
||||
|
||||
expect(result.current.state.trace).toBeNull();
|
||||
});
|
||||
|
||||
it("finishes every collection load when a direct detail selection starts first", async () => {
|
||||
const artifacts = deferred<ArtifactList>();
|
||||
const deployments = deferred<DeploymentList>();
|
||||
const runs = deferred<RunList>();
|
||||
const clients = makeClients({
|
||||
artifacts: { list: vi.fn().mockReturnValue(artifacts.promise) },
|
||||
deployments: { list: vi.fn().mockReturnValue(deployments.promise) },
|
||||
runs: { list: vi.fn().mockReturnValue(runs.promise) },
|
||||
});
|
||||
const { result } = renderHook(() => useLifecycleExplorer(clients));
|
||||
|
||||
act(() => {
|
||||
result.current.selectArtifact("report@2");
|
||||
result.current.selectDeployment("report.default");
|
||||
result.current.selectRun("run_123");
|
||||
});
|
||||
artifacts.resolve(artifactList);
|
||||
deployments.resolve(deploymentList);
|
||||
runs.resolve(runList);
|
||||
await act(async () => await Promise.all([
|
||||
artifacts.promise,
|
||||
deployments.promise,
|
||||
runs.promise,
|
||||
]));
|
||||
|
||||
expect(result.current.state.artifactList.phase).toBe("loaded");
|
||||
expect(result.current.state.deploymentList.phase).toBe("loaded");
|
||||
expect(result.current.state.runList.phase).toBe("loaded");
|
||||
});
|
||||
|
||||
it("clears lifecycle state when the client bundle is disconnected", async () => {
|
||||
const clients = makeClients();
|
||||
const { result, rerender } = renderHook(
|
||||
|
||||
@@ -27,9 +27,18 @@ export const useLifecycleExplorer = (
|
||||
): LifecycleExplorerController => {
|
||||
const [state, dispatch] = useReducer(lifecycleReducer, initialLifecycleState);
|
||||
const generationRef = useRef(0);
|
||||
const artifactGenerationRef = useRef(0);
|
||||
const deploymentGenerationRef = useRef(0);
|
||||
const runGenerationRef = useRef(0);
|
||||
const artifactListGenerationRef = useRef(0);
|
||||
const artifactDetailGenerationRef = useRef(0);
|
||||
const deploymentListGenerationRef = useRef(0);
|
||||
const deploymentDetailGenerationRef = useRef(0);
|
||||
const runListGenerationRef = useRef(0);
|
||||
const runDetailGenerationRef = useRef(0);
|
||||
|
||||
const invalidateDetailReads = useCallback((): void => {
|
||||
artifactDetailGenerationRef.current++;
|
||||
deploymentDetailGenerationRef.current++;
|
||||
runDetailGenerationRef.current++;
|
||||
}, []);
|
||||
|
||||
const executeRead = useCallback(
|
||||
async <T>(
|
||||
@@ -83,7 +92,7 @@ export const useLifecycleExplorer = (
|
||||
void executeRead(
|
||||
() => clients.artifacts.list({ limit: 50 }),
|
||||
artifactGeneration,
|
||||
artifactGenerationRef,
|
||||
artifactListGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setArtifactListPhase", phase: "loaded", value }),
|
||||
(message) => dispatch({ type: "setArtifactListPhase", phase: "error", message }),
|
||||
@@ -91,7 +100,7 @@ export const useLifecycleExplorer = (
|
||||
void executeRead(
|
||||
() => clients.deployments.list(),
|
||||
deploymentGeneration,
|
||||
deploymentGenerationRef,
|
||||
deploymentListGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setDeploymentListPhase", phase: "loaded", value }),
|
||||
(message) => dispatch({ type: "setDeploymentListPhase", phase: "error", message }),
|
||||
@@ -99,7 +108,7 @@ export const useLifecycleExplorer = (
|
||||
void executeRead(
|
||||
() => clients.runs.list({ limit: 50 }),
|
||||
runGeneration,
|
||||
runGenerationRef,
|
||||
runListGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setRunListPhase", phase: "loaded", value }),
|
||||
(message) => dispatch({ type: "setRunListPhase", phase: "error", message }),
|
||||
@@ -110,9 +119,10 @@ export const useLifecycleExplorer = (
|
||||
|
||||
useEffect(() => {
|
||||
const targetGeneration = ++generationRef.current;
|
||||
const artifactGeneration = ++artifactGenerationRef.current;
|
||||
const deploymentGeneration = ++deploymentGenerationRef.current;
|
||||
const runGeneration = ++runGenerationRef.current;
|
||||
const artifactGeneration = ++artifactListGenerationRef.current;
|
||||
const deploymentGeneration = ++deploymentListGenerationRef.current;
|
||||
const runGeneration = ++runListGenerationRef.current;
|
||||
invalidateDetailReads();
|
||||
dispatch({ type: "targetChanged" });
|
||||
startCollectionReads(
|
||||
artifactGeneration,
|
||||
@@ -120,65 +130,65 @@ export const useLifecycleExplorer = (
|
||||
runGeneration,
|
||||
targetGeneration,
|
||||
);
|
||||
}, [startCollectionReads]);
|
||||
}, [invalidateDetailReads, startCollectionReads]);
|
||||
|
||||
const selectArtifact = useCallback(
|
||||
(artifactKey: string | null): void => {
|
||||
invalidateDetailReads();
|
||||
dispatch({ type: "selectArtifact", artifactId: artifactKey });
|
||||
if (!artifactKey || !clients) return;
|
||||
artifactGenerationRef.current++;
|
||||
const generation = artifactGenerationRef.current;
|
||||
const generation = artifactDetailGenerationRef.current;
|
||||
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,
|
||||
artifactDetailGenerationRef,
|
||||
generationRef.current,
|
||||
(value) => dispatch({ type: "setArtifactDetail", detail: value }),
|
||||
);
|
||||
},
|
||||
[clients, executeRead],
|
||||
[clients, executeRead, invalidateDetailReads],
|
||||
);
|
||||
|
||||
const selectDeployment = useCallback(
|
||||
(deploymentId: string | null): void => {
|
||||
invalidateDetailReads();
|
||||
dispatch({ type: "selectDeployment", deploymentId });
|
||||
if (!deploymentId || !clients) return;
|
||||
deploymentGenerationRef.current++;
|
||||
const generation = deploymentGenerationRef.current;
|
||||
const generation = deploymentDetailGenerationRef.current;
|
||||
const targetGeneration = generationRef.current;
|
||||
// Inspection and validation describe one URL-owned deployment selection.
|
||||
void executeRead(
|
||||
() => clients.deployments.inspect(deploymentId),
|
||||
generation,
|
||||
deploymentGenerationRef,
|
||||
deploymentDetailGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setDeploymentDetail", detail: value }),
|
||||
);
|
||||
void executeRead(
|
||||
() => clients.deployments.validate(deploymentId),
|
||||
generation,
|
||||
deploymentGenerationRef,
|
||||
deploymentDetailGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setDeploymentValidation", validation: value }),
|
||||
);
|
||||
},
|
||||
[clients, executeRead],
|
||||
[clients, executeRead, invalidateDetailReads],
|
||||
);
|
||||
|
||||
const selectRun = useCallback(
|
||||
(runId: string | null): void => {
|
||||
invalidateDetailReads();
|
||||
dispatch({ type: "selectRun", runId });
|
||||
if (!runId || !clients) return;
|
||||
runGenerationRef.current++;
|
||||
const generation = runGenerationRef.current;
|
||||
const generation = runDetailGenerationRef.current;
|
||||
const targetGeneration = generationRef.current;
|
||||
void executeRead(
|
||||
() => clients.runs.inspect(runId),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
runDetailGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => {
|
||||
dispatch({ type: "setRunDetail", detail: value });
|
||||
@@ -186,7 +196,7 @@ export const useLifecycleExplorer = (
|
||||
void executeRead(
|
||||
() => clients.runs.trace(runId, 0, 50),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
runDetailGenerationRef,
|
||||
targetGeneration,
|
||||
(trace) => dispatch({ type: "setTrace", trace }),
|
||||
);
|
||||
@@ -194,33 +204,33 @@ export const useLifecycleExplorer = (
|
||||
},
|
||||
);
|
||||
},
|
||||
[clients, executeRead],
|
||||
[clients, executeRead, invalidateDetailReads],
|
||||
);
|
||||
|
||||
const refresh = useCallback((): void => {
|
||||
if (!clients) return;
|
||||
generationRef.current++;
|
||||
artifactGenerationRef.current++;
|
||||
deploymentGenerationRef.current++;
|
||||
runGenerationRef.current++;
|
||||
invalidateDetailReads();
|
||||
artifactListGenerationRef.current++;
|
||||
deploymentListGenerationRef.current++;
|
||||
runListGenerationRef.current++;
|
||||
startCollectionReads(
|
||||
artifactGenerationRef.current,
|
||||
deploymentGenerationRef.current,
|
||||
runGenerationRef.current,
|
||||
artifactListGenerationRef.current,
|
||||
deploymentListGenerationRef.current,
|
||||
runListGenerationRef.current,
|
||||
generationRef.current,
|
||||
);
|
||||
}, [clients, startCollectionReads]);
|
||||
}, [clients, invalidateDetailReads, startCollectionReads]);
|
||||
|
||||
const loadMoreArtifacts = useCallback((): void => {
|
||||
const current = state.artifactList;
|
||||
if (current.phase !== "loaded" || !current.value.nextCursor || !clients) return;
|
||||
const cursor = current.value.nextCursor;
|
||||
artifactGenerationRef.current++;
|
||||
const generation = artifactGenerationRef.current;
|
||||
const generation = ++artifactListGenerationRef.current;
|
||||
void executeRead(
|
||||
() => clients.artifacts.list({ cursor, limit: 50 }),
|
||||
generation,
|
||||
artifactGenerationRef,
|
||||
artifactListGenerationRef,
|
||||
generationRef.current,
|
||||
(value) => dispatch({ type: "appendArtifactList", value }),
|
||||
);
|
||||
@@ -230,12 +240,11 @@ export const useLifecycleExplorer = (
|
||||
const current = state.runList;
|
||||
if (current.phase !== "loaded" || !current.value.nextCursor || !clients) return;
|
||||
const cursor = current.value.nextCursor;
|
||||
runGenerationRef.current++;
|
||||
const generation = runGenerationRef.current;
|
||||
const generation = ++runListGenerationRef.current;
|
||||
void executeRead(
|
||||
() => clients.runs.list({ cursor, limit: 50 }),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
runListGenerationRef,
|
||||
generationRef.current,
|
||||
(value) => dispatch({ type: "appendRunList", value }),
|
||||
);
|
||||
@@ -244,14 +253,13 @@ export const useLifecycleExplorer = (
|
||||
const loadTrace = useCallback(
|
||||
(start: number, limit: number): void => {
|
||||
if (!state.selectedRunId || !clients) return;
|
||||
runGenerationRef.current++;
|
||||
const generation = runGenerationRef.current;
|
||||
const generation = ++runDetailGenerationRef.current;
|
||||
const targetGeneration = generationRef.current;
|
||||
const runId = state.selectedRunId;
|
||||
void executeRead(
|
||||
() => clients.runs.trace(runId, start, limit),
|
||||
generation,
|
||||
runGenerationRef,
|
||||
runDetailGenerationRef,
|
||||
targetGeneration,
|
||||
(value) => dispatch({ type: "setTrace", trace: value }),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user