@@ -3,6 +3,12 @@ import {
33 Badge ,
44 Button ,
55 Card ,
6+ CardContent ,
7+ CardDescription ,
8+ CardHeader ,
9+ CardTitle ,
10+ CreateEntity ,
11+ EditEntity ,
612 Input ,
713 Label ,
814 Select ,
@@ -16,9 +22,18 @@ import {
1622 TableHead ,
1723 TableHeader ,
1824 TableRow ,
25+ ViewEntity ,
1926} from "@databricks/appkit-ui/react" ;
2027import { createFileRoute } from "@tanstack/react-router" ;
21- import { useCallback , useEffect , useId , useMemo , useState } from "react" ;
28+ import {
29+ type FormEvent ,
30+ useCallback ,
31+ useEffect ,
32+ useId ,
33+ useMemo ,
34+ useState ,
35+ } from "react" ;
36+ import { codeToHtml } from "shiki" ;
2237
2338/**
2439 * Database plugin demo: `db.cases` is typed from `config/database/schema.ts`;
@@ -41,44 +56,266 @@ const RISK_BADGE: Record<string, "default" | "secondary" | "destructive"> = {
4156 Low : "default" ,
4257} ;
4358
59+ const CASE_VIEW_FIELDS = [
60+ "case_id" ,
61+ "entity_name" ,
62+ "risk_level" ,
63+ "status" ,
64+ "assigned_to" ,
65+ ] as const ;
66+
67+ const CASE_MUTATION_FIELDS = [
68+ "case_id" ,
69+ "entity_id" ,
70+ "entity_name" ,
71+ "risk_level" ,
72+ "status" ,
73+ ] as const ;
74+
75+ const MANUAL_DB_SNIPPET = `const base =
76+ status === "All" ? db.cases : db.cases.where({ status });
77+
78+ const [rows, count] = await Promise.all([
79+ base.order({ created_at: "desc" }).limit(50).toArray(),
80+ base.count(),
81+ ]);
82+
83+ await db.cases.create({
84+ case_id: "CASE-1001",
85+ entity_id: "ENT-5001",
86+ entity_name: "Acme Trading",
87+ risk_level: "Medium",
88+ status: "New",
89+ });
90+
91+ await db.cases.update("CASE-1001", {
92+ status: "Closed",
93+ updated_at: new Date().toISOString(),
94+ });
95+
96+ await db.cases.delete("CASE-1001");` ;
97+
98+ const ENTITY_COMPONENTS_SNIPPET = `const [createOpen, setCreateOpen] = useState(false);
99+ const [editingId, setEditingId] = useState<string | null>(null);
100+
101+ <ViewEntity
102+ entity="cases"
103+ fields={["case_id", "entity_name", "risk_level", "status", "assigned_to"]}
104+ order={{ created_at: "desc" }}
105+ limit={8}
106+ onRowClick={(row) => setEditingId(row.case_id)}
107+ />
108+
109+ <CreateEntity
110+ entity="cases"
111+ fields={["case_id", "entity_id", "entity_name", "risk_level", "status"]}
112+ open={createOpen}
113+ onOpenChange={setCreateOpen}
114+ />
115+
116+ {editingId && (
117+ <EditEntity
118+ entity="cases"
119+ id={editingId}
120+ fields={["entity_id", "entity_name", "risk_level", "status"]}
121+ open
122+ onOpenChange={(open) => !open && setEditingId(null)}
123+ />
124+ )}` ;
125+
44126export const Route = createFileRoute ( "/database" ) ( {
45127 component : DatabaseRoute ,
46128} ) ;
47129
48130function DatabaseRoute ( ) {
49131 return (
50132 < div className = "min-h-screen bg-background" >
51- < div className = "max-w-6xl mx-auto px-6 py-12" >
52- < div className = "mb-8" >
53- < h1 className = "text-3xl font-bold mb-2" > Database Plugin Demo</ h1 >
133+ < div className = "max-w-7xl mx-auto px-6 py-10" >
134+ < div className = "mb-8 max-w-3xl" >
135+ < div className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground mb-3" >
136+ Database plugin beta
137+ </ div >
138+ < h1 className = "text-3xl font-bold tracking-tight mb-3" >
139+ Two ways to build on the same typed entity API
140+ </ h1 >
54141 < p className = "text-base text-muted-foreground" >
55- Full CRUD flow against < code > cases</ code > via the typed{ " " }
56- < code > db</ code > client. Every action hits an auto-generated route at{ " " }
57- < code > /api/database/cases</ code > .
142+ Both sections hit the auto-mounted < code > /api/database/cases</ code > { " " }
143+ routes. The left side shows hand-built product UI using{ " " }
144+ < code > db.cases</ code > ; the right side shows the schema-driven entity
145+ components that generate the table and forms from metadata.
58146 </ p >
59147 </ div >
60148
61- < div className = "grid lg :grid-cols-[2fr_1fr] gap-6" >
62- < CaseList />
63- < CreateCase />
149+ < div className = "grid xl :grid-cols-2 gap-6 items-start " >
150+ < ManualDbSection />
151+ < EntityComponentsSection />
64152 </ div >
65153 </ div >
66154 </ div >
67155 ) ;
68156}
69157
70- function useCases ( status : string ) {
158+ function CodeBlock ( {
159+ code,
160+ lang = "typescript" ,
161+ } : {
162+ code : string ;
163+ lang ?: string ;
164+ } ) {
165+ const [ html , setHtml ] = useState ( "" ) ;
166+
167+ useEffect ( ( ) => {
168+ let active = true ;
169+ codeToHtml ( code , {
170+ lang,
171+ theme : "dark-plus" ,
172+ } ) . then ( ( highlighted ) => {
173+ if ( active ) setHtml ( highlighted ) ;
174+ } ) ;
175+ return ( ) => {
176+ active = false ;
177+ } ;
178+ } , [ code , lang ] ) ;
179+
180+ return (
181+ < div
182+ className = "rounded-md overflow-hidden border bg-zinc-950 [&>pre]:m-0 [&>pre]:max-h-[420px] [&>pre]:overflow-auto [&>pre]:p-4 [&>pre]:text-xs [&>pre]:leading-relaxed"
183+ dangerouslySetInnerHTML = { { __html : html } }
184+ />
185+ ) ;
186+ }
187+
188+ function CodeDisclosure ( {
189+ code,
190+ label = "Show snippet" ,
191+ } : {
192+ code : string ;
193+ label ?: string ;
194+ } ) {
195+ const [ open , setOpen ] = useState ( false ) ;
196+
197+ return (
198+ < div className = "space-y-3" >
199+ < Button
200+ type = "button"
201+ variant = "outline"
202+ size = "sm"
203+ onClick = { ( ) => setOpen ( ( value ) => ! value ) }
204+ >
205+ { open ? "Hide snippet" : label }
206+ </ Button >
207+ { open && < CodeBlock code = { code } /> }
208+ </ div >
209+ ) ;
210+ }
211+
212+ function ManualDbSection ( ) {
213+ const [ refreshToken , setRefreshToken ] = useState ( 0 ) ;
214+ const refresh = useCallback ( ( ) => setRefreshToken ( ( value ) => value + 1 ) , [ ] ) ;
215+
216+ return (
217+ < Card className = "overflow-hidden" >
218+ < CardHeader className = "border-b" >
219+ < div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground" >
220+ Side A
221+ </ div >
222+ < CardTitle > Hand-built UI, typed database client</ CardTitle >
223+ < CardDescription >
224+ Custom AML case workflow using direct, typed calls like{ " " }
225+ < code > db.cases.where(...)</ code > , < code > create</ code > ,{ " " }
226+ < code > update</ code > , and < code > delete</ code > .
227+ </ CardDescription >
228+ </ CardHeader >
229+ < CardContent className = "space-y-5" >
230+ < CodeDisclosure code = { MANUAL_DB_SNIPPET } />
231+ < CaseList refreshToken = { refreshToken } />
232+ < CreateCase onCreated = { refresh } />
233+ </ CardContent >
234+ </ Card >
235+ ) ;
236+ }
237+
238+ function EntityComponentsSection ( ) {
239+ const [ createOpen , setCreateOpen ] = useState ( false ) ;
240+ const [ editingId , setEditingId ] = useState < string | null > ( null ) ;
241+ const [ refreshToken , setRefreshToken ] = useState ( 0 ) ;
242+ const refresh = useCallback ( ( ) => setRefreshToken ( ( value ) => value + 1 ) , [ ] ) ;
243+
244+ return (
245+ < Card className = "overflow-hidden" >
246+ < CardHeader className = "border-b" >
247+ < div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground" >
248+ Side B
249+ </ div >
250+ < CardTitle > Entity components from the same schema</ CardTitle >
251+ < CardDescription >
252+ Generic table and mutation dialogs driven by column metadata from{ " " }
253+ < code > config/database/schema.ts</ code > .
254+ </ CardDescription >
255+ </ CardHeader >
256+ < CardContent className = "space-y-5" >
257+ < CodeDisclosure code = { ENTITY_COMPONENTS_SNIPPET } />
258+ < div className = "flex flex-wrap items-center justify-between gap-3 rounded-lg border bg-muted/20 p-3" >
259+ < div >
260+ < div className = "font-medium" > Cases entity</ div >
261+ < div className = "text-sm text-muted-foreground" >
262+ Click a row to open the generated edit dialog.
263+ </ div >
264+ </ div >
265+ < Button onClick = { ( ) => setCreateOpen ( true ) } >
266+ New with component
267+ </ Button >
268+ </ div >
269+ < ViewEntity
270+ key = { refreshToken }
271+ entity = "cases"
272+ fields = { CASE_VIEW_FIELDS }
273+ order = { { created_at : "desc" } }
274+ limit = { 8 }
275+ onRowClick = { ( row ) => setEditingId ( row . case_id ) }
276+ />
277+ < CreateEntity
278+ entity = "cases"
279+ fields = { CASE_MUTATION_FIELDS }
280+ open = { createOpen }
281+ onOpenChange = { setCreateOpen }
282+ onSuccess = { refresh }
283+ title = "Create case with Entity component"
284+ description = "The form is generated from database.columns.ts metadata."
285+ />
286+ { editingId && (
287+ < EditEntity
288+ entity = "cases"
289+ id = { editingId }
290+ fields = { [ "entity_id" , "entity_name" , "risk_level" , "status" ] }
291+ open
292+ onOpenChange = { ( open ) => {
293+ if ( ! open ) setEditingId ( null ) ;
294+ } }
295+ onSuccess = { refresh }
296+ title = { `Edit ${ editingId } ` }
297+ description = "Only editable, non-generated columns are shown."
298+ />
299+ ) }
300+ </ CardContent >
301+ </ Card >
302+ ) ;
303+ }
304+
305+ function useCases ( status : string , refreshToken : number ) {
71306 const [ data , setData ] = useState < Awaited <
72307 ReturnType < typeof db . cases . toArray >
73308 > | null > ( null ) ;
74309 const [ total , setTotal ] = useState < number | null > ( null ) ;
75310 const [ loading , setLoading ] = useState ( false ) ;
76311 const [ error , setError ] = useState < string | null > ( null ) ;
77- const [ _tick , setTick ] = useState ( 0 ) ;
312+ const [ tick , setTick ] = useState ( 0 ) ;
78313
79314 const refetch = useCallback ( ( ) => setTick ( ( n ) => n + 1 ) , [ ] ) ;
80315
81316 useEffect ( ( ) => {
317+ void refreshToken ;
318+ void tick ;
82319 const ctrl = new AbortController ( ) ;
83320 let active = true ;
84321
@@ -109,14 +346,17 @@ function useCases(status: string) {
109346 active = false ;
110347 ctrl . abort ( ) ;
111348 } ;
112- } , [ status ] ) ;
349+ } , [ status , refreshToken , tick ] ) ;
113350
114351 return { data, total, loading, error, refetch } ;
115352}
116353
117- function CaseList ( ) {
354+ function CaseList ( { refreshToken } : { refreshToken : number } ) {
118355 const [ statusFilter , setStatusFilter ] = useState < string > ( STATUS_FILTER_ALL ) ;
119- const { data, total, loading, error, refetch } = useCases ( statusFilter ) ;
356+ const { data, total, loading, error, refetch } = useCases (
357+ statusFilter ,
358+ refreshToken ,
359+ ) ;
120360 const statusFilterId = useId ( ) ;
121361
122362 const filterLabel = useMemo (
@@ -128,7 +368,7 @@ function CaseList() {
128368 ) ;
129369
130370 return (
131- < Card className = "p-6" >
371+ < div >
132372 < div className = "flex items-center justify-between gap-4 mb-4" >
133373 < div >
134374 < h2 className = "text-xl font-semibold" > Cases</ h2 >
@@ -205,7 +445,7 @@ function CaseList() {
205445 </ TableBody >
206446 </ Table >
207447 </ div >
208- </ Card >
448+ </ div >
209449 ) ;
210450}
211451
@@ -299,7 +539,7 @@ function CaseRowItem({
299539 ) ;
300540}
301541
302- function CreateCase ( ) {
542+ function CreateCase ( { onCreated } : { onCreated : ( ) => void } ) {
303543 const [ caseId , setCaseId ] = useState ( "" ) ;
304544 const [ entityId , setEntityId ] = useState ( "" ) ;
305545 const [ entityName , setEntityName ] = useState ( "" ) ;
@@ -319,7 +559,7 @@ function CreateCase() {
319559
320560 const disabled = busy || caseId . trim ( ) === "" || entityId . trim ( ) === "" ;
321561
322- const submit = async ( e : React . FormEvent ) => {
562+ const submit = async ( e : FormEvent ) => {
323563 e . preventDefault ( ) ;
324564 if ( disabled ) return ;
325565 setBusy ( true ) ;
@@ -336,6 +576,7 @@ function CreateCase() {
336576 setCaseId ( "" ) ;
337577 setEntityId ( "" ) ;
338578 setEntityName ( "" ) ;
579+ onCreated ( ) ;
339580 } catch ( err ) {
340581 setMessage ( { kind : "err" , text : describeError ( err ) } ) ;
341582 } finally {
0 commit comments