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,131 @@
1
+ import { useState } from 'react'
2
+ import authApi from '../../api/authApi'
3
+ import Modal from '../ui/Modal'
4
+ import Button from '../ui/Button'
5
+ import { Input } from '../ui/Field'
6
+ import { Alert } from '../ui/Feedback'
7
+
8
+ const MIN_PASSWORD_LENGTH = 4
9
+
10
+ const CreateUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
11
+ const [username, setUsername] = useState('')
12
+ const [password, setPassword] = useState('')
13
+ const [role, setRole] = useState('')
14
+ const [isSubmitting, setIsSubmitting] = useState(false)
15
+ const [error, setError] = useState('')
16
+
17
+ const handleClose = () => {
18
+ setUsername('')
19
+ setPassword('')
20
+ setRole('')
21
+ setError('')
22
+ setIsSubmitting(false)
23
+ onClose()
24
+ }
25
+
26
+ const handleSubmit = async (e) => {
27
+ e.preventDefault()
28
+
29
+ if (!username.trim()) {
30
+ setError('Username is required')
31
+ return
32
+ }
33
+ if (!/^[a-zA-Z0-9_]+$/.test(username)) {
34
+ setError('Username can only contain letters, numbers, and underscores')
35
+ return
36
+ }
37
+ if (!password || password.length < MIN_PASSWORD_LENGTH) {
38
+ setError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`)
39
+ return
40
+ }
41
+ const selectedRole = role || roles?.[0]?.roleName
42
+ if (!selectedRole) {
43
+ setError('Role is required')
44
+ return
45
+ }
46
+
47
+ setIsSubmitting(true)
48
+ setError('')
49
+
50
+ try {
51
+ await authApi.createUser(username, password, selectedRole)
52
+ onUserCreated()
53
+ handleClose()
54
+ } catch (err) {
55
+ setError(err.response?.data?.message || 'Failed to create user. Please try again.')
56
+ setIsSubmitting(false)
57
+ }
58
+ }
59
+
60
+ const UserIcon = (
61
+ <svg className='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
62
+ <path strokeLinecap='round' strokeLinejoin='round' d='M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z' />
63
+ </svg>
64
+ )
65
+
66
+ return (
67
+ <Modal
68
+ isOpen={isOpen}
69
+ onClose={handleClose}
70
+ title='Create user'
71
+ icon={UserIcon}
72
+ size='md'
73
+ footer={
74
+ <>
75
+ <Button variant='secondary' onClick={handleClose} disabled={isSubmitting}>
76
+ Cancel
77
+ </Button>
78
+ <Button onClick={handleSubmit} loading={isSubmitting} disabled={!username.trim() || !password}>
79
+ Create user
80
+ </Button>
81
+ </>
82
+ }
83
+ >
84
+ <form onSubmit={handleSubmit} className='space-y-4'>
85
+ <Input
86
+ label='Username'
87
+ value={username}
88
+ onChange={(event) => setUsername(event.target.value)}
89
+ placeholder='analyst'
90
+ mono
91
+ autoFocus
92
+ autoComplete='off'
93
+ hint='Letters, numbers and underscores only.'
94
+ />
95
+
96
+ <Input
97
+ label='Temporary password'
98
+ type='password'
99
+ value={password}
100
+ onChange={(event) => setPassword(event.target.value)}
101
+ autoComplete='new-password'
102
+ hint={`At least ${MIN_PASSWORD_LENGTH} characters. They must change it at first sign-in.`}
103
+ />
104
+
105
+ <div>
106
+ <label htmlFor='new-user-role' className='mb-1.5 block text-sm font-medium text-ink-700'>
107
+ Role
108
+ </label>
109
+ <select
110
+ id='new-user-role'
111
+ value={role || roles?.[0]?.roleName || ''}
112
+ onChange={(event) => setRole(event.target.value)}
113
+ className='w-full rounded-lg border border-ink-300 bg-white px-3 py-2 text-sm text-ink-900 shadow-sm transition-all focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/25'
114
+ >
115
+ {(roles ?? []).map((entry) => (
116
+ <option key={entry.roleName} value={entry.roleName}>{entry.roleName}</option>
117
+ ))}
118
+ </select>
119
+ <p className='mt-1.5 text-xs text-ink-500'>
120
+ Determines what this account can reach across the Dashboard, TCP and MCP surfaces.
121
+ </p>
122
+ </div>
123
+
124
+ {error && <Alert tone='danger'>{error}</Alert>}
125
+ <button type='submit' className='hidden' aria-hidden='true' tabIndex={-1} />
126
+ </form>
127
+ </Modal>
128
+ )
129
+ }
130
+
131
+ export default CreateUserModal
@@ -0,0 +1,132 @@
1
+ import React, { useState } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import authApi from "../../api/authApi";
4
+ import { useAuthStore } from "../../store/authStore";
5
+
6
+ const MIN_PASSWORD_LENGTH = 4;
7
+
8
+ const ForcePasswordChangeModal = ({ isOpen, onSuccess }) => {
9
+ const [currentPassword, setCurrentPassword] = useState("");
10
+ const [newPassword, setNewPassword] = useState("");
11
+ const [confirmPassword, setConfirmPassword] = useState("");
12
+ const [error, setError] = useState("");
13
+ const [isSubmitting, setIsSubmitting] = useState(false);
14
+
15
+ const setSession = useAuthStore((state) => state.setSession);
16
+
17
+ if (!isOpen) return null;
18
+
19
+ const handleSubmit = async (e) => {
20
+ e.preventDefault();
21
+
22
+ if (!currentPassword || !newPassword || !confirmPassword) {
23
+ setError("All fields are required.");
24
+ return;
25
+ }
26
+ if (newPassword.length < MIN_PASSWORD_LENGTH) {
27
+ setError(`New password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
28
+ return;
29
+ }
30
+ if (newPassword !== confirmPassword) {
31
+ setError("New password and confirmation do not match.");
32
+ return;
33
+ }
34
+
35
+ setIsSubmitting(true);
36
+ setError("");
37
+
38
+ try {
39
+ const response = await authApi.changePassword(currentPassword, newPassword);
40
+ const { username, role, permissions, mustChangePassword } = response.data.data;
41
+ setSession({ username, role, permissions, mustChangePassword });
42
+ onSuccess();
43
+ } catch (err) {
44
+ setError(err.response?.data?.message || "Failed to update password. Please try again.");
45
+ } finally {
46
+ setIsSubmitting(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <AnimatePresence>
52
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm">
53
+ <motion.div
54
+ initial={{ scale: 0.95, opacity: 0, y: 10 }}
55
+ animate={{ scale: 1, opacity: 1, y: 0 }}
56
+ exit={{ scale: 0.95, opacity: 0 }}
57
+ className="w-full max-w-md rounded-2xl bg-white border border-slate-200 p-6 shadow-2xl relative overflow-hidden"
58
+ >
59
+ <div className="flex items-center gap-3 mb-5">
60
+ <div className="h-10 w-10 rounded-xl bg-amber-50 text-amber-600 border border-amber-200 flex items-center justify-center">
61
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
62
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
63
+ </svg>
64
+ </div>
65
+ <div>
66
+ <h2 className="text-base font-bold text-slate-900">Password Change Required</h2>
67
+ <p className="text-xs text-slate-500">For security, you must set a new password to continue.</p>
68
+ </div>
69
+ </div>
70
+
71
+ <form onSubmit={handleSubmit} className="space-y-4">
72
+ <div>
73
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Current Password</label>
74
+ <input
75
+ type="password"
76
+ value={currentPassword}
77
+ onChange={(e) => setCurrentPassword(e.target.value)}
78
+ placeholder="Enter current password"
79
+ required
80
+ autoFocus
81
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/20"
82
+ />
83
+ </div>
84
+
85
+ <div>
86
+ <label className="block text-xs font-semibold text-slate-700 mb-1">New Password</label>
87
+ <input
88
+ type="password"
89
+ value={newPassword}
90
+ onChange={(e) => setNewPassword(e.target.value)}
91
+ placeholder="At least 4 characters"
92
+ required
93
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/20"
94
+ />
95
+ </div>
96
+
97
+ <div>
98
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Confirm New Password</label>
99
+ <input
100
+ type="password"
101
+ value={confirmPassword}
102
+ onChange={(e) => setConfirmPassword(e.target.value)}
103
+ placeholder="Re-enter new password"
104
+ required
105
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/20"
106
+ />
107
+ </div>
108
+
109
+ {error && (
110
+ <div className="p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-xs flex items-center gap-2">
111
+ <svg className="w-4 h-4 shrink-0 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
112
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
113
+ </svg>
114
+ <span>{error}</span>
115
+ </div>
116
+ )}
117
+
118
+ <button
119
+ type="submit"
120
+ disabled={isSubmitting}
121
+ className="w-full mt-2 py-2.5 rounded-xl bg-amber-600 hover:bg-amber-700 text-white font-semibold text-xs shadow-sm transition-all disabled:opacity-50 cursor-pointer"
122
+ >
123
+ {isSubmitting ? "Updating Password..." : "Update Password & Continue"}
124
+ </button>
125
+ </form>
126
+ </motion.div>
127
+ </div>
128
+ </AnimatePresence>
129
+ );
130
+ };
131
+
132
+ export default ForcePasswordChangeModal;
@@ -0,0 +1,42 @@
1
+ import { Navigate, useLocation } from 'react-router-dom'
2
+ import { useAuthStore } from '../../store/authStore'
3
+
4
+ const ForbiddenNotice = () => (
5
+ <div className='max-w-3xl mx-auto px-4 py-16 text-center'>
6
+ <h1 className='text-2xl font-bold text-ink-900 mb-2'>Access Denied</h1>
7
+ <p className='text-ink-600'>
8
+ Your role does not have permission to view this page.
9
+ </p>
10
+ </div>
11
+ )
12
+
13
+ const ProtectedRoute = ({ children, requiredPermission }) => {
14
+ const { isAuthenticated, isLoading, mustChangePassword, permissions } = useAuthStore(
15
+ (state) => state
16
+ )
17
+ const location = useLocation()
18
+
19
+ if (isLoading) {
20
+ return (
21
+ <div className='flex items-center justify-center min-h-[60vh]'>
22
+ <div className='animate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full' />
23
+ </div>
24
+ )
25
+ }
26
+
27
+ if (!isAuthenticated) {
28
+ return <Navigate to='/login' replace />
29
+ }
30
+
31
+ if (mustChangePassword && location.pathname !== '/force-password-change') {
32
+ return <Navigate to='/force-password-change' replace />
33
+ }
34
+
35
+ if (requiredPermission && !permissions.includes(requiredPermission)) {
36
+ return <ForbiddenNotice />
37
+ }
38
+
39
+ return children
40
+ }
41
+
42
+ export default ProtectedRoute
@@ -0,0 +1,90 @@
1
+ import { useState } from 'react'
2
+ import authApi from '../../api/authApi'
3
+ import Modal from '../ui/Modal'
4
+ import Button from '../ui/Button'
5
+ import { Input } from '../ui/Field'
6
+ import { Alert } from '../ui/Feedback'
7
+
8
+ const MIN_PASSWORD_LENGTH = 4
9
+
10
+ const ResetPasswordModal = ({ isOpen, username, onClose, onPasswordReset }) => {
11
+ const [newPassword, setNewPassword] = useState('')
12
+ const [isSubmitting, setIsSubmitting] = useState(false)
13
+ const [error, setError] = useState('')
14
+
15
+ const handleClose = () => {
16
+ setNewPassword('')
17
+ setError('')
18
+ setIsSubmitting(false)
19
+ onClose()
20
+ }
21
+
22
+ const handleSubmit = async (e) => {
23
+ e.preventDefault()
24
+
25
+ if (!newPassword || newPassword.length < MIN_PASSWORD_LENGTH) {
26
+ setError(`New password must be at least ${MIN_PASSWORD_LENGTH} characters`)
27
+ return
28
+ }
29
+
30
+ setIsSubmitting(true)
31
+ setError('')
32
+
33
+ try {
34
+ await authApi.resetUserPassword(username, newPassword)
35
+ onPasswordReset()
36
+ handleClose()
37
+ } catch (err) {
38
+ setError(err.response?.data?.message || 'Failed to reset password. Please try again.')
39
+ setIsSubmitting(false)
40
+ }
41
+ }
42
+
43
+ const KeyIcon = (
44
+ <svg className='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
45
+ <path strokeLinecap='round' strokeLinejoin='round' d='M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' />
46
+ </svg>
47
+ )
48
+
49
+ return (
50
+ <Modal
51
+ isOpen={isOpen}
52
+ onClose={handleClose}
53
+ title='Reset password'
54
+ subtitle={username}
55
+ icon={KeyIcon}
56
+ size='md'
57
+ footer={
58
+ <>
59
+ <Button variant='secondary' onClick={handleClose} disabled={isSubmitting}>
60
+ Cancel
61
+ </Button>
62
+ <Button onClick={handleSubmit} loading={isSubmitting} disabled={!newPassword}>
63
+ Reset password
64
+ </Button>
65
+ </>
66
+ }
67
+ >
68
+ <form onSubmit={handleSubmit}>
69
+ <Input
70
+ label='New password'
71
+ type='password'
72
+ value={newPassword}
73
+ onChange={(event) => setNewPassword(event.target.value)}
74
+ autoComplete='new-password'
75
+ autoFocus
76
+ error={error || undefined}
77
+ hint={`At least ${MIN_PASSWORD_LENGTH} characters.`}
78
+ />
79
+ <button type='submit' className='hidden' aria-hidden='true' tabIndex={-1} />
80
+ </form>
81
+
82
+ <Alert tone='warn' className='mt-4'>
83
+ Every active session for <strong>{username}</strong> is revoked immediately, and they
84
+ must change this password at next sign-in.
85
+ </Alert>
86
+ </Modal>
87
+ )
88
+ }
89
+
90
+ export default ResetPasswordModal
@@ -0,0 +1,103 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useAuthStore } from '../../store/authStore'
4
+ import authApi from '../../api/authApi'
5
+ import { Badge } from '../ui/Feedback'
6
+
7
+ /**
8
+ * Account menu in the header.
9
+ *
10
+ * Adds outside-click and Escape dismissal - previously the panel only closed on navigation
11
+ * or on a second click of the trigger, so it could sit open over the page indefinitely.
12
+ */
13
+ const UserAvatarMenu = ({ isOpen, onToggle, onClose }) => {
14
+ const navigate = useNavigate()
15
+ const { username, role, clearSession } = useAuthStore((state) => state)
16
+ const containerRef = useRef(null)
17
+
18
+ useEffect(() => {
19
+ if (!isOpen) return
20
+
21
+ const onPointerDown = (event) => {
22
+ if (!containerRef.current?.contains(event.target)) onClose()
23
+ }
24
+ const onKeyDown = (event) => {
25
+ if (event.key === 'Escape') onClose()
26
+ }
27
+
28
+ document.addEventListener('mousedown', onPointerDown)
29
+ document.addEventListener('keydown', onKeyDown)
30
+ return () => {
31
+ document.removeEventListener('mousedown', onPointerDown)
32
+ document.removeEventListener('keydown', onKeyDown)
33
+ }
34
+ }, [isOpen, onClose])
35
+
36
+ const handleLogout = async () => {
37
+ try {
38
+ await authApi.logout()
39
+ } finally {
40
+ clearSession()
41
+ onClose()
42
+ navigate('/login', { replace: true })
43
+ }
44
+ }
45
+
46
+ const handleChangePassword = () => {
47
+ onClose()
48
+ navigate('/force-password-change')
49
+ }
50
+
51
+ const itemClass =
52
+ 'flex w-full items-center gap-2.5 px-4 py-2.5 text-left text-sm transition-colors'
53
+
54
+ return (
55
+ <div className='relative' ref={containerRef}>
56
+ <button
57
+ onClick={onToggle}
58
+ aria-haspopup='menu'
59
+ aria-expanded={isOpen}
60
+ className='flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium text-ink-300 transition-colors hover:bg-white/10 hover:text-white'
61
+ >
62
+ <span className='flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-brand-400 to-brand-600 text-xs font-bold uppercase text-white'>
63
+ {username ? username.charAt(0) : '?'}
64
+ </span>
65
+ <span className='hidden sm:inline'>{username}</span>
66
+ <svg
67
+ className={`hidden h-3.5 w-3.5 transition-transform duration-200 sm:block ${isOpen ? 'rotate-180' : ''}`}
68
+ fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2.5} aria-hidden='true'
69
+ >
70
+ <path strokeLinecap='round' strokeLinejoin='round' d='M19 9l-7 7-7-7' />
71
+ </svg>
72
+ </button>
73
+
74
+ {isOpen && (
75
+ <div
76
+ role='menu'
77
+ className='animate-popIn absolute right-0 z-50 mt-2 w-60 overflow-hidden rounded-xl border border-ink-200 bg-white shadow-float'
78
+ >
79
+ <div className='border-b border-ink-100 px-4 py-3'>
80
+ <p className='truncate text-sm font-semibold text-ink-900'>{username}</p>
81
+ <Badge tone='brand' className='mt-1.5'>{role}</Badge>
82
+ </div>
83
+
84
+ <button onClick={handleChangePassword} role='menuitem' className={`${itemClass} text-ink-700 hover:bg-ink-50`}>
85
+ <svg className='h-4 w-4 text-ink-400' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2} aria-hidden='true'>
86
+ <path strokeLinecap='round' strokeLinejoin='round' d='M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' />
87
+ </svg>
88
+ Change password
89
+ </button>
90
+
91
+ <button onClick={handleLogout} role='menuitem' className={`${itemClass} border-t border-ink-100 text-danger-600 hover:bg-danger-50`}>
92
+ <svg className='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2} aria-hidden='true'>
93
+ <path strokeLinecap='round' strokeLinejoin='round' d='M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1' />
94
+ </svg>
95
+ Sign out
96
+ </button>
97
+ </div>
98
+ )}
99
+ </div>
100
+ )
101
+ }
102
+
103
+ export default UserAvatarMenu
@@ -0,0 +1,116 @@
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 { Input } from '../ui/Field'
6
+
7
+ const CreateCollectionModal = ({
8
+ isOpen,
9
+ onClose,
10
+ onCollectionCreated,
11
+ databaseName
12
+ }) => {
13
+ const [collectionName, setCollectionName] = useState('')
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
+ setCollectionName('')
20
+ setError('')
21
+ setIsSubmitting(false)
22
+ onClose()
23
+ }
24
+
25
+ const handleSubmit = async (e) => {
26
+ e.preventDefault()
27
+
28
+ // Validate input
29
+ if (!collectionName.trim()) {
30
+ setError('Collection name is required')
31
+ return
32
+ }
33
+
34
+ // Alphanumeric validation (plus underscores)
35
+ if (!/^[a-zA-Z0-9_]+$/.test(collectionName)) {
36
+ setError(
37
+ 'Collection name can only contain letters, numbers, and underscores'
38
+ )
39
+ return
40
+ }
41
+
42
+ setIsSubmitting(true)
43
+ setError('')
44
+
45
+ try {
46
+ const response = await apiClient.post('/api/collection/create-collection', {
47
+ dbName: databaseName,
48
+ collectionName
49
+ })
50
+
51
+ if (
52
+ response.data.statusCode === 200 ||
53
+ response.data.statusCode === 201
54
+ ) {
55
+ onCollectionCreated(collectionName)
56
+ handleClose()
57
+ } else {
58
+ throw new Error('Failed to create collection')
59
+ }
60
+ } catch (error) {
61
+ console.error('Error creating collection:', error)
62
+ setError(
63
+ error.response?.data?.message ||
64
+ 'Failed to create collection. Please try again.'
65
+ )
66
+ setIsSubmitting(false)
67
+ }
68
+ }
69
+
70
+ const FolderIcon = (
71
+ <svg className='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
72
+ <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' />
73
+ </svg>
74
+ )
75
+
76
+ return (
77
+ <Modal
78
+ isOpen={isOpen}
79
+ onClose={handleClose}
80
+ title='Create collection'
81
+ subtitle={databaseName}
82
+ icon={FolderIcon}
83
+ size='md'
84
+ footer={
85
+ <>
86
+ <Button variant='secondary' onClick={handleClose} disabled={isSubmitting}>
87
+ Cancel
88
+ </Button>
89
+ <Button onClick={handleSubmit} loading={isSubmitting} disabled={!collectionName.trim()}>
90
+ Create collection
91
+ </Button>
92
+ </>
93
+ }
94
+ >
95
+ <form onSubmit={handleSubmit} className='space-y-4'>
96
+ <Input label='Database' value={databaseName} disabled mono className='opacity-70' />
97
+
98
+ <Input
99
+ label='Collection name'
100
+ value={collectionName}
101
+ onChange={(event) => setCollectionName(event.target.value)}
102
+ placeholder='users'
103
+ mono
104
+ autoFocus
105
+ error={error || undefined}
106
+ hint='Letters, numbers and underscores only. AxioDB is schema-less, so there is nothing else to define.'
107
+ />
108
+
109
+ {/* Lets Enter submit the form without rendering a second visible button. */}
110
+ <button type='submit' className='hidden' aria-hidden='true' tabIndex={-1} />
111
+ </form>
112
+ </Modal>
113
+ )
114
+ }
115
+
116
+ export default CreateCollectionModal