@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.8
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/dist/nextcloud-vue.cjs.js +13575 -2374
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +1238 -270
- package/dist/nextcloud-vue.esm.js +13517 -2336
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +11 -7
- package/src/components/CnActionsBar/CnActionsBar.vue +20 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
- package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
- package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +6 -2
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
- package/src/components/CnIcon/CnIcon.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +63 -9
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
- package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +5 -0
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
- package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
- package/src/components/index.js +11 -0
- package/src/composables/useDashboardView.js +58 -12
- package/src/composables/useDetailView.js +3 -2
- package/src/composables/useListView.js +7 -6
- package/src/composables/useSubResource.js +3 -3
- package/src/css/badge.css +32 -0
- package/src/css/card.css +1 -0
- package/src/css/detail-page.css +74 -7
- package/src/index.js +16 -0
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +5 -15
- package/src/store/index.js +1 -0
- package/src/store/plugins/auditTrails.js +346 -6
- package/src/store/plugins/lifecycle.js +4 -4
- package/src/store/plugins/registerMapping.js +18 -8
- package/src/store/plugins/relations.js +1 -1
- package/src/store/plugins/search.js +21 -8
- package/src/store/useObjectStore.js +30 -36
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +13 -3
- package/src/utils/index.js +1 -0
- package/src/utils/schema.js +3 -3
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnObjectCard/eslint-setup.md +0 -235
- package/src/components/CnObjectCard/package.json-or.json +0 -132
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cn-progress-bar">
|
|
3
|
+
<div class="cn-progress-bar__items">
|
|
4
|
+
<div
|
|
5
|
+
v-for="(item, index) in items"
|
|
6
|
+
:key="item.key || index"
|
|
7
|
+
class="cn-progress-bar__item">
|
|
8
|
+
<div class="cn-progress-bar__label">
|
|
9
|
+
<span
|
|
10
|
+
class="cn-progress-bar__name"
|
|
11
|
+
:class="{ 'cn-progress-bar__name--tooltip': item.tooltip }"
|
|
12
|
+
:title="item.tooltip || undefined">
|
|
13
|
+
<slot :name="'label-' + (item.key || index)" :item="item">
|
|
14
|
+
{{ item.label }}
|
|
15
|
+
</slot>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="cn-progress-bar__value">
|
|
18
|
+
<slot :name="'value-' + (item.key || index)" :item="item" :percentage="getPercentage(item)">
|
|
19
|
+
{{ formatValue(item) }}
|
|
20
|
+
</slot>
|
|
21
|
+
</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div
|
|
24
|
+
class="cn-progress-bar__track"
|
|
25
|
+
:class="{ 'cn-progress-bar__track--rounded': rounded }"
|
|
26
|
+
:style="{ height: barHeight + 'px' }">
|
|
27
|
+
<div
|
|
28
|
+
class="cn-progress-bar__fill"
|
|
29
|
+
:class="[
|
|
30
|
+
'cn-progress-bar__fill--' + resolveVariant(item),
|
|
31
|
+
{ 'cn-progress-bar__fill--rounded': rounded },
|
|
32
|
+
]"
|
|
33
|
+
:style="{ width: getPercentage(item) + '%' }" />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
/**
|
|
42
|
+
* CnProgressBar — Labeled horizontal progress bars with variant colors.
|
|
43
|
+
*
|
|
44
|
+
* Renders a list of items as labeled progress bars. Each item has a label, a count,
|
|
45
|
+
* and a percentage-width fill bar. Percentages are calculated automatically from the
|
|
46
|
+
* items' count values relative to the total (sum of all counts), or can be provided
|
|
47
|
+
* explicitly via `item.percentage`.
|
|
48
|
+
*
|
|
49
|
+
* Suitable for distribution visualizations (e.g., query complexity, action breakdown,
|
|
50
|
+
* status distribution).
|
|
51
|
+
*
|
|
52
|
+
* @example Basic usage
|
|
53
|
+
* <CnProgressBar :items="[
|
|
54
|
+
* { key: 'simple', label: 'Simple', count: 50, variant: 'success' },
|
|
55
|
+
* { key: 'medium', label: 'Medium', count: 30, variant: 'warning' },
|
|
56
|
+
* { key: 'complex', label: 'Complex', count: 20, variant: 'error' },
|
|
57
|
+
* ]" />
|
|
58
|
+
*
|
|
59
|
+
* @example With explicit percentages
|
|
60
|
+
* <CnProgressBar :items="[
|
|
61
|
+
* { key: 'cpu', label: 'CPU', percentage: 72, variant: 'warning' },
|
|
62
|
+
* { key: 'mem', label: 'Memory', percentage: 45, variant: 'success' },
|
|
63
|
+
* ]" show-percentage />
|
|
64
|
+
*
|
|
65
|
+
* @example With tooltips
|
|
66
|
+
* <CnProgressBar :items="[
|
|
67
|
+
* { key: 'simple', label: 'Simple', count: 50, variant: 'success', tooltip: 'Basic text searches' },
|
|
68
|
+
* ]" />
|
|
69
|
+
*/
|
|
70
|
+
export default {
|
|
71
|
+
name: 'CnProgressBar',
|
|
72
|
+
|
|
73
|
+
props: {
|
|
74
|
+
/**
|
|
75
|
+
* Array of progress bar items.
|
|
76
|
+
*
|
|
77
|
+
* Each item: `{ key?, label, count?, percentage?, variant?, tooltip? }`
|
|
78
|
+
*
|
|
79
|
+
* - `key` — Unique identifier (optional, falls back to index)
|
|
80
|
+
* - `label` — Display label
|
|
81
|
+
* - `count` — Numeric value (used to calculate percentage from total if `percentage` not given)
|
|
82
|
+
* - `total` — Per-item total for percentage calculation (e.g. count=50, total=500 → 10%). Defaults to sum of all item counts.
|
|
83
|
+
* - `percentage` — Explicit percentage (0-100), overrides both count and total
|
|
84
|
+
* - `variant` — Color variant: a string ('default'|'primary'|'success'|'warning'|'error'|'info') or a function `({ item, count, total, percentage }) => string` for dynamic resolution
|
|
85
|
+
* - `tooltip` — Hover tooltip text for the label
|
|
86
|
+
*/
|
|
87
|
+
items: {
|
|
88
|
+
type: Array,
|
|
89
|
+
default: () => [],
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default color variant for all bars. Individual items can override via `item.variant`.
|
|
94
|
+
* One of: 'default', 'primary', 'success', 'warning', 'error', 'info'
|
|
95
|
+
*/
|
|
96
|
+
variant: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: 'primary',
|
|
99
|
+
validator: (v) => ['default', 'primary', 'success', 'warning', 'error', 'info'].includes(v),
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** Height of the progress bar track in pixels */
|
|
103
|
+
barHeight: {
|
|
104
|
+
type: Number,
|
|
105
|
+
default: 8,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** Whether to use rounded corners on the track and fill */
|
|
109
|
+
rounded: {
|
|
110
|
+
type: Boolean,
|
|
111
|
+
default: true,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** Whether to show the percentage value instead of the count */
|
|
115
|
+
showPercentage: {
|
|
116
|
+
type: Boolean,
|
|
117
|
+
default: false,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
computed: {
|
|
122
|
+
/**
|
|
123
|
+
* Total count across all items (for percentage calculation).
|
|
124
|
+
* @return {number}
|
|
125
|
+
*/
|
|
126
|
+
totalCount() {
|
|
127
|
+
return this.items.reduce((sum, item) => sum + (item.count || 0), 0)
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
methods: {
|
|
132
|
+
/**
|
|
133
|
+
* Get the percentage for an item.
|
|
134
|
+
* Uses item.percentage if provided, otherwise calculates from count/total.
|
|
135
|
+
* @param {object} item - The progress bar item
|
|
136
|
+
* @return {number} Percentage (0-100)
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the variant for an item. Supports string or function.
|
|
140
|
+
* @param {object} item - The progress bar item
|
|
141
|
+
* @return {string} Resolved variant name
|
|
142
|
+
*/
|
|
143
|
+
resolveVariant(item) {
|
|
144
|
+
const itemVariant = item.variant
|
|
145
|
+
if (typeof itemVariant === 'function') {
|
|
146
|
+
return itemVariant({
|
|
147
|
+
item,
|
|
148
|
+
count: item.count ?? 0,
|
|
149
|
+
total: item.total ?? this.totalCount,
|
|
150
|
+
percentage: this.getPercentage(item),
|
|
151
|
+
}) || this.variant
|
|
152
|
+
}
|
|
153
|
+
return itemVariant || this.variant
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
getPercentage(item) {
|
|
157
|
+
if (item.percentage !== undefined && item.percentage !== null) {
|
|
158
|
+
return Math.min(100, Math.max(0, item.percentage))
|
|
159
|
+
}
|
|
160
|
+
const total = (item.total !== undefined && item.total !== null) ? item.total : this.totalCount
|
|
161
|
+
if (total === 0) return 0
|
|
162
|
+
return Math.round(((item.count || 0) / total) * 100)
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Format the display value for an item.
|
|
167
|
+
* @param {object} item - The progress bar item
|
|
168
|
+
* @return {string} Formatted value
|
|
169
|
+
*/
|
|
170
|
+
formatValue(item) {
|
|
171
|
+
if (this.showPercentage) {
|
|
172
|
+
return `${this.getPercentage(item)}%`
|
|
173
|
+
}
|
|
174
|
+
return String(item.count ?? 0)
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<style scoped>
|
|
181
|
+
.cn-progress-bar__items {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
gap: 8px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.cn-progress-bar__item {
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
gap: 4px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.cn-progress-bar__label {
|
|
194
|
+
display: flex;
|
|
195
|
+
justify-content: space-between;
|
|
196
|
+
align-items: center;
|
|
197
|
+
font-size: 0.9rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.cn-progress-bar__name {
|
|
201
|
+
color: var(--color-main-text);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.cn-progress-bar__name--tooltip {
|
|
205
|
+
cursor: help;
|
|
206
|
+
text-decoration: underline;
|
|
207
|
+
text-decoration-style: dotted;
|
|
208
|
+
text-underline-offset: 2px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.cn-progress-bar__name--tooltip:hover {
|
|
212
|
+
text-decoration-style: solid;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.cn-progress-bar__value {
|
|
216
|
+
color: var(--color-text-maxcontrast);
|
|
217
|
+
font-variant-numeric: tabular-nums;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.cn-progress-bar__track {
|
|
221
|
+
background: var(--color-background-dark, var(--color-background-darker));
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.cn-progress-bar__track--rounded {
|
|
226
|
+
border-radius: 4px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.cn-progress-bar__fill {
|
|
230
|
+
height: 100%;
|
|
231
|
+
transition: width 0.3s ease;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.cn-progress-bar__fill--rounded {
|
|
235
|
+
border-radius: 4px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Variant colors */
|
|
239
|
+
.cn-progress-bar__fill--default {
|
|
240
|
+
background: var(--color-main-text);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.cn-progress-bar__fill--primary {
|
|
244
|
+
background: var(--color-primary-element);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.cn-progress-bar__fill--success {
|
|
248
|
+
background: var(--color-success);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.cn-progress-bar__fill--warning {
|
|
252
|
+
background: var(--color-warning);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.cn-progress-bar__fill--error {
|
|
256
|
+
background: var(--color-error);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cn-progress-bar__fill--info {
|
|
260
|
+
background: var(--color-info, #0082c9);
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnProgressBar } from './CnProgressBar.vue'
|
|
@@ -179,7 +179,7 @@ export default {
|
|
|
179
179
|
sortedProperties() {
|
|
180
180
|
const properties = this.schema.properties || {}
|
|
181
181
|
return Object.entries(properties)
|
|
182
|
-
.sort(([
|
|
182
|
+
.sort(([, propA], [, propB]) => {
|
|
183
183
|
const orderA = propA.order || 0
|
|
184
184
|
const orderB = propB.order || 0
|
|
185
185
|
if (orderA > 0 && orderB > 0) {
|
|
@@ -259,16 +259,22 @@ export default {
|
|
|
259
259
|
color: inherit;
|
|
260
260
|
border: 2px solid transparent;
|
|
261
261
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
262
|
+
height: 100%;
|
|
263
|
+
width: 100%;
|
|
264
|
+
box-sizing: border-box;
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
min-width: 0;
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
.cn-stats-block--horizontal {
|
|
265
270
|
flex-direction: row;
|
|
266
|
-
align-items:
|
|
267
|
-
gap:
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 12px;
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
.cn-stats-block--horizontal .cn-stats-block__content {
|
|
271
276
|
text-align: left;
|
|
277
|
+
min-width: 0;
|
|
272
278
|
}
|
|
273
279
|
|
|
274
280
|
.cn-stats-block--horizontal .cn-stats-block__count {
|
|
@@ -305,6 +311,8 @@ export default {
|
|
|
305
311
|
|
|
306
312
|
.cn-stats-block--horizontal .cn-stats-block__icon {
|
|
307
313
|
margin-bottom: 0;
|
|
314
|
+
width: 36px;
|
|
315
|
+
height: 36px;
|
|
308
316
|
}
|
|
309
317
|
|
|
310
318
|
.cn-stats-block__icon--primary {
|
|
@@ -314,17 +322,17 @@ export default {
|
|
|
314
322
|
|
|
315
323
|
.cn-stats-block__icon--success {
|
|
316
324
|
background: rgba(70, 186, 97, 0.1);
|
|
317
|
-
color: var(--color-success);
|
|
325
|
+
color: var(--color-element-success, var(--color-success));
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
.cn-stats-block__icon--warning {
|
|
321
329
|
background: rgba(232, 163, 24, 0.1);
|
|
322
|
-
color: var(--color-warning);
|
|
330
|
+
color: var(--color-element-warning, var(--color-warning));
|
|
323
331
|
}
|
|
324
332
|
|
|
325
333
|
.cn-stats-block__icon--error {
|
|
326
334
|
background: rgba(224, 36, 36, 0.1);
|
|
327
|
-
color: var(--color-error);
|
|
335
|
+
color: var(--color-element-error, var(--color-error));
|
|
328
336
|
}
|
|
329
337
|
|
|
330
338
|
/* Content */
|
|
@@ -336,34 +344,42 @@ export default {
|
|
|
336
344
|
|
|
337
345
|
.cn-stats-block__header h4 {
|
|
338
346
|
margin-top: 0;
|
|
339
|
-
margin-bottom: 0.
|
|
347
|
+
margin-bottom: 0.25rem;
|
|
340
348
|
color: var(--color-main-text);
|
|
341
349
|
font-size: 14px;
|
|
342
350
|
font-weight: 600;
|
|
351
|
+
white-space: nowrap;
|
|
352
|
+
overflow: hidden;
|
|
353
|
+
text-overflow: ellipsis;
|
|
343
354
|
}
|
|
344
355
|
|
|
345
356
|
.cn-stats-block__count {
|
|
346
357
|
display: flex;
|
|
347
358
|
align-items: baseline;
|
|
348
359
|
justify-content: center;
|
|
349
|
-
gap: 0.
|
|
360
|
+
gap: 0.25rem;
|
|
350
361
|
font-size: 1.2rem;
|
|
351
|
-
margin-bottom: 0.
|
|
362
|
+
margin-bottom: 0.25rem;
|
|
363
|
+
white-space: nowrap;
|
|
364
|
+
overflow: hidden;
|
|
352
365
|
}
|
|
353
366
|
|
|
354
367
|
.cn-stats-block__count-value {
|
|
355
368
|
font-size: 2rem;
|
|
356
369
|
font-weight: bold;
|
|
357
370
|
color: var(--color-primary-element);
|
|
371
|
+
flex-shrink: 0;
|
|
358
372
|
}
|
|
359
373
|
|
|
360
374
|
.cn-stats-block--primary .cn-stats-block__count-value { color: var(--color-primary-element); }
|
|
361
|
-
.cn-stats-block--success .cn-stats-block__count-value { color: var(--color-success); }
|
|
362
|
-
.cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-warning); }
|
|
363
|
-
.cn-stats-block--error .cn-stats-block__count-value { color: var(--color-error); }
|
|
375
|
+
.cn-stats-block--success .cn-stats-block__count-value { color: var(--color-element-success, var(--color-success)); }
|
|
376
|
+
.cn-stats-block--warning .cn-stats-block__count-value { color: var(--color-element-warning, var(--color-warning)); }
|
|
377
|
+
.cn-stats-block--error .cn-stats-block__count-value { color: var(--color-element-error, var(--color-error)); }
|
|
364
378
|
|
|
365
379
|
.cn-stats-block__count-label {
|
|
366
380
|
color: var(--color-text-maxcontrast);
|
|
381
|
+
overflow: hidden;
|
|
382
|
+
text-overflow: ellipsis;
|
|
367
383
|
}
|
|
368
384
|
|
|
369
385
|
.cn-stats-block__loading {
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cn-stats-panel">
|
|
3
|
+
<!-- Header slot for filters/selectors -->
|
|
4
|
+
<div v-if="$slots.header" class="cn-stats-panel__header">
|
|
5
|
+
<slot name="header" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Global loading state -->
|
|
9
|
+
<div v-if="loading" class="cn-stats-panel__loading">
|
|
10
|
+
<NcLoadingIcon :size="20" />
|
|
11
|
+
<span>{{ loadingLabel }}</span>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Sections -->
|
|
15
|
+
<template v-else>
|
|
16
|
+
<div
|
|
17
|
+
v-for="section in sections"
|
|
18
|
+
:key="section.id"
|
|
19
|
+
class="cn-stats-panel__section">
|
|
20
|
+
<!-- Section title -->
|
|
21
|
+
<h4 v-if="section.title" class="cn-stats-panel__section-title">
|
|
22
|
+
{{ section.title }}
|
|
23
|
+
</h4>
|
|
24
|
+
|
|
25
|
+
<!-- Section-level loading -->
|
|
26
|
+
<div v-if="section.loading" class="cn-stats-panel__loading">
|
|
27
|
+
<NcLoadingIcon :size="20" />
|
|
28
|
+
<span>{{ loadingLabel }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Empty section -->
|
|
32
|
+
<div v-else-if="!section.items || !section.items.length" class="cn-stats-panel__empty">
|
|
33
|
+
{{ section.emptyLabel || emptyLabel }}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Stats section -->
|
|
37
|
+
<template v-else-if="section.type === 'stats'">
|
|
38
|
+
<slot :name="'section-' + section.id" :section="section">
|
|
39
|
+
<!-- Stack layout -->
|
|
40
|
+
<div v-if="section.layout === 'stack'" class="cn-stats-panel__stack">
|
|
41
|
+
<CnStatsBlock
|
|
42
|
+
v-for="(item, index) in section.items"
|
|
43
|
+
:key="index"
|
|
44
|
+
:title="item.title"
|
|
45
|
+
:count="item.count"
|
|
46
|
+
:count-label="item.countLabel"
|
|
47
|
+
:variant="item.variant || 'default'"
|
|
48
|
+
:icon="isComponentIcon(item.icon) ? item.icon : null"
|
|
49
|
+
:icon-size="item.iconSize || 24"
|
|
50
|
+
:horizontal="item.horizontal !== undefined ? item.horizontal : true"
|
|
51
|
+
:show-zero-count="item.showZeroCount !== undefined ? item.showZeroCount : true"
|
|
52
|
+
:breakdown="item.breakdown || null"
|
|
53
|
+
:route="item.route || null"
|
|
54
|
+
:clickable="item.clickable || false"
|
|
55
|
+
:loading="item.loading || false"
|
|
56
|
+
@click="$emit('stat-click', { section: section.id, item, index })">
|
|
57
|
+
<template v-if="typeof item.icon === 'string'" #icon>
|
|
58
|
+
<CnIcon :name="item.icon" :size="item.iconSize || 24" />
|
|
59
|
+
</template>
|
|
60
|
+
</CnStatsBlock>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Grid layout -->
|
|
64
|
+
<CnKpiGrid
|
|
65
|
+
v-else-if="section.layout === 'grid'"
|
|
66
|
+
grid-class="remove-margin"
|
|
67
|
+
:columns="section.columns || 2">
|
|
68
|
+
<CnStatsBlock
|
|
69
|
+
v-for="(item, index) in section.items"
|
|
70
|
+
:key="index"
|
|
71
|
+
:title="item.title"
|
|
72
|
+
:count="item.count"
|
|
73
|
+
:count-label="item.countLabel"
|
|
74
|
+
:variant="item.variant || 'default'"
|
|
75
|
+
:icon="isComponentIcon(item.icon) ? item.icon : null"
|
|
76
|
+
:icon-size="item.iconSize || 24"
|
|
77
|
+
:horizontal="item.horizontal !== undefined ? item.horizontal : false"
|
|
78
|
+
:show-zero-count="item.showZeroCount !== undefined ? item.showZeroCount : true"
|
|
79
|
+
:breakdown="item.breakdown || null"
|
|
80
|
+
:route="item.route || null"
|
|
81
|
+
:clickable="item.clickable || false"
|
|
82
|
+
:loading="item.loading || false"
|
|
83
|
+
@click="$emit('stat-click', { section: section.id, item, index })">
|
|
84
|
+
<template v-if="typeof item.icon === 'string'" #icon>
|
|
85
|
+
<CnIcon :name="item.icon" :size="item.iconSize || 24" />
|
|
86
|
+
</template>
|
|
87
|
+
</CnStatsBlock>
|
|
88
|
+
</CnKpiGrid>
|
|
89
|
+
</slot>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<!-- Progress section -->
|
|
93
|
+
<template v-else-if="section.type === 'progress'">
|
|
94
|
+
<slot :name="'section-' + section.id" :section="section">
|
|
95
|
+
<CnProgressBar
|
|
96
|
+
:items="section.items"
|
|
97
|
+
:variant="section.variant || 'primary'"
|
|
98
|
+
:bar-height="section.barHeight || 8"
|
|
99
|
+
:rounded="section.rounded !== undefined ? section.rounded : true"
|
|
100
|
+
:show-percentage="section.showPercentage || false" />
|
|
101
|
+
</slot>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<!-- List section -->
|
|
105
|
+
<template v-else-if="section.type === 'list'">
|
|
106
|
+
<slot :name="'section-' + section.id" :section="section">
|
|
107
|
+
<div class="cn-stats-panel__list">
|
|
108
|
+
<NcListItem
|
|
109
|
+
v-for="item in section.items"
|
|
110
|
+
:key="item.key"
|
|
111
|
+
:name="item.name"
|
|
112
|
+
:bold="item.bold || false"
|
|
113
|
+
@click="$emit('list-click', { section: section.id, item })">
|
|
114
|
+
<template #icon>
|
|
115
|
+
<slot :name="'item-icon-' + section.id" :item="item">
|
|
116
|
+
<CnIcon
|
|
117
|
+
v-if="typeof item.icon === 'string'"
|
|
118
|
+
:name="item.icon"
|
|
119
|
+
:size="item.iconSize || 32" />
|
|
120
|
+
<component
|
|
121
|
+
:is="item.icon"
|
|
122
|
+
v-else-if="item.icon"
|
|
123
|
+
:size="item.iconSize || 32" />
|
|
124
|
+
</slot>
|
|
125
|
+
</template>
|
|
126
|
+
<template #subname>
|
|
127
|
+
<slot :name="'item-subname-' + section.id" :item="item">
|
|
128
|
+
{{ item.subname }}
|
|
129
|
+
</slot>
|
|
130
|
+
</template>
|
|
131
|
+
</NcListItem>
|
|
132
|
+
</div>
|
|
133
|
+
</slot>
|
|
134
|
+
</template>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<!-- Footer slot -->
|
|
139
|
+
<div v-if="$slots.footer" class="cn-stats-panel__footer">
|
|
140
|
+
<slot name="footer" />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
144
|
+
|
|
145
|
+
<script>
|
|
146
|
+
import { NcLoadingIcon, NcListItem } from '@nextcloud/vue'
|
|
147
|
+
import { CnStatsBlock } from '../CnStatsBlock/index.js'
|
|
148
|
+
import { CnKpiGrid } from '../CnKpiGrid/index.js'
|
|
149
|
+
import { CnIcon } from '../CnIcon/index.js'
|
|
150
|
+
import { CnProgressBar } from '../CnProgressBar/index.js'
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* CnStatsPanel — Configurable statistics panel with sections of stat blocks and list items.
|
|
154
|
+
*
|
|
155
|
+
* Renders statistics content from a declarative sections array. Each section can be
|
|
156
|
+
* either a 'stats' section (renders CnStatsBlocks in stack or grid layout) or a
|
|
157
|
+
* 'list' section (renders NcListItems). Suitable for sidebar tabs, dashboard widgets,
|
|
158
|
+
* or any panel that displays statistics.
|
|
159
|
+
*
|
|
160
|
+
* @example Stats stack (vertical)
|
|
161
|
+
* <CnStatsPanel :sections="[{
|
|
162
|
+
* type: 'stats',
|
|
163
|
+
* id: 'totals',
|
|
164
|
+
* title: 'System Totals',
|
|
165
|
+
* layout: 'stack',
|
|
166
|
+
* items: [
|
|
167
|
+
* { title: 'Objects', count: 42, countLabel: 'objects', variant: 'primary', icon: PackageIcon },
|
|
168
|
+
* { title: 'Files', count: 128, countLabel: 'files', icon: FileIcon },
|
|
169
|
+
* ],
|
|
170
|
+
* }]" />
|
|
171
|
+
*
|
|
172
|
+
* @example Stats grid (2-column)
|
|
173
|
+
* <CnStatsPanel :sections="[{
|
|
174
|
+
* type: 'stats',
|
|
175
|
+
* id: 'operations',
|
|
176
|
+
* title: 'Operations',
|
|
177
|
+
* layout: 'grid',
|
|
178
|
+
* columns: 2,
|
|
179
|
+
* items: [
|
|
180
|
+
* { title: 'Create', count: 10, countLabel: 'ops', variant: 'success', icon: PlusIcon },
|
|
181
|
+
* { title: 'Delete', count: 3, countLabel: 'ops', variant: 'error', icon: DeleteIcon },
|
|
182
|
+
* ],
|
|
183
|
+
* }]" />
|
|
184
|
+
*
|
|
185
|
+
* @example List section
|
|
186
|
+
* <CnStatsPanel :sections="[{
|
|
187
|
+
* type: 'list',
|
|
188
|
+
* id: 'topObjects',
|
|
189
|
+
* title: 'Most Active',
|
|
190
|
+
* items: [
|
|
191
|
+
* { key: '1', name: 'Object A', subname: '42 entries', icon: CogIcon },
|
|
192
|
+
* ],
|
|
193
|
+
* }]" />
|
|
194
|
+
*
|
|
195
|
+
* @example With header slot for filters
|
|
196
|
+
* <CnStatsPanel :sections="sections">
|
|
197
|
+
* <template #header>
|
|
198
|
+
* <NcSelect v-bind="registerOptions" />
|
|
199
|
+
* </template>
|
|
200
|
+
* </CnStatsPanel>
|
|
201
|
+
*/
|
|
202
|
+
export default {
|
|
203
|
+
name: 'CnStatsPanel',
|
|
204
|
+
|
|
205
|
+
components: {
|
|
206
|
+
NcLoadingIcon,
|
|
207
|
+
NcListItem,
|
|
208
|
+
CnStatsBlock,
|
|
209
|
+
CnKpiGrid,
|
|
210
|
+
CnIcon,
|
|
211
|
+
CnProgressBar,
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
props: {
|
|
215
|
+
/**
|
|
216
|
+
* Array of section definitions to render.
|
|
217
|
+
* Each section has a `type` of 'stats', 'list', or 'progress'.
|
|
218
|
+
*
|
|
219
|
+
* Stats sections: `{ type: 'stats', id, title, layout: 'stack'|'grid', columns?, loading?, items: StatItem[] }`
|
|
220
|
+
* List sections: `{ type: 'list', id, title, loading?, items: ListItem[] }`
|
|
221
|
+
* Progress sections: `{ type: 'progress', id, title, variant?, barHeight?, rounded?, showPercentage?, loading?, items: ProgressItem[] }`
|
|
222
|
+
*
|
|
223
|
+
* StatItem: `{ title, count, countLabel, variant?, icon?, iconSize?, horizontal?, showZeroCount?, breakdown?, route?, clickable?, loading? }`
|
|
224
|
+
* ListItem: `{ key, name, subname?, bold?, icon?, iconSize? }`
|
|
225
|
+
* ProgressItem: `{ key?, label, count?, percentage?, variant?, tooltip? }`
|
|
226
|
+
*/
|
|
227
|
+
sections: {
|
|
228
|
+
type: Array,
|
|
229
|
+
default: () => [],
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/** Whether the entire panel is in a loading state */
|
|
233
|
+
loading: {
|
|
234
|
+
type: Boolean,
|
|
235
|
+
default: false,
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/** Label shown during loading state */
|
|
239
|
+
loadingLabel: {
|
|
240
|
+
type: String,
|
|
241
|
+
default: 'Loading...',
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/** Default text shown when a section has no items. Can be overridden per section via `section.emptyLabel`. */
|
|
245
|
+
emptyLabel: {
|
|
246
|
+
type: String,
|
|
247
|
+
default: 'No data available',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
emits: ['stat-click', 'list-click', 'progress-click'],
|
|
252
|
+
|
|
253
|
+
methods: {
|
|
254
|
+
/**
|
|
255
|
+
* Check if an icon value is a component reference (not a string name).
|
|
256
|
+
* @param {*} icon - Icon value to check
|
|
257
|
+
* @return {boolean}
|
|
258
|
+
*/
|
|
259
|
+
isComponentIcon(icon) {
|
|
260
|
+
return icon != null && typeof icon !== 'string'
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
</script>
|
|
265
|
+
|
|
266
|
+
<style scoped>
|
|
267
|
+
.cn-stats-panel__section {
|
|
268
|
+
padding: 12px 0;
|
|
269
|
+
border-bottom: 1px solid var(--color-border);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.remove-margin {
|
|
273
|
+
margin: 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.cn-stats-panel__section:last-child {
|
|
277
|
+
border-bottom: none;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.cn-stats-panel__section-title {
|
|
281
|
+
color: var(--color-text-maxcontrast);
|
|
282
|
+
font-size: 14px;
|
|
283
|
+
font-weight: bold;
|
|
284
|
+
padding: 0 16px;
|
|
285
|
+
margin: 0 0 12px 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.cn-stats-panel__stack {
|
|
289
|
+
display: flex;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
gap: 12px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cn-stats-panel__loading {
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 8px;
|
|
298
|
+
padding: 0 16px;
|
|
299
|
+
color: var(--color-text-maxcontrast);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.cn-stats-panel__empty {
|
|
303
|
+
padding: 0 16px;
|
|
304
|
+
color: var(--color-text-maxcontrast);
|
|
305
|
+
font-style: italic;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.cn-stats-panel__header {
|
|
309
|
+
padding-bottom: 12px;
|
|
310
|
+
border-bottom: 1px solid var(--color-border);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.cn-stats-panel__footer {
|
|
314
|
+
padding-top: 12px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.cn-stats-panel__list {
|
|
318
|
+
margin-top: 4px;
|
|
319
|
+
}
|
|
320
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnStatsPanel } from './CnStatsPanel.vue'
|