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,343 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { tokenize } from './queryLanguage'
|
|
3
|
+
|
|
4
|
+
/** Light VS Code / GitHub Light palette */
|
|
5
|
+
const TOKEN_COLORS = {
|
|
6
|
+
key: '#0284c7', // sky-600
|
|
7
|
+
operator: '#9333ea', // purple-600
|
|
8
|
+
string: '#15803d', // green-700
|
|
9
|
+
number: '#b45309', // amber-700
|
|
10
|
+
literal: '#0369a1', // cyan-700
|
|
11
|
+
method: '#7c3aed', // violet-600
|
|
12
|
+
identifier: '#0f766e', // teal-700
|
|
13
|
+
punctuation: '#475569', // slate-600
|
|
14
|
+
space: 'inherit'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FONT = '13px/20px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
18
|
+
const PADDING_Y = 10
|
|
19
|
+
const PADDING_X = 12
|
|
20
|
+
const GUTTER = 38
|
|
21
|
+
const LINE_HEIGHT = 20
|
|
22
|
+
|
|
23
|
+
const NO_SUGGESTIONS = { items: [], replaceFrom: 0, prefix: '' }
|
|
24
|
+
|
|
25
|
+
const PAIRS = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' }
|
|
26
|
+
const CLOSERS = new Set([')', ']', '}', '"', "'", '`'])
|
|
27
|
+
const QUOTES = new Set(['"', "'", '`'])
|
|
28
|
+
|
|
29
|
+
function useHighlighted (value, diagnostics) {
|
|
30
|
+
return useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
tokenize(value).map((token, index) => {
|
|
33
|
+
const end = token.start + token.value.length
|
|
34
|
+
const diagnostic = diagnostics.find((d) => d.start < end && d.end > token.start)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<span
|
|
38
|
+
key={index}
|
|
39
|
+
style={{
|
|
40
|
+
color: TOKEN_COLORS[token.type] ?? TOKEN_COLORS.punctuation,
|
|
41
|
+
textDecoration: diagnostic ? 'underline wavy' : undefined,
|
|
42
|
+
textDecorationColor: diagnostic
|
|
43
|
+
? diagnostic.severity === 'error' ? '#dc2626' : '#d97706'
|
|
44
|
+
: undefined,
|
|
45
|
+
textUnderlineOffset: diagnostic ? '3px' : undefined
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{token.value}
|
|
49
|
+
</span>
|
|
50
|
+
)
|
|
51
|
+
}),
|
|
52
|
+
[value, diagnostics]
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const CodeEditor = ({
|
|
57
|
+
value,
|
|
58
|
+
onChange,
|
|
59
|
+
diagnostics = [],
|
|
60
|
+
onSubmit,
|
|
61
|
+
suggest,
|
|
62
|
+
suggestAll,
|
|
63
|
+
minHeight = 170,
|
|
64
|
+
maxHeight = 260,
|
|
65
|
+
ariaLabel = 'Code editor'
|
|
66
|
+
}) => {
|
|
67
|
+
const textareaRef = useRef(null)
|
|
68
|
+
const highlightRef = useRef(null)
|
|
69
|
+
const [suggestions, setSuggestions] = useState(NO_SUGGESTIONS)
|
|
70
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
71
|
+
const [caret, setCaret] = useState(0)
|
|
72
|
+
const [scrollTop, setScrollTop] = useState(0)
|
|
73
|
+
|
|
74
|
+
const highlighted = useHighlighted(value, diagnostics)
|
|
75
|
+
const lineCount = useMemo(() => value.split('\n').length, [value])
|
|
76
|
+
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
if (highlightRef.current) highlightRef.current.scrollTop = scrollTop
|
|
79
|
+
}, [scrollTop])
|
|
80
|
+
|
|
81
|
+
useEffect(() => setActiveIndex(0), [suggestions.items])
|
|
82
|
+
|
|
83
|
+
const closeSuggestions = () => setSuggestions(NO_SUGGESTIONS)
|
|
84
|
+
|
|
85
|
+
const refreshSuggestions = (text, position) => {
|
|
86
|
+
setSuggestions(suggest ? suggest(text, position) : NO_SUGGESTIONS)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleChange = (event) => {
|
|
90
|
+
const next = event.target.value
|
|
91
|
+
const position = event.target.selectionStart
|
|
92
|
+
onChange(next)
|
|
93
|
+
setCaret(position)
|
|
94
|
+
refreshSuggestions(next, position)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const applySuggestion = (item) => {
|
|
98
|
+
const next = value.slice(0, suggestions.replaceFrom) + item.insert + value.slice(caret)
|
|
99
|
+
const caretTarget = suggestions.replaceFrom + (item.caretOffset ?? item.insert.length)
|
|
100
|
+
|
|
101
|
+
onChange(next)
|
|
102
|
+
closeSuggestions()
|
|
103
|
+
|
|
104
|
+
requestAnimationFrame(() => {
|
|
105
|
+
const textarea = textareaRef.current
|
|
106
|
+
if (!textarea) return
|
|
107
|
+
textarea.focus()
|
|
108
|
+
textarea.setSelectionRange(caretTarget, caretTarget)
|
|
109
|
+
setCaret(caretTarget)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleKeyDown = (event) => {
|
|
114
|
+
const open = suggestions.items.length > 0
|
|
115
|
+
|
|
116
|
+
if (event.key === 'Escape' && open) {
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
event.stopPropagation()
|
|
119
|
+
closeSuggestions()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (open && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
|
124
|
+
event.preventDefault()
|
|
125
|
+
const delta = event.key === 'ArrowDown' ? 1 : -1
|
|
126
|
+
setActiveIndex((current) => {
|
|
127
|
+
const next = current + delta
|
|
128
|
+
if (next < 0) return suggestions.items.length - 1
|
|
129
|
+
if (next >= suggestions.items.length) return 0
|
|
130
|
+
return next
|
|
131
|
+
})
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (open && (event.key === 'Enter' || event.key === 'Tab')) {
|
|
136
|
+
event.preventDefault()
|
|
137
|
+
applySuggestion(suggestions.items[activeIndex])
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (event.code === 'Space' && (event.ctrlKey || event.metaKey) && suggestAll) {
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
const position = event.target.selectionStart
|
|
144
|
+
setCaret(position)
|
|
145
|
+
setSuggestions(suggestAll(value, position))
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
onSubmit?.()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (event.key === 'Tab') {
|
|
156
|
+
event.preventDefault()
|
|
157
|
+
const start = event.target.selectionStart
|
|
158
|
+
const next = value.slice(0, start) + ' ' + value.slice(event.target.selectionEnd)
|
|
159
|
+
onChange(next)
|
|
160
|
+
requestAnimationFrame(() => textareaRef.current?.setSelectionRange(start + 2, start + 2))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const start = event.target.selectionStart
|
|
165
|
+
const end = event.target.selectionEnd
|
|
166
|
+
const setCaretTo = (from, to = from) =>
|
|
167
|
+
requestAnimationFrame(() => textareaRef.current?.setSelectionRange(from, to))
|
|
168
|
+
|
|
169
|
+
if (CLOSERS.has(event.key) && start === end && value[start] === event.key) {
|
|
170
|
+
event.preventDefault()
|
|
171
|
+
setCaret(start + 1)
|
|
172
|
+
setCaretTo(start + 1)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (Object.prototype.hasOwnProperty.call(PAIRS, event.key)) {
|
|
177
|
+
const close = PAIRS[event.key]
|
|
178
|
+
|
|
179
|
+
if (start !== end) {
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
const selected = value.slice(start, end)
|
|
182
|
+
onChange(`${value.slice(0, start)}${event.key}${selected}${close}${value.slice(end)}`)
|
|
183
|
+
setCaretTo(start + 1, end + 1)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const previous = value[start - 1] ?? ''
|
|
188
|
+
if (QUOTES.has(event.key) && /[A-Za-z0-9_$]/.test(previous)) return
|
|
189
|
+
|
|
190
|
+
event.preventDefault()
|
|
191
|
+
onChange(`${value.slice(0, start)}${event.key}${close}${value.slice(start)}`)
|
|
192
|
+
setCaret(start + 1)
|
|
193
|
+
setCaretTo(start + 1)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (event.key === 'Backspace' && start === end && start > 0) {
|
|
198
|
+
const before = value[start - 1]
|
|
199
|
+
if (PAIRS[before] && value[start] === PAIRS[before]) {
|
|
200
|
+
event.preventDefault()
|
|
201
|
+
onChange(value.slice(0, start - 1) + value.slice(start + 1))
|
|
202
|
+
setCaret(start - 1)
|
|
203
|
+
setCaretTo(start - 1)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const handleSelect = (event) => {
|
|
209
|
+
const position = event.target.selectionStart
|
|
210
|
+
setCaret(position)
|
|
211
|
+
if (suggestions.items.length > 0) refreshSuggestions(value, position)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const caretLine = value.slice(0, suggestions.replaceFrom).split('\n').length - 1
|
|
215
|
+
const caretColumn =
|
|
216
|
+
suggestions.replaceFrom - (value.lastIndexOf('\n', suggestions.replaceFrom - 1) + 1)
|
|
217
|
+
const popupTop = PADDING_Y + (caretLine + 1) * LINE_HEIGHT - scrollTop + 4
|
|
218
|
+
const popupLeft = Math.min(GUTTER + PADDING_X + caretColumn * 7.22, 340)
|
|
219
|
+
|
|
220
|
+
const layerStyle = {
|
|
221
|
+
font: FONT,
|
|
222
|
+
padding: `${PADDING_Y}px ${PADDING_X}px`,
|
|
223
|
+
paddingLeft: GUTTER + PADDING_X
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div className='relative'>
|
|
228
|
+
<div
|
|
229
|
+
className='relative overflow-hidden rounded-lg border border-slate-200 shadow-xs bg-slate-50'
|
|
230
|
+
>
|
|
231
|
+
{/* Line-number gutter */}
|
|
232
|
+
<div
|
|
233
|
+
aria-hidden='true'
|
|
234
|
+
className='absolute bottom-0 left-0 top-0 select-none text-right'
|
|
235
|
+
style={{
|
|
236
|
+
width: GUTTER,
|
|
237
|
+
padding: `${PADDING_Y}px 8px 0 0`,
|
|
238
|
+
font: FONT,
|
|
239
|
+
color: '#94a3b8',
|
|
240
|
+
background: '#f1f5f9',
|
|
241
|
+
borderRight: '1px solid #e2e8f0',
|
|
242
|
+
zIndex: 2
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<div style={{ transform: `translateY(-${scrollTop}px)` }}>
|
|
246
|
+
{Array.from({ length: lineCount }, (_, index) => (
|
|
247
|
+
<div key={index} style={{ height: LINE_HEIGHT }}>{index + 1}</div>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Colour layer */}
|
|
253
|
+
<pre
|
|
254
|
+
ref={highlightRef}
|
|
255
|
+
aria-hidden='true'
|
|
256
|
+
className='m-0 overflow-hidden whitespace-pre-wrap break-words'
|
|
257
|
+
style={{ ...layerStyle, minHeight, maxHeight, color: '#0f172a' }}
|
|
258
|
+
>
|
|
259
|
+
{highlighted}
|
|
260
|
+
{'\n'}
|
|
261
|
+
</pre>
|
|
262
|
+
|
|
263
|
+
{/* Input layer */}
|
|
264
|
+
<textarea
|
|
265
|
+
ref={textareaRef}
|
|
266
|
+
value={value}
|
|
267
|
+
onChange={handleChange}
|
|
268
|
+
onKeyDown={handleKeyDown}
|
|
269
|
+
onSelect={handleSelect}
|
|
270
|
+
onScroll={(event) => setScrollTop(event.target.scrollTop)}
|
|
271
|
+
onBlur={() => setTimeout(closeSuggestions, 120)}
|
|
272
|
+
spellCheck='false'
|
|
273
|
+
autoComplete='off'
|
|
274
|
+
autoCorrect='off'
|
|
275
|
+
autoCapitalize='off'
|
|
276
|
+
aria-label={ariaLabel}
|
|
277
|
+
className='absolute inset-0 resize-none overflow-auto whitespace-pre-wrap break-words bg-transparent outline-none'
|
|
278
|
+
style={{ ...layerStyle, color: 'transparent', caretColor: '#0f172a' }}
|
|
279
|
+
/>
|
|
280
|
+
|
|
281
|
+
{/* Completion popup */}
|
|
282
|
+
{suggestions.items.length > 0 && (
|
|
283
|
+
<div
|
|
284
|
+
className='absolute z-20 overflow-hidden rounded-md border border-slate-200 bg-white shadow-xl'
|
|
285
|
+
style={{
|
|
286
|
+
top: popupTop,
|
|
287
|
+
left: popupLeft,
|
|
288
|
+
width: 320
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<ul className='max-h-48 overflow-y-auto py-1'>
|
|
292
|
+
{suggestions.items.map((item, index) => (
|
|
293
|
+
<li key={item.label}>
|
|
294
|
+
<button
|
|
295
|
+
type='button'
|
|
296
|
+
onMouseDown={(event) => {
|
|
297
|
+
event.preventDefault()
|
|
298
|
+
applySuggestion(item)
|
|
299
|
+
}}
|
|
300
|
+
onMouseEnter={() => setActiveIndex(index)}
|
|
301
|
+
className='flex w-full items-baseline gap-2 px-3 py-1 text-left transition-colors'
|
|
302
|
+
style={{
|
|
303
|
+
font: FONT,
|
|
304
|
+
background: index === activeIndex ? '#ecfdf5' : 'transparent',
|
|
305
|
+
color: index === activeIndex ? '#059669' : '#1e293b'
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
<span style={{ color: '#0284c7' }}>{item.label}</span>
|
|
309
|
+
<span className='truncate text-xs text-slate-400'>
|
|
310
|
+
{item.detail}
|
|
311
|
+
</span>
|
|
312
|
+
</button>
|
|
313
|
+
</li>
|
|
314
|
+
))}
|
|
315
|
+
</ul>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{diagnostics.length > 0 && (
|
|
321
|
+
<ul className='mt-2 space-y-1'>
|
|
322
|
+
{diagnostics.map((diagnostic, index) => (
|
|
323
|
+
<li
|
|
324
|
+
key={index}
|
|
325
|
+
className={`flex items-start gap-2 rounded-md border px-3 py-1.5 text-xs ${
|
|
326
|
+
diagnostic.severity === 'error'
|
|
327
|
+
? 'border-red-200 bg-red-50 text-red-700'
|
|
328
|
+
: 'border-amber-200 bg-amber-50 text-amber-800'
|
|
329
|
+
}`}
|
|
330
|
+
>
|
|
331
|
+
<span className='mt-px font-bold'>
|
|
332
|
+
{diagnostic.severity === 'error' ? '✕' : '!'}
|
|
333
|
+
</span>
|
|
334
|
+
<span className='font-mono leading-relaxed'>{diagnostic.message}</span>
|
|
335
|
+
</li>
|
|
336
|
+
))}
|
|
337
|
+
</ul>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default CodeEditor
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import CodeEditor from './CodeEditor'
|
|
3
|
+
import { validateDocument, QUERY_OPERATORS } from './queryLanguage'
|
|
4
|
+
|
|
5
|
+
const COMMON_FIELDS = [
|
|
6
|
+
'name', 'title', 'email', 'status', 'role', 'type', 'age', 'price',
|
|
7
|
+
'description', 'isActive', 'tags', 'metadata', 'userId', 'category', 'count'
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
const COMMON_LITERALS = [
|
|
11
|
+
{ label: 'true', detail: 'boolean', doc: 'Boolean true', insert: 'true' },
|
|
12
|
+
{ label: 'false', detail: 'boolean', doc: 'Boolean false', insert: 'false' },
|
|
13
|
+
{ label: 'null', detail: 'null', doc: 'Null value', insert: 'null' },
|
|
14
|
+
{ label: 'array', detail: '[]', doc: 'Empty array literal', insert: '[]' },
|
|
15
|
+
{ label: 'object', detail: '{}', doc: 'Empty object literal', insert: '{\n \n}' },
|
|
16
|
+
{ label: '$set', detail: 'operator', doc: 'Update operator to set fields', insert: '$set: {}' },
|
|
17
|
+
{ label: '$unset', detail: 'operator', doc: 'Update operator to delete fields', insert: '$unset: {}' },
|
|
18
|
+
{ label: '$inc', detail: 'operator', doc: 'Increment numeric field', insert: '$inc: {}' },
|
|
19
|
+
{ label: '$push', detail: 'operator', doc: 'Append value to array', insert: '$push: {}' }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const getObjectSuggestions = (text, caret, fields = []) => {
|
|
23
|
+
const before = text.slice(0, caret)
|
|
24
|
+
|
|
25
|
+
// If typing an operator ($...)
|
|
26
|
+
const opMatch = /"?(\$[A-Za-z]*)$/.exec(before)
|
|
27
|
+
if (opMatch) {
|
|
28
|
+
const prefix = opMatch[1]
|
|
29
|
+
return {
|
|
30
|
+
items: QUERY_OPERATORS.filter((o) => o.label.startsWith(prefix)),
|
|
31
|
+
replaceFrom: caret - prefix.length,
|
|
32
|
+
prefix
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If typing a property/field name or literal
|
|
37
|
+
const idMatch = /([A-Za-z_][A-Za-z0-9_]*)$/.exec(before)
|
|
38
|
+
if (idMatch) {
|
|
39
|
+
const prefix = idMatch[1]
|
|
40
|
+
const candidateFields = fields && fields.length > 0 ? fields : COMMON_FIELDS
|
|
41
|
+
const items = []
|
|
42
|
+
|
|
43
|
+
candidateFields.forEach((f) => {
|
|
44
|
+
if (f.startsWith(prefix)) {
|
|
45
|
+
items.push({
|
|
46
|
+
label: f,
|
|
47
|
+
detail: 'field',
|
|
48
|
+
doc: `Document property "${f}"`,
|
|
49
|
+
insert: `${f}: `
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
COMMON_LITERALS.forEach((lit) => {
|
|
55
|
+
if (lit.label.startsWith(prefix)) {
|
|
56
|
+
items.push(lit)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (items.length > 0) {
|
|
61
|
+
return {
|
|
62
|
+
items,
|
|
63
|
+
replaceFrom: caret - prefix.length,
|
|
64
|
+
prefix
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { items: [], replaceFrom: caret, prefix: '' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const getAllObjectSuggestions = (text, caret, fields = []) => {
|
|
73
|
+
const candidateFields = fields && fields.length > 0 ? fields : COMMON_FIELDS
|
|
74
|
+
const items = candidateFields.map((f) => ({
|
|
75
|
+
label: f,
|
|
76
|
+
detail: 'field',
|
|
77
|
+
doc: `Document property "${f}"`,
|
|
78
|
+
insert: `${f}: `
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
items.push(...COMMON_LITERALS)
|
|
82
|
+
return { items, replaceFrom: caret, prefix: '' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Document editor: accepts JavaScript object-literal syntax (unquoted keys, single quotes, trailing commas)
|
|
87
|
+
* with full autocomplete suggestions for fields, operators, and literals.
|
|
88
|
+
*/
|
|
89
|
+
const ObjectEditor = ({
|
|
90
|
+
value,
|
|
91
|
+
onChange,
|
|
92
|
+
onSubmit,
|
|
93
|
+
fields = [],
|
|
94
|
+
minHeight = 220,
|
|
95
|
+
maxHeight = 320
|
|
96
|
+
}) => {
|
|
97
|
+
const diagnostics = useMemo(() => validateDocument(value), [value])
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<CodeEditor
|
|
101
|
+
value={value}
|
|
102
|
+
onChange={onChange}
|
|
103
|
+
diagnostics={diagnostics}
|
|
104
|
+
onSubmit={onSubmit}
|
|
105
|
+
suggest={(text, caret) => getObjectSuggestions(text, caret, fields)}
|
|
106
|
+
suggestAll={(text, caret) => getAllObjectSuggestions(text, caret, fields)}
|
|
107
|
+
minHeight={minHeight}
|
|
108
|
+
maxHeight={maxHeight}
|
|
109
|
+
ariaLabel='Document editor'
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default ObjectEditor
|
|
115
|
+
export { validateDocument }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { formatLiteral, tokenize } from './queryLanguage'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read-only, syntax-highlighted rendering of a value as a JavaScript object literal.
|
|
6
|
+
* Light VS Code / GitHub Light palette for maximum readability on white backgrounds.
|
|
7
|
+
*/
|
|
8
|
+
const TOKEN_COLORS = {
|
|
9
|
+
key: '#0284c7', // sky-600
|
|
10
|
+
operator: '#9333ea', // purple-600
|
|
11
|
+
string: '#15803d', // green-700
|
|
12
|
+
number: '#b45309', // amber-700
|
|
13
|
+
literal: '#0369a1', // cyan-700
|
|
14
|
+
method: '#7c3aed', // violet-600
|
|
15
|
+
identifier: '#0f766e', // teal-700
|
|
16
|
+
punctuation: '#475569', // slate-600
|
|
17
|
+
space: 'inherit'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ObjectView = ({ value, className = '', maxHeight = 220 }) => {
|
|
21
|
+
const nodes = useMemo(() => {
|
|
22
|
+
const source = typeof value === 'string' ? value : formatLiteral(value)
|
|
23
|
+
return tokenize(source).map((token, index) => (
|
|
24
|
+
<span key={index} style={{ color: TOKEN_COLORS[token.type] ?? TOKEN_COLORS.punctuation }}>
|
|
25
|
+
{token.value}
|
|
26
|
+
</span>
|
|
27
|
+
))
|
|
28
|
+
}, [value])
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<pre
|
|
32
|
+
className={`m-0 overflow-auto rounded-lg px-3.5 py-3 text-[12px] leading-5 border border-slate-200 bg-slate-50 ${className}`}
|
|
33
|
+
style={{
|
|
34
|
+
color: '#0f172a',
|
|
35
|
+
maxHeight,
|
|
36
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{nodes}
|
|
40
|
+
</pre>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default ObjectView
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import CodeEditor from './CodeEditor'
|
|
2
|
+
import { getAllSuggestions, getSuggestions, validate } from './queryLanguage'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Query Console's editor: the shared {@link CodeEditor} shell bound to the query
|
|
6
|
+
* language's completion, fields auto-completion, and validation.
|
|
7
|
+
*/
|
|
8
|
+
const QueryEditor = ({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
collectionName,
|
|
12
|
+
fields = [],
|
|
13
|
+
onSubmit,
|
|
14
|
+
diagnostics,
|
|
15
|
+
minHeight = 140,
|
|
16
|
+
maxHeight = 220
|
|
17
|
+
}) => (
|
|
18
|
+
<CodeEditor
|
|
19
|
+
value={value}
|
|
20
|
+
onChange={onChange}
|
|
21
|
+
diagnostics={diagnostics}
|
|
22
|
+
onSubmit={onSubmit}
|
|
23
|
+
minHeight={minHeight}
|
|
24
|
+
maxHeight={maxHeight}
|
|
25
|
+
ariaLabel='Query editor'
|
|
26
|
+
suggest={(text, caret) => getSuggestions(text, caret, collectionName, fields)}
|
|
27
|
+
suggestAll={(text, caret) => getAllSuggestions(text, caret, collectionName, fields)}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export default QueryEditor
|
|
32
|
+
export { validate }
|