axiodb 22.2.2 → 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.
- package/electron/electron-builder.json +58 -0
- package/electron/main/main.cts +585 -0
- package/electron/main/preload.cts +51 -0
- package/electron/package-lock.json +8123 -0
- package/electron/package.json +52 -0
- package/electron/public/AXioDB.png +0 -0
- package/electron/resources/after-install.sh +45 -0
- package/electron/resources/after-remove.sh +29 -0
- package/electron/resources/icon.png +0 -0
- package/electron/resources/icons/128x128.png +0 -0
- package/electron/resources/icons/16x16.png +0 -0
- package/electron/resources/icons/24x24.png +0 -0
- package/electron/resources/icons/256x256.png +0 -0
- package/electron/resources/icons/32x32.png +0 -0
- package/electron/resources/icons/48x48.png +0 -0
- package/electron/resources/icons/512x512.png +0 -0
- package/electron/resources/icons/64x64.png +0 -0
- package/electron/src/App.jsx +128 -0
- package/electron/src/api/authApi.js +83 -0
- package/electron/src/api/client.js +102 -0
- package/electron/src/assets/AXioDB.png +0 -0
- package/electron/src/components/auth/CreateRoleModal.jsx +189 -0
- package/electron/src/components/auth/CreateUserModal.jsx +131 -0
- package/electron/src/components/auth/ForcePasswordChangeModal.jsx +132 -0
- package/electron/src/components/auth/ProtectedRoute.jsx +42 -0
- package/electron/src/components/auth/ResetPasswordModal.jsx +90 -0
- package/electron/src/components/auth/UserAvatarMenu.jsx +103 -0
- package/electron/src/components/collection/CreateCollectionModal.jsx +116 -0
- package/electron/src/components/collection/DeleteCollectionModal.jsx +85 -0
- package/electron/src/components/collection/SchemaViewModal.jsx +369 -0
- package/electron/src/components/dashboard/CollectionsChart.jsx +94 -0
- package/electron/src/components/dashboard/DatabaseTreeView.jsx +130 -0
- package/electron/src/components/dashboard/InMemoryCacheCard.jsx +60 -0
- package/electron/src/components/dashboard/StorageDonut.jsx +76 -0
- package/electron/src/components/dashboard/StorageUsageCard.jsx +59 -0
- package/electron/src/components/dashboard/TotalCollectionsCard.jsx +47 -0
- package/electron/src/components/dashboard/TotalDatabasesCard.jsx +43 -0
- package/electron/src/components/dashboard/TotalDocumentsCard.jsx +44 -0
- package/electron/src/components/database/CreateDatabaseModal.jsx +109 -0
- package/electron/src/components/database/DeleteDatabaseModal.jsx +97 -0
- package/electron/src/components/query/CodeEditor.jsx +343 -0
- package/electron/src/components/query/ObjectEditor.jsx +115 -0
- package/electron/src/components/query/ObjectView.jsx +44 -0
- package/electron/src/components/query/QueryEditor.jsx +32 -0
- package/electron/src/components/query/queryLanguage.js +755 -0
- package/electron/src/components/ui/Button.jsx +58 -0
- package/electron/src/components/ui/Card.jsx +29 -0
- package/electron/src/components/ui/ErrorBoundary.jsx +108 -0
- package/electron/src/components/ui/Feedback.jsx +66 -0
- package/electron/src/components/ui/Field.jsx +65 -0
- package/electron/src/components/ui/MetricCard.jsx +104 -0
- package/electron/src/components/ui/Modal.jsx +119 -0
- package/electron/src/components/ui/Page.jsx +40 -0
- package/electron/src/config/key.js +11 -0
- package/electron/src/index.css +181 -0
- package/electron/src/index.html +13 -0
- package/electron/src/layout/Sidebar.jsx +478 -0
- package/electron/src/layout/StatusBar.jsx +70 -0
- package/electron/src/layout/Titlebar.jsx +128 -0
- package/electron/src/main.jsx +42 -0
- package/electron/src/pages/ConnectionHub.jsx +464 -0
- package/electron/src/pages/Dashboard.jsx +128 -0
- package/electron/src/pages/Documents.jsx +823 -0
- package/electron/src/pages/Import.jsx +527 -0
- package/electron/src/pages/UserManagement.jsx +294 -0
- package/electron/src/pages/Welcome.jsx +157 -0
- package/electron/src/store/authStore.js +30 -0
- package/electron/src/store/connectionStore.js +103 -0
- package/electron/src/store/dbStore.js +165 -0
- package/electron/src/store/store.js +8 -0
- package/electron/src/utils/format.js +39 -0
- package/electron/vite.config.js +28 -0
- package/lib/Services/Indexation.operation.js +1 -1
- 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
|