qdadm 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +2 -1
  2. package/src/components/forms/FormPage.vue +1 -1
  3. package/src/components/layout/AppLayout.vue +13 -1
  4. package/src/components/layout/Zone.vue +40 -23
  5. package/src/composables/index.js +1 -0
  6. package/src/composables/useAuth.js +44 -4
  7. package/src/composables/useCurrentEntity.js +44 -0
  8. package/src/composables/useFormPageBuilder.js +3 -3
  9. package/src/composables/useNavContext.js +24 -8
  10. package/src/debug/AuthCollector.js +340 -0
  11. package/src/debug/Collector.js +235 -0
  12. package/src/debug/DebugBridge.js +163 -0
  13. package/src/debug/DebugModule.js +215 -0
  14. package/src/debug/EntitiesCollector.js +403 -0
  15. package/src/debug/ErrorCollector.js +66 -0
  16. package/src/debug/LocalStorageAdapter.js +150 -0
  17. package/src/debug/SignalCollector.js +87 -0
  18. package/src/debug/ToastCollector.js +82 -0
  19. package/src/debug/ZonesCollector.js +300 -0
  20. package/src/debug/components/DebugBar.vue +1232 -0
  21. package/src/debug/components/ObjectTree.vue +194 -0
  22. package/src/debug/components/index.js +8 -0
  23. package/src/debug/components/panels/AuthPanel.vue +174 -0
  24. package/src/debug/components/panels/EntitiesPanel.vue +712 -0
  25. package/src/debug/components/panels/EntriesPanel.vue +188 -0
  26. package/src/debug/components/panels/ToastsPanel.vue +112 -0
  27. package/src/debug/components/panels/ZonesPanel.vue +232 -0
  28. package/src/debug/components/panels/index.js +8 -0
  29. package/src/debug/index.js +31 -0
  30. package/src/entity/EntityManager.js +142 -20
  31. package/src/entity/auth/CompositeAuthAdapter.js +212 -0
  32. package/src/entity/auth/factory.js +207 -0
  33. package/src/entity/auth/factory.test.js +257 -0
  34. package/src/entity/auth/index.js +14 -0
  35. package/src/entity/storage/MockApiStorage.js +51 -2
  36. package/src/entity/storage/index.js +9 -2
  37. package/src/index.js +7 -0
  38. package/src/kernel/Kernel.js +468 -48
  39. package/src/kernel/KernelContext.js +385 -0
  40. package/src/kernel/Module.js +111 -0
  41. package/src/kernel/ModuleLoader.js +573 -0
  42. package/src/kernel/SignalBus.js +2 -7
  43. package/src/kernel/index.js +14 -0
  44. package/src/toast/ToastBridgeModule.js +70 -0
  45. package/src/toast/ToastListener.vue +47 -0
  46. package/src/toast/index.js +15 -0
  47. package/src/toast/useSignalToast.js +113 -0
