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,92 @@
1
+ <script setup>
2
+ /**
3
+ * CopyableId - Read-only ID field with copy-to-clipboard functionality
4
+ *
5
+ * Usage:
6
+ * <CopyableId :value="entityId" label="ID" />
7
+ * <CopyableId :value="apiKey" label="API Key" />
8
+ */
9
+ import { ref } from 'vue'
10
+ import { useToast } from 'primevue/usetoast'
11
+ import Button from 'primevue/button'
12
+
13
+ const props = defineProps({
14
+ value: {
15
+ type: String,
16
+ required: true
17
+ },
18
+ label: {
19
+ type: String,
20
+ default: 'ID'
21
+ }
22
+ })
23
+
24
+ const toast = useToast()
25
+ const copied = ref(false)
26
+
27
+ async function copyToClipboard() {
28
+ try {
29
+ await navigator.clipboard.writeText(props.value)
30
+ copied.value = true
31
+ toast.add({
32
+ severity: 'success',
33
+ summary: 'Copied',
34
+ detail: `${props.label} copied to clipboard`,
35
+ life: 2000
36
+ })
37
+ setTimeout(() => {
38
+ copied.value = false
39
+ }, 2000)
40
+ } catch {
41
+ toast.add({
42
+ severity: 'error',
43
+ summary: 'Error',
44
+ detail: 'Failed to copy to clipboard',
45
+ life: 3000
46
+ })
47
+ }
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <div class="form-field">
53
+ <label class="form-field-label">{{ label }}</label>
54
+ <div class="copyable-id">
55
+ <Button
56
+ type="button"
57
+ :icon="copied ? 'pi pi-check' : 'pi pi-copy'"
58
+ :severity="copied ? 'success' : 'secondary'"
59
+ size="small"
60
+ text
61
+ rounded
62
+ @click="copyToClipboard"
63
+ :aria-label="`Copy ${label}`"
64
+ class="copyable-id-button"
65
+ />
66
+ <code class="copyable-id-value">{{ value }}</code>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <style scoped>
72
+ .copyable-id {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: 0.5rem;
76
+ background: var(--p-surface-100);
77
+ padding: 0.5rem 0.75rem;
78
+ border-radius: 4px;
79
+ max-width: 100%;
80
+ }
81
+
82
+ .copyable-id-value {
83
+ font-family: monospace;
84
+ font-size: 0.875rem;
85
+ color: var(--p-surface-700);
86
+ word-break: break-all;
87
+ }
88
+
89
+ .copyable-id-button {
90
+ flex-shrink: 0;
91
+ }
92
+ </style>
@@ -0,0 +1,114 @@
1
+ <script setup>
2
+ /**
3
+ * EmptyState - Reusable empty state display
4
+ *
5
+ * Usage:
6
+ * <EmptyState
7
+ * icon="pi-users"
8
+ * title="No agents found"
9
+ * description="Assign agents from their profile page"
10
+ * />
11
+ *
12
+ * <!-- With slot for custom content -->
13
+ * <EmptyState icon="pi-inbox" title="No items">
14
+ * <Button label="Create Item" @click="create" />
15
+ * </EmptyState>
16
+ */
17
+
18
+ defineProps({
19
+ // PrimeIcon name (without "pi-" prefix if you want, both work)
20
+ icon: {
21
+ type: String,
22
+ default: 'inbox'
23
+ },
24
+ // Main message
25
+ title: {
26
+ type: String,
27
+ default: 'No items found'
28
+ },
29
+ // Secondary description (optional)
30
+ description: {
31
+ type: String,
32
+ default: ''
33
+ },
34
+ // Size variant
35
+ size: {
36
+ type: String,
37
+ default: 'md', // sm, md, lg
38
+ validator: (v) => ['sm', 'md', 'lg'].includes(v)
39
+ }
40
+ })
41
+
42
+ // Normalize icon name (accept both "pi-inbox" and "inbox")
43
+ function getIconClass(icon) {
44
+ if (icon.startsWith('pi-')) {
45
+ return `pi ${icon}`
46
+ }
47
+ return `pi pi-${icon}`
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <div class="empty-state" :class="`empty-state--${size}`">
53
+ <i :class="getIconClass(icon)" class="empty-state-icon" />
54
+ <p class="empty-state-title">{{ title }}</p>
55
+ <small v-if="description" class="empty-state-description">{{ description }}</small>
56
+ <div v-if="$slots.default" class="empty-state-actions">
57
+ <slot />
58
+ </div>
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .empty-state {
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: center;
67
+ justify-content: center;
68
+ text-align: center;
69
+ padding: 2rem;
70
+ color: var(--text-color-secondary);
71
+ }
72
+
73
+ .empty-state--sm {
74
+ padding: 1rem;
75
+ }
76
+
77
+ .empty-state--lg {
78
+ padding: 3rem;
79
+ }
80
+
81
+ .empty-state-icon {
82
+ font-size: 2rem;
83
+ margin-bottom: 0.5rem;
84
+ opacity: 0.5;
85
+ }
86
+
87
+ .empty-state--sm .empty-state-icon {
88
+ font-size: 1.5rem;
89
+ }
90
+
91
+ .empty-state--lg .empty-state-icon {
92
+ font-size: 3rem;
93
+ margin-bottom: 1rem;
94
+ }
95
+
96
+ .empty-state-title {
97
+ margin: 0 0 0.25rem 0;
98
+ font-weight: 500;
99
+ color: var(--text-color-secondary);
100
+ }
101
+
102
+ .empty-state--lg .empty-state-title {
103
+ font-size: 1.1rem;
104
+ }
105
+
106
+ .empty-state-description {
107
+ opacity: 0.7;
108
+ max-width: 300px;
109
+ }
110
+
111
+ .empty-state-actions {
112
+ margin-top: 1rem;
113
+ }
114
+ </style>
@@ -0,0 +1,171 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, inject } from 'vue'
3
+
4
+ const props = defineProps({
5
+ value: {
6
+ type: Number,
7
+ required: true
8
+ },
9
+ showValue: {
10
+ type: Boolean,
11
+ default: true
12
+ },
13
+ size: {
14
+ type: String,
15
+ default: 'md', // sm, md, lg
16
+ validator: (v) => ['sm', 'md', 'lg'].includes(v)
17
+ },
18
+ // Threshold configuration
19
+ threshold: {
20
+ type: Number,
21
+ default: null // If provided, use static threshold instead of loading from API
22
+ },
23
+ configEndpoint: {
24
+ type: String,
25
+ default: null // Endpoint to load threshold config from API
26
+ },
27
+ configThresholdKey: {
28
+ type: String,
29
+ default: 'threshold' // Key in config response containing threshold value
30
+ }
31
+ })
32
+
33
+ // Get API adapter (optional - only needed if loading from endpoint)
34
+ const api = inject('apiAdapter', null)
35
+
36
+ // Threshold value
37
+ const loadedThreshold = ref(8) // default fallback
38
+ const configLoaded = ref(false)
39
+
40
+ // Computed threshold - use prop if provided, otherwise loaded value
41
+ const currentThreshold = computed(() => props.threshold ?? loadedThreshold.value)
42
+
43
+ // Singleton pattern - load config once for all instances
44
+ let configPromise = null
45
+
46
+ async function loadConfig() {
47
+ // Skip if using static threshold or no endpoint configured
48
+ if (props.threshold !== null || !props.configEndpoint) {
49
+ configLoaded.value = true
50
+ return
51
+ }
52
+
53
+ // Skip if no API adapter
54
+ if (!api) {
55
+ console.warn('[IntensityBar] apiAdapter not provided, using default threshold')
56
+ configLoaded.value = true
57
+ return
58
+ }
59
+
60
+ if (configPromise) return configPromise
61
+
62
+ configPromise = api.request('GET', props.configEndpoint)
63
+ .then(data => {
64
+ if (data?.[props.configThresholdKey]) {
65
+ loadedThreshold.value = data[props.configThresholdKey]
66
+ }
67
+ configLoaded.value = true
68
+ })
69
+ .catch(() => {
70
+ console.warn('[IntensityBar] Could not load config, using defaults')
71
+ configLoaded.value = true
72
+ })
73
+
74
+ return configPromise
75
+ }
76
+
77
+ // Calculate bar properties based on threshold tiers
78
+ const barProps = computed(() => {
79
+ const threshold = currentThreshold.value
80
+ const tier = Math.floor(props.value / threshold)
81
+ const withinTier = props.value % threshold
82
+ const percentage = (withinTier / threshold) * 100
83
+
84
+ // Color: green (tier 0), orange (tier 1), red (tier 2+)
85
+ let color = 'var(--p-green-500)'
86
+ let severity = 'success'
87
+ if (tier === 1) {
88
+ color = 'var(--p-orange-500)'
89
+ severity = 'warning'
90
+ } else if (tier >= 2) {
91
+ color = 'var(--p-red-500)'
92
+ severity = 'danger'
93
+ }
94
+
95
+ return { percentage, tier, color, severity, threshold }
96
+ })
97
+
98
+ // Size classes
99
+ const sizeClass = computed(() => `intensity-bar--${props.size}`)
100
+
101
+ onMounted(() => {
102
+ loadConfig()
103
+ })
104
+ </script>
105
+
106
+ <template>
107
+ <div class="intensity-container" :class="sizeClass">
108
+ <div
109
+ class="intensity-bar"
110
+ :style="{ '--bar-color': barProps.color, '--bar-width': barProps.percentage + '%' }"
111
+ :title="`Tier ${barProps.tier} (threshold: ${barProps.threshold})`"
112
+ >
113
+ <div class="intensity-bar-fill"></div>
114
+ </div>
115
+ <span v-if="showValue" class="intensity-value" :style="{ color: barProps.color }">
116
+ {{ value.toFixed(2) }}
117
+ </span>
118
+ </div>
119
+ </template>
120
+
121
+ <style scoped>
122
+ .intensity-container {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ }
127
+
128
+ .intensity-bar {
129
+ background: var(--p-surface-200);
130
+ border-radius: 4px;
131
+ overflow: hidden;
132
+ /* Default size (md) */
133
+ width: 80px;
134
+ height: 8px;
135
+ }
136
+
137
+ .intensity-bar--sm .intensity-bar {
138
+ width: 60px;
139
+ height: 6px;
140
+ }
141
+
142
+ .intensity-bar--lg .intensity-bar {
143
+ width: 120px;
144
+ height: 10px;
145
+ }
146
+
147
+ .intensity-bar-fill {
148
+ height: 100%;
149
+ width: var(--bar-width);
150
+ background: var(--bar-color);
151
+ border-radius: 4px;
152
+ transition: width 0.3s ease, background 0.3s ease;
153
+ }
154
+
155
+ .intensity-value {
156
+ font-weight: 600;
157
+ min-width: 45px;
158
+ }
159
+
160
+ .intensity-bar--sm .intensity-value {
161
+ font-size: 0.8em;
162
+ }
163
+
164
+ .intensity-bar--md .intensity-value {
165
+ font-size: 0.9em;
166
+ }
167
+
168
+ .intensity-bar--lg .intensity-value {
169
+ font-size: 1em;
170
+ }
171
+ </style>
@@ -0,0 +1,220 @@
1
+ <script setup>
2
+ /**
3
+ * RichCardsGrid - Grid of rich stat cards with title, value, subtitle
4
+ *
5
+ * Props:
6
+ * - cards: Array of card objects
7
+ * - columns: Number of columns (2, 3, 4, or 'auto')
8
+ *
9
+ * Card object:
10
+ * {
11
+ * name: 'users', // unique key
12
+ * title: 'Users', // card title
13
+ * value: 42, // main value
14
+ * subtitle: '10 active', // optional subtitle
15
+ * icon: 'pi pi-users', // optional icon
16
+ * severity: 'success', // optional: success, danger, warning, info
17
+ * onClick: () => {}, // optional click handler
18
+ * to: '/dashboard/users', // optional router link
19
+ * custom: true // use slot instead
20
+ * }
21
+ */
22
+ import { useRouter } from 'vue-router'
23
+
24
+ const router = useRouter()
25
+
26
+ defineProps({
27
+ cards: {
28
+ type: Array,
29
+ default: () => []
30
+ },
31
+ columns: {
32
+ type: [Number, String],
33
+ default: 'auto',
34
+ validator: (v) => ['auto', 2, 3, 4, 5, 6].includes(v)
35
+ }
36
+ })
37
+
38
+ function handleClick(card) {
39
+ if (card.onClick) {
40
+ card.onClick()
41
+ } else if (card.to) {
42
+ router.push(card.to)
43
+ }
44
+ }
45
+
46
+ function isClickable(card) {
47
+ return card.onClick || card.to
48
+ }
49
+
50
+ function getSeverityClass(severity) {
51
+ if (!severity) return ''
52
+ return `rich-card--${severity}`
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div v-if="cards.length > 0" class="rich-cards-grid" :class="`rich-cards-grid--cols-${columns}`">
58
+ <template v-for="card in cards" :key="card.name">
59
+ <!-- Custom card: render slot -->
60
+ <div v-if="card.custom" class="rich-card rich-card--custom" :class="card.class">
61
+ <slot :name="`card-${card.name}`" :card="card" ></slot>
62
+ </div>
63
+
64
+ <!-- Rich stat card -->
65
+ <div
66
+ v-else
67
+ class="rich-card"
68
+ :class="[
69
+ getSeverityClass(card.severity),
70
+ { 'rich-card--clickable': isClickable(card) },
71
+ card.class
72
+ ]"
73
+ @click="handleClick(card)"
74
+ >
75
+ <div class="rich-card-header">
76
+ <span class="rich-card-title">{{ card.title }}</span>
77
+ <i v-if="card.icon" :class="card.icon" class="rich-card-icon"></i>
78
+ </div>
79
+ <div class="rich-card-value">{{ card.value }}</div>
80
+ <div v-if="card.subtitle" class="rich-card-subtitle">{{ card.subtitle }}</div>
81
+ </div>
82
+ </template>
83
+ </div>
84
+ </template>
85
+
86
+ <style scoped>
87
+ .rich-cards-grid {
88
+ display: grid;
89
+ gap: 1rem;
90
+ margin-bottom: 1.5rem;
91
+ }
92
+
93
+ .rich-cards-grid--cols-auto {
94
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
95
+ }
96
+
97
+ .rich-cards-grid--cols-2 {
98
+ grid-template-columns: repeat(2, 1fr);
99
+ }
100
+
101
+ .rich-cards-grid--cols-3 {
102
+ grid-template-columns: repeat(3, 1fr);
103
+ }
104
+
105
+ .rich-cards-grid--cols-4 {
106
+ grid-template-columns: repeat(4, 1fr);
107
+ }
108
+
109
+ .rich-cards-grid--cols-5 {
110
+ grid-template-columns: repeat(5, 1fr);
111
+ }
112
+
113
+ .rich-cards-grid--cols-6 {
114
+ grid-template-columns: repeat(6, 1fr);
115
+ }
116
+
117
+ .rich-card {
118
+ background: var(--p-surface-0);
119
+ border: 1px solid var(--p-surface-200);
120
+ border-radius: 0.5rem;
121
+ padding: 1.25rem;
122
+ }
123
+
124
+ .rich-card--clickable {
125
+ cursor: pointer;
126
+ transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
127
+ }
128
+
129
+ .rich-card--clickable:hover {
130
+ border-color: var(--p-primary-300);
131
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
132
+ transform: translateY(-2px);
133
+ }
134
+
135
+ .rich-card-header {
136
+ display: flex;
137
+ justify-content: space-between;
138
+ align-items: center;
139
+ margin-bottom: 0.75rem;
140
+ }
141
+
142
+ .rich-card-title {
143
+ font-size: 0.875rem;
144
+ font-weight: 500;
145
+ color: var(--p-surface-600);
146
+ }
147
+
148
+ .rich-card-icon {
149
+ font-size: 1.25rem;
150
+ color: var(--p-surface-400);
151
+ }
152
+
153
+ .rich-card-value {
154
+ font-size: 2rem;
155
+ font-weight: 700;
156
+ color: var(--p-surface-900);
157
+ line-height: 1.2;
158
+ }
159
+
160
+ .rich-card-subtitle {
161
+ font-size: 0.8rem;
162
+ color: var(--p-surface-500);
163
+ margin-top: 0.25rem;
164
+ }
165
+
166
+ /* Severity variants */
167
+ .rich-card--success .rich-card-value {
168
+ color: var(--p-green-600);
169
+ }
170
+
171
+ .rich-card--success .rich-card-icon {
172
+ color: var(--p-green-500);
173
+ }
174
+
175
+ .rich-card--danger .rich-card-value {
176
+ color: var(--p-red-600);
177
+ }
178
+
179
+ .rich-card--danger .rich-card-icon {
180
+ color: var(--p-red-500);
181
+ }
182
+
183
+ .rich-card--warning .rich-card-value {
184
+ color: var(--p-orange-600);
185
+ }
186
+
187
+ .rich-card--warning .rich-card-icon {
188
+ color: var(--p-orange-500);
189
+ }
190
+
191
+ .rich-card--info .rich-card-value {
192
+ color: var(--p-blue-600);
193
+ }
194
+
195
+ .rich-card--info .rich-card-icon {
196
+ color: var(--p-blue-500);
197
+ }
198
+
199
+ @media (max-width: 1024px) {
200
+ .rich-cards-grid--cols-5,
201
+ .rich-cards-grid--cols-6 {
202
+ grid-template-columns: repeat(3, 1fr);
203
+ }
204
+ }
205
+
206
+ @media (max-width: 768px) {
207
+ .rich-cards-grid--cols-3,
208
+ .rich-cards-grid--cols-4,
209
+ .rich-cards-grid--cols-5,
210
+ .rich-cards-grid--cols-6 {
211
+ grid-template-columns: repeat(2, 1fr);
212
+ }
213
+ }
214
+
215
+ @media (max-width: 480px) {
216
+ .rich-cards-grid {
217
+ grid-template-columns: 1fr;
218
+ }
219
+ }
220
+ </style>