1- import type { Provenance } from "@/api/portal/types" ;
1+ import { useState } from "react" ;
2+ import {
3+ Database ,
4+ Search ,
5+ FileText ,
6+ Info ,
7+ Link2 ,
8+ Terminal ,
9+ type LucideIcon ,
10+ } from "lucide-react" ;
11+ import * as Dialog from "@radix-ui/react-dialog" ;
12+ import type { Provenance , ProvenanceToolCall } from "@/api/portal/types" ;
213
314interface Props {
415 provenance : Provenance ;
516}
617
18+ interface ToolMeta {
19+ label : string ;
20+ icon : LucideIcon ;
21+ }
22+
23+ const TOOL_LABELS : Record < string , ToolMeta > = {
24+ trino_query : { label : "SQL Query" , icon : Database } ,
25+ trino_execute : { label : "SQL Execute" , icon : Database } ,
26+ trino_describe_table : { label : "Describe Table" , icon : Database } ,
27+ trino_list_tables : { label : "List Tables" , icon : Database } ,
28+ trino_list_schemas : { label : "List Schemas" , icon : Database } ,
29+ trino_list_catalogs : { label : "List Catalogs" , icon : Database } ,
30+ trino_explain : { label : "Query Plan" , icon : Database } ,
31+ datahub_search : { label : "Catalog Search" , icon : Search } ,
32+ datahub_get_schema : { label : "Schema Lookup" , icon : FileText } ,
33+ datahub_get_entity : { label : "Entity Details" , icon : Info } ,
34+ datahub_get_lineage : { label : "Lineage" , icon : Link2 } ,
35+ datahub_get_column_lineage : { label : "Column Lineage" , icon : Link2 } ,
36+ datahub_get_queries : { label : "Saved Queries" , icon : FileText } ,
37+ datahub_get_data_product : { label : "Data Product" , icon : Info } ,
38+ datahub_get_glossary_term : { label : "Glossary Term" , icon : FileText } ,
39+ datahub_list_data_products : { label : "Data Products" , icon : Search } ,
40+ datahub_list_domains : { label : "Domains" , icon : Search } ,
41+ datahub_list_tags : { label : "Tags" , icon : Search } ,
42+ platform_info : { label : "Platform Info" , icon : Info } ,
43+ s3_list_objects : { label : "List Files" , icon : FileText } ,
44+ s3_get_object : { label : "Get File" , icon : FileText } ,
45+ s3_list_buckets : { label : "List Buckets" , icon : FileText } ,
46+ } ;
47+
48+ function getToolMeta ( toolName : string ) : ToolMeta {
49+ return TOOL_LABELS [ toolName ] ?? { label : toolName , icon : Terminal } ;
50+ }
51+
52+ /** Extract a human-readable summary from the raw summary JSON string. */
53+ function extractSummary ( call : ProvenanceToolCall ) : string | null {
54+ const raw = call . summary ;
55+ if ( ! raw ) return null ;
56+
57+ // Try to parse as JSON to extract useful fields
58+ try {
59+ const parsed = JSON . parse ( raw ) ;
60+ if ( typeof parsed === "string" ) return parsed ;
61+
62+ // SQL queries
63+ if ( parsed . sql ) {
64+ const sql = String ( parsed . sql ) . trim ( ) ;
65+ return sql . length > 120 ? sql . slice ( 0 , 120 ) + "..." : sql ;
66+ }
67+
68+ // Search queries
69+ if ( parsed . query ) return `"${ parsed . query } "` ;
70+
71+ // URN-based lookups
72+ if ( parsed . urn ) return String ( parsed . urn ) ;
73+
74+ // Table operations
75+ if ( parsed . table ) {
76+ const parts = [ parsed . catalog , parsed . schema , parsed . table ] . filter ( Boolean ) ;
77+ return parts . join ( "." ) ;
78+ }
79+
80+ // Bucket/key for S3
81+ if ( parsed . bucket ) {
82+ return parsed . key ? `${ parsed . bucket } /${ parsed . key } ` : parsed . bucket ;
83+ }
84+
85+ // Fall back to first string value
86+ const firstStr = Object . values ( parsed ) . find ( ( v ) => typeof v === "string" ) ;
87+ if ( firstStr ) return String ( firstStr ) ;
88+ } catch {
89+ // Not JSON — use as-is if short enough
90+ if ( raw . length <= 150 ) return raw ;
91+ return raw . slice ( 0 , 147 ) + "..." ;
92+ }
93+
94+ return null ;
95+ }
96+
97+ /** Pretty-print the raw summary for the detail modal. */
98+ function formatDetail ( summary : string | undefined ) : string {
99+ if ( ! summary ) return "(no parameters)" ;
100+ try {
101+ return JSON . stringify ( JSON . parse ( summary ) , null , 2 ) ;
102+ } catch {
103+ return summary ;
104+ }
105+ }
106+
107+ function relativeTime ( timestamp : string ) : string {
108+ const now = Date . now ( ) ;
109+ const then = new Date ( timestamp ) . getTime ( ) ;
110+ const diff = Math . max ( 0 , now - then ) ;
111+ const seconds = Math . floor ( diff / 1000 ) ;
112+ if ( seconds < 60 ) return "just now" ;
113+ const minutes = Math . floor ( seconds / 60 ) ;
114+ if ( minutes < 60 ) return `${ minutes } min ago` ;
115+ const hours = Math . floor ( minutes / 60 ) ;
116+ if ( hours < 24 ) return `${ hours } h ago` ;
117+ const days = Math . floor ( hours / 24 ) ;
118+ return `${ days } d ago` ;
119+ }
120+
121+ function ProvenanceCard ( {
122+ call,
123+ onClick,
124+ } : {
125+ call : ProvenanceToolCall ;
126+ onClick : ( ) => void ;
127+ } ) {
128+ const meta = getToolMeta ( call . tool_name ) ;
129+ const Icon = meta . icon ;
130+ const summary = extractSummary ( call ) ;
131+
132+ return (
133+ < button
134+ type = "button"
135+ onClick = { onClick }
136+ className = "w-full text-left rounded-md border bg-background p-3 transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
137+ >
138+ < div className = "flex items-start gap-2.5" >
139+ < div className = "mt-0.5 rounded bg-muted p-1.5" >
140+ < Icon className = "h-3.5 w-3.5 text-muted-foreground" />
141+ </ div >
142+ < div className = "min-w-0 flex-1" >
143+ < div className = "flex items-center justify-between gap-2" >
144+ < span className = "text-sm font-medium" > { meta . label } </ span >
145+ < span
146+ className = "shrink-0 text-[11px] text-muted-foreground"
147+ title = { new Date ( call . timestamp ) . toLocaleString ( ) }
148+ >
149+ { relativeTime ( call . timestamp ) }
150+ </ span >
151+ </ div >
152+ { summary && (
153+ < p className = "mt-0.5 truncate text-xs text-muted-foreground font-mono" >
154+ { summary }
155+ </ p >
156+ ) }
157+ </ div >
158+ </ div >
159+ </ button >
160+ ) ;
161+ }
162+
163+ function DetailModal ( {
164+ call,
165+ open,
166+ onOpenChange,
167+ } : {
168+ call : ProvenanceToolCall | null ;
169+ open : boolean ;
170+ onOpenChange : ( open : boolean ) => void ;
171+ } ) {
172+ if ( ! call ) return null ;
173+ const meta = getToolMeta ( call . tool_name ) ;
174+ const Icon = meta . icon ;
175+ const detail = formatDetail ( call . summary ) ;
176+
177+ return (
178+ < Dialog . Root open = { open } onOpenChange = { onOpenChange } >
179+ < Dialog . Portal >
180+ < Dialog . Overlay className = "fixed inset-0 z-50 bg-black/40" />
181+ < Dialog . Content className = "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-card p-6 shadow-lg focus:outline-none" >
182+ < Dialog . Title className = "flex items-center gap-2 text-base font-semibold" >
183+ < Icon className = "h-4 w-4 text-muted-foreground" />
184+ { meta . label }
185+ </ Dialog . Title >
186+ < Dialog . Description className = "mt-1 text-xs text-muted-foreground" >
187+ { call . tool_name } · { new Date ( call . timestamp ) . toLocaleString ( ) }
188+ </ Dialog . Description >
189+
190+ < div className = "mt-4" >
191+ < p className = "mb-1.5 text-xs font-medium text-muted-foreground" >
192+ { call . tool_name . startsWith ( "trino_" ) && detail . includes ( "SELECT" )
193+ ? "SQL Query"
194+ : "Parameters" }
195+ </ p >
196+ < pre className = "max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap break-words" >
197+ { detail }
198+ </ pre >
199+ </ div >
200+
201+ < div className = "mt-4 flex justify-end" >
202+ < Dialog . Close asChild >
203+ < button
204+ type = "button"
205+ className = "rounded-md bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground hover:bg-secondary/80"
206+ >
207+ Close
208+ </ button >
209+ </ Dialog . Close >
210+ </ div >
211+ </ Dialog . Content >
212+ </ Dialog . Portal >
213+ </ Dialog . Root >
214+ ) ;
215+ }
216+
7217export function ProvenancePanel ( { provenance } : Props ) {
8218 const calls = provenance . tool_calls ?? [ ] ;
219+ const [ selected , setSelected ] = useState < ProvenanceToolCall | null > ( null ) ;
220+
9221 if ( calls . length === 0 ) {
10222 return (
11223 < p className = "text-sm text-muted-foreground" > No provenance data available.</ p >
@@ -14,25 +226,30 @@ export function ProvenancePanel({ provenance }: Props) {
14226
15227 return (
16228 < div className = "space-y-3" >
17- < h3 className = "text-sm font-medium" > Provenance</ h3 >
18- < div className = "relative pl-4 border-l-2 border-primary/20 space-y-3" >
229+ < div className = "flex items-center justify-between" >
230+ < h3 className = "text-sm font-medium" > Provenance</ h3 >
231+ < span className = "text-xs text-muted-foreground" >
232+ { calls . length } { calls . length === 1 ? "call" : "calls" }
233+ </ span >
234+ </ div >
235+
236+ < div className = "space-y-2" >
19237 { calls . map ( ( call , i ) => (
20- < div key = { i } className = "relative" >
21- < div className = "absolute -left-[calc(0.5rem+1px)] top-1.5 h-2 w-2 rounded-full bg-primary" />
22- < div className = "text-sm" >
23- < span className = "font-mono text-xs bg-muted px-1.5 py-0.5 rounded" >
24- { call . tool_name }
25- </ span >
26- { call . summary && (
27- < p className = "text-muted-foreground mt-0.5" > { call . summary } </ p >
28- ) }
29- < p className = "text-xs text-muted-foreground mt-0.5" >
30- { new Date ( call . timestamp ) . toLocaleString ( ) }
31- </ p >
32- </ div >
33- </ div >
238+ < ProvenanceCard
239+ key = { i }
240+ call = { call }
241+ onClick = { ( ) => setSelected ( call ) }
242+ />
34243 ) ) }
35244 </ div >
245+
246+ < DetailModal
247+ call = { selected }
248+ open = { selected !== null }
249+ onOpenChange = { ( open ) => {
250+ if ( ! open ) setSelected ( null ) ;
251+ } }
252+ />
36253 </ div >
37254 ) ;
38255}
0 commit comments