@@ -0,0 +1,194 @@
1
+ <script setup>
2
+ /**
3
+ * ObjectTree - VSCode-style collapsible object explorer
4
+ *
5
+ * Renders objects/arrays as a tree with collapsible nodes.
6
+ * Primitive values are shown inline, objects/arrays are expandable.
7
+ */
8
+ import { ref, computed } from 'vue'
9
+
10
+ const props = defineProps({
11
+ data: { type: [Object, Array, String, Number, Boolean, null], default: null },
12
+ depth: { type: Number, default: 0 },
13
+ maxDepth: { type: Number, default: 10 },
14
+ name: { type: String, default: null },
15
+ defaultExpanded: { type: Boolean, default: false }
16
+ })
17
+
18
+ const expanded = ref(props.defaultExpanded && props.depth < 2)
19
+
20
+ const isObject = computed(() => props.data !== null && typeof props.data === 'object')
21
+ const isArray = computed(() => Array.isArray(props.data))
22
+ const isEmpty = computed(() => {
23
+ if (!isObject.value) return false
24
+ return isArray.value ? props.data.length === 0 : Object.keys(props.data).length === 0
25
+ })
26
+
27
+ const keys = computed(() => {
28
+ if (!isObject.value) return []
29
+ return Object.keys(props.data).slice(0, 100)
30
+ })
31
+
32
+ const preview = computed(() => {
33
+ if (!isObject.value) return ''
34
+ if (isArray.value) {
35
+ return `Array(${props.data.length})`
36
+ }
37
+ const keyCount = Object.keys(props.data).length
38
+ const firstKeys = Object.keys(props.data).slice(0, 3).join(', ')
39
+ return keyCount <= 3 ? `{${firstKeys}}` : `{${firstKeys}, ...}`
40
+ })
41
+
42
+ const valueClass = computed(() => {
43
+ if (props.data === null) return 'obj-null'
44
+ if (props.data === undefined) return 'obj-undefined'
45
+ switch (typeof props.data) {
46
+ case 'string': return 'obj-string'
47
+ case 'number': return 'obj-number'
48
+ case 'boolean': return 'obj-boolean'
49
+ case 'function': return 'obj-function'
50
+ default: return ''
51
+ }
52
+ })
53
+
54
+ function formatValue(val) {
55
+ if (val === null) return 'null'
56
+ if (val === undefined) return 'undefined'
57
+ if (typeof val === 'string') {
58
+ const truncated = val.length > 100 ? val.slice(0, 100) + '...' : val
59
+ return `"${truncated}"`
60
+ }
61
+ if (typeof val === 'function') return 'ƒ()'
62
+ return String(val)
63
+ }
64
+
65
+ function toggleExpand() {
66
+ if (isObject.value && !isEmpty.value) {
67
+ expanded.value = !expanded.value
68
+ }
69
+ }
70
+ </script>
71
+
72
+ <template>
73
+ <div class="obj-node" :style="{ paddingLeft: depth > 0 ? '12px' : '0' }">
74
+ <!-- Expandable object/array -->
75
+ <template v-if="isObject && !isEmpty">
76
+ <div class="obj-line" @click="toggleExpand">
77
+ <span class="obj-toggle" :class="{ 'obj-expanded': expanded }">▶</span>
78
+ <span v-if="name !== null" class="obj-key">{{ name }}:</span>
79
+ <span class="obj-preview">{{ preview }}</span>
80
+ </div>
81
+ <div v-if="expanded && depth < maxDepth" class="obj-children">
82
+ <ObjectTree
83
+ v-for="key in keys"
84
+ :key="key"
85
+ :data="data[key]"
86
+ :name="key"
87
+ :depth="depth + 1"
88
+ :maxDepth="maxDepth"
89
+ />
90
+ <div v-if="Object.keys(data).length > 100" class="obj-truncated">
91
+ ... {{ Object.keys(data).length - 100 }} more
92
+ </div>
93
+ </div>
94
+ <div v-else-if="expanded" class="obj-max-depth">[Max depth]</div>
95
+ </template>
96
+
97
+ <!-- Empty object/array -->
98
+ <template v-else-if="isObject && isEmpty">
99
+ <div class="obj-line">
100
+ <span v-if="name !== null" class="obj-key">{{ name }}:</span>
101
+ <span class="obj-empty">{{ isArray ? '[]' : '{}' }}</span>
102
+ </div>
103
+ </template>
104
+
105
+ <!-- Primitive value -->
106
+ <template v-else>
107
+ <div class="obj-line">
108
+ <span v-if="name !== null" class="obj-key">{{ name }}:</span>
109
+ <span :class="valueClass">{{ formatValue(data) }}</span>
110
+ </div>
111
+ </template>
112
+ </div>
113
+ </template>
114
+
115
+ <style scoped>
116
+ .obj-node {
117
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
118
+ font-size: 12px;
119
+ line-height: 1.4;
120
+ }
121
+
122
+ .obj-line {
123
+ display: flex;
124
+ align-items: flex-start;
125
+ gap: 4px;
126
+ padding: 1px 0;
127
+ cursor: default;
128
+ white-space: nowrap;
129
+ }
130
+
131
+ .obj-line:hover {
132
+ background: rgba(255, 255, 255, 0.05);
133
+ }
134
+
135
+ .obj-toggle {
136
+ color: #71717a;
137
+ font-size: 10px;
138
+ width: 12px;
139
+ flex-shrink: 0;
140
+ cursor: pointer;
141
+ transition: transform 0.1s;
142
+ display: inline-block;
143
+ }
144
+
145
+ .obj-toggle.obj-expanded {
146
+ transform: rotate(90deg);
147
+ }
148
+
149
+ .obj-key {
150
+ color: #a78bfa;
151
+ }
152
+
153
+ .obj-preview {
154
+ color: #71717a;
155
+ font-style: italic;
156
+ }
157
+
158
+ .obj-empty {
159
+ color: #71717a;
160
+ }
161
+
162
+ .obj-children {
163
+ border-left: 1px solid #3f3f46;
164
+ margin-left: 5px;
165
+ }
166
+
167
+ .obj-string {
168
+ color: #fbbf24;
169
+ }
170
+
171
+ .obj-number {
172
+ color: #34d399;
173
+ }
174
+
175
+ .obj-boolean {
176
+ color: #60a5fa;
177
+ }
178
+
179
+ .obj-null, .obj-undefined {
180
+ color: #71717a;
181
+ font-style: italic;
182
+ }
183
+
184
+ .obj-function {
185
+ color: #c084fc;
186
+ font-style: italic;
187
+ }
188
+
189
+ .obj-truncated, .obj-max-depth {
190
+ color: #71717a;
191
+ font-style: italic;
192
+ padding-left: 16px;
193
+ }
194
+ </style>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Debug Components Exports
3
+ *
4
+ * Vue components for the debug panel UI.
5
+ */
6
+
7
+ export { default as DebugBar } from './DebugBar.vue'
8
+ export { default as ObjectTree } from './ObjectTree.vue'
@@ -0,0 +1,174 @@
1
+ <script setup>
2
+ /**
3
+ * AuthPanel - Auth collector display with activity indicator
4
+ */
5
+ import { onMounted, computed } from 'vue'
6
+ import ObjectTree from '../ObjectTree.vue'
7
+
8
+ const props = defineProps({
9
+ collector: { type: Object, required: true },
10
+ entries: { type: Array, required: true }
11
+ })
12
+
13
+ // Mark events as seen when panel is viewed (badge resets but events stay visible)
14
+ onMounted(() => {
15
+ props.collector.markSeen?.()
16
+ })
17
+
18
+ // Get all recent events (stacked display)
19
+ const recentEvents = computed(() => props.collector.getRecentEvents?.() || [])
20
+
21
+ function getIcon(type) {
22
+ const icons = {
23
+ user: 'pi-user',
24
+ token: 'pi-key',
25
+ permissions: 'pi-shield',
26
+ adapter: 'pi-cog'
27
+ }
28
+ return icons[type] || 'pi-info-circle'
29
+ }
30
+
31
+ function formatTime(date) {
32
+ return date.toLocaleTimeString('en-US', { hour12: false })
33
+ }
34
+
35
+ function getEventIcon(type) {
36
+ const icons = {
37
+ login: 'pi-sign-in',
38
+ logout: 'pi-sign-out',
39
+ impersonate: 'pi-user-edit',
40
+ 'impersonate-stop': 'pi-user'
41
+ }
42
+ return icons[type] || 'pi-info-circle'
43
+ }
44
+
45
+ function getEventLabel(event) {
46
+ if (event.type === 'login' && event.data) {
47
+ const user = event.data.user
48
+ const username = user?.username || user?.name || user?.email
49
+ if (username) {
50
+ return `User ${username} logged in`
51
+ }
52
+ }
53
+ if (event.type === 'impersonate' && event.data) {
54
+ // Payload structure: { target: { username }, original: { username } }
55
+ // Or signal object: { data: { target: { username } } }
56
+ const data = event.data.data || event.data
57
+ const username = data.target?.username
58
+ || data.username
59
+ || (typeof data === 'string' ? data : null)
60
+ if (username) {
61
+ return `Impersonating user ${username}`
62
+ }
63
+ }
64
+ if (event.type === 'impersonate-stop') {
65
+ const data = event.data?.data || event.data
66
+ const username = data?.original?.username
67
+ if (username) {
68
+ return `Back to ${username}`
69
+ }
70
+ }
71
+ const labels = {
72
+ login: 'User logged in',
73
+ logout: 'User logged out',
74
+ impersonate: 'Impersonating user',
75
+ 'impersonate-stop': 'Stopped impersonation'
76
+ }
77
+ return labels[event.type] || event.type
78
+ }
79
+ </script>
80
+
81
+ <template>
82
+ <div class="auth-panel">
83
+ <!-- Invariant entries (user, token, permissions, adapter) -->
84
+ <div v-for="(entry, idx) in entries" :key="idx" class="auth-item">
85
+ <div class="auth-header">
86
+ <i :class="['pi', getIcon(entry.type)]" />
87
+ <span class="auth-label">{{ entry.label || entry.type }}</span>
88
+ </div>
89
+ <div v-if="entry.message" class="auth-message">{{ entry.message }}</div>
90
+ <ObjectTree v-else-if="entry.data" :data="entry.data" :maxDepth="4" />
91
+ </div>
92
+
93
+ <!-- Recent auth events (stacked below, newest first) -->
94
+ <div
95
+ v-for="event in recentEvents"
96
+ :key="event.id"
97
+ class="auth-activity"
98
+ :class="event.type"
99
+ >
100
+ <i :class="['pi', getEventIcon(event.type)]" />
101
+ <span>{{ getEventLabel(event) }}</span>
102
+ <span class="auth-time">{{ formatTime(event.timestamp) }}</span>
103
+ </div>
104
+ </div>
105
+ </template>
106
+
107
+ <style scoped>
108
+ .auth-panel {
109
+ padding: 8px;
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 8px;
113
+ }
114
+ /* Activity indicator */
115
+ .auth-activity {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ padding: 8px 12px;
120
+ border-radius: 4px;
121
+ font-size: 12px;
122
+ font-weight: 600;
123
+ animation: pulse 2s ease-in-out infinite;
124
+ }
125
+ .auth-activity.login {
126
+ background: linear-gradient(90deg, rgba(34, 197, 94, 0.2) 0%, rgba(34, 197, 94, 0.05) 100%);
127
+ border-left: 3px solid #22c55e;
128
+ color: #22c55e;
129
+ }
130
+ .auth-activity.logout {
131
+ background: linear-gradient(90deg, rgba(239, 68, 68, 0.2) 0%, rgba(239, 68, 68, 0.05) 100%);
132
+ border-left: 3px solid #ef4444;
133
+ color: #ef4444;
134
+ }
135
+ .auth-activity.impersonate {
136
+ background: linear-gradient(90deg, rgba(249, 115, 22, 0.2) 0%, rgba(249, 115, 22, 0.05) 100%);
137
+ border-left: 3px solid #f97316;
138
+ color: #f97316;
139
+ }
140
+ .auth-activity.impersonate-stop {
141
+ background: linear-gradient(90deg, rgba(161, 161, 170, 0.2) 0%, rgba(161, 161, 170, 0.05) 100%);
142
+ border-left: 3px solid #a1a1aa;
143
+ color: #a1a1aa;
144
+ }
145
+ .auth-time {
146
+ margin-left: auto;
147
+ font-size: 10px;
148
+ opacity: 0.6;
149
+ font-weight: 400;
150
+ }
151
+ @keyframes pulse {
152
+ 0%, 100% { opacity: 1; }
153
+ 50% { opacity: 0.7; }
154
+ }
155
+ .auth-item {
156
+ background: #27272a;
157
+ border-radius: 4px;
158
+ padding: 8px;
159
+ }
160
+ .auth-header {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 6px;
164
+ margin-bottom: 6px;
165
+ color: #10b981;
166
+ }
167
+ .auth-label {
168
+ font-weight: 600;
169
+ }
170
+ .auth-message {
171
+ color: #71717a;
172
+ font-size: 12px;
173
+ }
174
+ </style>