Skip to content

Commit 1b6f30a

Browse files
maazghaniEItanyapeterj
authored
implement UI namespace filtering (#1923)
## Summary part 2 of addressing #1812 This connects the agents list UI to the namespace filtering support added in the previous API/client PR. `/agents` stays the all-namespaces view. `/agents?namespace=<namespace>` now fetches and renders agents scoped to that namespace. The namespace picker drives both the URL and the agents request, so there is no “trust me, the request was scoped” UI state. The scoped empty state also carries the namespace into `New Agent`, but only as the starting value. The create form still lets the user change the namespace before saving. ## Notes `AgentList` owns list fetching now because the selected namespace comes from the route. `AgentsProvider` still handles shared create/edit dependencies without fetching every agent on mount. Delete refresh gets passed down from the current list view so deletes refresh the same scoped query. ## Tests Added coverage for: - `/agents` fetching all agents - `/agents?namespace=kagent` fetching scoped agents - namespace selector URL updates - clearing back to all namespaces - scoped empty state linking to `New Agent` - create form initializing from `?namespace=...` - `getAgents()` normalizing a missing data field to `[]` <img width="1068" height="1102" alt="IMG_3505" src="https://github.com/user-attachments/assets/cc734fd9-5ab1-4f3b-912e-9ef887d81548" /> <img width="1083" height="1174" alt="IMG_3506" src="https://github.com/user-attachments/assets/910fecbc-f099-416e-8fa6-217138c9c53e" /> <img width="1061" height="716" alt="IMG_3507" src="https://github.com/user-attachments/assets/6c39e278-6a69-4069-9609-23a0c5bef49f" /> --------- Signed-off-by: Maaz Ghani <maazghani@gmail.com> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io> Co-authored-by: Peter Jausovec <peterj@users.noreply.github.com>
1 parent 92d5b16 commit 1b6f30a

14 files changed

Lines changed: 571 additions & 82 deletions

ui/src/app/actions/agents.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,9 @@ export async function getAgents(opts: { namespace?: string } = {}): Promise<Base
571571
try {
572572
const path = opts.namespace ? `/agents?namespace=${encodeURIComponent(opts.namespace)}` : `/agents`;
573573
const { data } = await fetchApi<BaseResponse<AgentResponse[]>>(path);
574+
const agents = Array.isArray(data) ? data : [];
574575

575-
const sortedData = data?.sort((a, b) => {
576+
const sortedData = agents.sort((a, b) => {
576577
const aRef = k8sRefUtils.toRef(a.agent.metadata.namespace || "", a.agent.metadata.name);
577578
const bRef = k8sRefUtils.toRef(b.agent.metadata.namespace || "", b.agent.metadata.name);
578579
return aRef.localeCompare(bRef);

ui/src/app/agents/new/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const DEFAULT_SYSTEM_PROMPT = `You're a helpful agent, made by the kagent team.
6363
function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageContentProps) {
6464
const router = useRouter();
6565
const { models, loading, error, createNewAgent, updateAgent, getAgent, validateAgentData } = useAgents();
66+
const initialNamespace = !isEditMode && agentNamespace?.trim() ? agentNamespace.trim() : "default";
6667

6768
type SelectedModelType = ModelConfig;
6869

@@ -101,7 +102,7 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo
101102

102103
const [state, setState] = useState<FormState>({
103104
name: "",
104-
namespace: "default",
105+
namespace: initialNamespace,
105106
description: "",
106107
agentType: "Declarative",
107108
systemPrompt: isEditMode ? "" : DEFAULT_SYSTEM_PROMPT,
@@ -487,7 +488,11 @@ function AgentPageContent({ isEditMode, agentName, agentNamespace }: AgentPageCo
487488
}
488489

489490
setFormDirty(false);
490-
router.push(`/agents`);
491+
const returnPath =
492+
!isEditMode && agentNamespace
493+
? `/agents?namespace=${encodeURIComponent(state.namespace)}`
494+
: "/agents";
495+
router.push(returnPath);
491496
} catch (e) {
492497
console.error(`Error ${isEditMode ? "updating" : "creating"} agent:`, e);
493498
const errorMessage =
@@ -882,7 +887,7 @@ export default function AgentPage() {
882887
const isEditMode = searchParams.get("edit") === "true";
883888
const agentName = searchParams.get("name");
884889
const agentNamespace = searchParams.get("namespace");
885-
const formKey = isEditMode ? `edit-${agentName}-${agentNamespace}` : "create";
890+
const formKey = isEditMode ? `edit-${agentName}-${agentNamespace}` : `create-${agentNamespace || "default"}`;
886891

887892
return (
888893
<Suspense fallback={<LoadingState />}>

ui/src/components/AgentCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import { cn } from "@/lib/utils";
2929

3030
interface AgentCardProps {
3131
agentResponse: AgentResponse;
32+
onAgentsChanged?: () => Promise<void> | void;
3233
}
3334

34-
export function AgentCard({ agentResponse }: AgentCardProps) {
35+
export function AgentCard({ agentResponse, onAgentsChanged }: AgentCardProps) {
3536
const { agent, model, modelProvider, deploymentReady, accepted } = agentResponse;
3637
const router = useRouter();
3738
const [memoriesOpen, setMemoriesOpen] = useState(false);
@@ -197,6 +198,7 @@ export function AgentCard({ agentResponse }: AgentCardProps) {
197198
<DeleteButton
198199
agentName={agent.metadata.name}
199200
namespace={agent.metadata.namespace || ''}
201+
onDeleted={onAgentsChanged}
200202
externalOpen={deleteOpen}
201203
onExternalOpenChange={setDeleteOpen}
202204
/>

ui/src/components/AgentGrid.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { k8sRefUtils } from "@/lib/k8sUtils";
44

55
interface AgentGridProps {
66
agentResponse: AgentResponse[];
7+
onAgentsChanged?: () => Promise<void> | void;
78
}
89

9-
export function AgentGrid({ agentResponse }: AgentGridProps) {
10+
export function AgentGrid({ agentResponse, onAgentsChanged }: AgentGridProps) {
1011

1112
return (
1213
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -15,8 +16,8 @@ export function AgentGrid({ agentResponse }: AgentGridProps) {
1516
item.agent.metadata.namespace || '',
1617
item.agent.metadata.name || '');
1718

18-
return <AgentCard key={agentRef} agentResponse={item} />
19+
return <AgentCard key={agentRef} agentResponse={item} onAgentsChanged={onAgentsChanged} />
1920
})}
2021
</div>
2122
);
22-
}
23+
}

ui/src/components/AgentList.tsx

Lines changed: 134 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
"use client";
2-
import { useCallback, useEffect, useState } from "react";
2+
import { useCallback, useEffect, useRef, useState } from "react";
33
import { AgentGrid } from "@/components/AgentGrid";
44
import { AgentListView } from "@/components/AgentListView";
55
import { Plus, LayoutGrid, List } from "lucide-react";
66
import KagentLogo from "@/components/kagent-logo";
77
import Link from "next/link";
8+
import { useRouter, useSearchParams } from "next/navigation";
89
import { ErrorState } from "./ErrorState";
910
import { Button } from "./ui/button";
1011
import { LoadingState } from "./LoadingState";
11-
import { useAgents } from "./AgentsProvider";
12+
import { getAgents } from "@/app/actions/agents";
1213
import { AppPageFrame } from "@/components/layout/AppPageFrame";
1314
import { PageHeader } from "@/components/layout/PageHeader";
1415
import { cn } from "@/lib/utils";
16+
import { NamespaceCombobox } from "@/components/NamespaceCombobox";
17+
import type { AgentResponse } from "@/types";
1518

1619
const AGENTS_VIEW_KEY = "kagent-agents-view";
1720
type AgentsView = "grid" | "list";
@@ -25,8 +28,41 @@ function readStoredView(): AgentsView {
2528
}
2629

2730
export default function AgentList() {
28-
const { agents , loading, error } = useAgents();
31+
const router = useRouter();
32+
const searchParams = useSearchParams();
33+
const selectedNamespace = searchParams.get("namespace")?.trim() || "";
34+
const [agents, setAgents] = useState<AgentResponse[]>([]);
35+
const [loading, setLoading] = useState(true);
36+
const [error, setError] = useState("");
2937
const [view, setView] = useState<AgentsView>("grid");
38+
const latestFetchRequestId = useRef(0);
39+
40+
const fetchAgents = useCallback(async () => {
41+
const requestId = latestFetchRequestId.current + 1;
42+
latestFetchRequestId.current = requestId;
43+
44+
try {
45+
setLoading(true);
46+
const result = await getAgents(selectedNamespace ? { namespace: selectedNamespace } : {});
47+
if (requestId !== latestFetchRequestId.current) {
48+
return;
49+
}
50+
if (result.error) {
51+
throw new Error(result.error || "Failed to fetch agents");
52+
}
53+
setAgents(result.data || []);
54+
setError("");
55+
} catch (err) {
56+
if (requestId !== latestFetchRequestId.current) {
57+
return;
58+
}
59+
setError(err instanceof Error ? err.message : "An unexpected error occurred");
60+
} finally {
61+
if (requestId === latestFetchRequestId.current) {
62+
setLoading(false);
63+
}
64+
}
65+
}, [selectedNamespace]);
3066

3167
useEffect(() => {
3268
const id = requestAnimationFrame(() => {
@@ -35,6 +71,10 @@ export default function AgentList() {
3571
return () => cancelAnimationFrame(id);
3672
}, []);
3773

74+
useEffect(() => {
75+
void fetchAgents();
76+
}, [fetchAgents]);
77+
3878
const setViewAndPersist = useCallback((next: AgentsView) => {
3979
setView(next);
4080
try {
@@ -44,6 +84,22 @@ export default function AgentList() {
4484
}
4585
}, []);
4686

87+
const handleNamespaceChange = useCallback(
88+
(nextNamespace: string) => {
89+
const namespace = nextNamespace.trim();
90+
if (!namespace) {
91+
router.push("/agents");
92+
return;
93+
}
94+
router.push(`/agents?namespace=${encodeURIComponent(namespace)}`);
95+
},
96+
[router],
97+
);
98+
99+
const createHref = selectedNamespace
100+
? `/agents/new?namespace=${encodeURIComponent(selectedNamespace)}`
101+
: "/agents/new";
102+
47103
if (error) {
48104
return <ErrorState message={error} />;
49105
}
@@ -57,69 +113,98 @@ export default function AgentList() {
57113
<PageHeader
58114
titleId="agents-page-title"
59115
title="Agents"
116+
description={
117+
selectedNamespace ? (
118+
<>
119+
Showing agents in namespace <code>{selectedNamespace}</code>.
120+
</>
121+
) : (
122+
"Showing agents across all namespaces."
123+
)
124+
}
60125
className="mb-8"
61126
end={
62-
agents && agents.length > 0 ? (
63-
<div
64-
className="flex w-full min-w-0 items-center justify-end gap-1 rounded-lg border border-border/60 bg-muted/20 p-1"
65-
role="group"
66-
aria-label="Layout"
67-
>
68-
<Button
69-
type="button"
70-
variant="ghost"
71-
size="sm"
72-
className={cn(
73-
"h-8 gap-1.5 px-2.5 text-muted-foreground",
74-
view === "grid" && "bg-card text-foreground shadow-sm",
75-
)}
76-
aria-pressed={view === "grid"}
77-
aria-label="Show agents as cards"
78-
onClick={() => setViewAndPersist("grid")}
79-
>
80-
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
81-
<span className="hidden sm:inline" aria-hidden>
82-
Cards
83-
</span>
84-
</Button>
85-
<Button
86-
type="button"
87-
variant="ghost"
88-
size="sm"
89-
className={cn(
90-
"h-8 gap-1.5 px-2.5 text-muted-foreground",
91-
view === "list" && "bg-card text-foreground shadow-sm",
92-
)}
93-
aria-pressed={view === "list"}
94-
aria-label="Show agents as a list"
95-
onClick={() => setViewAndPersist("list")}
96-
>
97-
<List className="h-4 w-4 shrink-0" aria-hidden />
98-
<span className="hidden sm:inline" aria-hidden>
99-
List
100-
</span>
101-
</Button>
127+
<div className="flex w-full min-w-0 flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end">
128+
<div className="w-full sm:w-72">
129+
<NamespaceCombobox
130+
value={selectedNamespace}
131+
onValueChange={handleNamespaceChange}
132+
includeAllNamespaces
133+
autoSelectDefault={false}
134+
ariaLabel="Namespace"
135+
placeholder="All namespaces"
136+
/>
102137
</div>
103-
) : null
138+
{agents && agents.length > 0 ? (
139+
<div
140+
className="flex w-full min-w-0 items-center justify-end gap-1 rounded-lg border border-border/60 bg-muted/20 p-1 sm:w-auto"
141+
role="group"
142+
aria-label="Layout"
143+
>
144+
<Button
145+
type="button"
146+
variant="ghost"
147+
size="sm"
148+
className={cn(
149+
"h-8 gap-1.5 px-2.5 text-muted-foreground",
150+
view === "grid" && "bg-card text-foreground shadow-sm",
151+
)}
152+
aria-pressed={view === "grid"}
153+
aria-label="Show agents as cards"
154+
onClick={() => setViewAndPersist("grid")}
155+
>
156+
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
157+
<span className="hidden sm:inline" aria-hidden>
158+
Cards
159+
</span>
160+
</Button>
161+
<Button
162+
type="button"
163+
variant="ghost"
164+
size="sm"
165+
className={cn(
166+
"h-8 gap-1.5 px-2.5 text-muted-foreground",
167+
view === "list" && "bg-card text-foreground shadow-sm",
168+
)}
169+
aria-pressed={view === "list"}
170+
aria-label="Show agents as a list"
171+
onClick={() => setViewAndPersist("list")}
172+
>
173+
<List className="h-4 w-4 shrink-0" aria-hidden />
174+
<span className="hidden sm:inline" aria-hidden>
175+
List
176+
</span>
177+
</Button>
178+
</div>
179+
) : null}
180+
</div>
104181
}
105182
/>
106183

107184
{agents?.length === 0 ? (
108185
<div className="rounded-xl border border-border/60 bg-card/30 py-12 text-center shadow-sm">
109186
<KagentLogo className="mx-auto mb-4 h-16 w-16" />
110-
<h2 className="mb-2 text-lg font-medium tracking-tight">No agents yet</h2>
111-
<p className="mb-6 text-pretty text-sm text-muted-foreground">Create an agent to run it in your cluster and wire models and tools in one place.</p>
187+
<h2 className="mb-2 text-lg font-medium tracking-tight">
188+
{selectedNamespace
189+
? `No agents found in namespace "${selectedNamespace}".`
190+
: "No agents yet"}
191+
</h2>
192+
<p className="mb-6 text-pretty text-sm text-muted-foreground">
193+
{selectedNamespace
194+
? "Create an agent in this namespace or switch namespaces."
195+
: "Create an agent to run it in your cluster and wire models and tools in one place."}
196+
</p>
112197
<Button asChild size="lg" className="min-w-[12rem]">
113-
<Link href="/agents/new">
198+
<Link href={createHref}>
114199
<Plus className="mr-2 h-4 w-4" aria-hidden />
115200
New Agent
116201
</Link>
117202
</Button>
118203
</div>
119204
) : view === "list" ? (
120-
<AgentListView agentResponse={agents || []} />
205+
<AgentListView agentResponse={agents || []} onAgentsChanged={fetchAgents} />
121206
) : (
122-
<AgentGrid agentResponse={agents || []} />
207+
<AgentGrid agentResponse={agents || []} onAgentsChanged={fetchAgents} />
123208
)}
124209
</AppPageFrame>
125210
);

ui/src/components/AgentListView.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { isOpenshellSandboxRow, openshellTerminalHref } from "@/lib/openshellSan
2828

2929
interface AgentListViewProps {
3030
agentResponse: AgentResponse[];
31+
onAgentsChanged?: () => Promise<void> | void;
3132
}
3233

3334
type SortKey = "name" | "type" | "providerModel" | "toolCount" | "skillsCount" | "state";
@@ -214,7 +215,7 @@ function SortableTh({ col, label, className, textAlign = "left", sort, onSort }:
214215
);
215216
}
216217

217-
function AgentListRow({ item }: { item: AgentResponse }) {
218+
function AgentListRow({ item, onAgentsChanged }: { item: AgentResponse; onAgentsChanged?: () => Promise<void> | void }) {
218219
const { agent, deploymentReady, accepted } = item;
219220
const router = useRouter();
220221
const [memoriesOpen, setMemoriesOpen] = useState(false);
@@ -389,6 +390,7 @@ function AgentListRow({ item }: { item: AgentResponse }) {
389390
<DeleteButton
390391
agentName={name}
391392
namespace={namespace}
393+
onDeleted={onAgentsChanged}
392394
externalOpen={deleteOpen}
393395
onExternalOpenChange={setDeleteOpen}
394396
/>
@@ -406,7 +408,7 @@ function AgentListRow({ item }: { item: AgentResponse }) {
406408
return trBody;
407409
}
408410

409-
export function AgentListView({ agentResponse }: AgentListViewProps) {
411+
export function AgentListView({ agentResponse, onAgentsChanged }: AgentListViewProps) {
410412
const [sort, setSort] = useState<{ key: SortKey; dir: SortDir }>({ key: "name", dir: "asc" });
411413

412414
const onSort = useCallback((col: SortKey) => {
@@ -453,10 +455,10 @@ export function AgentListView({ agentResponse }: AgentListViewProps) {
453455
item.agent.metadata.namespace || "",
454456
item.agent.metadata.name || "",
455457
);
456-
return <AgentListRow key={key} item={item} />;
458+
return <AgentListRow key={key} item={item} onAgentsChanged={onAgentsChanged} />;
457459
})}
458460
</tbody>
459461
</table>
460462
</div>
461463
);
462-
}
464+
}

0 commit comments

Comments
 (0)