axiodb 22.3.1 → 22.4.2

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 (74) hide show
  1. package/electron/electron-builder.json +58 -0
  2. package/electron/main/main.cts +585 -0
  3. package/electron/main/preload.cts +51 -0
  4. package/electron/package-lock.json +8123 -0
  5. package/electron/package.json +52 -0
  6. package/electron/public/AXioDB.png +0 -0
  7. package/electron/resources/after-install.sh +45 -0
  8. package/electron/resources/after-remove.sh +29 -0
  9. package/electron/resources/icon.png +0 -0
  10. package/electron/resources/icons/128x128.png +0 -0
  11. package/electron/resources/icons/16x16.png +0 -0
  12. package/electron/resources/icons/24x24.png +0 -0
  13. package/electron/resources/icons/256x256.png +0 -0
  14. package/electron/resources/icons/32x32.png +0 -0
  15. package/electron/resources/icons/48x48.png +0 -0
  16. package/electron/resources/icons/512x512.png +0 -0
  17. package/electron/resources/icons/64x64.png +0 -0
  18. package/electron/src/App.jsx +128 -0
  19. package/electron/src/api/authApi.js +83 -0
  20. package/electron/src/api/client.js +102 -0
  21. package/electron/src/assets/AXioDB.png +0 -0
  22. package/electron/src/components/auth/CreateRoleModal.jsx +189 -0
  23. package/electron/src/components/auth/CreateUserModal.jsx +131 -0
  24. package/electron/src/components/auth/ForcePasswordChangeModal.jsx +132 -0
  25. package/electron/src/components/auth/ProtectedRoute.jsx +42 -0
  26. package/electron/src/components/auth/ResetPasswordModal.jsx +90 -0
  27. package/electron/src/components/auth/UserAvatarMenu.jsx +103 -0
  28. package/electron/src/components/collection/CreateCollectionModal.jsx +116 -0
  29. package/electron/src/components/collection/DeleteCollectionModal.jsx +85 -0
  30. package/electron/src/components/collection/SchemaViewModal.jsx +369 -0
  31. package/electron/src/components/dashboard/CollectionsChart.jsx +94 -0
  32. package/electron/src/components/dashboard/DatabaseTreeView.jsx +130 -0
  33. package/electron/src/components/dashboard/InMemoryCacheCard.jsx +60 -0
  34. package/electron/src/components/dashboard/StorageDonut.jsx +76 -0
  35. package/electron/src/components/dashboard/StorageUsageCard.jsx +59 -0
  36. package/electron/src/components/dashboard/TotalCollectionsCard.jsx +47 -0
  37. package/electron/src/components/dashboard/TotalDatabasesCard.jsx +43 -0
  38. package/electron/src/components/dashboard/TotalDocumentsCard.jsx +44 -0
  39. package/electron/src/components/database/CreateDatabaseModal.jsx +109 -0
  40. package/electron/src/components/database/DeleteDatabaseModal.jsx +97 -0
  41. package/electron/src/components/query/CodeEditor.jsx +343 -0
  42. package/electron/src/components/query/ObjectEditor.jsx +115 -0
  43. package/electron/src/components/query/ObjectView.jsx +44 -0
  44. package/electron/src/components/query/QueryEditor.jsx +32 -0
  45. package/electron/src/components/query/queryLanguage.js +755 -0
  46. package/electron/src/components/ui/Button.jsx +58 -0
  47. package/electron/src/components/ui/Card.jsx +29 -0
  48. package/electron/src/components/ui/ErrorBoundary.jsx +108 -0
  49. package/electron/src/components/ui/Feedback.jsx +66 -0
  50. package/electron/src/components/ui/Field.jsx +65 -0
  51. package/electron/src/components/ui/MetricCard.jsx +104 -0
  52. package/electron/src/components/ui/Modal.jsx +119 -0
  53. package/electron/src/components/ui/Page.jsx +40 -0
  54. package/electron/src/config/key.js +11 -0
  55. package/electron/src/index.css +181 -0
  56. package/electron/src/index.html +13 -0
  57. package/electron/src/layout/Sidebar.jsx +478 -0
  58. package/electron/src/layout/StatusBar.jsx +70 -0
  59. package/electron/src/layout/Titlebar.jsx +128 -0
  60. package/electron/src/main.jsx +42 -0
  61. package/electron/src/pages/ConnectionHub.jsx +464 -0
  62. package/electron/src/pages/Dashboard.jsx +128 -0
  63. package/electron/src/pages/Documents.jsx +823 -0
  64. package/electron/src/pages/Import.jsx +527 -0
  65. package/electron/src/pages/UserManagement.jsx +294 -0
  66. package/electron/src/pages/Welcome.jsx +157 -0
  67. package/electron/src/store/authStore.js +30 -0
  68. package/electron/src/store/connectionStore.js +103 -0
  69. package/electron/src/store/dbStore.js +165 -0
  70. package/electron/src/store/store.js +8 -0
  71. package/electron/src/utils/format.js +39 -0
  72. package/electron/vite.config.js +28 -0
  73. package/lib/Services/Indexation.operation.js +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,85 @@
