@@ -16,7 +16,6 @@ import { Textarea } from "@/components/ui/textarea";
1616import { useUpdateConfig } from "@/lib/config-update" ;
1717import { cn } from "@/lib/utils" ;
1818import {
19- ArrowClockwiseIcon ,
2019 CaretDownIcon ,
2120 CaretRightIcon ,
2221 CheckCircleIcon ,
@@ -28,279 +27,34 @@ import {
2827 PlusIcon ,
2928 SpinnerGapIcon ,
3029 TrashIcon ,
31- WarningCircleIcon
3230} from "@phosphor-icons/react" ;
3331import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto" ;
3432import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises" ;
35- import { useVirtualizer } from "@tanstack/react-virtual" ;
36- import { useCallback , useMemo , useRef , useState } from "react" ;
33+ import { useCallback , useMemo , useState } from "react" ;
3734import { AppEnabledGuard } from "../../app-enabled-guard" ;
3835import { PageLayout } from "../../page-layout" ;
3936import { useAdminApp } from "../../use-admin-app" ;
40-
41- type RowData = Record < string , unknown > ;
42-
43- type ConfigFolder = {
44- displayName : string ,
45- sortOrder ?: number ,
46- queries : Record < string , {
47- displayName : string ,
48- sqlQuery : string ,
49- description ?: string ,
50- } > ,
51- } ;
52-
53- type FolderWithId = {
54- id : string ,
55- displayName : string ,
56- sortOrder : number ,
57- queries : Array < {
58- id : string ,
59- displayName : string ,
60- sqlQuery : string ,
61- description ?: string ,
62- } > ,
63- } ;
64-
65- // Detect if a value is a date string
66- function isDateValue ( value : unknown ) : value is string {
67- if ( typeof value !== "string" ) return false ;
68- return / ^ \d { 4 } - \d { 2 } - \d { 2 } ( [ T ] \d { 2 } : \d { 2 } : \d { 2 } ) ? / . test ( value ) ;
69- }
70-
71- // Detect if a value is JSON
72- function isJsonValue ( value : unknown ) : boolean {
73- return typeof value === "object" && value !== null ;
74- }
75-
76- // Parse ClickHouse date string as UTC
77- function parseClickHouseDate ( value : string ) : Date {
78- const trimmed = value . trim ( ) ;
79- // Handle date-only strings (YYYY-MM-DD) by appending time
80- if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / . test ( trimmed ) ) {
81- return new Date ( trimmed + "T00:00:00Z" ) ;
82- }
83- const normalized = trimmed . replace ( " " , "T" ) + ( trimmed . includes ( "Z" ) || trimmed . includes ( "+" ) ? "" : "Z" ) ;
84- return new Date ( normalized ) ;
85- }
86-
87- // Component for displaying JSON values
88- function JsonValue ( { value, truncate = true } : { value : unknown , truncate ?: boolean } ) {
89- const formatted = JSON . stringify ( value , null , 2 ) ;
90- const preview = JSON . stringify ( value ) ;
91-
92- if ( truncate && preview . length > 60 ) {
93- return (
94- < SimpleTooltip tooltip = { < pre className = "text-xs max-w-md overflow-auto max-h-64" > { formatted } </ pre > } >
95- < span className = "cursor-help text-muted-foreground" >
96- { preview . slice ( 0 , 57 ) } ...
97- </ span >
98- </ SimpleTooltip >
99- ) ;
100- }
101-
102- return < span className = "text-muted-foreground" > { preview } </ span > ;
103- }
104-
105- // Format a cell value for display
106- function CellValue ( { value, truncate = true } : { value : unknown , truncate ?: boolean } ) {
107- if ( value === null || value === undefined ) {
108- return < span className = "text-muted-foreground/50" > —</ span > ;
109- }
110-
111- if ( isDateValue ( value ) ) {
112- const date = parseClickHouseDate ( value ) ;
113- return < span > { date . toLocaleString ( ) } </ span > ;
114- }
115-
116- if ( isJsonValue ( value ) ) {
117- return < JsonValue value = { value } truncate = { truncate } /> ;
118- }
119-
120- const str = String ( value ) ;
121- if ( truncate && str . length > 100 ) {
122- return (
123- < SimpleTooltip tooltip = { str } >
124- < span className = "cursor-help" > { str . slice ( 0 , 97 ) } ...</ span >
125- </ SimpleTooltip >
126- ) ;
127- }
128-
129- return < span > { str } </ span > ;
130- }
131-
132- // Row detail dialog
133- function RowDetailDialog ( {
134- row,
135- columns,
136- open,
137- onOpenChange,
138- } : {
139- row : RowData | null ,
140- columns : string [ ] ,
141- open : boolean ,
142- onOpenChange : ( open : boolean ) => void ,
143- } ) {
144- if ( ! row ) return null ;
145-
146- return (
147- < Dialog open = { open } onOpenChange = { onOpenChange } >
148- < DialogContent className = "max-w-2xl max-h-[80vh]" >
149- < DialogHeader >
150- < DialogTitle > Row Details</ DialogTitle >
151- </ DialogHeader >
152- < DialogBody >
153- < div className = "space-y-4" >
154- { columns . map ( ( column ) => (
155- < div key = { column } className = "space-y-1" >
156- < Label className = "text-xs font-semibold uppercase tracking-wide text-muted-foreground" >
157- { column }
158- </ Label >
159- < div className = "font-mono text-sm bg-muted/30 rounded px-3 py-2 overflow-auto max-h-48" >
160- { isJsonValue ( row [ column ] ) ? (
161- < pre className = "whitespace-pre-wrap break-all" >
162- { JSON . stringify ( row [ column ] , null , 2 ) }
163- </ pre >
164- ) : (
165- < CellValue value = { row [ column ] } truncate = { false } />
166- ) }
167- </ div >
168- </ div >
169- ) ) }
170- </ div >
171- </ DialogBody >
172- </ DialogContent >
173- </ Dialog >
174- ) ;
175- }
176-
177- // Virtualized flat table component
178- function VirtualizedFlatTable ( {
179- columns,
180- rows,
181- onRowClick,
182- } : {
183- columns : string [ ] ,
184- rows : RowData [ ] ,
185- onRowClick : ( row : RowData ) => void ,
186- } ) {
187- const parentRef = useRef < HTMLDivElement > ( null ) ;
188-
189- const rowVirtualizer = useVirtualizer ( {
190- count : rows . length ,
191- getScrollElement : ( ) => parentRef . current ,
192- estimateSize : ( ) => 36 ,
193- overscan : 10 ,
194- } ) ;
195-
196- // Column widths - distribute based on content type
197- const columnWidths = useMemo ( ( ) => {
198- const widths = new Map < string , string > ( ) ;
199- columns . forEach ( ( col ) => {
200- if ( col . includes ( "id" ) && col !== "project_id" ) {
201- widths . set ( col , "minmax(200px, 1fr)" ) ;
202- } else if ( col . includes ( "_at" ) || col . includes ( "date" ) ) {
203- widths . set ( col , "minmax(100px, 140px)" ) ;
204- } else if ( col === "data" || col . includes ( "json" ) ) {
205- widths . set ( col , "minmax(180px, 2fr)" ) ;
206- } else if ( col === "event_type" || col === "type" ) {
207- widths . set ( col , "minmax(100px, 160px)" ) ;
208- } else {
209- widths . set ( col , "minmax(80px, 1fr)" ) ;
210- }
211- } ) ;
212- return widths ;
213- } , [ columns ] ) ;
214-
215- const gridTemplateColumns = columns . map ( ( col ) => columnWidths . get ( col ) ?? "1fr" ) . join ( " " ) ;
216- const minContentWidth = columns . length * 120 ;
217-
218- return (
219- < div className = "flex flex-col flex-1 min-h-0 overflow-hidden" >
220- < div
221- ref = { parentRef }
222- className = "flex-1 overflow-auto"
223- >
224- < div style = { { minWidth : `${ minContentWidth } px` } } >
225- { /* Sticky header */ }
226- < div
227- className = "grid gap-3 px-3 py-1.5 border-b border-border/50 bg-muted/40 backdrop-blur-sm sticky top-0 z-10"
228- style = { { gridTemplateColumns } }
229- >
230- { columns . map ( ( column ) => (
231- < span
232- key = { column }
233- className = "font-mono text-xs font-medium text-muted-foreground"
234- >
235- { column }
236- </ span >
237- ) ) }
238- </ div >
239-
240- { /* Virtualized rows container */ }
241- < div
242- style = { {
243- height : `${ rowVirtualizer . getTotalSize ( ) } px` ,
244- width : "100%" ,
245- position : "relative" ,
246- } }
247- >
248- { rowVirtualizer . getVirtualItems ( ) . map ( ( virtualRow ) => {
249- const row = rows [ virtualRow . index ] ;
250-
251- return (
252- < div
253- key = { virtualRow . index }
254- className = { cn (
255- "absolute left-0 right-0 grid gap-3 px-3 items-center cursor-pointer" ,
256- "border-b border-border/30 hover:bg-muted/30 transition-colors hover:transition-none" ,
257- virtualRow . index % 2 === 0 ? "bg-transparent" : "bg-muted/10"
258- ) }
259- style = { {
260- top : `${ virtualRow . start } px` ,
261- height : `${ virtualRow . size } px` ,
262- gridTemplateColumns,
263- } }
264- onClick = { ( ) => onRowClick ( row ) }
265- >
266- { columns . map ( ( column ) => (
267- < div key = { column } className = "font-mono text-[11px] truncate" >
268- < CellValue value = { row [ column ] } />
269- </ div >
270- ) ) }
271- </ div >
272- ) ;
273- } ) }
274- </ div >
275- </ div >
276- </ div >
277- </ div >
278- ) ;
279- }
280-
281- // Error display component
282- function ErrorDisplay ( { error, onRetry } : { error : unknown , onRetry : ( ) => void | Promise < void > } ) {
283- const message = error instanceof Error ? error . message : String ( error ) ;
284-
37+ import {
38+ ConfigFolder ,
39+ ErrorDisplay ,
40+ FolderWithId ,
41+ RowData ,
42+ RowDetailDialog ,
43+ VirtualizedFlatTable ,
44+ } from "../shared" ;
45+
46+ // Delete icon button for sidebar items
47+ function DeleteIconButton ( { onClick } : { onClick : ( ) => void } ) {
28548 return (
286- < div className = "flex flex-col items-center justify-center gap-4 p-6 text-center" >
287- < div className = "w-14 h-14 rounded-2xl bg-red-500/10 flex items-center justify-center" >
288- < WarningCircleIcon className = "h-7 w-7 text-red-500" />
289- </ div >
290- < div >
291- < h3 className = "text-sm font-semibold text-foreground mb-1" > Query Error</ h3 >
292- < p className = "text-xs text-muted-foreground max-w-md break-words font-mono whitespace-pre-wrap" >
293- { message }
294- </ p >
295- </ div >
296- < button
297- onClick = { ( ) => runAsynchronouslyWithAlert ( onRetry ) }
298- className = "flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium bg-foreground/[0.06] hover:bg-foreground/[0.1] transition-colors hover:transition-none"
299- >
300- < ArrowClockwiseIcon className = "h-3 w-3" />
301- Retry
302- </ button >
303- </ div >
49+ < button
50+ onClick = { ( e ) => {
51+ e . stopPropagation ( ) ;
52+ onClick ( ) ;
53+ } }
54+ className = "opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-red-500/20 transition-colors hover:transition-none"
55+ >
56+ < TrashIcon className = "h-3 w-3 text-red-500" />
57+ </ button >
30458 ) ;
30559}
30660
0 commit comments