@object-ui/plugin-dashboard 0.1.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/dist/index.css +1 -0
  3. package/dist/index.js +5797 -266
  4. package/dist/index.umd.cjs +5 -2
  5. package/dist/src/DashboardGridLayout.d.ts +11 -0
  6. package/dist/src/DashboardGridLayout.d.ts.map +1 -0
  7. package/dist/src/DashboardRenderer.d.ts +1 -1
  8. package/dist/src/DashboardRenderer.d.ts.map +1 -1
  9. package/dist/src/MetricCard.d.ts +16 -0
  10. package/dist/src/MetricCard.d.ts.map +1 -0
  11. package/dist/src/MetricWidget.d.ts +1 -1
  12. package/dist/src/MetricWidget.d.ts.map +1 -1
  13. package/dist/src/ReportBuilder.d.ts +11 -0
  14. package/dist/src/ReportBuilder.d.ts.map +1 -0
  15. package/dist/src/ReportRenderer.d.ts +15 -0
  16. package/dist/src/ReportRenderer.d.ts.map +1 -0
  17. package/dist/src/ReportViewer.d.ts +11 -0
  18. package/dist/src/ReportViewer.d.ts.map +1 -0
  19. package/dist/src/index.d.ts +19 -1
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/package.json +10 -8
  22. package/src/DashboardGridLayout.tsx +210 -0
  23. package/src/DashboardRenderer.tsx +108 -20
  24. package/src/MetricCard.tsx +75 -0
  25. package/src/MetricWidget.tsx +13 -3
  26. package/src/ReportBuilder.tsx +625 -0
  27. package/src/ReportRenderer.tsx +89 -0
  28. package/src/ReportViewer.tsx +232 -0
  29. package/src/__tests__/DashboardGridLayout.test.tsx +199 -0
  30. package/src/__tests__/MetricCard.test.tsx +59 -0
  31. package/src/__tests__/ReportBuilder.test.tsx +115 -0
  32. package/src/__tests__/ReportViewer.test.tsx +107 -0
  33. package/src/index.tsx +122 -3
  34. package/vite.config.ts +19 -0
  35. package/vitest.config.ts +9 -0
  36. package/vitest.setup.tsx +18 -0
