@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,572 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * EntitiesPanel - Entities collector display with expandable details
4
+ */
5
+ import { ref, onMounted } from 'vue'
6
+ import { ObjectTree } from '@quazardous/qddebug'
7
+
8
+ interface StorageCapabilities {
9
+ supportsTotal?: boolean
10
+ supportsFilters?: boolean
11
+ supportsPagination?: boolean
12
+ supportsCaching?: boolean
13
+ requiresAuth?: boolean
14
+ [key: string]: boolean | undefined
15
+ }
16
+
17
+ interface StorageInfo {
18
+ type: string
19
+ endpoint?: string
20
+ capabilities?: StorageCapabilities
21
+ hasNormalize?: boolean
22
+ hasDenormalize?: boolean
23
+ }
24
+
25
+ interface MultiStorage {
26
+ enabled: boolean
27
+ storages: Array<{
28
+ name: string
29
+ type: string
30
+ endpoint?: string
31
+ capabilities?: StorageCapabilities
32
+ hasNormalize?: boolean
33
+ hasDenormalize?: boolean
34
+ }>
35
+ }
36
+
37
+ interface DetailCacheInfo {
38
+ enabled: boolean
39
+ ttlMs: number
40
+ size: number
41
+ maxSize: number
42
+ }
43
+
44
+ interface CacheInfo {
45
+ enabled: boolean
46
+ valid?: boolean
47
+ itemCount?: number
48
+ total?: number
49
+ threshold?: number
50
+ items?: Record<string, unknown>[]
51
+ /** TTL in milliseconds (0=disabled, -1=infinite, >0=TTL) */
52
+ ttlMs?: number
53
+ /** Timestamp when cache expires (null if no TTL) */
54
+ expiresAt?: number | null
55
+ /** Whether the cache has expired based on TTL */
56
+ expired?: boolean
57
+ /** Whether the entity uses asymmetric mode */
58
+ asymmetric?: boolean
59
+ /** Detail cache info */
60
+ detailCache?: DetailCacheInfo | null
61
+ }
62
+
63
+ interface RelationInfo {
64
+ key: string
65
+ entity: string
66
+ }
67
+
68
+ interface ReferenceInfo {
69
+ field: string
70
+ entity: string
71
+ }
72
+
73
+ interface EntityPermissions {
74
+ canCreate: boolean
75
+ canUpdate: boolean
76
+ canDelete: boolean
77
+ readOnly?: boolean
78
+ }
79
+
80
+ interface EntityStats {
81
+ list: number
82
+ get: number
83
+ create: number
84
+ update: number
85
+ delete: number
86
+ cacheHits: number
87
+ cacheMisses: number
88
+ detailCacheHits: number
89
+ detailCacheMisses: number
90
+ maxItemsSeen: number
91
+ maxTotal: number
92
+ }
93
+
94
+ interface EntityEntry {
95
+ type?: string
96
+ message?: string
97
+ name: string
98
+ label?: string
99
+ hasActivity?: boolean
100
+ system?: boolean
101
+ authSensitive?: boolean
102
+ permissions: EntityPermissions
103
+ storage: StorageInfo
104
+ multiStorage?: MultiStorage
105
+ cache: CacheInfo
106
+ fields: {
107
+ count: number
108
+ required: string[]
109
+ }
110
+ relations: {
111
+ parents: RelationInfo[]
112
+ children: RelationInfo[]
113
+ references?: ReferenceInfo[]
114
+ }
115
+ stats?: EntityStats
116
+ [key: string]: unknown
117
+ }
118
+
119
+ interface StorageTestResult {
120
+ success: boolean
121
+ count?: number
122
+ error?: string
123
+ status?: number | string
124
+ }
125
+
126
+ interface EntitiesCollector {
127
+ markSeen?: () => void
128
+ refreshCache: (entityName: string, reload: boolean) => Promise<void> | void
129
+ testStorageFetch: (entityName: string, storageName: string) => Promise<StorageTestResult>
130
+ [key: string]: unknown
131
+ }
132
+
133
+ const props = defineProps<{
134
+ collector: EntitiesCollector
135
+ entries: EntityEntry[]
136
+ }>()
137
+
138
+ const emit = defineEmits<{
139
+ (e: 'update'): void
140
+ }>()
141
+
142
+ // Mark all entities as seen when panel is viewed
143
+ onMounted(() => {
144
+ props.collector.markSeen?.()
145
+ })
146
+
147
+ const expandedEntities = ref<Set<string>>(new Set())
148
+ const loadingCache = ref<Set<string>>(new Set())
149
+ const testingStorages = ref<Set<string>>(new Set()) // "entityName:storageName"
150
+ const storageTestResults = ref<Map<string, StorageTestResult>>(new Map()) // "entityName:storageName" -> { success, count, error, status }
151
+
152
+ function toggleExpand(name: string): void {
153
+ if (expandedEntities.value.has(name)) {
154
+ expandedEntities.value.delete(name)
155
+ } else {
156
+ expandedEntities.value.add(name)
157
+ }
158
+ // Trigger reactivity
159
+ expandedEntities.value = new Set(expandedEntities.value)
160
+ }
161
+
162
+ function isExpanded(name: string): boolean {
163
+ return expandedEntities.value.has(name)
164
+ }
165
+
166
+ async function loadCache(entityName: string): Promise<void> {
167
+ if (loadingCache.value.has(entityName)) return
168
+ loadingCache.value.add(entityName)
169
+ loadingCache.value = new Set(loadingCache.value)
170
+ try {
171
+ await props.collector.refreshCache(entityName, true) // reload=true
172
+ emit('update')
173
+ } finally {
174
+ loadingCache.value.delete(entityName)
175
+ loadingCache.value = new Set(loadingCache.value)
176
+ }
177
+ }
178
+
179
+ function invalidateCache(entityName: string): void {
180
+ props.collector.refreshCache(entityName, false) // just invalidate
181
+ emit('update')
182
+ }
183
+
184
+ function isLoading(name: string): boolean {
185
+ return loadingCache.value.has(name)
186
+ }
187
+
188
+ function isTestingStorage(entityName: string, storageName: string): boolean {
189
+ return testingStorages.value.has(`${entityName}:${storageName}`)
190
+ }
191
+
192
+ function getStorageTestResult(entityName: string, storageName: string): StorageTestResult | undefined {
193
+ return storageTestResults.value.get(`${entityName}:${storageName}`)
194
+ }
195
+
196
+ async function testStorageFetch(entityName: string, storageName: string): Promise<void> {
197
+ const key = `${entityName}:${storageName}`
198
+ if (testingStorages.value.has(key)) return
199
+ testingStorages.value.add(key)
200
+ testingStorages.value = new Set(testingStorages.value)
201
+ storageTestResults.value.delete(key)
202
+ storageTestResults.value = new Map(storageTestResults.value)
203
+ try {
204
+ const result = await props.collector.testStorageFetch(entityName, storageName)
205
+ storageTestResults.value.set(key, result)
206
+ storageTestResults.value = new Map(storageTestResults.value)
207
+ } finally {
208
+ testingStorages.value.delete(key)
209
+ testingStorages.value = new Set(testingStorages.value)
210
+ }
211
+ }
212
+
213
+ function getCapabilityIcon(cap: PropertyKey): string {
214
+ const icons: Record<string, string> = {
215
+ supportsTotal: 'pi-hashtag',
216
+ supportsFilters: 'pi-filter',
217
+ supportsPagination: 'pi-ellipsis-h',
218
+ supportsCaching: 'pi-database',
219
+ requiresAuth: 'pi-lock'
220
+ }
221
+ return icons[cap as string] || 'pi-question-circle'
222
+ }
223
+
224
+ function getCapabilityLabel(cap: PropertyKey): string {
225
+ const labels: Record<string, string> = {
226
+ supportsTotal: 'Total count',
227
+ supportsFilters: 'Filters',
228
+ supportsPagination: 'Pagination',
229
+ supportsCaching: 'Caching',
230
+ requiresAuth: 'Requires authentication'
231
+ }
232
+ return labels[cap as string] || String(cap)
233
+ }
234
+
235
+ function formatTtl(ttlMs: number | undefined): string {
236
+ if (ttlMs === undefined) return '?'
237
+ if (ttlMs === -1) return '∞'
238
+ if (ttlMs === 0) return 'off'
239
+ if (ttlMs < 1000) return `${ttlMs}ms`
240
+ if (ttlMs < 60000) return `${Math.round(ttlMs / 1000)}s`
241
+ if (ttlMs < 3600000) return `${Math.round(ttlMs / 60000)}min`
242
+ return `${Math.round(ttlMs / 3600000)}h`
243
+ }
244
+
245
+ function formatExpiresIn(expiresAt: number | null | undefined): string {
246
+ if (!expiresAt) return ''
247
+ const diff = expiresAt - Date.now()
248
+ if (diff <= 0) return 'expired'
249
+ if (diff < 1000) return `${diff}ms`
250
+ if (diff < 60000) return `${Math.round(diff / 1000)}s`
251
+ if (diff < 3600000) return `${Math.round(diff / 60000)}min`
252
+ return `${Math.round(diff / 3600000)}h`
253
+ }
254
+ </script>
255
+
256
+ <template>
257
+ <div class="entities-panel">
258
+ <div v-if="entries[0]?.type === 'status'" class="entities-status">
259
+ {{ entries[0].message }}
260
+ </div>
261
+ <div v-else v-for="entity in entries" :key="entity.name" class="entity-item" :class="{ 'entity-active': entity.hasActivity, 'entity-system': entity.system }">
262
+ <div class="entity-header" @click="toggleExpand(entity.name)">
263
+ <button class="entity-expand">
264
+ <i :class="['pi', isExpanded(entity.name) ? 'pi-chevron-down' : 'pi-chevron-right']" />
265
+ </button>
266
+ <i class="pi pi-database" />
267
+ <span class="entity-name">{{ entity.name }}</span>
268
+ <div class="entity-perms-icons">
269
+ <i
270
+ class="pi pi-plus"
271
+ :class="entity.permissions.canCreate ? 'perm-icon-granted' : 'perm-icon-denied'"
272
+ :title="entity.permissions.canCreate ? 'Create allowed' : 'Create denied'"
273
+ />
274
+ <i
275
+ class="pi pi-pencil"
276
+ :class="entity.permissions.canUpdate ? 'perm-icon-granted' : 'perm-icon-denied'"
277
+ :title="entity.permissions.canUpdate ? 'Update allowed' : 'Update denied'"
278
+ />
279
+ <i
280
+ class="pi pi-trash"
281
+ :class="entity.permissions.canDelete ? 'perm-icon-granted' : 'perm-icon-denied'"
282
+ :title="entity.permissions.canDelete ? 'Delete allowed' : 'Delete denied'"
283
+ />
284
+ <i
285
+ v-if="entity.permissions.readOnly"
286
+ class="pi pi-eye perm-icon-readonly"
287
+ title="Read-only entity"
288
+ />
289
+ <i
290
+ v-if="entity.authSensitive"
291
+ class="pi pi-shield perm-icon-auth-sensitive"
292
+ title="Auth-sensitive"
293
+ />
294
+ <i
295
+ v-if="entity.cache.asymmetric"
296
+ class="pi pi-arrows-v perm-icon-asymmetric"
297
+ title="Asymmetric: list and detail return different structures"
298
+ />
299
+ </div>
300
+ <span class="entity-label">{{ entity.label }}</span>
301
+ <span v-if="entity.cache.enabled" class="entity-cache" :class="{ 'entity-cache-valid': entity.cache.valid }">
302
+ <i :class="['pi', entity.cache.valid ? 'pi-check-circle' : 'pi-hourglass']" />
303
+ <template v-if="entity.cache.valid">{{ entity.cache.itemCount }}/{{ entity.cache.total }}</template>
304
+ <template v-else>pending</template>
305
+ </span>
306
+ <span v-else class="entity-cache entity-cache-disabled">
307
+ <i class="pi pi-ban" />
308
+ </span>
309
+ </div>
310
+
311
+ <!-- Collapsed summary -->
312
+ <div v-if="!isExpanded(entity.name)" class="entity-summary">
313
+ <span class="entity-storage">{{ entity.storage.type }}</span>
314
+ <span v-if="entity.storage.endpoint" class="entity-endpoint">{{ entity.storage.endpoint }}</span>
315
+ <span class="entity-fields">{{ entity.fields.count }} fields</span>
316
+ </div>
317
+
318
+ <!-- Expanded details -->
319
+ <div v-else class="entity-details">
320
+ <!-- Storages section -->
321
+ <div class="entity-storages-section">
322
+ <div class="entity-storages-header">
323
+ <i class="pi pi-database" />
324
+ <span>Storages</span>
325
+ <span v-if="entity.multiStorage?.enabled" class="entity-multi-badge" title="Multi-storage routing">
326
+ <i class="pi pi-sitemap" />
327
+ {{ 1 + entity.multiStorage.storages.length }}
328
+ </span>
329
+ </div>
330
+
331
+ <!-- Primary storage -->
332
+ <div class="entity-storage-card">
333
+ <div class="entity-storage-header">
334
+ <span class="entity-storage-name">storage</span>
335
+ <span class="entity-storage-type">{{ entity.storage.type }}</span>
336
+ <span v-if="entity.storage.hasNormalize || entity.storage.hasDenormalize" class="entity-normalize-badge" title="Has normalize/denormalize">
337
+ <i class="pi pi-arrows-h" />
338
+ </span>
339
+ <button
340
+ class="entity-storage-fetch-btn"
341
+ :disabled="isTestingStorage(entity.name, 'storage')"
342
+ @click.stop="testStorageFetch(entity.name, 'storage')"
343
+ title="Test fetch on this storage"
344
+ >
345
+ <i :class="['pi', isTestingStorage(entity.name, 'storage') ? 'pi-spin pi-spinner' : 'pi-download']" />
346
+ </button>
347
+ </div>
348
+ <div v-if="entity.storage.endpoint" class="entity-storage-endpoint">
349
+ {{ entity.storage.endpoint }}
350
+ </div>
351
+ <div class="entity-storage-row">
352
+ <div v-if="entity.storage.capabilities && Object.keys(entity.storage.capabilities).length > 0" class="entity-storage-caps">
353
+ <span
354
+ v-for="(enabled, cap) in entity.storage.capabilities"
355
+ :key="cap"
356
+ class="entity-cap"
357
+ :class="[cap === 'requiresAuth' && enabled ? 'entity-cap-auth' : (enabled ? 'entity-cap-enabled' : 'entity-cap-disabled')]"
358
+ :title="getCapabilityLabel(cap) + (enabled ? ' ✓' : ' ✗')"
359
+ >
360
+ <i :class="['pi', getCapabilityIcon(cap)]" />
361
+ </span>
362
+ </div>
363
+ <span v-if="getStorageTestResult(entity.name, 'storage')" class="entity-storage-test-result" :class="getStorageTestResult(entity.name, 'storage')?.success ? 'test-success' : 'test-error'">
364
+ <template v-if="getStorageTestResult(entity.name, 'storage')?.success">
365
+ <i class="pi pi-check-circle" /> {{ getStorageTestResult(entity.name, 'storage')?.count }}
366
+ </template>
367
+ <template v-else>
368
+ <i class="pi pi-times-circle" /> {{ getStorageTestResult(entity.name, 'storage')?.status || 'ERR' }}
369
+ </template>
370
+ </span>
371
+ </div>
372
+ </div>
373
+
374
+ <!-- Additional storages -->
375
+ <div v-for="s in entity.multiStorage?.storages || []" :key="s.name" class="entity-storage-card entity-storage-alt">
376
+ <div class="entity-storage-header">
377
+ <span class="entity-storage-name">{{ s.name }}</span>
378
+ <span class="entity-storage-type">{{ s.type }}</span>
379
+ <span v-if="s.hasNormalize || s.hasDenormalize" class="entity-normalize-badge" title="Has normalize/denormalize">
380
+ <i class="pi pi-arrows-h" />
381
+ </span>
382
+ <button
383
+ class="entity-storage-fetch-btn"
384
+ :disabled="isTestingStorage(entity.name, s.name)"
385
+ @click.stop="testStorageFetch(entity.name, s.name)"
386
+ title="Test fetch on this storage"
387
+ >
388
+ <i :class="['pi', isTestingStorage(entity.name, s.name) ? 'pi-spin pi-spinner' : 'pi-download']" />
389
+ </button>
390
+ </div>
391
+ <div v-if="s.endpoint" class="entity-storage-endpoint">
392
+ {{ s.endpoint }}
393
+ </div>
394
+ <div class="entity-storage-row">
395
+ <div v-if="s.capabilities && Object.keys(s.capabilities).length > 0" class="entity-storage-caps">
396
+ <span
397
+ v-for="(enabled, cap) in s.capabilities"
398
+ :key="cap"
399
+ class="entity-cap"
400
+ :class="[cap === 'requiresAuth' && enabled ? 'entity-cap-auth' : (enabled ? 'entity-cap-enabled' : 'entity-cap-disabled')]"
401
+ :title="getCapabilityLabel(cap) + (enabled ? ' ✓' : ' ✗')"
402
+ >
403
+ <i :class="['pi', getCapabilityIcon(cap)]" />
404
+ </span>
405
+ </div>
406
+ <span v-if="getStorageTestResult(entity.name, s.name)" class="entity-storage-test-result" :class="getStorageTestResult(entity.name, s.name)?.success ? 'test-success' : 'test-error'">
407
+ <template v-if="getStorageTestResult(entity.name, s.name)?.success">
408
+ <i class="pi pi-check-circle" /> {{ getStorageTestResult(entity.name, s.name)?.count }}
409
+ </template>
410
+ <template v-else>
411
+ <i class="pi pi-times-circle" /> {{ getStorageTestResult(entity.name, s.name)?.status || 'ERR' }}
412
+ </template>
413
+ </span>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ <div v-if="entity.cache.enabled" class="entity-row">
418
+ <span class="entity-key">Cache:</span>
419
+ <span class="entity-value">
420
+ {{ entity.cache.valid ? 'Valid' : 'Not loaded' }}
421
+ <template v-if="entity.cache.valid">
422
+ ({{ entity.cache.itemCount }} items, threshold {{ entity.cache.threshold }})
423
+ </template>
424
+ <span class="entity-cache-ttl" :class="{ 'ttl-expired': entity.cache.expired }">
425
+ TTL: {{ formatTtl(entity.cache.ttlMs) }}
426
+ <template v-if="entity.cache.valid && entity.cache.expiresAt && !entity.cache.expired">
427
+ ({{ formatExpiresIn(entity.cache.expiresAt) }} left)
428
+ </template>
429
+ <template v-if="entity.cache.expired">
430
+ <i class="pi pi-exclamation-triangle" title="Cache expired" />
431
+ </template>
432
+ </span>
433
+ </span>
434
+ <button
435
+ v-if="!entity.cache.valid || entity.cache.expired"
436
+ class="entity-load-btn"
437
+ :disabled="isLoading(entity.name)"
438
+ @click.stop="loadCache(entity.name)"
439
+ >
440
+ <i :class="['pi', isLoading(entity.name) ? 'pi-spin pi-spinner' : 'pi-download']" />
441
+ {{ isLoading(entity.name) ? 'Loading...' : (entity.cache.expired ? 'Reload' : 'Load') }}
442
+ </button>
443
+ <button
444
+ v-if="entity.cache.valid && !entity.cache.expired"
445
+ class="entity-invalidate-btn"
446
+ title="Invalidate cache"
447
+ @click.stop="invalidateCache(entity.name)"
448
+ >
449
+ <i class="pi pi-times-circle" />
450
+ </button>
451
+ </div>
452
+ <div v-else class="entity-row">
453
+ <span class="entity-key">Cache:</span>
454
+ <span class="entity-value entity-cache-na">Disabled</span>
455
+ </div>
456
+ <div v-if="entity.cache.detailCache" class="entity-row">
457
+ <span class="entity-key">Detail cache:</span>
458
+ <span class="entity-value">
459
+ {{ entity.cache.detailCache.size }}<template v-if="entity.cache.detailCache.maxSize > 0">/{{ entity.cache.detailCache.maxSize }}</template> items
460
+ <span class="entity-cache-ttl">
461
+ TTL: {{ formatTtl(entity.cache.detailCache.ttlMs) }}
462
+ </span>
463
+ </span>
464
+ </div>
465
+ <div class="entity-row">
466
+ <span class="entity-key">Fields:</span>
467
+ <span class="entity-value">{{ entity.fields.count }} fields</span>
468
+ <span v-if="entity.fields.required.length > 0" class="entity-required">
469
+ {{ entity.fields.required.length }} required
470
+ </span>
471
+ </div>
472
+ <div v-if="entity.relations.parents.length > 0 || entity.relations.children.length > 0 || (entity.relations.references?.length ?? 0) > 0" class="entity-row">
473
+ <span class="entity-key">Relations:</span>
474
+ <span class="entity-value">
475
+ <span v-for="p in entity.relations.parents" :key="p.key" class="entity-relation" title="Parent relation">
476
+ <i class="pi pi-arrow-up" />{{ p.key }}→{{ p.entity }}
477
+ </span>
478
+ <span v-for="c in entity.relations.children" :key="c.key" class="entity-relation" title="Child relation">
479
+ <i class="pi pi-arrow-down" />{{ c.key }}→{{ c.entity }}
480
+ </span>
481
+ <span v-for="r in entity.relations.references" :key="r.field" class="entity-relation entity-reference" title="Field reference">
482
+ <i class="pi pi-link" />{{ r.field }}→{{ r.entity }}
483
+ </span>
484
+ </span>
485
+ </div>
486
+ <!-- Operation stats -->
487
+ <div v-if="entity.stats" class="entity-stats">
488
+ <div class="entity-stats-header">
489
+ <i class="pi pi-chart-bar" />
490
+ <span>Stats</span>
491
+ </div>
492
+ <div class="entity-stats-grid">
493
+ <div class="entity-stat">
494
+ <span class="entity-stat-value">{{ entity.stats.list }}</span>
495
+ <span class="entity-stat-label">list</span>
496
+ </div>
497
+ <div class="entity-stat">
498
+ <span class="entity-stat-value">{{ entity.stats.get }}</span>
499
+ <span class="entity-stat-label">get</span>
500
+ </div>
501
+ <div class="entity-stat">
502
+ <span class="entity-stat-value">{{ entity.stats.create }}</span>
503
+ <span class="entity-stat-label">create</span>
504
+ </div>
505
+ <div class="entity-stat">
506
+ <span class="entity-stat-value">{{ entity.stats.update }}</span>
507
+ <span class="entity-stat-label">update</span>
508
+ </div>
509
+ <div class="entity-stat">
510
+ <span class="entity-stat-value">{{ entity.stats.delete }}</span>
511
+ <span class="entity-stat-label">delete</span>
512
+ </div>
513
+ <div class="entity-stat entity-stat-cache">
514
+ <span class="entity-stat-value" :class="{ 'stat-positive': entity.stats.cacheHits > 0 }">
515
+ {{ entity.stats.cacheHits }}
516
+ </span>
517
+ <span class="entity-stat-label">hits</span>
518
+ </div>
519
+ <div class="entity-stat entity-stat-cache">
520
+ <span class="entity-stat-value" :class="{ 'stat-negative': entity.stats.cacheMisses > 0 }">
521
+ {{ entity.stats.cacheMisses }}
522
+ </span>
523
+ <span class="entity-stat-label">miss</span>
524
+ </div>
525
+ <div v-if="entity.cache.asymmetric" class="entity-stat entity-stat-cache">
526
+ <span class="entity-stat-value" :class="{ 'stat-positive': entity.stats.detailCacheHits > 0 }">
527
+ {{ entity.stats.detailCacheHits }}
528
+ </span>
529
+ <span class="entity-stat-label">d-hits</span>
530
+ </div>
531
+ <div v-if="entity.cache.asymmetric" class="entity-stat entity-stat-cache">
532
+ <span class="entity-stat-value" :class="{ 'stat-negative': entity.stats.detailCacheMisses > 0 }">
533
+ {{ entity.stats.detailCacheMisses }}
534
+ </span>
535
+ <span class="entity-stat-label">d-miss</span>
536
+ </div>
537
+ <div class="entity-stat entity-stat-max">
538
+ <span class="entity-stat-value">{{ entity.stats.maxItemsSeen }}</span>
539
+ <span class="entity-stat-label">items</span>
540
+ </div>
541
+ <div class="entity-stat entity-stat-max">
542
+ <span class="entity-stat-value">{{ entity.stats.maxTotal }}</span>
543
+ <span class="entity-stat-label">total</span>
544
+ </div>
545
+ </div>
546
+ </div>
547
+ <!-- Cached items -->
548
+ <div v-if="entity.cache.items && entity.cache.items.length > 0" class="entity-items">
549
+ <div class="entity-items-header">
550
+ <i class="pi pi-list" />
551
+ <span>Cached Items ({{ entity.cache.items.length }}/{{ entity.cache.itemCount }})</span>
552
+ </div>
553
+ <div class="entity-items-list">
554
+ <div v-for="(item, idx) in entity.cache.items.slice(0, 10)" :key="idx" class="entity-item-row">
555
+ <ObjectTree :data="item" :maxDepth="3" :collapsed="true" />
556
+ </div>
557
+ <div v-if="entity.cache.items.length > 10" class="entity-items-more">
558
+ ... et {{ entity.cache.items.length - 10 }} de plus
559
+ </div>
560
+ </div>
561
+ </div>
562
+
563
+ <!-- Full entity config tree -->
564
+ <details class="entity-config">
565
+ <summary>Configuration complète</summary>
566
+ <ObjectTree :data="entity" :maxDepth="6" />
567
+ </details>
568
+ </div>
569
+ </div>
570
+ </div>
571
+ </template>
572
+