Skip to content

Commit fe9abb2

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/docs/npm_and_yarn-599c0cf0ef
2 parents b402e74 + 7149354 commit fe9abb2

2 files changed

Lines changed: 166 additions & 13 deletions

File tree

components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/__tests__/sessions-sidebar.test.tsx

100644100755
Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,23 @@ vi.mock('@/services/queries/use-project-access', () => ({
3434
useProjectAccess: () => ({ data: { userRole: 'admin' } }),
3535
}));
3636

37+
const mockCurrentUser = vi.fn(() => ({ data: { authenticated: true, userId: 'user-1', displayName: 'Test User' } }));
38+
vi.mock('@/services/queries/use-auth', () => ({
39+
useCurrentUser: () => mockCurrentUser(),
40+
}));
41+
3742
vi.mock('@/services/queries/use-version', () => ({
3843
useVersion: () => ({ data: '1.0.0' }),
3944
}));
4045

46+
const localStorageValues: Record<string, unknown> = {};
47+
vi.mock('@/hooks/use-local-storage', () => ({
48+
useLocalStorage: (key: string, initial: unknown) => {
49+
const value = key in localStorageValues ? localStorageValues[key] : initial;
50+
return [value, vi.fn(), vi.fn()];
51+
},
52+
}));
53+
4154
vi.mock('@/components/session-status-dot', () => ({
4255
SessionStatusDot: ({ phase }: { phase: string }) => (
4356
<span data-testid="status-dot">{phase}</span>
@@ -49,7 +62,12 @@ vi.mock('date-fns', () => ({
4962
formatDistanceToNow: () => '2 hours',
5063
}));
5164

52-
function makeSessions(count: number) {
65+
type MakeSessionsOptions = {
66+
userId?: string;
67+
phase?: string;
68+
};
69+
70+
function makeSessions(count: number, options: MakeSessionsOptions = {}) {
5371
return Array.from({ length: count }, (_, i) => ({
5472
metadata: {
5573
name: `session-${i}`,
@@ -62,8 +80,9 @@ function makeSessions(count: number) {
6280
initialPrompt: 'test',
6381
llmSettings: { model: 'test', temperature: 0, maxTokens: 100 },
6482
timeout: 3600,
83+
...(options.userId ? { userContext: { userId: options.userId, displayName: 'User', groups: [] } } : {}),
6584
},
66-
status: { phase: 'Running' as const },
85+
status: { phase: options.phase ?? 'Running' },
6786
})) as unknown as AgenticSession[];
6887
}
6988

@@ -76,6 +95,7 @@ describe('SessionsSidebar', () => {
7695

7796
beforeEach(() => {
7897
vi.clearAllMocks();
98+
Object.keys(localStorageValues).forEach((k) => delete localStorageValues[k]);
7999
mockUseSessionsPaginated.mockReturnValue({
80100
data: { items: [] },
81101
isLoading: false,
@@ -184,4 +204,59 @@ describe('SessionsSidebar', () => {
184204
fireEvent.click(collapseBtn);
185205
expect(onCollapse).toHaveBeenCalled();
186206
});
207+
208+
it('renders filter chips', () => {
209+
render(<SessionsSidebar {...defaultProps} />);
210+
expect(screen.getByTestId('sidebar-filters')).toBeDefined();
211+
expect(screen.getByTestId('filter-mine')).toBeDefined();
212+
expect(screen.getByTestId('filter-running')).toBeDefined();
213+
expect(screen.getByTestId('filter-completed')).toBeDefined();
214+
expect(screen.getByTestId('filter-failed')).toBeDefined();
215+
});
216+
217+
it('"Mine" filter chip has correct label', () => {
218+
render(<SessionsSidebar {...defaultProps} />);
219+
expect(screen.getByTestId('filter-mine').textContent).toContain('Mine');
220+
});
221+
222+
it('shows "No matching sessions" when status filter excludes all sessions', () => {
223+
localStorageValues['acp:sidebar:status:test-project'] = 'failed';
224+
mockUseSessionsPaginated.mockReturnValue({
225+
data: { items: makeSessions(3, { phase: 'Running' }) },
226+
isLoading: false,
227+
});
228+
229+
render(<SessionsSidebar {...defaultProps} />);
230+
expect(screen.getByText('No matching sessions')).toBeDefined();
231+
});
232+
233+
it('shows "No matching sessions" when "Mine" filter excludes all sessions', () => {
234+
localStorageValues['acp:sidebar:mine:test-project'] = true;
235+
mockCurrentUser.mockReturnValue({ data: { authenticated: true, userId: 'user-1', displayName: 'Test User' } });
236+
mockUseSessionsPaginated.mockReturnValue({
237+
data: { items: makeSessions(3, { userId: 'other-user' }) },
238+
isLoading: false,
239+
});
240+
241+
render(<SessionsSidebar {...defaultProps} />);
242+
expect(screen.getByText('No matching sessions')).toBeDefined();
243+
});
244+
245+
it('shows only matching sessions when status filter is active', () => {
246+
localStorageValues['acp:sidebar:status:test-project'] = 'failed';
247+
const runningSession = makeSessions(1, { phase: 'Running' })[0];
248+
const failedSession = makeSessions(1, { phase: 'Failed' })[0];
249+
failedSession.metadata.name = 'failed-1';
250+
failedSession.metadata.uid = 'uid-failed-1';
251+
failedSession.spec.displayName = 'Failed Session';
252+
253+
mockUseSessionsPaginated.mockReturnValue({
254+
data: { items: [runningSession, failedSession] },
255+
isLoading: false,
256+
});
257+
258+
render(<SessionsSidebar {...defaultProps} />);
259+
expect(screen.getByText('Failed Session')).toBeDefined();
260+
expect(screen.queryByText('Session 0')).toBeNull();
261+
});
187262
});

components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/sessions-sidebar.tsx

100644100755
Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useMemo, useState } from "react";
3+
import { useMemo, useState, useCallback } from "react";
44
import { useRouter, usePathname } from "next/navigation";
55
import Link from "next/link";
66
import { toast } from "sonner";
@@ -38,6 +38,7 @@ import {
3838
Square,
3939
ArrowRight,
4040
Trash2,
41+
User,
4142
} from "lucide-react";
4243
import { formatDistanceToNow } from "date-fns";
4344
import {
@@ -48,10 +49,14 @@ import {
4849
useUpdateSessionDisplayName,
4950
} from "@/services/queries/use-sessions";
5051
import { useProjectAccess } from "@/services/queries/use-project-access";
52+
import { useCurrentUser } from "@/services/queries/use-auth";
5153
import { useVersion } from "@/services/queries/use-version";
54+
import { useLocalStorage } from "@/hooks/use-local-storage";
5255
import { cn } from "@/lib/utils";
5356
import type { AgenticSession } from "@/types/api";
5457

58+
type StatusFilter = "all" | "running" | "completed" | "failed";
59+
5560
type SessionsSidebarProps = {
5661
projectName: string;
5762
currentSessionName: string;
@@ -102,20 +107,59 @@ export function SessionsSidebar({
102107
const continueMutation = useContinueSession();
103108
const updateDisplayNameMutation = useUpdateSessionDisplayName();
104109

110+
const { data: currentUser } = useCurrentUser();
111+
105112
const [editingSession, setEditingSession] = useState<{ name: string; displayName: string } | null>(null);
106113
const [deletingSessionName, setDeletingSessionName] = useState<string | null>(null);
114+
const [mineOnly, setMineOnly] = useLocalStorage(`acp:sidebar:mine:${projectName}`, false);
115+
const [statusFilter, setStatusFilter] = useLocalStorage<StatusFilter>(`acp:sidebar:status:${projectName}`, "all");
116+
117+
const toggleMineOnly = useCallback(() => setMineOnly((prev: boolean) => !prev), [setMineOnly]);
118+
const toggleStatusFilter = useCallback(
119+
(filter: StatusFilter) => setStatusFilter((prev: StatusFilter) => prev === filter ? "all" : filter),
120+
[setStatusFilter],
121+
);
107122

108123
const sessions = useMemo(() => {
109124
const items = data?.items ?? [];
110125
return [...items].sort((a, b) => getActivityTime(b) - getActivityTime(a));
111126
}, [data?.items]);
112127

128+
const filteredSessions = useMemo(() => {
129+
let result = sessions;
130+
131+
if (mineOnly && currentUser?.userId) {
132+
result = result.filter(
133+
(s) => s.spec.userContext?.userId === currentUser.userId,
134+
);
135+
}
136+
137+
if (statusFilter !== "all") {
138+
result = result.filter((s) => {
139+
const phase = s.status?.phase;
140+
switch (statusFilter) {
141+
case "running":
142+
return phase === "Running" || phase === "Pending" || phase === "Creating";
143+
case "completed":
144+
return phase === "Completed" || phase === "Stopped";
145+
case "failed":
146+
return phase === "Failed";
147+
default:
148+
return true;
149+
}
150+
});
151+
}
152+
153+
return result;
154+
}, [sessions, mineOnly, currentUser?.userId, statusFilter]);
155+
113156
const visibleSessions = useMemo(() => {
114-
if (showAll) return sessions;
115-
return sessions.slice(0, INITIAL_RECENTS_COUNT);
116-
}, [sessions, showAll]);
157+
if (showAll) return filteredSessions;
158+
return filteredSessions.slice(0, INITIAL_RECENTS_COUNT);
159+
}, [filteredSessions, showAll]);
117160

118-
const hasMore = sessions.length > INITIAL_RECENTS_COUNT && !showAll;
161+
const hasMore = filteredSessions.length > INITIAL_RECENTS_COUNT && !showAll;
162+
const isFiltered = mineOnly || statusFilter !== "all";
119163

120164
const navItems: NavItem[] = useMemo(
121165
() => [
@@ -307,15 +351,49 @@ export function SessionsSidebar({
307351
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
308352
Recents
309353
</span>
310-
<button
311-
type="button"
354+
<Button
355+
variant="ghost"
356+
size="sm"
312357
onClick={() => refetch()}
313358
disabled={isFetching}
314-
className="text-muted-foreground/60 hover:text-muted-foreground transition-colors disabled:opacity-50"
359+
className="h-5 w-5 p-0 text-muted-foreground/60 hover:text-muted-foreground"
315360
title={dataUpdatedAt ? `Last updated ${formatDistanceToNow(new Date(dataUpdatedAt), { addSuffix: true })}` : "Refresh"}
316361
>
317362
<RefreshCw className="h-3 w-3" />
318-
</button>
363+
</Button>
364+
</div>
365+
366+
{/* Filter chips */}
367+
<div className="flex items-center gap-1 px-3 pb-1 flex-wrap" data-testid="sidebar-filters">
368+
<Button
369+
variant={mineOnly ? "default" : "secondary"}
370+
size="sm"
371+
onClick={toggleMineOnly}
372+
className={cn(
373+
"h-auto rounded-full px-2 py-0.5 text-[0.6875rem] font-medium gap-1",
374+
!mineOnly && "text-muted-foreground",
375+
)}
376+
title="Show only my sessions"
377+
data-testid="filter-mine"
378+
>
379+
<User className="h-3 w-3" />
380+
Mine
381+
</Button>
382+
{(["running", "completed", "failed"] as const).map((filter) => (
383+
<Button
384+
key={filter}
385+
variant={statusFilter === filter ? "default" : "secondary"}
386+
size="sm"
387+
onClick={() => toggleStatusFilter(filter)}
388+
className={cn(
389+
"h-auto rounded-full px-2 py-0.5 text-[0.6875rem] font-medium capitalize",
390+
statusFilter !== filter && "text-muted-foreground",
391+
)}
392+
data-testid={`filter-${filter}`}
393+
>
394+
{filter}
395+
</Button>
396+
))}
319397
</div>
320398

321399
<div className="flex-1 overflow-y-auto">
@@ -325,9 +403,9 @@ export function SessionsSidebar({
325403
<Skeleton key={i} className="h-10 w-full rounded-md" />
326404
))}
327405
</div>
328-
) : sessions.length === 0 ? (
406+
) : filteredSessions.length === 0 ? (
329407
<div className="p-4 text-center text-sm text-muted-foreground">
330-
No sessions yet
408+
{isFiltered ? "No matching sessions" : "No sessions yet"}
331409
</div>
332410
) : (
333411
<div className="space-y-0.5 p-1">

0 commit comments

Comments
 (0)