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.
- package/package.json +2 -1
- package/src/components/forms/FormPage.vue +1 -1
- package/src/components/layout/AppLayout.vue +13 -1
- package/src/components/layout/Zone.vue +40 -23
- package/src/composables/index.js +1 -0
- package/src/composables/useAuth.js +44 -4
- package/src/composables/useCurrentEntity.js +44 -0
- package/src/composables/useFormPageBuilder.js +3 -3
- package/src/composables/useNavContext.js +24 -8
- package/src/debug/AuthCollector.js +340 -0
- package/src/debug/Collector.js +235 -0
- package/src/debug/DebugBridge.js +163 -0
- package/src/debug/DebugModule.js +215 -0
- package/src/debug/EntitiesCollector.js +403 -0
- package/src/debug/ErrorCollector.js +66 -0
- package/src/debug/LocalStorageAdapter.js +150 -0
- package/src/debug/SignalCollector.js +87 -0
- package/src/debug/ToastCollector.js +82 -0
- package/src/debug/ZonesCollector.js +300 -0
- package/src/debug/components/DebugBar.vue +1232 -0
- package/src/debug/components/ObjectTree.vue +194 -0
- package/src/debug/components/index.js +8 -0
- package/src/debug/components/panels/AuthPanel.vue +174 -0
- package/src/debug/components/panels/EntitiesPanel.vue +712 -0
- package/src/debug/components/panels/EntriesPanel.vue +188 -0
- package/src/debug/components/panels/ToastsPanel.vue +112 -0
- package/src/debug/components/panels/ZonesPanel.vue +232 -0
- package/src/debug/components/panels/index.js +8 -0
- package/src/debug/index.js +31 -0
- package/src/entity/EntityManager.js +142 -20
- package/src/entity/auth/CompositeAuthAdapter.js +212 -0
- package/src/entity/auth/factory.js +207 -0
- package/src/entity/auth/factory.test.js +257 -0
- package/src/entity/auth/index.js +14 -0
- package/src/entity/storage/MockApiStorage.js +51 -2
- package/src/entity/storage/index.js +9 -2
- package/src/index.js +7 -0
- package/src/kernel/Kernel.js +468 -48
- package/src/kernel/KernelContext.js +385 -0
- package/src/kernel/Module.js +111 -0
- package/src/kernel/ModuleLoader.js +573 -0
- package/src/kernel/SignalBus.js +2 -7
- package/src/kernel/index.js +14 -0
- package/src/toast/ToastBridgeModule.js +70 -0
- package/src/toast/ToastListener.vue +47 -0
- package/src/toast/index.js +15 -0
- 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,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>
|