@@ -0,0 +1,625 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import React, { useState } from 'react';
10
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Tabs, TabsContent, TabsList, TabsTrigger } from '@object-ui/components';
11
+ import type { ReportBuilderSchema, ReportSchema, ReportField, ReportFilter, ReportGroupBy, ReportSection } from '@object-ui/types';
12
+ import { Plus, Trash2, Save, X, Settings, Filter, Layers, Calendar } from 'lucide-react';
13
+ import { ReportViewer } from './ReportViewer';
14
+
15
+ export interface ReportBuilderProps {
16
+ schema: ReportBuilderSchema;
17
+ }
18
+
19
+ /**
20
+ * ReportBuilder - Interactive report builder component
21
+ * Allows users to configure report fields, filters, grouping, sections, and export settings
22
+ */
23
+ export const ReportBuilder: React.FC<ReportBuilderProps> = ({ schema }) => {
24
+ const {
25
+ report: initialReport,
26
+ dataSources = [],
27
+ availableFields = [],
28
+ showPreview = true,
29
+ onSave,
30
+ onCancel
31
+ } = schema;
32
+
33
+ const [report, setReport] = useState<ReportSchema>(initialReport || {
34
+ type: 'report',
35
+ title: 'New Report',
36
+ fields: [],
37
+ filters: [],
38
+ groupBy: [],
39
+ sections: [],
40
+ showExportButtons: true,
41
+ showPrintButton: true
42
+ });
43
+
44
+ const [selectedFields, setSelectedFields] = useState<ReportField[]>(report.fields || []);
45
+ const [filters, setFilters] = useState<ReportFilter[]>(report.filters || []);
46
+ const [groupBy, setGroupBy] = useState<ReportGroupBy[]>(report.groupBy || []);
47
+ const [sections, setSections] = useState<ReportSection[]>(report.sections || []);
48
+
49
+ // Field Management
50
+ const handleAddField = () => {
51
+ if (availableFields.length > 0 && selectedFields.length < availableFields.length) {
52
+ const nextField = availableFields.find(
53
+ f => !selectedFields.some(sf => sf.name === f.name)
54
+ );
55
+ if (nextField) {
56
+ const newFields = [...selectedFields, nextField];
57
+ setSelectedFields(newFields);
58
+ setReport({ ...report, fields: newFields });
59
+ }
60
+ }
61
+ };
62
+
63
+ const handleRemoveField = (index: number) => {
64
+ const newFields = selectedFields.filter((_, i) => i !== index);
65
+ setSelectedFields(newFields);
66
+ setReport({ ...report, fields: newFields });
67
+ };
68
+
69
+ const handleFieldChange = (index: number, field: ReportField) => {
70
+ const newFields = [...selectedFields];
71
+ newFields[index] = field;
72
+ setSelectedFields(newFields);
73
+ setReport({ ...report, fields: newFields });
74
+ };
75
+
76
+ // Filter Management
77
+ const handleAddFilter = () => {
78
+ const newFilter: ReportFilter = {
79
+ field: availableFields[0]?.name || '',
80
+ operator: 'equals',
81
+ value: ''
82
+ };
83
+ const newFilters = [...filters, newFilter];
84
+ setFilters(newFilters);
85
+ setReport({ ...report, filters: newFilters });
86
+ };
87
+
88
+ const handleRemoveFilter = (index: number) => {
89
+ const newFilters = filters.filter((_, i) => i !== index);
90
+ setFilters(newFilters);
91
+ setReport({ ...report, filters: newFilters });
92
+ };
93
+
94
+ const handleFilterChange = (index: number, filter: ReportFilter) => {
95
+ const newFilters = [...filters];
96
+ newFilters[index] = filter;
97
+ setFilters(newFilters);
98
+ setReport({ ...report, filters: newFilters });
99
+ };
100
+
101
+ // Group By Management
102
+ const handleAddGroupBy = () => {
103
+ const newGroupBy: ReportGroupBy = {
104
+ field: availableFields[0]?.name || '',
105
+ sort: 'asc'
106
+ };
107
+ const newGroupByList = [...groupBy, newGroupBy];
108
+ setGroupBy(newGroupByList);
109
+ setReport({ ...report, groupBy: newGroupByList });
110
+ };
111
+
112
+ const handleRemoveGroupBy = (index: number) => {
113
+ const newGroupByList = groupBy.filter((_, i) => i !== index);
114
+ setGroupBy(newGroupByList);
115
+ setReport({ ...report, groupBy: newGroupByList });
116
+ };
117
+
118
+ const handleGroupByChange = (index: number, group: ReportGroupBy) => {
119
+ const newGroupByList = [...groupBy];
120
+ newGroupByList[index] = group;
121
+ setGroupBy(newGroupByList);
122
+ setReport({ ...report, groupBy: newGroupByList });
123
+ };
124
+
125
+ // Section Management
126
+ const handleAddSection = (type: ReportSection['type']) => {
127
+ const newSection: ReportSection = {
128
+ type,
129
+ title: `New ${type} Section`
130
+ };
131
+ const newSections = [...sections, newSection];
132
+ setSections(newSections);
133
+ setReport({ ...report, sections: newSections });
134
+ };
135
+
136
+ const handleRemoveSection = (index: number) => {
137
+ const newSections = sections.filter((_, i) => i !== index);
138
+ setSections(newSections);
139
+ setReport({ ...report, sections: newSections });
140
+ };
141
+
142
+ const handleSectionChange = (index: number, section: ReportSection) => {
143
+ const newSections = [...sections];
144
+ newSections[index] = section;
145
+ setSections(newSections);
146
+ setReport({ ...report, sections: newSections });
147
+ };
148
+
149
+ const handleSave = () => {
150
+ console.log('Saving report:', report);
151
+ if (onSave) {
152
+ alert('Report saved! (Handler: ' + onSave + ')');
153
+ }
154
+ };
155
+
156
+ const handleCancel = () => {
157
+ console.log('Canceling report builder');
158
+ if (onCancel) {
159
+ // TODO: Trigger onCancel handler from schema
160
+ }
161
+ };
162
+
163
+ return (
164
+ <div className="space-y-4">
165
+ {/* Builder Form */}
166
+ <Card>
167
+ <CardHeader>
168
+ <CardTitle>Report Builder</CardTitle>
169
+ <CardDescription>Configure your report settings, fields, filters, and sections</CardDescription>
170
+ </CardHeader>
171
+ <CardContent>
172
+ <Tabs defaultValue="basic" className="w-full">
173
+ <TabsList className="grid w-full grid-cols-5">
174
+ <TabsTrigger value="basic">
175
+ <Settings className="h-4 w-4 mr-2" />
176
+ Basic
177
+ </TabsTrigger>
178
+ <TabsTrigger value="fields">
179
+ <Layers className="h-4 w-4 mr-2" />
180
+ Fields
181
+ </TabsTrigger>
182
+ <TabsTrigger value="filters">
183
+ <Filter className="h-4 w-4 mr-2" />
184
+ Filters
185
+ </TabsTrigger>
186
+ <TabsTrigger value="grouping">
187
+ <Layers className="h-4 w-4 mr-2" />
188
+ Group By
189
+ </TabsTrigger>
190
+ <TabsTrigger value="sections">
191
+ <Layers className="h-4 w-4 mr-2" />
192
+ Sections
193
+ </TabsTrigger>
194
+ </TabsList>
195
+
196
+ {/* Basic Settings Tab */}
197
+ <TabsContent value="basic" className="space-y-4 mt-4">
198
+ <div className="space-y-4">
199
+ <div className="space-y-2">
200
+ <Label htmlFor="report-title">Report Title</Label>
201
+ <Input
202
+ id="report-title"
203
+ value={report.title || ''}
204
+ onChange={(e) => setReport({ ...report, title: e.target.value })}
205
+ placeholder="Enter report title"
206
+ />
207
+ </div>
208
+
209
+ <div className="space-y-2">
210
+ <Label htmlFor="report-description">Description</Label>
211
+ <Input
212
+ id="report-description"
213
+ value={report.description || ''}
214
+ onChange={(e) => setReport({ ...report, description: e.target.value })}
215
+ placeholder="Enter report description"
216
+ />
217
+ </div>
218
+
219
+ {dataSources.length > 0 && (
220
+ <div className="space-y-2">
221
+ <Label>Data Source</Label>
222
+ <select className="w-full border rounded-md p-2">
223
+ <option value="">Select a data source</option>
224
+ {dataSources.map((_ds, idx) => (
225
+ <option key={idx} value={idx}>
226
+ Data Source {idx + 1}
227
+ </option>
228
+ ))}
229
+ </select>
230
+ </div>
231
+ )}
232
+
233
+ <div className="space-y-2">
234
+ <Label>Export Options</Label>
235
+ <div className="flex items-center gap-4">
236
+ <label className="flex items-center gap-2">
237
+ <input
238
+ type="checkbox"
239
+ checked={report.showExportButtons || false}
240
+ onChange={(e) => setReport({ ...report, showExportButtons: e.target.checked })}
241
+ />
242
+ <span className="text-sm">Show Export Buttons</span>
243
+ </label>
244
+ <label className="flex items-center gap-2">
245
+ <input
246
+ type="checkbox"
247
+ checked={report.showPrintButton || false}
248
+ onChange={(e) => setReport({ ...report, showPrintButton: e.target.checked })}
249
+ />
250
+ <span className="text-sm">Show Print Button</span>
251
+ </label>
252
+ </div>
253
+ </div>
254
+
255
+ <div className="space-y-2">
256
+ <Label htmlFor="export-format">Default Export Format</Label>
257
+ <select
258
+ id="export-format"
259
+ className="w-full border rounded-md p-2"
260
+ value={report.defaultExportFormat || 'pdf'}
261
+ onChange={(e) => setReport({ ...report, defaultExportFormat: e.target.value as any })}
262
+ >
263
+ <option value="pdf">PDF</option>
264
+ <option value="excel">Excel</option>
265
+ <option value="csv">CSV</option>
266
+ <option value="json">JSON</option>
267
+ <option value="html">HTML</option>
268
+ </select>
269
+ </div>
270
+ </div>
271
+ </TabsContent>
272
+
273
+ {/* Fields Tab */}
274
+ <TabsContent value="fields" className="space-y-4 mt-4">
275
+ <div className="flex items-center justify-between">
276
+ <Label>Report Fields</Label>
277
+ <Button
278
+ size="sm"
279
+ variant="outline"
280
+ onClick={handleAddField}
281
+ disabled={selectedFields.length >= availableFields.length}
282
+ >
283
+ <Plus className="h-4 w-4 mr-2" />
284
+ Add Field
285
+ </Button>
286
+ </div>
287
+
288
+ <div className="space-y-2">
289
+ {selectedFields.map((field, index) => (
290
+ <div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
291
+ <div className="flex-1 grid grid-cols-4 gap-2">
292
+ <div>
293
+ <Label className="text-xs">Field Name</Label>
294
+ <div className="text-sm font-medium">{field.name}</div>
295
+ </div>
296
+ <div>
297
+ <Label className="text-xs">Label</Label>
298
+ <Input
299
+ className="h-8 text-sm"
300
+ value={field.label || field.name}
301
+ onChange={(e) =>
302
+ handleFieldChange(index, { ...field, label: e.target.value })
303
+ }
304
+ />
305
+ </div>
306
+ <div>
307
+ <Label className="text-xs">Aggregation</Label>
308
+ <select
309
+ className="w-full border rounded p-1 text-sm h-8"
310
+ value={field.aggregation || ''}
311
+ onChange={(e) =>
312
+ handleFieldChange(index, {
313
+ ...field,
314
+ aggregation: e.target.value as any
315
+ })
316
+ }
317
+ >
318
+ <option value="">None</option>
319
+ <option value="sum">Sum</option>
320
+ <option value="avg">Average</option>
321
+ <option value="min">Min</option>
322
+ <option value="max">Max</option>
323
+ <option value="count">Count</option>
324
+ <option value="distinct">Distinct</option>
325
+ </select>
326
+ </div>
327
+ <div>
328
+ <Label className="text-xs">Show in Summary</Label>
329
+ <input
330
+ type="checkbox"
331
+ checked={field.showInSummary || false}
332
+ onChange={(e) =>
333
+ handleFieldChange(index, {
334
+ ...field,
335
+ showInSummary: e.target.checked
336
+ })
337
+ }
338
+ className="mt-2"
339
+ />
340
+ </div>
341
+ </div>
342
+ <Button
343
+ size="sm"
344
+ variant="ghost"
345
+ onClick={() => handleRemoveField(index)}
346
+ >
347
+ <Trash2 className="h-4 w-4 text-destructive" />
348
+ </Button>
349
+ </div>
350
+ ))}
351
+
352
+ {selectedFields.length === 0 && (
353
+ <div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
354
+ No fields selected. Click "Add Field" to get started.
355
+ </div>
356
+ )}
357
+ </div>
358
+ </TabsContent>
359
+
360
+ {/* Filters Tab */}
361
+ <TabsContent value="filters" className="space-y-4 mt-4">
362
+ <div className="flex items-center justify-between">
363
+ <Label>Report Filters</Label>
364
+ <Button
365
+ size="sm"
366
+ variant="outline"
367
+ onClick={handleAddFilter}
368
+ disabled={availableFields.length === 0}
369
+ >
370
+ <Plus className="h-4 w-4 mr-2" />
371
+ Add Filter
372
+ </Button>
373
+ </div>
374
+
375
+ <div className="space-y-2">
376
+ {filters.map((filter, index) => (
377
+ <div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
378
+ <div className="flex-1 grid grid-cols-3 gap-2">
379
+ <div>
380
+ <Label className="text-xs">Field</Label>
381
+ <select
382
+ className="w-full border rounded p-1 text-sm h-8"
383
+ value={filter.field}
384
+ onChange={(e) =>
385
+ handleFilterChange(index, { ...filter, field: e.target.value })
386
+ }
387
+ >
388
+ {availableFields.map((f) => (
389
+ <option key={f.name} value={f.name}>
390
+ {f.label || f.name}
391
+ </option>
392
+ ))}
393
+ </select>
394
+ </div>
395
+ <div>
396
+ <Label className="text-xs">Operator</Label>
397
+ <select
398
+ className="w-full border rounded p-1 text-sm h-8"
399
+ value={filter.operator}
400
+ onChange={(e) =>
401
+ handleFilterChange(index, { ...filter, operator: e.target.value as any })
402
+ }
403
+ >
404
+ <option value="equals">Equals</option>
405
+ <option value="not_equals">Not Equals</option>
406
+ <option value="contains">Contains</option>
407
+ <option value="greater_than">Greater Than</option>
408
+ <option value="less_than">Less Than</option>
409
+ <option value="between">Between</option>
410
+ <option value="in">In</option>
411
+ <option value="not_in">Not In</option>
412
+ </select>
413
+ </div>
414
+ <div>
415
+ <Label className="text-xs">Value</Label>
416
+ <Input
417
+ className="h-8 text-sm"
418
+ value={filter.value || ''}
419
+ onChange={(e) =>
420
+ handleFilterChange(index, { ...filter, value: e.target.value })
421
+ }
422
+ placeholder="Enter value"
423
+ />
424
+ </div>
425
+ </div>
426
+ <Button
427
+ size="sm"
428
+ variant="ghost"
429
+ onClick={() => handleRemoveFilter(index)}
430
+ >
431
+ <Trash2 className="h-4 w-4 text-destructive" />
432
+ </Button>
433
+ </div>
434
+ ))}
435
+
436
+ {filters.length === 0 && (
437
+ <div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
438
+ No filters defined. Click "Add Filter" to filter your report data.
439
+ </div>
440
+ )}
441
+ </div>
442
+ </TabsContent>
443
+
444
+ {/* Group By Tab */}
445
+ <TabsContent value="grouping" className="space-y-4 mt-4">
446
+ <div className="flex items-center justify-between">
447
+ <Label>Group By Fields</Label>
448
+ <Button
449
+ size="sm"
450
+ variant="outline"
451
+ onClick={handleAddGroupBy}
452
+ disabled={availableFields.length === 0}
453
+ >
454
+ <Plus className="h-4 w-4 mr-2" />
455
+ Add Group
456
+ </Button>
457
+ </div>
458
+
459
+ <div className="space-y-2">
460
+ {groupBy.map((group, index) => (
461
+ <div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
462
+ <div className="flex-1 grid grid-cols-3 gap-2">
463
+ <div>
464
+ <Label className="text-xs">Field</Label>
465
+ <select
466
+ className="w-full border rounded p-1 text-sm h-8"
467
+ value={group.field}
468
+ onChange={(e) =>
469
+ handleGroupByChange(index, { ...group, field: e.target.value })
470
+ }
471
+ >
472
+ {availableFields.map((f) => (
473
+ <option key={f.name} value={f.name}>
474
+ {f.label || f.name}
475
+ </option>
476
+ ))}
477
+ </select>
478
+ </div>
479
+ <div>
480
+ <Label className="text-xs">Label</Label>
481
+ <Input
482
+ className="h-8 text-sm"
483
+ value={group.label || ''}
484
+ onChange={(e) =>
485
+ handleGroupByChange(index, { ...group, label: e.target.value })
486
+ }
487
+ placeholder="Optional label"
488
+ />
489
+ </div>
490
+ <div>
491
+ <Label className="text-xs">Sort</Label>
492
+ <select
493
+ className="w-full border rounded p-1 text-sm h-8"
494
+ value={group.sort || 'asc'}
495
+ onChange={(e) =>
496
+ handleGroupByChange(index, { ...group, sort: e.target.value as 'asc' | 'desc' })
497
+ }
498
+ >
499
+ <option value="asc">Ascending</option>
500
+ <option value="desc">Descending</option>
501
+ </select>
502
+ </div>
503
+ </div>
504
+ <Button
505
+ size="sm"
506
+ variant="ghost"
507
+ onClick={() => handleRemoveGroupBy(index)}
508
+ >
509
+ <Trash2 className="h-4 w-4 text-destructive" />
510
+ </Button>
511
+ </div>
512
+ ))}
513
+
514
+ {groupBy.length === 0 && (
515
+ <div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
516
+ No grouping defined. Click "Add Group" to group your report data.
517
+ </div>
518
+ )}
519
+ </div>
520
+ </TabsContent>
521
+
522
+ {/* Sections Tab */}
523
+ <TabsContent value="sections" className="space-y-4 mt-4">
524
+ <div className="flex items-center justify-between">
525
+ <Label>Report Sections</Label>
526
+ <div className="flex gap-2">
527
+ <Button size="sm" variant="outline" onClick={() => handleAddSection('header')}>
528
+ <Plus className="h-4 w-4 mr-2" />
529
+ Header
530
+ </Button>
531
+ <Button size="sm" variant="outline" onClick={() => handleAddSection('summary')}>
532
+ <Plus className="h-4 w-4 mr-2" />
533
+ Summary
534
+ </Button>
535
+ <Button size="sm" variant="outline" onClick={() => handleAddSection('chart')}>
536
+ <Plus className="h-4 w-4 mr-2" />
537
+ Chart
538
+ </Button>
539
+ <Button size="sm" variant="outline" onClick={() => handleAddSection('table')}>
540
+ <Plus className="h-4 w-4 mr-2" />
541
+ Table
542
+ </Button>
543
+ </div>
544
+ </div>
545
+
546
+ <div className="space-y-2">
547
+ {sections.map((section, index) => (
548
+ <div key={index} className="flex items-center gap-2 p-3 border rounded-lg">
549
+ <div className="flex-1 space-y-2">
550
+ <div className="flex items-center gap-2">
551
+ <span className="text-xs font-medium text-muted-foreground uppercase">
552
+ {section.type}
553
+ </span>
554
+ <Input
555
+ className="h-8 text-sm"
556
+ value={section.title || ''}
557
+ onChange={(e) =>
558
+ handleSectionChange(index, { ...section, title: e.target.value })
559
+ }
560
+ placeholder="Section title"
561
+ />
562
+ </div>
563
+ {section.type === 'text' && (
564
+ <Input
565
+ className="h-8 text-sm"
566
+ value={section.text || ''}
567
+ onChange={(e) =>
568
+ handleSectionChange(index, { ...section, text: e.target.value })
569
+ }
570
+ placeholder="Text content"
571
+ />
572
+ )}
573
+ </div>
574
+ <Button
575
+ size="sm"
576
+ variant="ghost"
577
+ onClick={() => handleRemoveSection(index)}
578
+ >
579
+ <Trash2 className="h-4 w-4 text-destructive" />
580
+ </Button>
581
+ </div>
582
+ ))}
583
+
584
+ {sections.length === 0 && (
585
+ <div className="text-center py-8 text-muted-foreground border rounded-lg border-dashed">
586
+ No sections defined. Click section buttons above to add report sections.
587
+ </div>
588
+ )}
589
+ </div>
590
+ </TabsContent>
591
+ </Tabs>
592
+
593
+ {/* Actions */}
594
+ <div className="flex items-center justify-end gap-2 pt-4 mt-6 border-t">
595
+ <Button variant="outline" onClick={handleCancel}>
596
+ <X className="h-4 w-4 mr-2" />
597
+ Cancel
598
+ </Button>
599
+ <Button onClick={handleSave}>
600
+ <Save className="h-4 w-4 mr-2" />
601
+ Save Report
602
+ </Button>
603
+ </div>
604
+ </CardContent>
605
+ </Card>
606
+
607
+ {/* Preview */}
608
+ {showPreview && (
609
+ <div>
610
+ <h3 className="text-lg font-semibold mb-2">Preview</h3>
611
+ <ReportViewer
612
+ schema={{
613
+ type: 'report-viewer',
614
+ report,
615
+ data: [],
616
+ showToolbar: false,
617
+ allowExport: false,
618
+ allowPrint: false
619
+ }}
620
+ />
621
+ </div>
622
+ )}
623
+ </div>
624
+ );
625
+ };