@mixd-id/web-scaffold 0.2.250801014 → 0.2.260801002
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 +25 -1
- package/docs/components/AIDashboard.md +117 -32
- package/docs/components/Alert.md +3 -1
- package/docs/components/Card.md +5 -0
- package/docs/components/PivotList.md +207 -0
- package/docs/components/Textbox.md +20 -1
- package/docs/components/Tooltip.md +148 -0
- package/package.json +17 -61
- package/src/components/AIChat.vue +14 -5
- package/src/components/AIChatItem.vue +34 -3
- package/src/components/AIDashboard.vue +144 -98
- package/src/components/Alert.vue +58 -35
- package/src/components/Card.vue +55 -6
- package/src/components/Carousel.vue +1 -1
- package/src/{widgets → components}/CartSetting.vue +1 -0
- package/src/components/Chart.vue +2 -2
- package/src/components/Checkbox.vue +22 -24
- package/src/components/CodeEditor.vue +2 -0
- package/src/{widgets → components}/CollapsiblePanelSetting.vue +1 -0
- package/src/components/ColorPicker.vue +2 -2
- package/src/{widgets → components}/ComponentSetting2.vue +1 -1
- package/src/components/Confirm.vue +2 -0
- package/src/{widgets → components}/ContactForm.vue +1 -1
- package/src/components/CopyToClipboard.vue +0 -1
- package/src/components/Datepicker.vue +2 -0
- package/src/components/Datetimepicker.vue +1 -1
- package/src/components/Dropdown.vue +1 -1
- package/src/{widgets → components}/FeatureList.vue +1 -1
- package/src/components/Gmaps.vue +1 -1
- package/src/{widgets → components}/IconList.vue +1 -1
- package/src/components/Image.vue +1 -1
- package/src/components/ImagePreview.vue +32 -51
- package/src/components/List.vue +45 -8
- package/src/components/MarkdownEdit.vue +1 -1
- package/src/components/MarkdownPreview.vue +4 -1
- package/src/{widgets → components}/MenuItem1Setting.vue +1 -0
- package/src/components/Metric.vue +30 -7
- package/src/components/MultiDropdown.vue +3 -1
- package/src/{widgets → components}/MultiValueSetting.vue +2 -2
- package/src/{widgets → components}/MultiValueSetting2.vue +2 -2
- package/src/components/OTPField.vue +3 -1
- package/src/components/PivotList.vue +1239 -0
- package/src/{widgets → components}/PresetBar.vue +1 -1
- package/src/components/PresetSelectorFilterItem.vue +1 -1
- package/src/components/Radio.vue +1 -1
- package/src/{widgets → components}/Share.vue +1 -1
- package/src/components/SvgEditor.vue +1 -1
- package/src/components/Switch.vue +1 -1
- package/src/components/TextWithTag.vue +2 -2
- package/src/components/Textbox.vue +63 -5
- package/src/components/Toast.vue +2 -1
- package/src/components/Tooltip.vue +626 -0
- package/src/components/VirtualGrid.vue +2 -2
- package/src/components/VirtualTable.vue +4 -3
- package/src/{widgets → components}/WebDatasourceSelector.vue +2 -0
- package/src/{widgets → components}/WebLayoutSelector.vue +1 -1
- package/src/directives/tooltip.js +131 -0
- package/src/index.js +11 -235
- package/src/themes/default/index.js +16 -0
- package/src/utils/ai-dashboard.js +163 -38
- package/src/utils/helpers.mjs +52 -5
- package/src/utils/list.mjs +4 -1
- package/src/utils/pivot-formula.mjs +358 -0
- package/src/utils/pivot-list.mjs +557 -0
- package/src/utils/tooltip.mjs +243 -0
- package/src/utils/wss.js +26 -6
- package/src/utils/wss.mjs +29 -2
- /package/src/{widgets → components}/AhrefSetting.vue +0 -0
- /package/src/{widgets → components}/ArticleSetting.vue +0 -0
- /package/src/{widgets → components}/BackgroundColorSetting.vue +0 -0
- /package/src/{widgets → components}/BlockSetting.vue +0 -0
- /package/src/{widgets → components}/BorderColorSetting.vue +0 -0
- /package/src/{widgets → components}/ButtonSetting.vue +0 -0
- /package/src/{widgets → components}/CarouselSetting.vue +0 -0
- /package/src/{widgets → components}/ColumnSelector.vue +0 -0
- /package/src/{widgets → components}/ComponentSetting.vue +0 -0
- /package/src/{widgets → components}/ContactFormSetting.vue +0 -0
- /package/src/{widgets → components}/CountdownSetting.vue +0 -0
- /package/src/{widgets → components}/DataListSetting.vue +0 -0
- /package/src/{widgets → components}/EmbeddedVideo.vue +0 -0
- /package/src/{widgets → components}/EmbeddedVideoSetting.vue +0 -0
- /package/src/{widgets → components}/FAQ.vue +0 -0
- /package/src/{widgets → components}/FAQSetting.vue +0 -0
- /package/src/{widgets → components}/FeatureListSetting.vue +0 -0
- /package/src/{widgets → components}/FlexSetting.vue +0 -0
- /package/src/{widgets → components}/GridSetting.vue +0 -0
- /package/src/{widgets → components}/Header2.vue +0 -0
- /package/src/{widgets → components}/Header2Setting.vue +0 -0
- /package/src/{widgets → components}/IconListSetting.vue +0 -0
- /package/src/{widgets → components}/ImageSetting.vue +0 -0
- /package/src/{widgets → components}/LLMChat.vue +0 -0
- /package/src/{widgets → components}/LinkSetting.vue +0 -0
- /package/src/{widgets → components}/LinkSettingModal.vue +0 -0
- /package/src/{widgets → components}/ModalSetting.vue +0 -0
- /package/src/{widgets → components}/OGSettingModal.vue +0 -0
- /package/src/{widgets → components}/ParagraphSetting.vue +0 -0
- /package/src/{widgets → components}/PositionSetting.vue +0 -0
- /package/src/{widgets → components}/PresetBarPivot.vue +0 -0
- /package/src/{widgets → components}/PresetBarPivotColumnEdit.vue +0 -0
- /package/src/{widgets → components}/PresetBarPivotValueEdit.vue +0 -0
- /package/src/{widgets → components}/PresetSelector.vue +0 -0
- /package/src/{widgets → components}/Review.vue +0 -0
- /package/src/{widgets → components}/ReviewSetting.vue +0 -0
- /package/src/{widgets → components}/SearchModalSetting.vue +0 -0
- /package/src/{widgets → components}/ShareSetting.vue +0 -0
- /package/src/{widgets → components}/SvgSetting.vue +0 -0
- /package/src/{widgets → components}/TableSetting.vue +0 -0
- /package/src/{widgets → components}/TestimonialSetting.vue +0 -0
- /package/src/{widgets → components}/TextBlockSetting.vue +0 -0
- /package/src/{widgets → components}/ThumbnailSetting.vue +0 -0
- /package/src/{widgets → components}/WebComponentSelector.vue +0 -0
- /package/src/{widgets → components}/WebPageBuilder.vue +0 -0
- /package/src/{widgets → components}/YoutubeVideoSetting.vue +0 -0
package/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mixd-id/web-scaffold",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "vite serve",
|
|
7
|
-
"build": "vite build",
|
|
8
|
-
"start": "node server.js"
|
|
9
|
-
},
|
|
4
|
+
"version": "0.2.260801002",
|
|
10
5
|
"main": "./src/index.js",
|
|
11
6
|
"exports": {
|
|
12
7
|
".": "./src/index.js",
|
|
13
8
|
"./themes/default": "./src/themes/default/index.js",
|
|
14
9
|
"./components/*": "./src/components/*",
|
|
15
|
-
"./widgets/*": "./src/
|
|
10
|
+
"./widgets/*": "./src/components/*",
|
|
16
11
|
"./mixin/component": "./src/mixin/component.js",
|
|
17
12
|
"./helpers": {
|
|
18
13
|
"require": "./src/utils/helpers.js",
|
|
@@ -28,6 +23,7 @@
|
|
|
28
23
|
"./wss.mjs": "./src/utils/wss.mjs",
|
|
29
24
|
"./dashboard": "./src/utils/dashboard.js",
|
|
30
25
|
"./ai-dashboard": "./src/utils/ai-dashboard.js",
|
|
26
|
+
"./ai-dashboard-api": "./src/utils/ai-dashboard-api.js",
|
|
31
27
|
"./ai-dashboard-chat": "./src/utils/ai-dashboard-chat.mjs",
|
|
32
28
|
"./ai-chat": "./src/utils/ai-chat.js",
|
|
33
29
|
"./ai-chat-answer": "./src/utils/ai-chat-answer.mjs",
|
|
@@ -36,86 +32,46 @@
|
|
|
36
32
|
"import": "./src/utils/preset-selector.mjs"
|
|
37
33
|
},
|
|
38
34
|
"./hooks/*": "./src/hooks/*",
|
|
35
|
+
"./directives/*": "./src/directives/*",
|
|
36
|
+
"./tooltip": {
|
|
37
|
+
"import": "./src/utils/tooltip.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./tooltip.mjs": "./src/utils/tooltip.mjs",
|
|
39
40
|
"./preset-selector.mjs": "./src/utils/preset-selector.mjs",
|
|
40
41
|
"./list": {
|
|
41
42
|
"import": "./src/utils/list.mjs"
|
|
42
43
|
},
|
|
43
44
|
"./list.mjs": "./src/utils/list.mjs",
|
|
44
|
-
"./
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"./
|
|
48
|
-
"./
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"./
|
|
52
|
-
"./docs/components
|
|
53
|
-
"./docs/components/CartIcon": "./docs/components/CartIcon.md",
|
|
54
|
-
"./docs/components/Chart": "./docs/components/Chart.md",
|
|
55
|
-
"./docs/components/ChatTyping": "./docs/components/ChatTyping.md",
|
|
56
|
-
"./docs/components/Checkbox": "./docs/components/Checkbox.md",
|
|
57
|
-
"./docs/components/CodeEditor": "./docs/components/CodeEditor.md",
|
|
58
|
-
"./docs/components/CollapsiblePanel": "./docs/components/CollapsiblePanel.md",
|
|
59
|
-
"./docs/components/ColorPicker": "./docs/components/ColorPicker.md",
|
|
60
|
-
"./docs/components/Confirm": "./docs/components/Confirm.md",
|
|
61
|
-
"./docs/components/ContextMenu": "./docs/components/ContextMenu.md",
|
|
62
|
-
"./docs/components/ContextMenuItem": "./docs/components/ContextMenuItem.md",
|
|
63
|
-
"./docs/components/CopyToClipboard": "./docs/components/CopyToClipboard.md",
|
|
64
|
-
"./docs/components/Countdown": "./docs/components/Countdown.md",
|
|
65
|
-
"./docs/components/Datepicker": "./docs/components/Datepicker.md",
|
|
66
|
-
"./docs/components/Datetimepicker": "./docs/components/Datetimepicker.md",
|
|
67
|
-
"./docs/components/Dropdown": "./docs/components/Dropdown.md",
|
|
68
|
-
"./docs/components/Flex": "./docs/components/Flex.md",
|
|
69
|
-
"./docs/components/Gmaps": "./docs/components/Gmaps.md",
|
|
70
|
-
"./docs/components/Grid": "./docs/components/Grid.md",
|
|
71
|
-
"./docs/components/HTMLEditor": "./docs/components/HTMLEditor.md",
|
|
72
|
-
"./docs/components/Image": "./docs/components/Image.md",
|
|
73
|
-
"./docs/components/Link": "./docs/components/Link.md",
|
|
74
|
-
"./docs/components/MultiDropdown": "./docs/components/MultiDropdown.md",
|
|
75
|
-
"./docs/components/Radio": "./docs/components/Radio.md",
|
|
76
|
-
"./docs/components/Slider": "./docs/components/Slider.md",
|
|
77
|
-
"./docs/components/Switch": "./docs/components/Switch.md",
|
|
78
|
-
"./docs/components/Table": "./docs/components/Table.md",
|
|
79
|
-
"./docs/components/Tabs": "./docs/components/Tabs.md",
|
|
80
|
-
"./docs/components/Text": "./docs/components/Text.md",
|
|
81
|
-
"./docs/components/Textarea": "./docs/components/Textarea.md",
|
|
82
|
-
"./docs/components/Textbox": "./docs/components/Textbox.md",
|
|
83
|
-
"./docs/components/Timepicker": "./docs/components/Timepicker.md",
|
|
84
|
-
"./docs/components/Toast": "./docs/components/Toast.md",
|
|
85
|
-
"./docs/components/TreeView": "./docs/components/TreeView.md",
|
|
86
|
-
"./docs/components/Uploader": "./docs/components/Uploader.md",
|
|
87
|
-
"./docs/components/Video": "./docs/components/Video.md"
|
|
45
|
+
"./pivot-list": {
|
|
46
|
+
"import": "./src/utils/pivot-list.mjs"
|
|
47
|
+
},
|
|
48
|
+
"./pivot-list.mjs": "./src/utils/pivot-list.mjs",
|
|
49
|
+
"./pivot-formula": {
|
|
50
|
+
"import": "./src/utils/pivot-formula.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./pivot-formula.mjs": "./src/utils/pivot-formula.mjs",
|
|
53
|
+
"./docs/components/*": "./docs/components/*.md"
|
|
88
54
|
},
|
|
89
55
|
"dependencies": {
|
|
90
56
|
"@googlemaps/js-api-loader": "^1.16.8",
|
|
91
57
|
"@tailwindcss/line-clamp": "^0.4.0",
|
|
92
58
|
"@vueuse/core": "^9.0.2",
|
|
93
|
-
"adm-zip": "^0.5.10",
|
|
94
59
|
"axios": "1.8.2",
|
|
95
60
|
"bcrypt": "^5.1.1",
|
|
96
61
|
"chart.js": "^4.2.1",
|
|
97
|
-
"compression": "^1.7.4",
|
|
98
62
|
"crypto-js": "^4.2.0",
|
|
99
63
|
"dayjs": "^1.11.2",
|
|
100
64
|
"eventemitter2": "^6.4.7",
|
|
101
|
-
"exceljs": "^4.3.0",
|
|
102
|
-
"file-type": "^18.2.1",
|
|
103
|
-
"glob": "^8.0.3",
|
|
104
65
|
"highlight.js": "^11.11.1",
|
|
105
66
|
"lodash": "^4.17.21",
|
|
106
|
-
"lodash-es": "^4.17.21",
|
|
107
67
|
"marked": "^16.4.1",
|
|
108
68
|
"md5": "^2.3.0",
|
|
109
|
-
"mitt": "^3.0.1",
|
|
110
|
-
"pinia": "^2.0.2",
|
|
111
69
|
"prismjs": "1.30.0",
|
|
112
70
|
"redis": "^4.6.13",
|
|
113
71
|
"sequelize": "^6.37.7",
|
|
114
72
|
"tailwindcss": "^3.2.4",
|
|
115
|
-
"tinycolor2": "^1.6.0",
|
|
116
73
|
"vue": "^3.2.25",
|
|
117
74
|
"vue-chartjs": "^5.2.0",
|
|
118
|
-
"vue-router": "^4.0.14",
|
|
119
75
|
"ws": "^8.16.0"
|
|
120
76
|
},
|
|
121
77
|
"description": "This scaffold based on express vitejs vuejs",
|
|
@@ -236,11 +236,20 @@ You can use *italics*, **bold**, \`inline code\`, and [links](https://example.co
|
|
|
236
236
|
2. Ordered two
|
|
237
237
|
|
|
238
238
|
### Table
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
239
|
+
Long tables collapse behind a "show more" toggle after the first few rows:
|
|
240
|
+
|
|
241
|
+
| Feature | Supported |
|
|
242
|
+
| ---------- | --------- |
|
|
243
|
+
| Tables | ✅ |
|
|
244
|
+
| Code | ✅ |
|
|
245
|
+
| Images | ✅ |
|
|
246
|
+
| Files | ✅ |
|
|
247
|
+
| Markdown | ✅ |
|
|
248
|
+
| Streaming | ✅ |
|
|
249
|
+
| Blockquote | ✅ |
|
|
250
|
+
| Lists | ✅ |
|
|
251
|
+
| Links | ✅ |
|
|
252
|
+
| Headings | ✅ |
|
|
244
253
|
|
|
245
254
|
> Blockquotes work too — handy for callouts.`,
|
|
246
255
|
},
|
|
@@ -36,11 +36,12 @@
|
|
|
36
36
|
|
|
37
37
|
<!-- File attachments -->
|
|
38
38
|
<div v-if="files.length" class="mt-3 flex flex-wrap gap-2">
|
|
39
|
-
<
|
|
40
|
-
|
|
39
|
+
<button v-for="(url, n) in files" :key="n" type="button" @click="$download(url, fileName(url))"
|
|
40
|
+
class="flex items-center gap-2 p-2 pr-3 rounded-xl bg-base-300 hover:bg-base-500 transition-colors max-w-[260px]">
|
|
41
41
|
<span class="shrink-0 px-2 py-1 rounded-lg text-[10px] font-bold uppercase text-white" :style="{ background: fileColor(url) }">{{ fileExt(url) }}</span>
|
|
42
42
|
<span class="text-sm text-text truncate">{{ fileName(url) }}</span>
|
|
43
|
-
|
|
43
|
+
<svg width="12" height="12" class="shrink-0 fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H345.5l-45.3 45.3c-25 25-65.5 25-90.5 0L164.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
|
|
44
|
+
</button>
|
|
44
45
|
</div>
|
|
45
46
|
|
|
46
47
|
<!-- Footer meta -->
|
|
@@ -64,6 +65,9 @@ const { message } = defineProps({
|
|
|
64
65
|
message: { type: Object, required: true },
|
|
65
66
|
})
|
|
66
67
|
|
|
68
|
+
// Tables past this many data rows get collapsed behind a "Show N more rows" toggle.
|
|
69
|
+
const MAX_TABLE_ROWS = 8
|
|
70
|
+
|
|
67
71
|
// Local Marked instance — avoids mutating global marked state on each mount.
|
|
68
72
|
const markdown = new Marked({
|
|
69
73
|
gfm: true,
|
|
@@ -74,6 +78,23 @@ const markdown = new Marked({
|
|
|
74
78
|
const html = hljs.highlight(text, { language }).value
|
|
75
79
|
return `<pre><code class="hljs language-${language}">${html}</code></pre>`
|
|
76
80
|
},
|
|
81
|
+
table(token) {
|
|
82
|
+
const cellStyle = (align) => align ? ` style="text-align:${align}"` : ''
|
|
83
|
+
const cell = (tag, data, i) => `<${tag}${cellStyle(token.align[i])}>${this.parser.parseInline(data.tokens)}</${tag}>`
|
|
84
|
+
const row = (data) => `<tr>${data.map((d, i) => cell('td', d, i)).join('')}</tr>`
|
|
85
|
+
|
|
86
|
+
const head = `<tr>${token.header.map((d, i) => cell('th', d, i)).join('')}</tr>`
|
|
87
|
+
const visible = token.rows.slice(0, MAX_TABLE_ROWS)
|
|
88
|
+
const hidden = token.rows.slice(MAX_TABLE_ROWS)
|
|
89
|
+
|
|
90
|
+
let body = visible.map(row).join('')
|
|
91
|
+
if(hidden.length) {
|
|
92
|
+
body += `<tr class="table-more-row"><td colspan="${token.header.length}">`
|
|
93
|
+
+ `<details><summary>Show ${hidden.length} more row${hidden.length > 1 ? 's' : ''}</summary>`
|
|
94
|
+
+ `<table><tbody>${hidden.map(row).join('')}</tbody></table></details></td></tr>`
|
|
95
|
+
}
|
|
96
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`
|
|
97
|
+
},
|
|
77
98
|
},
|
|
78
99
|
})
|
|
79
100
|
|
|
@@ -134,6 +155,16 @@ function fileColor(url) {
|
|
|
134
155
|
.md :global(table) { border-collapse: collapse !important; width: 100% !important; margin: 0.8em 0 !important; display: block; overflow-x: auto; }
|
|
135
156
|
.md :global(th), .md :global(td) { border: 1px solid rgba(128,128,128,0.35) !important; padding: 0.4em 0.7em !important; text-align: left !important; }
|
|
136
157
|
.md :global(th) { font-weight: 600 !important; background: rgba(128,128,128,0.1) !important; }
|
|
158
|
+
.md :global(.table-more-row td) { border: none !important; padding: 0 !important; }
|
|
159
|
+
.md :global(.table-more-row summary) {
|
|
160
|
+
cursor: pointer !important;
|
|
161
|
+
padding: 0.4em 0.7em !important;
|
|
162
|
+
font-size: 0.85em !important;
|
|
163
|
+
color: rgba(128,128,128,0.9) !important;
|
|
164
|
+
list-style: none;
|
|
165
|
+
}
|
|
166
|
+
.md :global(.table-more-row summary::-webkit-details-marker) { display: none; }
|
|
167
|
+
.md :global(.table-more-row details table) { margin: 0 !important; }
|
|
137
168
|
.md :global(blockquote) { border-left: 3px solid rgba(128,128,128,0.4) !important; padding-left: 1em !important; margin: 0.6em 0 !important; opacity: 0.8; }
|
|
138
169
|
.md :global(hr) { border-color: rgba(128,128,128,0.3) !important; margin: 1em 0 !important; }
|
|
139
170
|
.md :global(:not(pre) > code) {
|
|
@@ -161,89 +161,119 @@
|
|
|
161
161
|
</template>
|
|
162
162
|
</template>
|
|
163
163
|
|
|
164
|
-
<!--
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
<!-- Leaf: authored payload (`data`) — the caption every widget card
|
|
165
|
+
shows, plus a metric's hand-written value / delta. Kept editable
|
|
166
|
+
even when a query drives the numbers: `load-dashboard` overwrites
|
|
167
|
+
value/series/rows, never label or delta. -->
|
|
168
|
+
<template v-else>
|
|
169
|
+
<label class="text-xs text-text-400">Caption</label>
|
|
170
|
+
<Textbox v-model="nodeData(selectedNode).label" :placeholder="selectedNode.name || labelFor(selectedNode.type)" @change="persist" />
|
|
171
|
+
|
|
172
|
+
<template v-if="selectedNode.type === 'metric'">
|
|
173
|
+
<template v-if="!selectedNode.query?.datasource">
|
|
174
|
+
<label class="text-xs text-text-400">Value</label>
|
|
175
|
+
<Textbox v-model="nodeData(selectedNode).value" type="tel" item-class="text-right" @change="persist" />
|
|
176
|
+
</template>
|
|
177
|
+
<label class="text-xs text-text-400">Delta % (optional)</label>
|
|
178
|
+
<Textbox v-model="nodeData(selectedNode).delta" type="tel" item-class="text-right" @change="persist" />
|
|
179
|
+
</template>
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
<!-- Query builder (data-source backed) -->
|
|
182
|
+
<template v-if="hasDatasources">
|
|
183
|
+
<label class="text-xs text-text-400 pt-1">Data source</label>
|
|
184
|
+
<Dropdown :model-value="query(selectedNode).datasource || ''"
|
|
185
|
+
@update:model-value="setDatasource(selectedNode, $event)">
|
|
186
|
+
<option value="">— none —</option>
|
|
187
|
+
<option v-for="ds in dsList" :key="ds.key" :value="ds.key">{{ ds.label }}</option>
|
|
188
|
+
</Dropdown>
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
</Dropdown>
|
|
190
|
-
<template v-if="query(selectedNode).aggregation && query(selectedNode).aggregation !== 'count'">
|
|
191
|
-
<label class="text-xs text-text-400">Field</label>
|
|
192
|
-
<Dropdown v-model="query(selectedNode).field" @change="onQueryEdit">
|
|
193
|
-
<option value="">— select —</option>
|
|
194
|
-
<option v-for="f in aggFields(selectedNode)" :key="f.key" :value="f.key">{{ f.label }}</option>
|
|
190
|
+
<template v-if="query(selectedNode).datasource">
|
|
191
|
+
<!-- metric + chart: what the number / each bar measures -->
|
|
192
|
+
<template v-if="selectedNode.type !== 'table'">
|
|
193
|
+
<label class="text-xs text-text-400">Aggregation</label>
|
|
194
|
+
<Dropdown v-model="query(selectedNode).aggregation" @change="onQueryEdit">
|
|
195
|
+
<option v-for="a in aggOptions(selectedNode)" :key="a" :value="a">{{ aggLabel(a) }}</option>
|
|
195
196
|
</Dropdown>
|
|
197
|
+
<template v-if="query(selectedNode).aggregation && query(selectedNode).aggregation !== 'count'">
|
|
198
|
+
<label class="text-xs text-text-400">Field</label>
|
|
199
|
+
<Dropdown v-model="query(selectedNode).field" @change="onQueryEdit">
|
|
200
|
+
<option value="">— select —</option>
|
|
201
|
+
<option v-for="f in aggFields(selectedNode)" :key="f.key" :value="f.key">{{ f.label }}</option>
|
|
202
|
+
</Dropdown>
|
|
203
|
+
</template>
|
|
196
204
|
</template>
|
|
197
|
-
</template>
|
|
198
205
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
</Dropdown>
|
|
206
|
-
<label class="text-xs text-text-400">Bucket</label>
|
|
207
|
-
<Dropdown v-model="query(selectedNode).dateModifier" @change="onQueryEdit">
|
|
208
|
-
<option value="">Exact value</option>
|
|
209
|
-
<option value="day">By day</option>
|
|
210
|
-
<option value="week">By week</option>
|
|
211
|
-
<option value="month">By month</option>
|
|
212
|
-
<option value="year">By year</option>
|
|
213
|
-
</Dropdown>
|
|
214
|
-
<label class="text-xs text-text-400">Limit</label>
|
|
215
|
-
<Textbox v-model.number="query(selectedNode).limit" type="tel" item-class="text-right" @change="onQueryEdit" />
|
|
216
|
-
</template>
|
|
217
|
-
|
|
218
|
-
<!-- table query -->
|
|
219
|
-
<template v-else-if="selectedNode.type === 'table'">
|
|
220
|
-
<label class="text-xs text-text-400">Page size</label>
|
|
221
|
-
<Textbox v-model.number="query(selectedNode).pageSize" type="tel" item-class="text-right" @change="onQueryEdit" />
|
|
222
|
-
<label class="text-xs text-text-400">Order by</label>
|
|
223
|
-
<Dropdown v-model="tableOrderField" @change="onQueryEdit">
|
|
224
|
-
<option value="">— default —</option>
|
|
225
|
-
<option v-for="f in fieldsOf(selectedNode)" :key="f.key" :value="f.key">{{ f.label }}</option>
|
|
226
|
-
</Dropdown>
|
|
227
|
-
</template>
|
|
228
|
-
|
|
229
|
-
<!-- Filters (applied to every data-backed widget) -->
|
|
230
|
-
<label class="text-xs text-text-400 pt-1">Filters</label>
|
|
231
|
-
<div class="flex flex-col gap-2">
|
|
232
|
-
<div v-for="(f, fi) in filtersOf(selectedNode)" :key="fi" class="flex flex-row items-center gap-1">
|
|
233
|
-
<Dropdown v-model="f.field" class="flex-1" @change="onFilterField(selectedNode, f)">
|
|
234
|
-
<option value="">field</option>
|
|
235
|
-
<option v-for="fl in fieldsOf(selectedNode)" :key="fl.key" :value="fl.key">{{ fl.label }}</option>
|
|
206
|
+
<!-- chart: the buckets -->
|
|
207
|
+
<template v-if="selectedNode.type === 'chart'">
|
|
208
|
+
<label class="text-xs text-text-400">Group by</label>
|
|
209
|
+
<Dropdown v-model="query(selectedNode).groupBy" @change="onQueryEdit">
|
|
210
|
+
<option value="">— select —</option>
|
|
211
|
+
<option v-for="f in fieldsOf(selectedNode)" :key="f.key" :value="f.key">{{ f.label }}</option>
|
|
236
212
|
</Dropdown>
|
|
237
|
-
<
|
|
238
|
-
|
|
213
|
+
<label class="text-xs text-text-400">Bucket</label>
|
|
214
|
+
<Dropdown v-model="query(selectedNode).dateModifier" @change="onQueryEdit">
|
|
215
|
+
<option value="">Exact value</option>
|
|
216
|
+
<option value="day">By day</option>
|
|
217
|
+
<option value="week">By week</option>
|
|
218
|
+
<option value="month">By month</option>
|
|
219
|
+
<option value="year">By year</option>
|
|
239
220
|
</Dropdown>
|
|
240
|
-
<
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
221
|
+
<label class="text-xs text-text-400">Limit</label>
|
|
222
|
+
<Textbox v-model.number="query(selectedNode).limit" type="tel" item-class="text-right" @change="onQueryEdit" />
|
|
223
|
+
</template>
|
|
224
|
+
|
|
225
|
+
<!-- table: columns, sorts, page size -->
|
|
226
|
+
<template v-else-if="selectedNode.type === 'table'">
|
|
227
|
+
<label class="text-xs text-text-400">Columns</label>
|
|
228
|
+
<div class="flex flex-col gap-1">
|
|
229
|
+
<label v-for="f in fieldsOf(selectedNode)" :key="f.key" class="flex flex-row items-center gap-2 text-xs cursor-pointer">
|
|
230
|
+
<input type="checkbox" :checked="hasColumn(selectedNode, f.key)" @change="toggleColumn(selectedNode, f)" />
|
|
231
|
+
<span class="flex-1 truncate text-text-300">{{ f.label }}</span>
|
|
232
|
+
<span class="text-[10px] uppercase tracking-wide text-text-400">{{ f.type }}</span>
|
|
233
|
+
</label>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<label class="text-xs text-text-400 pt-1">Order by</label>
|
|
237
|
+
<div class="flex flex-col gap-2">
|
|
238
|
+
<div v-for="(o, oi) in orderOf(selectedNode)" :key="oi" class="flex flex-row items-center gap-1">
|
|
239
|
+
<Dropdown v-model="o[0]" class="flex-1" @change="onQueryEdit">
|
|
240
|
+
<option value="">field</option>
|
|
241
|
+
<option v-for="f in fieldsOf(selectedNode)" :key="f.key" :value="f.key">{{ f.label }}</option>
|
|
242
|
+
</Dropdown>
|
|
243
|
+
<Dropdown v-model="o[1]" class="w-[86px]" @change="onQueryEdit">
|
|
244
|
+
<option value="asc">Asc</option>
|
|
245
|
+
<option value="desc">Desc</option>
|
|
246
|
+
</Dropdown>
|
|
247
|
+
<button type="button" class="px-1 text-text-300 hover:text-red-600" title="Remove sort" @click="removeOrder(selectedNode, oi)">
|
|
248
|
+
<svg width="11" height="11" class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
<button type="button" class="text-primary text-xs text-left hover:underline" @click="addOrder(selectedNode)">+ Add sort</button>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<label class="text-xs text-text-400 pt-1">Page size</label>
|
|
255
|
+
<Textbox v-model.number="query(selectedNode).pageSize" type="tel" item-class="text-right" @change="onQueryEdit" />
|
|
256
|
+
</template>
|
|
257
|
+
|
|
258
|
+
<!-- Filters (applied to every data-backed widget) -->
|
|
259
|
+
<label class="text-xs text-text-400 pt-1">Filters</label>
|
|
260
|
+
<div class="flex flex-col gap-2">
|
|
261
|
+
<div v-for="(f, fi) in filtersOf(selectedNode)" :key="fi" class="flex flex-row items-center gap-1">
|
|
262
|
+
<Dropdown v-model="f.field" class="flex-1" @change="onFilterField(selectedNode, f)">
|
|
263
|
+
<option value="">field</option>
|
|
264
|
+
<option v-for="fl in fieldsOf(selectedNode)" :key="fl.key" :value="fl.key">{{ fl.label }}</option>
|
|
265
|
+
</Dropdown>
|
|
266
|
+
<Dropdown v-model="f.op" class="w-[64px]" @change="onQueryEdit">
|
|
267
|
+
<option v-for="o in opsFor(fieldType(selectedNode, f.field))" :key="o" :value="o">{{ opLabel(o) }}</option>
|
|
268
|
+
</Dropdown>
|
|
269
|
+
<Textbox v-model="f.value" class="flex-1" placeholder="value" @change="onQueryEdit" />
|
|
270
|
+
<button type="button" class="px-1 text-text-300 hover:text-red-600" title="Remove filter" @click="removeFilter(selectedNode, fi)">
|
|
271
|
+
<svg width="11" height="11" class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
<button type="button" class="text-primary text-xs text-left hover:underline" @click="addFilter(selectedNode)">+ Add filter</button>
|
|
244
275
|
</div>
|
|
245
|
-
|
|
246
|
-
</div>
|
|
276
|
+
</template>
|
|
247
277
|
</template>
|
|
248
278
|
</template>
|
|
249
279
|
|
|
@@ -359,7 +389,7 @@
|
|
|
359
389
|
<script setup>
|
|
360
390
|
import { computed, defineComponent, h, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
|
361
391
|
import { Bar } from 'vue-chartjs'
|
|
362
|
-
import LLMChat from '
|
|
392
|
+
import LLMChat from './LLMChat.vue'
|
|
363
393
|
import VirtualTable from './VirtualTable.vue'
|
|
364
394
|
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Tooltip, Legend } from 'chart.js'
|
|
365
395
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend)
|
|
@@ -382,7 +412,7 @@ const { controller, socket, config, samples, datasources, autoSave, serverChat }
|
|
|
382
412
|
socket: { type: Object, default: null },
|
|
383
413
|
config: { type: Object, required: true },
|
|
384
414
|
samples: { type: Object, default: () => ({ structure: [], widgets: {} }) },
|
|
385
|
-
datasources: { type: Object, default: () => ({}) },
|
|
415
|
+
datasources: { type: [ Object, Array ], default: () => ({}) },
|
|
386
416
|
// Persist edits through `${controller}.save`. Turn off when a host page owns
|
|
387
417
|
// persistence (e.g. via loadConfig/saveConfig) to avoid a double write.
|
|
388
418
|
autoSave: { type: Boolean, default: true },
|
|
@@ -412,8 +442,9 @@ function normalizeDs(list) {
|
|
|
412
442
|
}))
|
|
413
443
|
}
|
|
414
444
|
|
|
415
|
-
const dsList = computed(() => serverDatasources.value
|
|
416
|
-
|
|
445
|
+
const dsList = computed(() => serverDatasources.value ?? normalizeDs(Array.isArray(datasources)
|
|
446
|
+
? datasources
|
|
447
|
+
: Object.entries(datasources).map(([key, v]) => ({ key, ...v }))))
|
|
417
448
|
const hasDatasources = computed(() => dsList.value.length > 0)
|
|
418
449
|
|
|
419
450
|
async function loadDatasources() {
|
|
@@ -677,7 +708,7 @@ function groupStyle(node) {
|
|
|
677
708
|
}
|
|
678
709
|
|
|
679
710
|
/* ---------- Inspector data accessors ---------- */
|
|
680
|
-
function
|
|
711
|
+
function nodeData(node) { if (!node.data) node.data = {}; return node.data }
|
|
681
712
|
function query(node) { if (!node.query) node.query = {}; return node.query }
|
|
682
713
|
|
|
683
714
|
function currentDs(node) { return dsList.value.find(d => d.key === query(node).datasource) }
|
|
@@ -690,11 +721,12 @@ function setDatasource(node, ds) {
|
|
|
690
721
|
q.field = ''
|
|
691
722
|
if (!ds) return
|
|
692
723
|
// seed sensible query defaults per node type
|
|
693
|
-
if (node.type
|
|
724
|
+
if (node.type !== 'table' && !q.aggregation) q.aggregation = 'count'
|
|
694
725
|
if (node.type === 'table') {
|
|
695
726
|
if (q.pageSize == null) q.pageSize = 10
|
|
696
727
|
// default columns = all datasource fields
|
|
697
728
|
q.columns = fieldsOf(node).map(f => ({ key: f.key, label: f.label }))
|
|
729
|
+
q.order = []
|
|
698
730
|
}
|
|
699
731
|
onQueryEdit()
|
|
700
732
|
}
|
|
@@ -741,6 +773,7 @@ function newMetricFrom(ds) {
|
|
|
741
773
|
if (!activePreset.value) return
|
|
742
774
|
const node = blankNode('metric')
|
|
743
775
|
node.name = ds.label
|
|
776
|
+
node.data = { label: ds.label, value: 0 }
|
|
744
777
|
node.query = { datasource: ds.key, aggregation: 'count', filters: [] }
|
|
745
778
|
const target = (selectedNode.value && selectedNode.value.type === 'group') ? selectedNode.value.items : structure.value
|
|
746
779
|
target.push(node)
|
|
@@ -749,18 +782,22 @@ function newMetricFrom(ds) {
|
|
|
749
782
|
onQueryEdit()
|
|
750
783
|
}
|
|
751
784
|
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
785
|
+
/* ---- Table columns + order (query.columns, query.order) ---- */
|
|
786
|
+
function columnsOf(node) { const q = query(node); if (!Array.isArray(q.columns)) q.columns = []; return q.columns }
|
|
787
|
+
function hasColumn(node, key) { return columnsOf(node).some(c => (c.key ?? c) === key) }
|
|
788
|
+
|
|
789
|
+
function toggleColumn(node, field) {
|
|
790
|
+
const cols = columnsOf(node)
|
|
791
|
+
const i = cols.findIndex(c => (c.key ?? c) === field.key)
|
|
792
|
+
if (i >= 0) cols.splice(i, 1)
|
|
793
|
+
else cols.push({ key: field.key, label: field.label })
|
|
794
|
+
onQueryEdit()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Order is stored the way Sequelize takes it: [[field, 'desc'], ...].
|
|
798
|
+
function orderOf(node) { const q = query(node); if (!Array.isArray(q.order)) q.order = []; return q.order }
|
|
799
|
+
function addOrder(node) { orderOf(node).push([ fieldsOf(node)[0]?.key ?? '', 'desc' ]); onQueryEdit() }
|
|
800
|
+
function removeOrder(node, i) { orderOf(node).splice(i, 1); onQueryEdit() }
|
|
764
801
|
|
|
765
802
|
/* ---------- Reset from samples ---------- */
|
|
766
803
|
function resetSamples() {
|
|
@@ -774,11 +811,17 @@ function resetSamples() {
|
|
|
774
811
|
/* ---------- Server: hydrate + persist ---------- */
|
|
775
812
|
const hydrating = ref(false)
|
|
776
813
|
|
|
777
|
-
//
|
|
814
|
+
// A query-backed node's value/series/rows are recomputed by the server, so they
|
|
815
|
+
// are not persisted; its authored `label` and `delta` are, since nothing else
|
|
816
|
+
// carries them (docs/dashboard.json metrics keep a delta beside their query).
|
|
778
817
|
function cleanNode(node) {
|
|
779
818
|
const { data, ...rest } = node
|
|
780
819
|
const out = { ...rest }
|
|
781
|
-
if (
|
|
820
|
+
if (node.query?.datasource) {
|
|
821
|
+
const { label, delta } = data ?? {}
|
|
822
|
+
if (label != null || delta != null) out.data = { label, delta }
|
|
823
|
+
}
|
|
824
|
+
else out.data = data
|
|
782
825
|
if (Array.isArray(node.items)) out.items = node.items.map(cleanNode)
|
|
783
826
|
return out
|
|
784
827
|
}
|
|
@@ -958,9 +1001,10 @@ const Widget = defineComponent({
|
|
|
958
1001
|
setup(props, { emit }) {
|
|
959
1002
|
const data = computed(() => props.node.data || {})
|
|
960
1003
|
|
|
1004
|
+
// The card header already shows the caption (`data.label ?? node.name`),
|
|
1005
|
+
// so the metric body is the number and its delta.
|
|
961
1006
|
function renderMetric() {
|
|
962
1007
|
return h('div', { class: 'flex flex-col gap-1' }, [
|
|
963
|
-
h('div', { class: 'text-text-300 text-sm' }, data.value.label || props.node.name),
|
|
964
1008
|
h('div', { class: 'text-4xl font-bold' }, formatNum(data.value.value)),
|
|
965
1009
|
data.value.delta != null && data.value.delta !== ''
|
|
966
1010
|
? h('div', { class: Number(data.value.delta) >= 0 ? 'text-green-600 text-sm' : 'text-red-600 text-sm' },
|
|
@@ -1069,7 +1113,7 @@ const Widget = defineComponent({
|
|
|
1069
1113
|
|
|
1070
1114
|
return () => h('div', { class: 'group bg-base-300 rounded-xl p-5 flex flex-col gap-3 h-full' }, [
|
|
1071
1115
|
h('div', { class: 'flex flex-row items-center gap-2' }, [
|
|
1072
|
-
h('div', { class: 'flex-1 text-sm text-text-300 truncate' }, props.node.name),
|
|
1116
|
+
h('div', { class: 'flex-1 text-sm text-text-300 truncate' }, data.value.label || props.node.name),
|
|
1073
1117
|
h('button', {
|
|
1074
1118
|
type: 'button',
|
|
1075
1119
|
class: 'opacity-0 group-hover:opacity-60 hover:!opacity-100',
|
|
@@ -1081,7 +1125,9 @@ const Widget = defineComponent({
|
|
|
1081
1125
|
]),
|
|
1082
1126
|
]),
|
|
1083
1127
|
]),
|
|
1084
|
-
|
|
1128
|
+
data.value.error
|
|
1129
|
+
? h('div', { class: 'text-red-600 text-sm' }, data.value.error)
|
|
1130
|
+
: props.node.type === 'metric' ? renderMetric()
|
|
1085
1131
|
: props.node.type === 'chart' ? renderChart()
|
|
1086
1132
|
: props.node.type === 'table' ? renderTable()
|
|
1087
1133
|
: h('div', { class: 'text-text-400 text-sm' }, 'Empty group — add child widgets.'),
|