Skip to content

Commit 13bf522

Browse files
committed
feat: add read-only datasets TUI
1 parent 45eb79e commit 13bf522

8 files changed

Lines changed: 422 additions & 2 deletions

File tree

src/components/DatasetPicker.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { DatasetSummary } from "@aws-sdk/client-bedrock-agentcore-control";
2+
import { useNavigate } from "react-router";
3+
import type { ScreenProps } from "../handlers/types";
4+
import { coreOptsFromCtx } from "../handlers/utils";
5+
import { formatTimestamp } from "./formatTimestamp";
6+
import { PaginatedTablePicker } from "./PaginatedTablePicker";
7+
import type { DataTableColumn } from "./ui/data-table";
8+
9+
interface DatasetRow extends Record<string, unknown> {
10+
datasetId: string;
11+
datasetName: string;
12+
status: string;
13+
schemaType: string;
14+
exampleCount: string;
15+
updatedAt: string;
16+
}
17+
18+
export const datasetColumns = [
19+
{ key: "datasetName", header: "name", flex: true },
20+
{ key: "status", header: "status", width: 14 },
21+
{ key: "schemaType", header: "schema", width: 12 },
22+
{ key: "exampleCount", header: "examples", width: 8 },
23+
{
24+
key: "updatedAt",
25+
header: "updated UTC",
26+
width: 16,
27+
render: formatTimestamp,
28+
},
29+
] satisfies DataTableColumn<DatasetRow>[];
30+
31+
function displaySchemaType(schemaType: DatasetSummary["schemaType"]): string {
32+
if (schemaType === "AGENTCORE_EVALUATION_PREDEFINED_V1") return "predefined";
33+
if (schemaType === "AGENTCORE_EVALUATION_SIMULATED_V1") return "simulated";
34+
return schemaType ?? "-";
35+
}
36+
37+
function toRow(dataset: DatasetSummary): DatasetRow {
38+
const id = dataset.datasetId ?? "";
39+
return {
40+
datasetId: id,
41+
datasetName: dataset.datasetName ?? id,
42+
status: dataset.status ?? "-",
43+
schemaType: displaySchemaType(dataset.schemaType),
44+
exampleCount: dataset.exampleCount?.toString() ?? "-",
45+
updatedAt: dataset.updatedAt?.toISOString() ?? "-",
46+
};
47+
}
48+
49+
export interface DatasetPickerProps extends ScreenProps {
50+
breadcrumb: string[];
51+
description?: string;
52+
onSelect: (datasetId: string) => void;
53+
onEscape?: () => void;
54+
}
55+
56+
export function DatasetPicker({
57+
ctx,
58+
core,
59+
breadcrumb,
60+
description,
61+
onSelect,
62+
onEscape,
63+
}: DatasetPickerProps) {
64+
const opts = coreOptsFromCtx(ctx);
65+
const navigate = useNavigate();
66+
const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/")));
67+
68+
return (
69+
<PaginatedTablePicker
70+
breadcrumb={breadcrumb}
71+
description={description}
72+
queryKey={["datasets", opts.region]}
73+
loadPage={async (token, pageSize) => {
74+
const response = await core.eval.listDatasets(token, pageSize, opts);
75+
return {
76+
items: response.datasets ?? [],
77+
nextToken: response.nextToken,
78+
};
79+
}}
80+
toRow={toRow}
81+
columns={datasetColumns}
82+
getValue={(row) => row.datasetId}
83+
onSelect={onSelect}
84+
onBack={goBack}
85+
loadingMessage="Loading datasets…"
86+
errorMessage={(error) => `Error: ${error.message}`}
87+
emptyMessage="No datasets found in this Region."
88+
emptyPageMessage="No datasets on this page."
89+
/>
90+
);
91+
}

