1- import { useMemo } from "react" ;
1+ import { useMemo , useState } from "react" ;
22import { Pin } from "lucide-react" ;
33
44import { AlertMessage } from "@/components/alert-message" ;
55import { ConfirmDialog } from "@/components/confirm-dialog" ;
66import { EmptyState } from "@/components/empty-state" ;
77import { Badge } from "@/components/ui/badge" ;
88import { Button } from "@/components/ui/button" ;
9+ import { Checkbox } from "@/components/ui/checkbox" ;
910import { SpinnerBlock } from "@/components/ui/spinner" ;
1011import {
1112 Table ,
@@ -17,7 +18,7 @@ import {
1718} from "@/components/ui/table" ;
1819import { PaginationControls } from "@/features/dashboard/components/filters/pagination-controls" ;
1920import { useStickySessions } from "@/features/sticky-sessions/hooks/use-sticky-sessions" ;
20- import type { StickySessionIdentifier , StickySessionKind } from "@/features/sticky-sessions/schemas" ;
21+ import type { StickySessionEntry , StickySessionIdentifier , StickySessionKind } from "@/features/sticky-sessions/schemas" ;
2122import { useDialogState } from "@/hooks/use-dialog-state" ;
2223import { getErrorMessageOrNull } from "@/utils/errors" ;
2324import { formatTimeLong } from "@/utils/formatters" ;
@@ -33,10 +34,18 @@ function kindLabel(kind: StickySessionKind): string {
3334 }
3435}
3536
37+ function stickySessionRowId ( entry : StickySessionIdentifier ) : string {
38+ return `${ entry . kind } :${ entry . key } ` ;
39+ }
40+
41+ const EMPTY_STICKY_SESSION_ENTRIES : StickySessionEntry [ ] = [ ] ;
42+
3643export function StickySessionsSection ( ) {
3744 const { params, setLimit, setOffset, stickySessionsQuery, deleteMutation, purgeMutation } = useStickySessions ( ) ;
3845 const deleteDialog = useDialogState < StickySessionIdentifier > ( ) ;
46+ const deleteSelectedDialog = useDialogState < StickySessionIdentifier [ ] > ( ) ;
3947 const purgeDialog = useDialogState ( ) ;
48+ const [ selectedRowIds , setSelectedRowIds ] = useState < string [ ] > ( [ ] ) ;
4049
4150 const mutationError = useMemo (
4251 ( ) =>
@@ -46,13 +55,40 @@ export function StickySessionsSection() {
4655 [ stickySessionsQuery . error , deleteMutation . error , purgeMutation . error ] ,
4756 ) ;
4857
49- const entries = stickySessionsQuery . data ?. entries ?? [ ] ;
58+ const entries = stickySessionsQuery . data ?. entries ?? EMPTY_STICKY_SESSION_ENTRIES ;
5059 const staleCount = stickySessionsQuery . data ?. stalePromptCacheCount ?? 0 ;
5160 const total = stickySessionsQuery . data ?. total ?? 0 ;
5261 const hasMore = stickySessionsQuery . data ?. hasMore ?? false ;
5362 const busy = deleteMutation . isPending || purgeMutation . isPending ;
5463 const hasEntries = entries . length > 0 ;
5564 const hasAnyRows = total > 0 ;
65+ const selectedRowIdSet = useMemo ( ( ) => new Set ( selectedRowIds ) , [ selectedRowIds ] ) ;
66+ const selectedEntries = useMemo (
67+ ( ) =>
68+ entries
69+ . filter ( ( entry ) => selectedRowIdSet . has ( stickySessionRowId ( entry ) ) )
70+ . map ( ( { key, kind } ) => ( { key, kind } ) ) ,
71+ [ entries , selectedRowIdSet ] ,
72+ ) ;
73+ const selectedCount = selectedEntries . length ;
74+ const allVisibleSelected = hasEntries && selectedCount === entries . length ;
75+ const someVisibleSelected = selectedCount > 0 && ! allVisibleSelected ;
76+ const selectedDeleteTargets = deleteSelectedDialog . data ?? [ ] ;
77+ const selectedDeleteCount = selectedDeleteTargets . length ;
78+
79+ const setSelected = ( target : StickySessionIdentifier , checked : boolean ) => {
80+ const rowId = stickySessionRowId ( target ) ;
81+ setSelectedRowIds ( ( current ) => {
82+ if ( checked ) {
83+ return current . includes ( rowId ) ? current : [ ...current , rowId ] ;
84+ }
85+ return current . filter ( ( value ) => value !== rowId ) ;
86+ } ) ;
87+ } ;
88+
89+ const setAllVisibleSelected = ( checked : boolean ) => {
90+ setSelectedRowIds ( checked ? entries . map ( ( entry ) => stickySessionRowId ( entry ) ) : [ ] ) ;
91+ } ;
5692
5793 return (
5894 < section className = "space-y-3 rounded-xl border bg-card p-5" >
@@ -80,17 +116,35 @@ export function StickySessionsSection() {
80116 < span className = "text-xs text-muted-foreground" > Stale prompt-cache</ span >
81117 < span className = "text-sm font-medium tabular-nums" > { staleCount } </ span >
82118 </ div >
119+ { selectedCount > 0 ? (
120+ < div className = "flex items-center gap-1.5" >
121+ < span className = "text-xs text-muted-foreground" > Selected</ span >
122+ < span className = "text-sm font-medium tabular-nums" > { selectedCount } </ span >
123+ </ div >
124+ ) : null }
125+ </ div >
126+ < div className = "flex flex-col gap-2 sm:flex-row sm:items-center" >
127+ < Button
128+ type = "button"
129+ size = "sm"
130+ variant = "destructive"
131+ className = "h-8 text-xs"
132+ disabled = { busy || selectedCount === 0 }
133+ onClick = { ( ) => deleteSelectedDialog . show ( selectedEntries ) }
134+ >
135+ Remove selected
136+ </ Button >
137+ < Button
138+ type = "button"
139+ size = "sm"
140+ variant = "outline"
141+ className = "h-8 text-xs"
142+ disabled = { busy || staleCount === 0 }
143+ onClick = { ( ) => purgeDialog . show ( ) }
144+ >
145+ Purge stale
146+ </ Button >
83147 </ div >
84- < Button
85- type = "button"
86- size = "sm"
87- variant = "outline"
88- className = "h-8 text-xs"
89- disabled = { busy || staleCount === 0 }
90- onClick = { ( ) => purgeDialog . show ( ) }
91- >
92- Purge stale
93- </ Button >
94148 </ div >
95149
96150 { stickySessionsQuery . isLoading && ! stickySessionsQuery . data ? (
@@ -110,7 +164,15 @@ export function StickySessionsSection() {
110164 < Table className = "table-fixed" >
111165 < TableHeader >
112166 < TableRow >
113- < TableHead className = "w-[30%] min-w-[14rem] pl-4 text-[11px] uppercase tracking-wider text-muted-foreground/80" >
167+ < TableHead className = "w-[5%] min-w-[3rem] pl-4 text-[11px] uppercase tracking-wider text-muted-foreground/80" >
168+ < Checkbox
169+ aria-label = "Select all visible sticky sessions"
170+ checked = { allVisibleSelected ? true : someVisibleSelected ? "indeterminate" : false }
171+ disabled = { busy || ! hasEntries }
172+ onCheckedChange = { ( checked ) => setAllVisibleSelected ( checked === true ) }
173+ />
174+ </ TableHead >
175+ < TableHead className = "w-[25%] min-w-[14rem] text-[11px] uppercase tracking-wider text-muted-foreground/80" >
114176 Key
115177 </ TableHead >
116178 < TableHead className = "w-[14%] min-w-[8rem] text-[11px] uppercase tracking-wider text-muted-foreground/80" >
@@ -134,9 +196,18 @@ export function StickySessionsSection() {
134196 { entries . map ( ( entry ) => {
135197 const updated = formatTimeLong ( entry . updatedAt ) ;
136198 const expires = entry . expiresAt ? formatTimeLong ( entry . expiresAt ) : null ;
199+ const selected = selectedRowIdSet . has ( stickySessionRowId ( entry ) ) ;
137200 return (
138- < TableRow key = { `${ entry . kind } :${ entry . key } ` } >
139- < TableCell className = "max-w-[18rem] truncate pl-4 font-mono text-xs" title = { entry . key } >
201+ < TableRow key = { `${ entry . kind } :${ entry . key } ` } data-state = { selected ? "selected" : undefined } >
202+ < TableCell className = "pl-4" >
203+ < Checkbox
204+ aria-label = { `Select sticky session ${ entry . key } ` }
205+ checked = { selected }
206+ disabled = { busy }
207+ onCheckedChange = { ( checked ) => setSelected ( entry , checked === true ) }
208+ />
209+ </ TableCell >
210+ < TableCell className = "max-w-[18rem] truncate font-mono text-xs" title = { entry . key } >
140211 { entry . key }
141212 </ TableCell >
142213 < TableCell >
@@ -207,12 +278,33 @@ export function StickySessionsSection() {
207278 if ( ! deleteDialog . data ) {
208279 return ;
209280 }
210- void deleteMutation . mutateAsync ( deleteDialog . data ) . finally ( ( ) => {
281+ void deleteMutation . mutateAsync ( [ deleteDialog . data ] ) . finally ( ( ) => {
211282 deleteDialog . hide ( ) ;
212283 } ) ;
213284 } }
214285 />
215286
287+ < ConfirmDialog
288+ open = { deleteSelectedDialog . open }
289+ title = "Remove selected sticky sessions"
290+ description = {
291+ selectedDeleteCount === 1
292+ ? "The selected sticky session will stop pinning future requests."
293+ : `${ selectedDeleteCount } selected sticky sessions will stop pinning future requests.`
294+ }
295+ confirmLabel = "Remove selected"
296+ onOpenChange = { deleteSelectedDialog . onOpenChange }
297+ onConfirm = { ( ) => {
298+ if ( selectedDeleteTargets . length === 0 ) {
299+ return ;
300+ }
301+ void deleteMutation . mutateAsync ( selectedDeleteTargets ) . finally ( ( ) => {
302+ setSelectedRowIds ( [ ] ) ;
303+ deleteSelectedDialog . hide ( ) ;
304+ } ) ;
305+ } }
306+ />
307+
216308 < ConfirmDialog
217309 open = { purgeDialog . open }
218310 title = "Purge stale prompt-cache mappings"
0 commit comments