qdadm 0.13.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 (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. package/src/utils/transformers.js +105 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * useNavigation - Navigation composable for AppLayout
3
+ *
4
+ * Provides reactive navigation state from moduleRegistry.
5
+ * All data comes from module init declarations.
6
+ * Nav items are filtered based on EntityManager.canRead() permissions.
7
+ */
8
+
9
+ import { computed, inject } from 'vue'
10
+ import { useRoute, useRouter } from 'vue-router'
11
+ import { getNavSections, isRouteInFamily } from '../module/moduleRegistry'
12
+
13
+ /**
14
+ * Navigation composable
15
+ */
16
+ export function useNavigation() {
17
+ const route = useRoute()
18
+ const router = useRouter()
19
+ const orchestrator = inject('qdadmOrchestrator', null)
20
+
21
+ /**
22
+ * Check if user can access a nav item based on its entity's canRead()
23
+ */
24
+ function canAccessNavItem(item) {
25
+ if (!item.entity || !orchestrator) return true
26
+ const manager = orchestrator.get(item.entity)
27
+ if (!manager) return true
28
+ return manager.canRead()
29
+ }
30
+
31
+ // Get nav sections from registry, filtering items based on permissions
32
+ const navSections = computed(() => {
33
+ const sections = getNavSections()
34
+ return sections
35
+ .map(section => ({
36
+ ...section,
37
+ items: section.items.filter(item => canAccessNavItem(item))
38
+ }))
39
+ .filter(section => section.items.length > 0) // Hide empty sections
40
+ })
41
+
42
+ /**
43
+ * Check if a nav item is currently active
44
+ */
45
+ function isNavActive(item) {
46
+ const currentRouteName = route.name
47
+
48
+ // Exact match mode
49
+ if (item.exact) {
50
+ return currentRouteName === item.route
51
+ }
52
+
53
+ // Use registry's family detection
54
+ return isRouteInFamily(currentRouteName, item.route)
55
+ }
56
+
57
+ /**
58
+ * Check if section contains active item
59
+ */
60
+ function sectionHasActiveItem(section) {
61
+ return section.items.some(item => isNavActive(item))
62
+ }
63
+
64
+ /**
65
+ * Handle nav click - force navigation if on same route with query params
66
+ */
67
+ function handleNavClick(event, item) {
68
+ if (route.name === item.route && Object.keys(route.query).length > 0) {
69
+ event.preventDefault()
70
+ router.push({ name: item.route })
71
+ }
72
+ }
73
+
74
+ return {
75
+ // Data (from moduleRegistry)
76
+ navSections,
77
+
78
+ // Active state
79
+ isNavActive,
80
+ sectionHasActiveItem,
81
+
82
+ // Event handlers
83
+ handleNavClick,
84
+
85
+ // Current route info (reactive)
86
+ currentRouteName: computed(() => route.name),
87
+ currentRoutePath: computed(() => route.path)
88
+ }
89
+ }
@@ -0,0 +1,334 @@
1
+ import { ref, computed } from 'vue'
2
+ import { useRouter } from 'vue-router'
3
+ import { useToast } from 'primevue/usetoast'
4
+ import { useConfirm } from 'primevue/useconfirm'
5
+
6
+ /**
7
+ * usePageBuilder - Base builder for dashboard pages
8
+ *
9
+ * Provides procedural API for:
10
+ * - Header actions (addHeaderAction, addRefreshAction, etc.)
11
+ * - Cards zone (addCard)
12
+ * - Main content configuration (datatable, tree, custom)
13
+ *
14
+ * Usage:
15
+ * const page = usePageBuilder()
16
+ *
17
+ * // Header
18
+ * page.addHeaderAction('refresh', { icon: 'pi pi-refresh', onClick: refresh })
19
+ *
20
+ * // Cards
21
+ * page.addCard('total', { value: 42, label: 'Total Events' })
22
+ *
23
+ * // Main content - DataTable
24
+ * page.setDataTable({ lazy: true, paginator: true })
25
+ * page.addColumn('name', { header: 'Name', sortable: true })
26
+ * page.addRowAction('edit', { icon: 'pi pi-pencil', onClick: edit })
27
+ *
28
+ * // Or Tree view
29
+ * page.setTreeView({ selectionMode: 'single' })
30
+ *
31
+ * // Or fully custom (use slot in template)
32
+ * page.setCustomContent()
33
+ */
34
+ export function usePageBuilder(_config = {}) {
35
+ const router = useRouter()
36
+ const toast = useToast()
37
+ const confirm = useConfirm()
38
+
39
+ // ============ HEADER ACTIONS ============
40
+ const headerActionsMap = ref(new Map())
41
+
42
+ /**
43
+ * Add a header action button
44
+ */
45
+ function addHeaderAction(name, actionConfig) {
46
+ headerActionsMap.value.set(name, {
47
+ name,
48
+ severity: 'secondary',
49
+ ...actionConfig
50
+ })
51
+ }
52
+
53
+ function removeHeaderAction(name) {
54
+ headerActionsMap.value.delete(name)
55
+ }
56
+
57
+ function updateHeaderAction(name, updates) {
58
+ const existing = headerActionsMap.value.get(name)
59
+ if (existing) {
60
+ headerActionsMap.value.set(name, { ...existing, ...updates })
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get header actions with resolved state
66
+ */
67
+ function getHeaderActions(state = {}) {
68
+ const actions = []
69
+ for (const [, action] of headerActionsMap.value) {
70
+ if (action.visible && !action.visible(state)) continue
71
+ actions.push({
72
+ ...action,
73
+ label: typeof action.label === 'function' ? action.label(state) : action.label,
74
+ isLoading: action.loading ? action.loading(state) : false
75
+ })
76
+ }
77
+ return actions
78
+ }
79
+
80
+ const headerActions = computed(() => getHeaderActions())
81
+
82
+ /**
83
+ * Add standard refresh action
84
+ */
85
+ function addRefreshAction(onClick, options = {}) {
86
+ const loadingRef = options.loadingRef || ref(false)
87
+ addHeaderAction('refresh', {
88
+ label: 'Refresh',
89
+ icon: 'pi pi-refresh',
90
+ onClick,
91
+ loading: () => loadingRef.value,
92
+ ...options
93
+ })
94
+ return loadingRef
95
+ }
96
+
97
+ // ============ CARDS ============
98
+ const cardsMap = ref(new Map())
99
+
100
+ function addCard(name, cardConfig) {
101
+ cardsMap.value.set(name, { name, ...cardConfig })
102
+ }
103
+
104
+ function updateCard(name, cardConfig) {
105
+ const existing = cardsMap.value.get(name)
106
+ if (existing) {
107
+ cardsMap.value.set(name, { ...existing, ...cardConfig })
108
+ }
109
+ }
110
+
111
+ function removeCard(name) {
112
+ cardsMap.value.delete(name)
113
+ }
114
+
115
+ const cards = computed(() => Array.from(cardsMap.value.values()))
116
+
117
+ // ============ MAIN CONTENT ============
118
+ const contentType = ref('custom') // 'datatable', 'tree', 'custom'
119
+ const contentConfig = ref({})
120
+
121
+ // DataTable specific
122
+ const columnsMap = ref(new Map())
123
+ const rowActionsMap = ref(new Map())
124
+ const items = ref([])
125
+ const loading = ref(false)
126
+ const selected = ref([])
127
+
128
+ // Pagination
129
+ const page = ref(1)
130
+ const pageSize = ref(10)
131
+ const totalRecords = ref(0)
132
+ const sortField = ref(null)
133
+ const sortOrder = ref(1)
134
+
135
+ // Search & Filters
136
+ const searchQuery = ref('')
137
+ const filtersMap = ref(new Map())
138
+ const filterValues = ref({})
139
+
140
+ /**
141
+ * Set main content to DataTable
142
+ */
143
+ function setDataTable(options = {}) {
144
+ contentType.value = 'datatable'
145
+ contentConfig.value = {
146
+ paginator: true,
147
+ rows: 10,
148
+ rowsPerPageOptions: [10, 25, 50],
149
+ stripedRows: true,
150
+ removableSort: true,
151
+ dataKey: 'id',
152
+ ...options
153
+ }
154
+ if (options.pageSize) pageSize.value = options.pageSize
155
+ }
156
+
157
+ /**
158
+ * Set main content to Tree view
159
+ */
160
+ function setTreeView(options = {}) {
161
+ contentType.value = 'tree'
162
+ contentConfig.value = {
163
+ selectionMode: 'single',
164
+ ...options
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Set main content to custom (use slot)
170
+ */
171
+ function setCustomContent() {
172
+ contentType.value = 'custom'
173
+ contentConfig.value = {}
174
+ }
175
+
176
+ /**
177
+ * Add a column to DataTable
178
+ */
179
+ function addColumn(field, columnConfig) {
180
+ columnsMap.value.set(field, {
181
+ field,
182
+ header: field.charAt(0).toUpperCase() + field.slice(1),
183
+ ...columnConfig
184
+ })
185
+ }
186
+
187
+ function removeColumn(field) {
188
+ columnsMap.value.delete(field)
189
+ }
190
+
191
+ const columns = computed(() => Array.from(columnsMap.value.values()))
192
+
193
+ /**
194
+ * Add a row action
195
+ */
196
+ function addRowAction(name, actionConfig) {
197
+ rowActionsMap.value.set(name, {
198
+ name,
199
+ severity: 'secondary',
200
+ ...actionConfig
201
+ })
202
+ }
203
+
204
+ function removeRowAction(name) {
205
+ rowActionsMap.value.delete(name)
206
+ }
207
+
208
+ function resolveValue(value, row) {
209
+ return typeof value === 'function' ? value(row) : value
210
+ }
211
+
212
+ function getRowActions(row) {
213
+ const actions = []
214
+ for (const [, action] of rowActionsMap.value) {
215
+ if (action.visible && !action.visible(row)) continue
216
+ actions.push({
217
+ name: action.name,
218
+ icon: resolveValue(action.icon, row),
219
+ tooltip: resolveValue(action.tooltip, row),
220
+ severity: resolveValue(action.severity, row) || 'secondary',
221
+ isDisabled: action.disabled ? action.disabled(row) : false,
222
+ handler: () => action.onClick(row)
223
+ })
224
+ }
225
+ return actions
226
+ }
227
+
228
+ /**
229
+ * Add a filter
230
+ */
231
+ function addFilter(name, filterConfig) {
232
+ filtersMap.value.set(name, {
233
+ name,
234
+ type: 'select',
235
+ placeholder: name,
236
+ ...filterConfig
237
+ })
238
+ if (filterValues.value[name] === undefined) {
239
+ filterValues.value[name] = filterConfig.default ?? null
240
+ }
241
+ }
242
+
243
+ function removeFilter(name) {
244
+ filtersMap.value.delete(name)
245
+ delete filterValues.value[name]
246
+ }
247
+
248
+ const filters = computed(() => Array.from(filtersMap.value.values()))
249
+
250
+ /**
251
+ * Set search config
252
+ */
253
+ const searchConfig = ref({ placeholder: 'Search...', fields: [] })
254
+
255
+ function setSearch(config) {
256
+ searchConfig.value = { ...searchConfig.value, ...config }
257
+ }
258
+
259
+ // Pagination handlers
260
+ function onPage(event) {
261
+ page.value = event.page + 1
262
+ pageSize.value = event.rows
263
+ }
264
+
265
+ function onSort(event) {
266
+ sortField.value = event.sortField
267
+ sortOrder.value = event.sortOrder
268
+ }
269
+
270
+ // Selection
271
+ const hasSelection = computed(() => selected.value.length > 0)
272
+ const selectionCount = computed(() => selected.value.length)
273
+
274
+ return {
275
+ // Header Actions
276
+ headerActions,
277
+ addHeaderAction,
278
+ removeHeaderAction,
279
+ updateHeaderAction,
280
+ getHeaderActions,
281
+ addRefreshAction,
282
+
283
+ // Cards
284
+ cards,
285
+ addCard,
286
+ updateCard,
287
+ removeCard,
288
+
289
+ // Main Content
290
+ contentType,
291
+ contentConfig,
292
+ setDataTable,
293
+ setTreeView,
294
+ setCustomContent,
295
+
296
+ // DataTable
297
+ columns,
298
+ addColumn,
299
+ removeColumn,
300
+ items,
301
+ loading,
302
+ selected,
303
+ hasSelection,
304
+ selectionCount,
305
+
306
+ // Row Actions
307
+ addRowAction,
308
+ removeRowAction,
309
+ getRowActions,
310
+
311
+ // Pagination
312
+ page,
313
+ pageSize,
314
+ totalRecords,
315
+ sortField,
316
+ sortOrder,
317
+ onPage,
318
+ onSort,
319
+
320
+ // Search & Filters
321
+ searchQuery,
322
+ searchConfig,
323
+ setSearch,
324
+ filters,
325
+ filterValues,
326
+ addFilter,
327
+ removeFilter,
328
+
329
+ // Utilities
330
+ toast,
331
+ confirm,
332
+ router
333
+ }
334
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * useStatus - Generic status composable
3
+ *
4
+ * Loads and formats status options from any API endpoint.
5
+ * Provides caching, label/severity lookup helpers.
6
+ *
7
+ * Usage:
8
+ * const { options, loadOptions, getLabel, getSeverity } = useStatus({
9
+ * endpoint: '/reference/queue-statuses',
10
+ * labelField: 'label',
11
+ * valueField: 'value',
12
+ * severityField: 'severity'
13
+ * })
14
+ */
15
+
16
+ import { ref, inject } from 'vue'
17
+
18
+ // Global cache for status options (keyed by endpoint)
19
+ const statusCache = new Map()
20
+
21
+ export function useStatus(config = {}) {
22
+ const {
23
+ endpoint,
24
+ labelField = 'label',
25
+ valueField = 'value',
26
+ severityField = 'severity',
27
+ // Optional: static options instead of API fetch
28
+ staticOptions = null,
29
+ // Optional: custom severity mapping function
30
+ severityMapper = null,
31
+ // Optional: default severity if not found
32
+ defaultSeverity = 'secondary'
33
+ } = config
34
+
35
+ const api = inject('apiAdapter')
36
+ const options = ref([])
37
+ const loading = ref(false)
38
+ const error = ref(null)
39
+
40
+ /**
41
+ * Load options from API or use static options
42
+ */
43
+ async function loadOptions(force = false) {
44
+ // Use static options if provided
45
+ if (staticOptions) {
46
+ options.value = staticOptions
47
+ return options.value
48
+ }
49
+
50
+ if (!endpoint) {
51
+ console.warn('[useStatus] No endpoint provided')
52
+ return []
53
+ }
54
+
55
+ // Check cache first (unless force refresh)
56
+ if (!force && statusCache.has(endpoint)) {
57
+ options.value = statusCache.get(endpoint)
58
+ return options.value
59
+ }
60
+
61
+ if (!api) {
62
+ console.warn('[useStatus] apiAdapter not provided')
63
+ return []
64
+ }
65
+
66
+ loading.value = true
67
+ error.value = null
68
+
69
+ try {
70
+ const response = await api.request('GET', endpoint)
71
+ // Handle both array response and { items: [] } response
72
+ const items = Array.isArray(response) ? response : (response?.items || response?.data || [])
73
+ options.value = items
74
+ statusCache.set(endpoint, items)
75
+ return items
76
+ } catch (e) {
77
+ error.value = e.message
78
+ console.error(`[useStatus] Failed to load options from ${endpoint}:`, e)
79
+ return []
80
+ } finally {
81
+ loading.value = false
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get label for a status value
87
+ */
88
+ function getLabel(value) {
89
+ if (value == null) return ''
90
+ const option = options.value.find(opt =>
91
+ (typeof opt === 'string' ? opt : opt[valueField]) === value
92
+ )
93
+ if (!option) return String(value)
94
+ return typeof option === 'string' ? option : option[labelField]
95
+ }
96
+
97
+ /**
98
+ * Get severity for a status value (for Tag/Badge color)
99
+ */
100
+ function getSeverity(value) {
101
+ if (value == null) return defaultSeverity
102
+
103
+ // Use custom mapper if provided
104
+ if (severityMapper) {
105
+ return severityMapper(value) || defaultSeverity
106
+ }
107
+
108
+ const option = options.value.find(opt =>
109
+ (typeof opt === 'string' ? opt : opt[valueField]) === value
110
+ )
111
+ if (!option || typeof option === 'string') return defaultSeverity
112
+ return option[severityField] || defaultSeverity
113
+ }
114
+
115
+ /**
116
+ * Get full option object for a value
117
+ */
118
+ function getOption(value) {
119
+ if (value == null) return null
120
+ return options.value.find(opt =>
121
+ (typeof opt === 'string' ? opt : opt[valueField]) === value
122
+ )
123
+ }
124
+
125
+ /**
126
+ * Clear cache for this endpoint or all endpoints
127
+ */
128
+ function clearCache(all = false) {
129
+ if (all) {
130
+ statusCache.clear()
131
+ } else if (endpoint) {
132
+ statusCache.delete(endpoint)
133
+ }
134
+ }
135
+
136
+ return {
137
+ options,
138
+ loading,
139
+ error,
140
+ loadOptions,
141
+ getLabel,
142
+ getSeverity,
143
+ getOption,
144
+ clearCache
145
+ }
146
+ }