src/components/Root.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import {
4545
OnlineEvalGetScreen,
4646
OnlineEvalGetJsonScreen,
4747
} from "../handlers/eval/online-eval/get/screen.tsx";
48+
import { DatasetScreen } from "../handlers/eval/dataset/screen.tsx";
49+
import { DatasetListScreen } from "../handlers/eval/dataset/list/screen.tsx";
50+
import { DatasetGetScreen, DatasetGetJsonScreen } from "../handlers/eval/dataset/get/screen.tsx";
4851
import { MemoryEventScreen } from "../handlers/memory/event/screen.tsx";
4952
import { MemoryEventGetScreen } from "../handlers/memory/event/get/screen.tsx";
5053
import { MemoryEventListScreen } from "../handlers/memory/event/list/screen.tsx";
@@ -359,6 +362,23 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
359362
path="agentcore/eval/online-eval/get/:configId/json"
360363
element={<OnlineEvalGetJsonScreen ctx={ctx} core={core} />}
361364
/>
365+
<Route path="agentcore/eval/dataset" element={<DatasetScreen ctx={ctx} core={core} />} />
366+
<Route
367+
path="agentcore/eval/dataset/list"
368+
element={<DatasetListScreen ctx={ctx} core={core} />}
369+
/>
370+
<Route
371+
path="agentcore/eval/dataset/get"
372+
element={<Navigate to="/agentcore/eval/dataset/list" replace />}
373+
/>
374+
<Route
375+
path="agentcore/eval/dataset/get/:datasetId"
376+
element={<DatasetGetScreen ctx={ctx} core={core} />}
377+
/>
378+
<Route
379+
path="agentcore/eval/dataset/get/:datasetId/json"
380+
element={<DatasetGetJsonScreen ctx={ctx} core={core} />}
381+
/>
362382
<Route
363383
path="agentcore/memory/event"
364384
element={<MemoryEventScreen ctx={ctx} core={core} />}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { afterEach, describe, expect, test } from "bun:test";
2+
import type { DatasetSummary, GetDatasetResponse } from "@aws-sdk/client-bedrock-agentcore-control";
3+
import {
4+
cleanupScreens,
5+
renderScreen,
6+
TestCoreClient,
7+
waitFor,
8+
waitForText,
9+
} from "../../../testing";
10+
11+
afterEach(cleanupScreens);
12+
13+
const evalEndpointUrl = "https://eval.test";
14+
15+
function datasetSummary(overrides: Partial<DatasetSummary> = {}): DatasetSummary {
16+
return {
17+
datasetArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:dataset/dataset-1",
18+
datasetId: "dataset-1",
19+
datasetName: "orders-regression",
20+
status: "ACTIVE",
21+
draftStatus: "MODIFIED",
22+
schemaType: "AGENTCORE_EVALUATION_PREDEFINED_V1",
23+
exampleCount: 2,
24+
createdAt: new Date("2026-08-01T01:02:03.000Z"),
25+
updatedAt: new Date("2026-08-02T12:34:56.000Z"),
26+
...overrides,
27+
};
28+
}
29+
30+
function getDatasetResponse(overrides: Partial<GetDatasetResponse> = {}): GetDatasetResponse {
31+
return {
32+
datasetArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:dataset/dataset-1",
33+
datasetId: "dataset-1",
34+
datasetVersion: "DRAFT",
35+
datasetName: "orders-regression",
36+
description: "Regression tests for the order-support agent",
37+
status: "ACTIVE",
38+
draftStatus: "MODIFIED",
39+
schemaType: "AGENTCORE_EVALUATION_PREDEFINED_V1",
40+
exampleCount: 2,
41+
createdAt: new Date("2026-08-01T01:02:03.000Z"),
42+
updatedAt: new Date("2026-08-02T12:34:56.000Z"),
43+
tags: { team: "agentcore" },
44+
...overrides,
45+
};
46+
}
47+
48+
function coreWithDatasets(datasets: DatasetSummary[]): TestCoreClient {
49+
const core = new TestCoreClient();
50+
core.eval.setListDatasetsResponse({ datasets });
51+
return core;
52+
}
53+
54+
describe("dataset menu", () => {
55+
test("offers only the read-only commands", async () => {
56+
const screen = renderScreen("/agentcore/eval/dataset");
57+
58+
await waitForText(screen.lastFrame, "get a dataset's metadata");
59+
const frame = screen.lastFrame()!;
60+
expect(frame).toContain("list");
61+
expect(frame).not.toContain("create");
62+
expect(frame).not.toContain("update");
63+
expect(frame).not.toContain("publish");
64+
expect(frame).not.toContain("delete");
65+
});
66+
});
67+
68+
describe("dataset picker", () => {
69+
test("renders name, status, schema, example count, and update time", async () => {
70+
const core = coreWithDatasets([
71+
datasetSummary({
72+
datasetName: "staging-regression",
73+
status: "UPDATE_FAILED",
74+
schemaType: "AGENTCORE_EVALUATION_SIMULATED_V1",
75+
exampleCount: 17,
76+
updatedAt: new Date("2026-08-03T02:03:04.000Z"),
77+
}),
78+
]);
79+
const screen = renderScreen("/agentcore/eval/dataset/list", { core });
80+
81+
await waitForText(screen.lastFrame, "staging-regression");
82+
const frame = screen.lastFrame()!;
83+
expect(frame).toContain("UPDATE_FAILED");
84+
expect(frame).toContain("simulated");
85+
expect(frame).toMatch(/17\s+2026-08-03 02:03/);
86+
});
87+
88+
test("calls listDatasets with exact Core options", async () => {
89+
const core = coreWithDatasets([datasetSummary()]);
90+
renderScreen("/agentcore/eval/dataset/list", { core, endpointUrl: evalEndpointUrl });
91+
92+
await waitFor(() => core.eval.calls.some((call) => call.method === "listDatasets"));
93+
expect(core.eval.calls.filter((call) => call.method === "listDatasets")).toEqual([
94+
{
95+
method: "listDatasets",
96+
args: [
97+
undefined,
98+
expect.any(Number),
99+
{ region: "us-east-1", endpointUrl: evalEndpointUrl },
100+
],
101+
},
102+
]);
103+
});
104+
105+
test("bare dataset get redirects to the picker", async () => {
106+
const core = coreWithDatasets([
107+
datasetSummary({ datasetId: "redirected-dataset", datasetName: "redirected-dataset" }),
108+
]);
109+
const screen = renderScreen("/agentcore/eval/dataset/get", { core });
110+
111+
await waitForText(screen.lastFrame, "redirected-dataset");
112+
expect(core.eval.calls[0]?.method).toBe("listDatasets");
113+
});
114+
115+
test("selection opens the matching dataset detail", async () => {
116+
const core = coreWithDatasets([datasetSummary({ datasetId: "dataset-1" })]);
117+
core.eval.setGetDatasetResponse(getDatasetResponse({ datasetId: "dataset-1" }));
118+
const screen = renderScreen("/agentcore/eval/dataset/list", { core });
119+
120+
await waitForText(screen.lastFrame, "orders-regression");
121+
await screen.press("return");
122+
await waitForText(screen.lastFrame, "agentcore → eval → dataset → get → dataset-1");
123+
await waitFor(() =>
124+
core.eval.calls.some((call) => call.method === "getDataset" && call.args[0] === "dataset-1"),
125+
);
126+
});
127+
128+
test("shows the empty state", async () => {
129+
const screen = renderScreen("/agentcore/eval/dataset/list");
130+
await waitForText(screen.lastFrame, "No datasets found in this Region.");
131+
});
132+
});
133+
134+
describe("dataset detail", () => {
135+
test("renders DRAFT metadata without downloading examples", async () => {
136+
const core = new TestCoreClient();
137+
core.eval.setGetDatasetResponse(getDatasetResponse());
138+
const screen = renderScreen("/agentcore/eval/dataset/get/dataset-1", {
139+
core,
140+
endpointUrl: evalEndpointUrl,
141+
});
142+
143+
await waitForText(screen.lastFrame, "show the full JSON metadata");
144+
const frame = screen.lastFrame()!;
145+
expect(frame).toContain("orders-regression");
146+
expect(frame).toMatch(/version\s+DRAFT/);
147+
expect(frame).toMatch(/draftStatus\s+MODIFIED/);
148+
expect(frame).toMatch(/examples\s+2/);
149+
expect(core.eval.calls).toEqual([
150+
{
151+
method: "getDataset",
152+
args: ["dataset-1", undefined, { region: "us-east-1", endpointUrl: evalEndpointUrl }],
153+
},
154+
]);
155+
});
156+
157+
test("shows failure details when present", async () => {
158+
const core = new TestCoreClient();
159+
core.eval.setGetDatasetResponse(
160+
getDatasetResponse({
161+
status: "UPDATE_FAILED",
162+
failureReason: "The source could not be read",
163+
}),
164+
);
165+
const screen = renderScreen("/agentcore/eval/dataset/get/dataset-1", { core });
166+
167+
await waitForText(screen.lastFrame, "The source could not be read");
168+
expect(screen.lastFrame()).toContain("UPDATE_FAILED");
169+
});
170+
171+
test("opens the complete dataset JSON", async () => {
172+
const core = new TestCoreClient();
173+
core.eval.setGetDatasetResponse(getDatasetResponse());
174+
const screen = renderScreen("/agentcore/eval/dataset/get/dataset-1", { core });
175+
176+
await waitForText(screen.lastFrame, "show the full JSON metadata");
177+
await screen.press("return");
178+
await waitForText(screen.lastFrame, "agentcore → eval → dataset → get → dataset-1 → json");
179+
expect(screen.lastFrame()).toContain('"team"');
180+
});
181+
182+
test("retries a failed detail query", async () => {
183+
const core = new TestCoreClient();
184+
core.eval.setError(new Error("dataset unavailable"));
185+
const screen = renderScreen("/agentcore/eval/dataset/get/dataset-1", { core });
186+
187+
await waitForText(screen.lastFrame, "dataset unavailable");
188+
expect(screen.lastFrame()).toContain("[r] retry");
189+
190+
core.eval.setError(undefined);
191+
core.eval.setGetDatasetResponse(getDatasetResponse());
192+
await screen.write("r");
193+
await waitForText(screen.lastFrame, "show the full JSON metadata");
194+
});
195+
});

src/handlers/eval/dataset/dataset.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ describe("eval dataset command hierarchy", () => {
9292
expect(stdout()).toContain("Usage: agentcore eval dataset");
9393
expect(core.eval.calls).toHaveLength(0);
9494
});
95+
96+
test.each([["get"], ["list"]] as const)(
97+
"opens the TUI for a bare `eval dataset %s` leaf",
98+
async (command) => {
99+
const { route } = testDatasetCommand();
100+
101+
await expect(route(["eval", "dataset", command])).rejects.toThrow(
102+
"interactive mode requires a TTY on stdin and stdout",
103+
);
104+
},
105+
);
95106
});
96107

97108
describe("dataset create", () => {

0 commit comments

Comments
 (0)