qdadm 0.13.0 → 0.14.1
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/README.md +26 -117
- package/package.json +1 -2
- package/src/components/SeverityTag.vue +71 -0
- package/src/components/editors/ScopeEditor.vue +1 -1
- package/src/components/index.js +1 -0
- package/src/components/layout/AppLayout.vue +29 -1
- package/src/components/layout/PageHeader.vue +71 -5
- package/src/components/layout/PageLayout.vue +4 -3
- package/src/composables/index.js +1 -0
- package/src/composables/useBareForm.js +76 -4
- package/src/composables/useBreadcrumb.js +23 -14
- package/src/composables/useForm.js +48 -2
- package/src/composables/useListPageBuilder.js +51 -74
- package/src/composables/useManager.js +20 -0
- package/src/entity/EntityManager.js +391 -9
- package/src/entity/storage/ApiStorage.js +5 -0
- package/src/entity/storage/LocalStorage.js +5 -0
- package/src/kernel/Kernel.js +25 -8
- package/src/plugin.js +3 -1
- package/src/styles/main.css +44 -0
- package/src/styles/theme/index.css +1 -1
- package/CHANGELOG.md +0 -270
|
@@ -131,8 +131,16 @@ export function useBreadcrumb(options = {}) {
|
|
|
131
131
|
return items
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Action segments that should be excluded from breadcrumb
|
|
135
|
+
const actionSegments = ['edit', 'create', 'show', 'view', 'new', 'delete']
|
|
136
|
+
|
|
134
137
|
/**
|
|
135
138
|
* Build breadcrumb automatically from route path
|
|
139
|
+
*
|
|
140
|
+
* Breadcrumb shows navigable parents only:
|
|
141
|
+
* - Excludes action segments (edit, create, show, etc.)
|
|
142
|
+
* - Excludes IDs
|
|
143
|
+
* - All items have links (to navigate back to parent)
|
|
136
144
|
*/
|
|
137
145
|
function buildFromPath(entity, getEntityLabel) {
|
|
138
146
|
const items = []
|
|
@@ -147,19 +155,17 @@ export function useBreadcrumb(options = {}) {
|
|
|
147
155
|
const segment = segments[i]
|
|
148
156
|
currentPath += `/${segment}`
|
|
149
157
|
|
|
150
|
-
//
|
|
158
|
+
// Skip action segments (edit, create, show, etc.)
|
|
159
|
+
if (actionSegments.includes(segment.toLowerCase())) {
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Skip IDs: numeric, UUID, ULID, or any alphanumeric string > 10 chars
|
|
151
164
|
const isId = /^\d+$/.test(segment) || // numeric
|
|
152
165
|
segment.match(/^[0-9a-f-]{36}$/i) || // UUID
|
|
153
166
|
segment.match(/^[0-7][0-9a-hjkmnp-tv-z]{25}$/i) || // ULID
|
|
154
|
-
(segment.match(/^[a-z0-9]+$/i) && segment.length > 10) // Generated ID
|
|
167
|
+
(segment.match(/^[a-z0-9]+$/i) && segment.length > 10) // Generated ID
|
|
155
168
|
if (isId) {
|
|
156
|
-
// If we have entity data, show its label instead of the ID
|
|
157
|
-
if (entity && getEntityLabel) {
|
|
158
|
-
const entityLabel = getEntityLabel(entity)
|
|
159
|
-
if (entityLabel) {
|
|
160
|
-
items.push({ label: entityLabel, to: null, icon: null })
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
169
|
continue
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -179,14 +185,17 @@ export function useBreadcrumb(options = {}) {
|
|
|
179
185
|
icon: i === 0 ? iconMap[segment] : null
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
// Last item has no link
|
|
183
|
-
if (i === segments.length - 1) {
|
|
184
|
-
item.to = null
|
|
185
|
-
}
|
|
186
|
-
|
|
187
188
|
items.push(item)
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// Remove last item if it matches current route (we only show parents)
|
|
192
|
+
if (items.length > 1) {
|
|
193
|
+
const lastItem = items[items.length - 1]
|
|
194
|
+
if (lastItem.to?.name === route.name) {
|
|
195
|
+
items.pop()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
190
199
|
return items
|
|
191
200
|
}
|
|
192
201
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* })
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
import { ref, watch, onMounted, inject } from 'vue'
|
|
29
|
+
import { ref, computed, watch, onMounted, inject, provide } from 'vue'
|
|
30
30
|
import { useBareForm } from './useBareForm'
|
|
31
31
|
import { deepClone } from '../utils/transformers'
|
|
32
32
|
|
|
@@ -83,6 +83,8 @@ export function useForm(options = {}) {
|
|
|
83
83
|
checkDirty,
|
|
84
84
|
// Helpers
|
|
85
85
|
cancel,
|
|
86
|
+
// Breadcrumb
|
|
87
|
+
breadcrumb,
|
|
86
88
|
// Guard dialog for unsaved changes
|
|
87
89
|
guardDialog
|
|
88
90
|
} = useBareForm({
|
|
@@ -224,6 +226,42 @@ export function useForm(options = {}) {
|
|
|
224
226
|
load()
|
|
225
227
|
})
|
|
226
228
|
|
|
229
|
+
// ============ COMPUTED TITLE ============
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Entity display label (e.g., "David Berlioz" for an agent)
|
|
233
|
+
* Uses manager.getEntityLabel() with labelField config
|
|
234
|
+
*/
|
|
235
|
+
const entityLabel = computed(() => {
|
|
236
|
+
return manager.getEntityLabel(form.value)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Auto-generated page title
|
|
241
|
+
* - Edit mode: "Edit Agent: David Berlioz"
|
|
242
|
+
* - Create mode: "Create Agent"
|
|
243
|
+
*/
|
|
244
|
+
const pageTitle = computed(() => {
|
|
245
|
+
if (isEdit.value) {
|
|
246
|
+
const label = entityLabel.value
|
|
247
|
+
return label ? `Edit ${entityName}: ${label}` : `Edit ${entityName}`
|
|
248
|
+
}
|
|
249
|
+
return `Create ${entityName}`
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Structured page title for decorated rendering
|
|
254
|
+
* Returns { action, entityName, entityLabel } for custom styling
|
|
255
|
+
*/
|
|
256
|
+
const pageTitleParts = computed(() => ({
|
|
257
|
+
action: isEdit.value ? 'Edit' : 'Create',
|
|
258
|
+
entityName,
|
|
259
|
+
entityLabel: isEdit.value ? entityLabel.value : null
|
|
260
|
+
}))
|
|
261
|
+
|
|
262
|
+
// Provide title parts for automatic PageHeader consumption
|
|
263
|
+
provide('qdadmPageTitleParts', pageTitleParts)
|
|
264
|
+
|
|
227
265
|
return {
|
|
228
266
|
// Manager access
|
|
229
267
|
manager,
|
|
@@ -248,7 +286,15 @@ export function useForm(options = {}) {
|
|
|
248
286
|
checkDirty,
|
|
249
287
|
isFieldDirty,
|
|
250
288
|
|
|
289
|
+
// Breadcrumb (auto-generated from route)
|
|
290
|
+
breadcrumb,
|
|
291
|
+
|
|
251
292
|
// Guard dialog (for UnsavedChangesDialog - pass to PageLayout)
|
|
252
|
-
guardDialog
|
|
293
|
+
guardDialog,
|
|
294
|
+
|
|
295
|
+
// Title helpers
|
|
296
|
+
entityLabel,
|
|
297
|
+
pageTitle,
|
|
298
|
+
pageTitleParts
|
|
253
299
|
}
|
|
254
300
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, computed, watch, onMounted, inject } from 'vue'
|
|
1
|
+
import { ref, computed, watch, onMounted, inject, provide } from 'vue'
|
|
2
2
|
import { useRouter, useRoute } from 'vue-router'
|
|
3
3
|
import { useToast } from 'primevue/usetoast'
|
|
4
4
|
import { useConfirm } from 'primevue/useconfirm'
|
|
@@ -155,6 +155,9 @@ export function useListPageBuilder(config = {}) {
|
|
|
155
155
|
}
|
|
156
156
|
const manager = orchestrator.get(entity)
|
|
157
157
|
|
|
158
|
+
// Provide entity context for child components (e.g., SeverityTag auto-discovery)
|
|
159
|
+
provide('mainEntity', entity)
|
|
160
|
+
|
|
158
161
|
// Read config from manager with option overrides
|
|
159
162
|
const entityName = config.entityName ?? manager.label
|
|
160
163
|
const entityNamePlural = config.entityNamePlural ?? manager.labelPlural
|
|
@@ -663,71 +666,35 @@ export function useListPageBuilder(config = {}) {
|
|
|
663
666
|
searchConfig.value = { ...searchConfig.value, ...searchCfg }
|
|
664
667
|
}
|
|
665
668
|
|
|
666
|
-
// ============
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
return manager?.localFilterThreshold ?? 100
|
|
671
|
-
})
|
|
669
|
+
// ============ CACHE MODE ============
|
|
670
|
+
// EntityManager handles caching and filtering automatically via query()
|
|
671
|
+
// This computed applies any custom local_filter callbacks on top
|
|
672
|
+
const fromCache = ref(false)
|
|
672
673
|
|
|
673
|
-
const effectiveFilterMode = computed(() => {
|
|
674
|
-
if (filterMode !== 'auto') return filterMode
|
|
675
|
-
// Switch to local if we have few items
|
|
676
|
-
return items.value.length > 0 && items.value.length < resolvedThreshold.value ? 'local' : 'manager'
|
|
677
|
-
})
|
|
678
|
-
|
|
679
|
-
// Computed for local filtering
|
|
680
|
-
// Only applied when effectiveFilterMode === 'local'
|
|
681
|
-
// In manager mode, items are already filtered by the manager
|
|
682
674
|
const filteredItems = computed(() => {
|
|
683
|
-
const isLocalMode = effectiveFilterMode.value === 'local'
|
|
684
|
-
|
|
685
|
-
// In manager mode, return items as-is (manager already filtered)
|
|
686
|
-
if (!isLocalMode) {
|
|
687
|
-
return items.value
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Local mode: apply all filtering client-side
|
|
691
675
|
let result = [...items.value]
|
|
692
676
|
|
|
693
|
-
// Apply
|
|
694
|
-
// local_search: false = manager only, skip in local mode
|
|
695
|
-
if (searchQuery.value && searchConfig.value.local_search !== false) {
|
|
696
|
-
const query = searchQuery.value.toLowerCase()
|
|
697
|
-
// Default: search on configured fields
|
|
698
|
-
const localSearch = searchConfig.value.local_search || (
|
|
699
|
-
searchConfig.value.fields?.length
|
|
700
|
-
? (item, q) => searchConfig.value.fields.some(field =>
|
|
701
|
-
String(item[field] || '').toLowerCase().includes(q)
|
|
702
|
-
)
|
|
703
|
-
: null
|
|
704
|
-
)
|
|
705
|
-
if (localSearch) {
|
|
706
|
-
result = result.filter(item => localSearch(item, query))
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Apply filters
|
|
677
|
+
// Apply custom local_filter callbacks (UI-specific post-filters)
|
|
711
678
|
for (const [name, value] of Object.entries(filterValues.value)) {
|
|
712
679
|
if (value === null || value === undefined || value === '') continue
|
|
713
680
|
const filterDef = filtersMap.value.get(name)
|
|
714
681
|
|
|
715
|
-
//
|
|
716
|
-
if (filterDef?.local_filter
|
|
682
|
+
// Only apply if there's a custom local_filter callback
|
|
683
|
+
if (typeof filterDef?.local_filter !== 'function') continue
|
|
717
684
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
result = result.filter(item =>
|
|
685
|
+
result = result.filter(item => filterDef.local_filter(item, value))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Apply custom local_search callback
|
|
689
|
+
if (searchQuery.value && typeof searchConfig.value.local_search === 'function') {
|
|
690
|
+
const query = searchQuery.value.toLowerCase()
|
|
691
|
+
result = result.filter(item => searchConfig.value.local_search(item, query))
|
|
725
692
|
}
|
|
726
693
|
|
|
727
694
|
return result
|
|
728
695
|
})
|
|
729
696
|
|
|
730
|
-
// Items to display
|
|
697
|
+
// Items to display
|
|
731
698
|
const displayItems = computed(() => filteredItems.value)
|
|
732
699
|
|
|
733
700
|
// ============ LOADING ============
|
|
@@ -736,36 +703,40 @@ export function useListPageBuilder(config = {}) {
|
|
|
736
703
|
async function loadItems(extraParams = {}, { force = false } = {}) {
|
|
737
704
|
if (!manager) return
|
|
738
705
|
|
|
739
|
-
//
|
|
740
|
-
if (
|
|
741
|
-
|
|
706
|
+
// If forced, invalidate the manager's cache first
|
|
707
|
+
if (force && manager.invalidateCache) {
|
|
708
|
+
manager.invalidateCache()
|
|
742
709
|
}
|
|
743
710
|
|
|
744
711
|
loading.value = true
|
|
745
712
|
try {
|
|
713
|
+
// Build query params
|
|
746
714
|
let params = { ...extraParams }
|
|
747
715
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
716
|
+
// Pagination and sorting
|
|
717
|
+
params.page = page.value
|
|
718
|
+
params.page_size = pageSize.value
|
|
719
|
+
if (sortField.value) {
|
|
720
|
+
params.sort_by = sortField.value
|
|
721
|
+
params.sort_order = sortOrder.value === 1 ? 'asc' : 'desc'
|
|
755
722
|
}
|
|
756
723
|
|
|
757
|
-
// Add search param
|
|
758
|
-
if (searchQuery.value && searchConfig.value.
|
|
724
|
+
// Add search param (skip if custom local_search callback)
|
|
725
|
+
if (searchQuery.value && typeof searchConfig.value.local_search !== 'function') {
|
|
759
726
|
params.search = searchQuery.value
|
|
760
727
|
}
|
|
761
728
|
|
|
762
|
-
// Add filter values
|
|
729
|
+
// Add filter values (skip filters with custom local_filter callback)
|
|
730
|
+
const filters = {}
|
|
763
731
|
for (const [name, value] of Object.entries(filterValues.value)) {
|
|
764
732
|
if (value === null || value === undefined || value === '') continue
|
|
765
733
|
const filterDef = filtersMap.value.get(name)
|
|
766
|
-
// Skip
|
|
767
|
-
if (filterDef?.local_filter) continue
|
|
768
|
-
|
|
734
|
+
// Skip filters with custom local_filter - they're applied in filteredItems
|
|
735
|
+
if (typeof filterDef?.local_filter === 'function') continue
|
|
736
|
+
filters[name] = value
|
|
737
|
+
}
|
|
738
|
+
if (Object.keys(filters).length > 0) {
|
|
739
|
+
params.filters = filters
|
|
769
740
|
}
|
|
770
741
|
|
|
771
742
|
// Hook: modify params before request
|
|
@@ -773,7 +744,13 @@ export function useListPageBuilder(config = {}) {
|
|
|
773
744
|
params = onBeforeLoad(params) || params
|
|
774
745
|
}
|
|
775
746
|
|
|
776
|
-
|
|
747
|
+
// Use manager.query() for automatic cache handling
|
|
748
|
+
const response = manager.query
|
|
749
|
+
? await manager.query(params)
|
|
750
|
+
: await manager.list(params)
|
|
751
|
+
|
|
752
|
+
// Track if response came from cache
|
|
753
|
+
fromCache.value = response.fromCache || false
|
|
777
754
|
|
|
778
755
|
// Hook: transform response
|
|
779
756
|
let processedData
|
|
@@ -812,13 +789,13 @@ export function useListPageBuilder(config = {}) {
|
|
|
812
789
|
pageSize.value = event.rows
|
|
813
790
|
// Save page size preference to cookie
|
|
814
791
|
setCookie(COOKIE_NAME, event.rows, COOKIE_EXPIRY_DAYS)
|
|
815
|
-
|
|
792
|
+
loadItems()
|
|
816
793
|
}
|
|
817
794
|
|
|
818
795
|
function onSort(event) {
|
|
819
796
|
sortField.value = event.sortField
|
|
820
797
|
sortOrder.value = event.sortOrder
|
|
821
|
-
|
|
798
|
+
loadItems()
|
|
822
799
|
}
|
|
823
800
|
|
|
824
801
|
// ============ NAVIGATION ============
|
|
@@ -827,11 +804,11 @@ export function useListPageBuilder(config = {}) {
|
|
|
827
804
|
}
|
|
828
805
|
|
|
829
806
|
function goToEdit(item) {
|
|
830
|
-
router.push({ name: `${routePrefix}-edit`, params: {
|
|
807
|
+
router.push({ name: `${routePrefix}-edit`, params: { id: item[resolvedDataKey] } })
|
|
831
808
|
}
|
|
832
809
|
|
|
833
810
|
function goToShow(item) {
|
|
834
|
-
router.push({ name: `${routePrefix}-show`, params: {
|
|
811
|
+
router.push({ name: `${routePrefix}-show`, params: { id: item[resolvedDataKey] } })
|
|
835
812
|
}
|
|
836
813
|
|
|
837
814
|
// ============ DELETE ============
|
|
@@ -1126,7 +1103,7 @@ export function useListPageBuilder(config = {}) {
|
|
|
1126
1103
|
filters,
|
|
1127
1104
|
filterValues,
|
|
1128
1105
|
filteredItems,
|
|
1129
|
-
|
|
1106
|
+
fromCache, // true if last query used cache
|
|
1130
1107
|
addFilter,
|
|
1131
1108
|
removeFilter,
|
|
1132
1109
|
setFilterValue,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useManager - Access EntityManager by name
|
|
3
|
+
*
|
|
4
|
+
* @param {string} entityName - Entity name (e.g., 'jobs', 'stories')
|
|
5
|
+
* @returns {EntityManager|null} - The entity manager or null if not found
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const jobs = useManager('jobs')
|
|
9
|
+
* const severity = jobs.getSeverity('status', 'completed')
|
|
10
|
+
*/
|
|
11
|
+
import { inject } from 'vue'
|
|
12
|
+
|
|
13
|
+
export function useManager(entityName) {
|
|
14
|
+
const orchestrator = inject('qdadmOrchestrator')
|
|
15
|
+
if (!orchestrator) {
|
|
16
|
+
console.warn('[qdadm] useManager: orchestrator not available')
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
return orchestrator.get(entityName)
|
|
20
|
+
}
|