1
+ import apiClient from '../../api/client'
2
+ import { useState } from 'react'
3
+ import Modal from '../ui/Modal'
4
+ import Button from '../ui/Button'
5
+ import { Alert } from '../ui/Feedback'
6
+
7
+ const DeleteCollectionModal = ({
8
+ isOpen,
9
+ onClose,
10
+ onCollectionDeleted,
11
+ databaseName,
12
+ collectionName
13
+ }) => {
14
+ const [isSubmitting, setIsSubmitting] = useState(false)
15
+ const [error, setError] = useState('')
16
+
17
+ // Reset form state when modal is opened/closed
18
+ const handleClose = () => {
19
+ setError('')
20
+ setIsSubmitting(false)
21
+ onClose()
22
+ }
23
+
24
+ const handleDelete = async () => {
25
+ setIsSubmitting(true)
26
+ setError('')
27
+
28
+ try {
29
+ const response = await apiClient.delete(
30
+ `/api/collection/delete-collection/?dbName=${encodeURIComponent(databaseName)}&collectionName=${encodeURIComponent(collectionName)}`
31
+ )
32
+
33
+ if (response.data.statusCode === 200) {
34
+ onCollectionDeleted(collectionName)
35
+ handleClose()
36
+ } else {
37
+ throw new Error('Failed to delete collection')
38
+ }
39
+ } catch (error) {
40
+ console.error('Error deleting collection:', error)
41
+ setError(
42
+ error.response?.data?.message ||
43
+ 'Failed to delete collection. Please try again.'
44
+ )
45
+ setIsSubmitting(false)
46
+ }
47
+ }
48
+
49
+ const WarningIcon = (
50
+ <svg className='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
51
+ <path strokeLinecap='round' strokeLinejoin='round' d='M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z' />
52
+ </svg>
53
+ )
54
+
55
+ return (
56
+ <Modal
57
+ isOpen={isOpen}
58
+ onClose={handleClose}
59
+ title='Delete collection'
60
+ subtitle={`${databaseName} \u203a ${collectionName}`}
61
+ icon={WarningIcon}
62
+ tone='danger'
63
+ size='md'
64
+ footer={
65
+ <>
66
+ <Button variant='secondary' onClick={handleClose} disabled={isSubmitting}>
67
+ Cancel
68
+ </Button>
69
+ <Button variant='danger' onClick={handleDelete} loading={isSubmitting}>
70
+ Delete permanently
71
+ </Button>
72
+ </>
73
+ }
74
+ >
75
+ <Alert tone='danger' title='This cannot be undone'>
76
+ Deleting <strong>{collectionName}</strong> removes every document it holds, along
77
+ with its indexes.
78
+ </Alert>
79
+
80
+ {error && <Alert tone='danger' className='mt-4'>{error}</Alert>}
81
+ </Modal>
82
+ )
83
+ }
84
+
85
+ export default DeleteCollectionModal
@@ -0,0 +1,369 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import React from 'react'
3
+ import { motion, AnimatePresence } from 'framer-motion'
4
+
5
+ const SchemaViewModal = ({ isOpen, onClose, schema, collectionName }) => {
6
+ if (!isOpen) return null
7
+
8
+ // Function to get a readable type label with appropriate color
9
+ const getTypeLabel = (type) => {
10
+ const typeColors = {
11
+ // Basic types
12
+ string: 'bg-indigo-100 text-indigo-800 border-indigo-200',
13
+ number: 'bg-brand-100 text-brand-800 border-green-200',
14
+ boolean: 'bg-warn-50 text-warn-700 border-yellow-200',
15
+ date: 'bg-purple-100 text-purple-800 border-purple-200',
16
+ object: 'bg-info-50 text-info-700 border-blue-200',
17
+ array: 'bg-pink-100 text-pink-800 border-pink-200',
18
+ any: 'bg-ink-100 text-ink-800 border-ink-200',
19
+
20
+ // Extended primitive types
21
+ binary: 'bg-cyan-100 text-cyan-800 border-cyan-200',
22
+ symbol: 'bg-fuchsia-100 text-fuchsia-800 border-fuchsia-200',
23
+ func: 'bg-rose-100 text-rose-800 border-rose-200',
24
+ function: 'bg-rose-100 text-rose-800 border-rose-200',
25
+ alternatives: 'bg-amber-100 text-amber-800 border-amber-200',
26
+ link: 'bg-sky-100 text-sky-800 border-sky-200',
27
+
28
+ // String specialized types
29
+ uuid: 'bg-violet-100 text-violet-800 border-violet-200',
30
+ guid: 'bg-violet-100 text-violet-800 border-violet-200',
31
+ email: 'bg-indigo-100 text-indigo-800 border-indigo-200',
32
+ uri: 'bg-info-50 text-info-700 border-blue-200',
33
+ url: 'bg-info-50 text-info-700 border-blue-200',
34
+ hostname: 'bg-teal-100 text-teal-800 border-teal-200',
35
+ ip: 'bg-cyan-100 text-cyan-800 border-cyan-200',
36
+ alphanum: 'bg-indigo-100 text-indigo-800 border-indigo-200',
37
+ token: 'bg-purple-100 text-purple-800 border-purple-200',
38
+ hex: 'bg-fuchsia-100 text-fuchsia-800 border-fuchsia-200',
39
+ base64: 'bg-info-50 text-info-700 border-blue-200',
40
+
41
+ // Number specialized types
42
+ integer: 'bg-emerald-100 text-emerald-800 border-emerald-200',
43
+ float: 'bg-brand-100 text-brand-800 border-green-200',
44
+ precision: 'bg-lime-100 text-lime-800 border-lime-200',
45
+ port: 'bg-teal-100 text-teal-800 border-teal-200',
46
+
47
+ // Object specialized types
48
+ pattern: 'bg-info-50 text-info-700 border-blue-200',
49
+ schema: 'bg-indigo-100 text-indigo-800 border-indigo-200',
50
+ instance: 'bg-violet-100 text-violet-800 border-violet-200',
51
+ ref: 'bg-amber-100 text-amber-800 border-amber-200',
52
+
53
+ // Array specialized types
54
+ items: 'bg-pink-100 text-pink-800 border-pink-200',
55
+ ordered: 'bg-rose-100 text-rose-800 border-rose-200',
56
+ tuple: 'bg-fuchsia-100 text-fuchsia-800 border-fuchsia-200',
57
+
58
+ // Advanced types
59
+ lazy: 'bg-purple-100 text-purple-800 border-purple-200',
60
+ custom: 'bg-red-100 text-red-800 border-red-200',
61
+ extensions: 'bg-amber-100 text-amber-800 border-amber-200'
62
+ }
63
+
64
+ // Handle compound types (e.g., "string.email")
65
+ if (type && type.includes('.')) {
66
+ const parts = type.split('.')
67
+ const baseType = parts[0]
68
+ const subType = parts[1]
69
+
70
+ // If we have a specific color for the subtype, use it
71
+ if (typeColors[subType]) {
72
+ return (
73
+ <span
74
+ className={`px-2 py-1 rounded-md text-xs font-medium border ${typeColors[subType]}`}
75
+ >
76
+ {type}
77
+ </span>
78
+ )
79
+ }
80
+
81
+ // Fall back to base type if available
82
+ if (typeColors[baseType]) {
83
+ return (
84
+ <span
85
+ className={`px-2 py-1 rounded-md text-xs font-medium border ${typeColors[baseType]}`}
86
+ >
87
+ {type}
88
+ </span>
89
+ )
90
+ }
91
+ }
92
+
93
+ const color = typeColors[type] || typeColors.any
94
+
95
+ return (
96
+ <span
97
+ className={`px-2 py-1 rounded-md text-xs font-medium border ${color}`}
98
+ >
99
+ {type}
100
+ </span>
101
+ )
102
+ }
103
+
104
+ // Function to get validation rules in a readable format
105
+ const getValidationRules = (rules) => {
106
+ if (!rules || !rules.length) return null
107
+
108
+ return (
109
+ <div className='mt-2'>
110
+ <h6 className='text-xs font-semibold text-ink-500 uppercase tracking-wider mb-1'>
111
+ Validations
112
+ </h6>
113
+ <ul className='space-y-1'>
114
+ {rules.map((rule, index) => {
115
+ let ruleText = rule.name
116
+
117
+ // Format args based on rule type
118
+ if (rule.args) {
119
+ if (
120
+ rule.name === 'min' ||
121
+ rule.name === 'max' ||
122
+ rule.name === 'length'
123
+ ) {
124
+ ruleText += `: ${rule.args.limit}`
125
+ } else if (rule.name === 'email') {
126
+ ruleText = 'must be a valid email'
127
+ } else if (rule.name === 'pattern') {
128
+ ruleText = `matches pattern: ${rule.args.regex}`
129
+ } else if (typeof rule.args === 'object') {
130
+ try {
131
+ ruleText += `: ${JSON.stringify(rule.args)}`
132
+ } catch (e) {
133
+ // fallback if can't stringify
134
+ ruleText += ': has constraints'
135
+ }
136
+ }
137
+ }
138
+
139
+ return (
140
+ <li
141
+ key={index}
142
+ className='flex items-center text-xs text-ink-600 pl-2 border-l-2 border-ink-200'
143
+ >
144
+ <svg
145
+ xmlns='http://www.w3.org/2000/svg'
146
+ className='h-3 w-3 mr-1 text-ink-500'
147
+ viewBox='0 0 20 20'
148
+ fill='currentColor'
149
+ >
150
+ <path
151
+ fillRule='evenodd'
152
+ d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'
153
+ clipRule='evenodd'
154
+ />
155
+ </svg>
156
+ {ruleText}
157
+ </li>
158
+ )
159
+ })}
160
+ </ul>
161
+ </div>
162
+ )
163
+ }
164
+
165
+ // Function to render a schema field
166
+ const renderSchemaField = (fieldName, fieldSchema) => {
167
+ const fieldType = fieldSchema.type || 'any'
168
+ const required = fieldSchema._flags?.presence === 'required'
169
+
170
+ return (
171
+ <motion.div
172
+ initial={{ opacity: 0, y: 10 }}
173
+ animate={{ opacity: 1, y: 0 }}
174
+ transition={{ duration: 0.2 }}
175
+ key={fieldName}
176
+ className='mb-5 bg-white rounded-lg p-4 shadow-sm border border-ink-200 hover:shadow-md transition-shadow'
177
+ >
178
+ <div className='flex flex-col'>
179
+ <div className='flex items-center justify-between mb-2'>
180
+ <div className='flex items-center'>
181
+ <span className='font-bold text-ink-800 mr-2'>{fieldName}</span>
182
+ {required && (
183
+ <span className='bg-red-100 text-red-800 text-xs px-2 py-0.5 rounded-full border border-red-200'>
184
+ required
185
+ </span>
186
+ )}
187
+ </div>
188
+ {getTypeLabel(fieldType)}
189
+ </div>
190
+
191
+ {getValidationRules(fieldSchema._rules)}
192
+
193
+ {/* Show additional properties if available */}
194
+ {fieldSchema._flags && Object.keys(fieldSchema._flags).length > 0 && (
195
+ <div className='mt-2 pt-2 border-t border-ink-100'>
196
+ <h6 className='text-xs font-semibold text-ink-500 uppercase tracking-wider mb-1'>
197
+ Properties
198
+ </h6>
199
+ <div className='grid grid-cols-2 gap-1'>
200
+ {Object.entries(fieldSchema._flags).map(
201
+ ([key, value]) =>
202
+ key !== 'presence' && (
203
+ <div key={key} className='text-xs text-ink-600'>
204
+ <span className='font-medium'>{key}:</span>{' '}
205
+ {String(value)}
206
+ </div>
207
+ )
208
+ )}
209
+ </div>
210
+ </div>
211
+ )}
212
+
213
+ {/* Display any nested objects if they exist */}
214
+ {fieldType === 'object' && fieldSchema.keys && (
215
+ <div className='mt-3 pt-3 border-t border-ink-100'>
216
+ <h6 className='text-xs font-semibold text-ink-500 uppercase tracking-wider mb-2'>
217
+ Nested Fields
218
+ </h6>
219
+ <div className='pl-4 border-l-2 border-indigo-100'>
220
+ {Object.entries(fieldSchema.keys).map(
221
+ ([nestedKey, nestedSchema]) =>
222
+ renderSchemaField(nestedKey, nestedSchema)
223
+ )}
224
+ </div>
225
+ </div>
226
+ )}
227
+ </div>
228
+ </motion.div>
229
+ )
230
+ }
231
+
232
+ return (
233
+ <AnimatePresence>
234
+ {isOpen && (
235
+ <motion.div
236
+ initial={{ opacity: 0 }}
237
+ animate={{ opacity: 1 }}
238
+ exit={{ opacity: 0 }}
239
+ className='fixed inset-0 bg-black bg-opacity-50 overflow-y-auto h-full w-full z-50 flex justify-center items-center backdrop-filter backdrop-blur-sm'
240
+ >
241
+ <motion.div
242
+ initial={{ scale: 0.9, opacity: 0 }}
243
+ animate={{ scale: 1, opacity: 1 }}
244
+ exit={{ scale: 0.9, opacity: 0 }}
245
+ transition={{ type: 'spring', damping: 25, stiffness: 300 }}
246
+ className='relative bg-ink-50 rounded-xl shadow-2xl max-w-3xl w-full mx-4 max-h-[90vh] flex flex-col overflow-hidden'
247
+ >
248
+ <div className='flex justify-between items-center p-6 border-b bg-gradient-to-r from-blue-50 to-indigo-50'>
249
+ <h3 className='text-xl font-semibold text-ink-900 flex items-center'>
250
+ <svg
251
+ xmlns='http://www.w3.org/2000/svg'
252
+ className='h-6 w-6 mr-2 text-brand-700'
253
+ viewBox='0 0 20 20'
254
+ fill='currentColor'
255
+ >
256
+ <path d='M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' />
257
+ </svg>
258
+ <span>Collection Schema</span>
259
+ <span className='ml-2 text-brand-700 font-normal'>
260
+ <span className='mx-1'>·</span> {collectionName}
261
+ </span>
262
+ </h3>
263
+ <button
264
+ onClick={onClose}
265
+ className='text-ink-500 hover:text-ink-700 focus:outline-none bg-white rounded-full p-1 hover:bg-ink-100 transition-colors'
266
+ >
267
+ <svg
268
+ className='h-6 w-6'
269
+ fill='none'
270
+ viewBox='0 0 24 24'
271
+ stroke='currentColor'
272
+ >
273
+ <path
274
+ strokeLinecap='round'
275
+ strokeLinejoin='round'
276
+ strokeWidth='2'
277
+ d='M6 18L18 6M6 6l12 12'
278
+ />
279
+ </svg>
280
+ </button>
281
+ </div>
282
+
283
+ <div className='p-6 overflow-y-auto flex-grow'>
284
+ <div className='mb-6 bg-blue-50 border-l-4 border-blue-500 p-4 rounded-md text-blue-700 flex items-start'>
285
+ <svg
286
+ xmlns='http://www.w3.org/2000/svg'
287
+ className='h-6 w-6 mr-3 flex-shrink-0 text-blue-500'
288
+ fill='none'
289
+ viewBox='0 0 24 24'
290
+ stroke='currentColor'
291
+ >
292
+ <path
293
+ strokeLinecap='round'
294
+ strokeLinejoin='round'
295
+ strokeWidth='2'
296
+ d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
297
+ />
298
+ </svg>
299
+ <div>
300
+ <p className='font-medium mb-1'>Schema Validation</p>
301
+ <p className='text-sm'>
302
+ This collection uses schema validation. Documents must
303
+ conform to the structure defined below before they can be
304
+ inserted.
305
+ </p>
306
+ </div>
307
+ </div>
308
+
309
+ {Object.keys(schema).length > 0
310
+ ? (
311
+ <div className='space-y-4'>
312
+ <div className='flex items-center justify-between mb-2'>
313
+ <h4 className='text-lg font-medium text-ink-800'>
314
+ Schema Fields
315
+ </h4>
316
+ <span className='text-sm text-ink-500'>
317
+ {Object.keys(schema).length} fields defined
318
+ </span>
319
+ </div>
320
+
321
+ {Object.entries(schema).map(([fieldName, fieldSchema]) =>
322
+ renderSchemaField(fieldName, fieldSchema)
323
+ )}
324
+ </div>
325
+ )
326
+ : (
327
+ <div className='text-center py-8'>
328
+ <svg
329
+ xmlns='http://www.w3.org/2000/svg'
330
+ className='h-16 w-16 mx-auto text-ink-300 mb-4'
331
+ fill='none'
332
+ viewBox='0 0 24 24'
333
+ stroke='currentColor'
334
+ >
335
+ <path
336
+ strokeLinecap='round'
337
+ strokeLinejoin='round'
338
+ strokeWidth='2'
339
+ d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
340
+ />
341
+ </svg>
342
+ <p className='text-ink-500 mb-2 text-lg'>
343
+ No schema fields defined
344
+ </p>
345
+ <p className='text-ink-400 text-sm'>
346
+ This collection has schema validation enabled but no fields
347
+ are specified.
348
+ </p>
349
+ </div>
350
+ )}
351
+ </div>
352
+
353
+ <div className='border-t border-ink-200 px-6 py-4 bg-white flex justify-end'>
354
+ <button
355
+ type='button'
356
+ onClick={onClose}
357
+ className='px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors shadow-sm flex items-center'
358
+ >
359
+ <span>Close</span>
360
+ </button>
361
+ </div>
362
+ </motion.div>
363
+ </motion.div>
364
+ )}
365
+ </AnimatePresence>
366
+ )
367
+ }
368
+
369
+ export default SchemaViewModal
@@ -0,0 +1,94 @@
1
+ import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
2
+ import Card, { CardHeader } from '../ui/Card'
3
+ import { EmptyState, Skeleton } from '../ui/Feedback'
4
+
5
+ const VIZ = [
6
+ 'var(--color-viz-1)', 'var(--color-viz-2)', 'var(--color-viz-3)',
7
+ 'var(--color-viz-4)', 'var(--color-viz-5)', 'var(--color-viz-6)'
8
+ ]
9
+
10
+ /** Recharts' default tooltip doesn't inherit the app's surface treatment. */
11
+ const ChartTooltip = ({ active, payload, label }) => {
12
+ if (!active || !payload?.length) return null
13
+
14
+ return (
15
+ <div className='rounded-lg border border-ink-200 bg-white px-3 py-2 shadow-float'>
16
+ <p className='text-xs font-semibold text-ink-900'>{label}</p>
17
+ <p className='mt-0.5 text-xs tabular-nums text-ink-600'>
18
+ {payload[0].value.toLocaleString()} documents
19
+ </p>
20
+ <p className='text-[10px] text-ink-400'>{payload[0].payload.database}</p>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ /**
26
+ * Documents per collection, flattened out of the dashboard's nodeTree.
27
+ *
28
+ * Horizontal bars because collection names are long and readable labels matter more than
29
+ * chart convention here. Capped at the busiest 8 - beyond that the bars are too thin to
30
+ * compare, and the tree view below already lists everything.
31
+ */
32
+ const CollectionsChart = ({ nodeTree = [], loading = false }) => {
33
+ const data = nodeTree
34
+ .flatMap((db) =>
35
+ (db.collections ?? []).map((collection) => ({
36
+ name: collection.name ?? collection,
37
+ database: db.name,
38
+ documents: collection.documentCount ?? 0
39
+ }))
40
+ )
41
+ .sort((a, b) => b.documents - a.documents)
42
+ .slice(0, 8)
43
+
44
+ return (
45
+ <Card className='flex flex-col'>
46
+ <CardHeader
47
+ title='Documents per collection'
48
+ subtitle={data.length === 8 ? 'Busiest 8 collections' : 'All collections'}
49
+ />
50
+ <div className='flex-1 p-5'>
51
+ {loading
52
+ ? <Skeleton className='h-56 w-full' />
53
+ : data.length === 0
54
+ ? (
55
+ <EmptyState
56
+ className='py-10'
57
+ title='No collections yet'
58
+ description='Create a collection and its document count will chart here.'
59
+ />
60
+ )
61
+ : (
62
+ <ResponsiveContainer width='100%' height={224}>
63
+ <BarChart data={data} layout='vertical' margin={{ left: 8, right: 16, top: 4, bottom: 4 }}>
64
+ <CartesianGrid horizontal={false} stroke='var(--color-ink-200)' />
65
+ <XAxis
66
+ type='number'
67
+ allowDecimals={false}
68
+ tick={{ fontSize: 11, fill: 'var(--color-ink-500)' }}
69
+ axisLine={false}
70
+ tickLine={false}
71
+ />
72
+ <YAxis
73
+ type='category'
74
+ dataKey='name'
75
+ width={110}
76
+ tick={{ fontSize: 11, fill: 'var(--color-ink-600)' }}
77
+ axisLine={false}
78
+ tickLine={false}
79
+ />
80
+ <Tooltip content={<ChartTooltip />} cursor={{ fill: 'var(--color-ink-100)' }} />
81
+ <Bar dataKey='documents' radius={[0, 6, 6, 0]} animationDuration={800}>
82
+ {data.map((entry, index) => (
83
+ <Cell key={entry.name} fill={VIZ[index % VIZ.length]} />
84
+ ))}
85
+ </Bar>
86
+ </BarChart>
87
+ </ResponsiveContainer>
88
+ )}
89
+ </div>
90
+ </Card>
91
+ )
92
+ }
93
+
94
+ export default CollectionsChart
@@ -0,0 +1,130 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import Card, { CardHeader } from '../ui/Card'
3
+ import { Badge, EmptyState, Skeleton } from '../ui/Feedback'
4
+
5
+ /**
6
+ * Expandable database → collection tree.
7
+ *
8
+ * Loading is now driven by the caller's `loading` prop. The previous version ran its own
9
+ * `setTimeout(500)` "to simulate loading" over data it already had in props, which just made
10
+ * the dashboard feel half a second slower than it was.
11
+ */
12
+ const DatabaseTreeView = ({ treeDB = [], loading = false }) => {
13
+ const [expanded, setExpanded] = useState({})
14
+
15
+ const tree = useMemo(
16
+ () =>
17
+ (Array.isArray(treeDB) ? treeDB : []).map((db, dbIndex) => ({
18
+ id: `db${dbIndex + 1}`,
19
+ name: db.name,
20
+ collections: Array.isArray(db.collections)
21
+ ? db.collections.map((collection, colIndex) => ({
22
+ id: `db${dbIndex + 1}_col${colIndex + 1}`,
23
+ // Handles both the current object shape and the older bare-string form.
24
+ name: collection.name ?? collection,
25
+ documentCount: collection.documentCount ?? 0
26
+ }))
27
+ : []
28
+ })),
29
+ [treeDB]
30
+ )
31
+
32
+ // Open the first database so the panel never reads as empty when data exists.
33
+ useEffect(() => {
34
+ if (tree.length > 0) setExpanded((current) => ({ [tree[0].id]: true, ...current }))
35
+ }, [tree])
36
+
37
+ const toggle = (id) => setExpanded((current) => ({ ...current, [id]: !current[id] }))
38
+
39
+ return (
40
+ <Card className='flex flex-col'>
41
+ <CardHeader
42
+ title='Database structure'
43
+ subtitle={`${tree.length} database${tree.length === 1 ? '' : 's'}`}
44
+ />
45
+
46
+ <div className='max-h-[19rem] flex-1 overflow-y-auto'>
47
+ {loading
48
+ ? (
49
+ <div className='space-y-3 p-5'>
50
+ {[0, 1, 2].map((i) => (
51
+ <div key={i} className='space-y-2'>
52
+ <Skeleton className='h-7 w-3/5' />
53
+ <div className='space-y-1.5 pl-8'>
54
+ <Skeleton className='h-5 w-2/3' />
55
+ <Skeleton className='h-5 w-1/2' />
56
+ </div>
57
+ </div>
58
+ ))}
59
+ </div>
60
+ )
61
+ : tree.length === 0
62
+ ? (
63
+ <EmptyState
64
+ className='py-10'
65
+ title='No databases yet'
66
+ description='Databases you create will appear here with their collections.'
67
+ />
68
+ )
69
+ : (
70
+ <ul className='divide-y divide-ink-100'>
71
+ {tree.map((db) => {
72
+ const isOpen = Boolean(expanded[db.id])
73
+
74
+ return (
75
+ <li key={db.id}>
76
+ <button
77
+ type='button'
78
+ onClick={() => toggle(db.id)}
79
+ aria-expanded={isOpen}
80
+ className='flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-ink-50'
81
+ >
82
+ <svg
83
+ className={`h-4 w-4 shrink-0 text-ink-400 transition-transform duration-200 ${isOpen ? 'rotate-90' : ''}`}
84
+ fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2} aria-hidden='true'
85
+ >
86
+ <path strokeLinecap='round' strokeLinejoin='round' d='M9 5l7 7-7 7' />
87
+ </svg>
88
+ <svg className='h-4 w-4 shrink-0 text-brand-600' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2} aria-hidden='true'>
89
+ <ellipse cx='12' cy='6' rx='8' ry='3' />
90
+ <path d='M4 6v12c0 1.66 3.58 3 8 3s8-1.34 8-3V6' />
91
+ </svg>
92
+ <span className='min-w-0 flex-1 truncate text-sm font-semibold text-ink-900'>
93
+ {db.name}
94
+ </span>
95
+ <Badge tone='neutral'>{db.collections.length}</Badge>
96
+ </button>
97
+
98
+ {isOpen && (
99
+ <ul className='ml-[1.6rem] space-y-0.5 border-l border-ink-200 pb-2 pl-3'>
100
+ {db.collections.length === 0
101
+ ? <li className='py-2 pl-3 text-xs text-ink-400'>No collections</li>
102
+ : db.collections.map((collection) => (
103
+ <li
104
+ key={collection.id}
105
+ className='animate-fadeIn flex items-center gap-2 rounded-md px-3 py-1.5 transition-colors hover:bg-ink-50'
106
+ >
107
+ <svg className='h-4 w-4 shrink-0 text-amber-500' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2} aria-hidden='true'>
108
+ <path strokeLinecap='round' strokeLinejoin='round' d='M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z' />
109
+ </svg>
110
+ <span className='min-w-0 flex-1 truncate text-sm text-ink-700'>
111
+ {collection.name}
112
+ </span>
113
+ <span className='shrink-0 text-xs tabular-nums text-ink-500'>
114
+ {collection.documentCount.toLocaleString()} docs
115
+ </span>
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ )}
120
+ </li>
121
+ )
122
+ })}
123
+ </ul>
124
+ )}
125
+ </div>
126
+ </Card>
127
+ )
128
+ }
129
+
130
+ export default DatabaseTreeView