@bitpoolos/edge-bacnet 1.5.2 → 1.6.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.
- package/CHANGELOG.md +151 -1
- package/bacnet_client.js +202 -108
- package/bacnet_device.js +3 -1
- package/bacnet_gateway.html +100 -2
- package/bacnet_gateway.js +382 -250
- package/bacnet_inspector.html +43 -0
- package/bacnet_inspector.js +1564 -0
- package/bacnet_inspector_worker.js +535 -0
- package/bacnet_read.html +47 -50
- package/bacnet_read.js +0 -3
- package/common.js +201 -38
- package/inspector.html +460 -0
- package/package.json +6 -2
- package/resources/Logo_Simplified_Positive.svg +32 -0
- package/resources/downloadAsHtml.js +654 -0
- package/resources/icons/device-id-change-icon.svg +4 -0
- package/resources/icons/device-id-conflict-icon.svg +4 -0
- package/resources/icons/favicon.ico +0 -0
- package/resources/icons/points-error-icon.svg +4 -0
- package/resources/icons/points-missing-icon.svg +4 -0
- package/resources/icons/points-ok-icon.svg +4 -0
- package/resources/icons/points-unmapped-icon.svg +5 -0
- package/resources/icons/points-warning-icon.svg +4 -0
- package/resources/inspector.css +25312 -0
- package/resources/inspectorStyle.css +254 -0
- package/resources/inspectorStyles.css +478 -0
- package/resources/node-bacstack-ts/dist/lib/client.js +7 -3
- package/resources/primevue.min.js +1 -0
- package/resources/style.css +17 -1
- package/resources/vue3513.global.prod.js +9 -0
- package/ssrHtmlExporter.js +535 -0
- package/treeBuilder.js +3 -3
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function captures the current state of the PrimeVue app and downloads it as an HTML file
|
|
3
|
+
* Specifically designed for the Bitpool BACnet Inspector
|
|
4
|
+
* @param {string} filename - The name of the downloaded file
|
|
5
|
+
* @param {Object} [data] - Optional data model to use instead of extracting from Vue app
|
|
6
|
+
*/
|
|
7
|
+
const downloadPrimeVueAppAsHtml = (filename = "bacnet-inspector-snapshot.html", data = null) => {
|
|
8
|
+
// Wait for Vue to finish updating the DOM
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
// Use nextTick to ensure all Vue updates are complete
|
|
11
|
+
Vue.nextTick(async () => {
|
|
12
|
+
// Get app data either from parameter or by extracting from Vue app
|
|
13
|
+
const appData = data || extractAppState();
|
|
14
|
+
|
|
15
|
+
// Process the HTML to create a standalone file with UMD dependencies
|
|
16
|
+
const processedHtml = await processPrimeVueHtml(appData);
|
|
17
|
+
|
|
18
|
+
// Create a blob from the processed HTML content
|
|
19
|
+
const blob = new Blob([processedHtml], { type: "text/html" });
|
|
20
|
+
|
|
21
|
+
// Create a temporary URL for the blob
|
|
22
|
+
const url = URL.createObjectURL(blob);
|
|
23
|
+
|
|
24
|
+
// Create a link element to trigger the download
|
|
25
|
+
const a = document.createElement("a");
|
|
26
|
+
a.href = url;
|
|
27
|
+
a.download = filename;
|
|
28
|
+
|
|
29
|
+
// Append the link to the document, click it, and remove it
|
|
30
|
+
document.body.appendChild(a);
|
|
31
|
+
a.click();
|
|
32
|
+
document.body.removeChild(a);
|
|
33
|
+
|
|
34
|
+
// Release the URL object
|
|
35
|
+
URL.revokeObjectURL(url);
|
|
36
|
+
|
|
37
|
+
resolve(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Process the HTML to make it standalone with PrimeVue UMD
|
|
44
|
+
* @param {string} html - The raw HTML content
|
|
45
|
+
* @param {Object} appData - The application data to include
|
|
46
|
+
* @returns {string} - The processed HTML with all required UMD dependencies
|
|
47
|
+
*/
|
|
48
|
+
async function processPrimeVueHtml(appData) {
|
|
49
|
+
const iconBase64Map = await getIconsAsBase64();
|
|
50
|
+
|
|
51
|
+
// Create a new HTML template
|
|
52
|
+
const newDoc = document.implementation.createHTMLDocument("Bitpool BACnet Inspector");
|
|
53
|
+
|
|
54
|
+
// Set basic meta tags
|
|
55
|
+
newDoc.head.innerHTML = `
|
|
56
|
+
<meta charset="UTF-8" />
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
58
|
+
<title>Bitpool BACnet Inspector</title>
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
// Add required scripts and CSS
|
|
62
|
+
addDependencies(newDoc);
|
|
63
|
+
|
|
64
|
+
// Create the app container
|
|
65
|
+
const appDiv = newDoc.createElement("div");
|
|
66
|
+
appDiv.id = "app";
|
|
67
|
+
newDoc.body.appendChild(appDiv);
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
// Add the captured app state and initialization script
|
|
71
|
+
const initScript = newDoc.createElement("script");
|
|
72
|
+
initScript.textContent = `
|
|
73
|
+
// Initialize the app when document is ready
|
|
74
|
+
const CAPTURED_APP_DATA = ${JSON.stringify(appData)};
|
|
75
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
76
|
+
const { createApp, ref } = Vue;
|
|
77
|
+
// Create app with captured data
|
|
78
|
+
const app = createApp({
|
|
79
|
+
data() {
|
|
80
|
+
return {
|
|
81
|
+
tableData: CAPTURED_APP_DATA.tableData || [],
|
|
82
|
+
loading: CAPTURED_APP_DATA.loading || false,
|
|
83
|
+
totalRecords: CAPTURED_APP_DATA.totalRecords || 0,
|
|
84
|
+
first: CAPTURED_APP_DATA.first || 0,
|
|
85
|
+
lazyParams: CAPTURED_APP_DATA.lazyParams || {},
|
|
86
|
+
siteName: CAPTURED_APP_DATA.siteName || 'BACnet Inspector',
|
|
87
|
+
statCounts: CAPTURED_APP_DATA.statCounts || {},
|
|
88
|
+
displayedRowsCount: CAPTURED_APP_DATA.displayedRowsCount || 0, // Track how many rows are visible
|
|
89
|
+
filters: {
|
|
90
|
+
global: { value: null, matchMode: "contains" },
|
|
91
|
+
deviceID: { value: null, matchMode: "contains" },
|
|
92
|
+
ObjectType: { value: null, matchMode: "contains" },
|
|
93
|
+
objectInstance: { value: null, matchMode: "contains" },
|
|
94
|
+
presentValue: { value: null, matchMode: "contains" },
|
|
95
|
+
dataModelStatus: { value: null, matchMode: "contains" },
|
|
96
|
+
pointName: { value: null, matchMode: "contains" },
|
|
97
|
+
discoveredBACnetPointName: { value: null, matchMode: "contains" },
|
|
98
|
+
displayName: { value: null, matchMode: "contains" },
|
|
99
|
+
deviceName: { value: null, matchMode: "contains" },
|
|
100
|
+
ipAddress: { value: null, matchMode: "contains" },
|
|
101
|
+
area: { value: null, matchMode: "contains" },
|
|
102
|
+
key: { value: null, matchMode: "contains" },
|
|
103
|
+
topic: { value: null, matchMode: "contains" },
|
|
104
|
+
lastSeen: { value: null, matchMode: "contains" },
|
|
105
|
+
error: { value: null, matchMode: "contains" },
|
|
106
|
+
},
|
|
107
|
+
allColumns: [
|
|
108
|
+
{ field: "deviceID", header: "Device ID" },
|
|
109
|
+
{ field: "objectType", header: "Object Type" },
|
|
110
|
+
{ field: "objectInstance", header: "Object Instance" },
|
|
111
|
+
{ field: "presentValue", header: "Present Value" },
|
|
112
|
+
{ field: "dataModelStatus", header: "Data Model Status" },
|
|
113
|
+
{ field: "pointName", header: "Mapped Point Name" },
|
|
114
|
+
{ field: "discoveredBACnetPointName", header: "Discovered Point Name" },
|
|
115
|
+
{ field: "displayName", header: "Display Name" },
|
|
116
|
+
{ field: "deviceName", header: "Device Name" },
|
|
117
|
+
{ field: "ipAddress", header: "IP Address" },
|
|
118
|
+
{ field: "area", header: "Area" },
|
|
119
|
+
{ field: "key", header: "Key" },
|
|
120
|
+
{ field: "topic", header: "Topic" },
|
|
121
|
+
{ field: "lastSeen", header: "Last Seen" },
|
|
122
|
+
{ field: "error", header: "Error" }
|
|
123
|
+
],
|
|
124
|
+
selectedColumns: CAPTURED_APP_DATA.selectedColumns || [],
|
|
125
|
+
selectedColumnValues: CAPTURED_APP_DATA.selectedColumnValues || [],
|
|
126
|
+
statPercentages: CAPTURED_APP_DATA.statPercentages || {
|
|
127
|
+
readCount: 0,
|
|
128
|
+
error: 0,
|
|
129
|
+
missing: 0,
|
|
130
|
+
warnings: 0,
|
|
131
|
+
deviceIdChange: 0,
|
|
132
|
+
deviceIdConflict: 0,
|
|
133
|
+
unmapped: 0
|
|
134
|
+
},
|
|
135
|
+
activeFilter: null,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
setup() {
|
|
139
|
+
return {};
|
|
140
|
+
},
|
|
141
|
+
mounted() {
|
|
142
|
+
let app = this;
|
|
143
|
+
this.selectedColumns = JSON.parse(JSON.stringify(this.allColumns));
|
|
144
|
+
|
|
145
|
+
// Calculate percentages
|
|
146
|
+
const total = this.tableData.length || 1; // Avoid division by zero
|
|
147
|
+
this.statPercentages = {
|
|
148
|
+
readCount: Math.round((this.statCounts.readCount / total) * 100),
|
|
149
|
+
error: Math.round((this.statCounts.statBlock.error / total) * 100),
|
|
150
|
+
missing: Math.round((this.statCounts.statBlock.missing / total) * 100),
|
|
151
|
+
warnings: Math.round((this.statCounts.statBlock.warnings / total) * 100),
|
|
152
|
+
deviceIdChange: Math.round((this.statCounts.statBlock.deviceIdChange / total) * 100),
|
|
153
|
+
deviceIdConflict: Math.round((this.statCounts.statBlock.deviceIdConflict / total) * 100 || 0),
|
|
154
|
+
unmapped: Math.round((this.statCounts.statBlock.unmapped / total) * 100)
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
computed: {
|
|
158
|
+
visibleColumns() {
|
|
159
|
+
return this.selectedColumns || []; // Handle null case
|
|
160
|
+
},
|
|
161
|
+
displayData() {
|
|
162
|
+
if (!this.tableData || this.tableData.length === 0) {
|
|
163
|
+
// Create empty rows to maintain height
|
|
164
|
+
return Array(20).fill({}).map(() => ({}));
|
|
165
|
+
}
|
|
166
|
+
return this.tableData;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
methods: {
|
|
170
|
+
statusItemClicked(e) {
|
|
171
|
+
// Get the filter category from the clicked item's text content
|
|
172
|
+
const clickedItem = e.currentTarget;
|
|
173
|
+
const statusType = clickedItem.querySelector('.statBlockKey').textContent.trim();
|
|
174
|
+
|
|
175
|
+
// Clear previous active status styling
|
|
176
|
+
document.querySelectorAll('.statusItem').forEach(item => {
|
|
177
|
+
item.classList.remove('active-filter');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// If clicking the already active filter, clear it
|
|
181
|
+
if (this.activeFilter === statusType) {
|
|
182
|
+
this.activeFilter = null;
|
|
183
|
+
this.filters['dataModelStatus'].value = null;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Set this item as active
|
|
188
|
+
clickedItem.classList.add('active-filter');
|
|
189
|
+
this.activeFilter = statusType;
|
|
190
|
+
|
|
191
|
+
// Apply the appropriate filter based on which status item was clicked
|
|
192
|
+
let filterValue = '';
|
|
193
|
+
switch (statusType) {
|
|
194
|
+
case 'Points OK':
|
|
195
|
+
filterValue = 'Point Ok';
|
|
196
|
+
break;
|
|
197
|
+
case 'Points Error':
|
|
198
|
+
filterValue = 'Point Error';
|
|
199
|
+
break;
|
|
200
|
+
case 'Points Missing':
|
|
201
|
+
filterValue = 'Point Missing';
|
|
202
|
+
break;
|
|
203
|
+
case 'Points Warnings':
|
|
204
|
+
filterValue = 'Point Warning';
|
|
205
|
+
break;
|
|
206
|
+
case 'Points Unmapped':
|
|
207
|
+
filterValue = 'Point Unmapped';
|
|
208
|
+
break;
|
|
209
|
+
case "Changed Device ID's":
|
|
210
|
+
filterValue = 'Device ID Changed';
|
|
211
|
+
break;
|
|
212
|
+
case "Conflicting Device ID's":
|
|
213
|
+
filterValue = 'Device ID Conflict';
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
filterValue = '';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Apply the filter
|
|
220
|
+
this.filters['dataModelStatus'].value = filterValue;
|
|
221
|
+
},
|
|
222
|
+
enforceTableHeight() {
|
|
223
|
+
// Force minimum height on table elements
|
|
224
|
+
const tableWrapper = document.querySelector('.p-datatable-wrapper');
|
|
225
|
+
if (tableWrapper) {
|
|
226
|
+
tableWrapper.style.minHeight = '800px';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const tableBody = document.querySelector('.p-datatable-tbody');
|
|
230
|
+
if (tableBody) {
|
|
231
|
+
tableBody.style.minHeight = '760px';
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
isSelected(option) {
|
|
235
|
+
if (!this.selectedColumns) return false;
|
|
236
|
+
return this.selectedColumns.some(item => item.field === option.field);
|
|
237
|
+
},
|
|
238
|
+
getRowClass(rowData) {
|
|
239
|
+
console.log("rowData", rowData);
|
|
240
|
+
if (rowData.dataModelStatus.includes("Point OK")) return "row-ok";
|
|
241
|
+
if (rowData.dataModelStatus.includes("Point Error")) return "row-error";
|
|
242
|
+
if (rowData.dataModelStatus.includes("Point Warning")) return "row-warning";
|
|
243
|
+
if (rowData.dataModelStatus.includes("Point Missing")) return "row-missing";
|
|
244
|
+
return ""; // Default, no class
|
|
245
|
+
},
|
|
246
|
+
onFilter(e) {
|
|
247
|
+
// e.filteredValue contains the new filtered dataset
|
|
248
|
+
if (e.filteredValue) {
|
|
249
|
+
this.displayedRowsCount = e.filteredValue.length;
|
|
250
|
+
} else {
|
|
251
|
+
// if no filter is applied, revert to full tableData length
|
|
252
|
+
this.displayedRowsCount = this.tableData ? this.tableData.length : 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.$nextTick(() => {
|
|
256
|
+
this.enforceTableHeight();
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
components: {
|
|
261
|
+
"p-button": PrimeVue.Button,
|
|
262
|
+
"p-card": PrimeVue.Card,
|
|
263
|
+
"p-input-text": PrimeVue.InputText,
|
|
264
|
+
"p-datatable": PrimeVue.DataTable,
|
|
265
|
+
"p-column": PrimeVue.Column,
|
|
266
|
+
"p-icon-field": PrimeVue.IconField,
|
|
267
|
+
"p-input-icon": PrimeVue.InputIcon,
|
|
268
|
+
"p-multiselect": PrimeVue.MultiSelect
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
app.use(PrimeVue.Config);
|
|
273
|
+
|
|
274
|
+
// Initialize the app template
|
|
275
|
+
document.getElementById('app').innerHTML = \`
|
|
276
|
+
<div class="card header-content">
|
|
277
|
+
<div class="header">
|
|
278
|
+
<div class="dividerRight">
|
|
279
|
+
<img src="data:image/svg+xml;base64,${await getLogoAsBase64()}" class="logo" alt="Bitpool" />
|
|
280
|
+
</div>
|
|
281
|
+
<div class="status">
|
|
282
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
283
|
+
<span class="status-icon-wrapper" style="background-color: #10B981;">
|
|
284
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.ok}" class="status-icon ok-icon" alt="Points OK" />
|
|
285
|
+
</span>
|
|
286
|
+
<div class="status-text">
|
|
287
|
+
<span class="statBlockValue">
|
|
288
|
+
{{statCounts?.readCount}}
|
|
289
|
+
<span class="stat-percentage">{{statPercentages.readCount}}%</span>
|
|
290
|
+
</span>
|
|
291
|
+
<span class="statBlockKey">Points OK</span>
|
|
292
|
+
</div>
|
|
293
|
+
</span>
|
|
294
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
295
|
+
<span class="status-icon-wrapper" style="background-color: #F1707B;">
|
|
296
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.error}" class="status-icon error-icon" alt="Points Error" />
|
|
297
|
+
</span>
|
|
298
|
+
<div class="status-text">
|
|
299
|
+
<span class="statBlockValue">
|
|
300
|
+
{{statCounts?.statBlock.error}}
|
|
301
|
+
<span class="stat-percentage">{{statPercentages.error}}%</span>
|
|
302
|
+
</span>
|
|
303
|
+
<span class="statBlockKey">Points Error</span>
|
|
304
|
+
</div>
|
|
305
|
+
</span>
|
|
306
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
307
|
+
<span class="status-icon-wrapper" style="background-color: #133547;">
|
|
308
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.missing}" class="status-icon missing-icon" alt="Points Missing" />
|
|
309
|
+
</span>
|
|
310
|
+
<div class="status-text">
|
|
311
|
+
<span class="statBlockValue">
|
|
312
|
+
{{statCounts?.statBlock.missing}}
|
|
313
|
+
<span class="stat-percentage">{{statPercentages.missing}}%</span>
|
|
314
|
+
</span>
|
|
315
|
+
<span class="statBlockKey">Points Missing</span>
|
|
316
|
+
</div>
|
|
317
|
+
</span>
|
|
318
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
319
|
+
<span class="status-icon-wrapper" style="background-color: #F59E0B;">
|
|
320
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.warning}" class="status-icon warning-icon" alt="Points Warning" />
|
|
321
|
+
</span>
|
|
322
|
+
<div class="status-text">
|
|
323
|
+
<span class="statBlockValue">
|
|
324
|
+
{{statCounts?.statBlock.warnings}}
|
|
325
|
+
<span class="stat-percentage">{{statPercentages.warnings}}%</span>
|
|
326
|
+
</span>
|
|
327
|
+
<span class="statBlockKey">Points Warnings</span>
|
|
328
|
+
</div>
|
|
329
|
+
</span>
|
|
330
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
331
|
+
<span class="status-icon-wrapper" style="background-color: #00ADEF;">
|
|
332
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.unmapped}" class="status-icon unmapped-icon" alt="Points Unmapped" />
|
|
333
|
+
</span>
|
|
334
|
+
<div class="status-text">
|
|
335
|
+
<span class="statBlockValue">
|
|
336
|
+
{{statCounts?.statBlock.unmapped}}
|
|
337
|
+
<span class="stat-percentage">{{statPercentages.unmapped}}%</span>
|
|
338
|
+
</span>
|
|
339
|
+
<span class="statBlockKey">Points Unmapped</span>
|
|
340
|
+
</div>
|
|
341
|
+
</span>
|
|
342
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
343
|
+
<span class="status-icon-wrapper" style="background-color: #0689BC;">
|
|
344
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.deviceIdChange}" class="status-icon device-id-change-icon" alt="Device ID Change" />
|
|
345
|
+
</span>
|
|
346
|
+
<div class="status-text">
|
|
347
|
+
<span class="statBlockValue">
|
|
348
|
+
{{statCounts?.statBlock.deviceIdChange}}
|
|
349
|
+
<span class="stat-percentage">{{statPercentages.deviceIdChange}}%</span>
|
|
350
|
+
</span>
|
|
351
|
+
<span class="statBlockKey">Changed Device ID's</span>
|
|
352
|
+
</div>
|
|
353
|
+
</span>
|
|
354
|
+
<span @click="statusItemClicked" class="statusItem status-with-icon">
|
|
355
|
+
<span class="status-icon-wrapper" style="background-color: #406C7D;">
|
|
356
|
+
<img src="data:image/svg+xml;base64,${iconBase64Map.deviceIdConflict}"
|
|
357
|
+
class="status-icon device-id-conflict-icon" alt="Device ID Conflict" />
|
|
358
|
+
</span>
|
|
359
|
+
<div class="status-text">
|
|
360
|
+
<span class="statBlockValue">
|
|
361
|
+
{{statCounts?.statBlock.deviceIdConflict}}
|
|
362
|
+
<span class="stat-percentage">{{statPercentages.deviceIdConflict}}%</span>
|
|
363
|
+
</span>
|
|
364
|
+
<span class="statBlockKey">Conflicting Device ID's</span>
|
|
365
|
+
</div>
|
|
366
|
+
</span>
|
|
367
|
+
</div>
|
|
368
|
+
<div class="actionButtons">
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
</div>
|
|
373
|
+
<div class="content-wrapper">
|
|
374
|
+
<div class="card datatable-card">
|
|
375
|
+
<div class="center">
|
|
376
|
+
<p class="headertext">{{siteName}} - Point Status Display</p>
|
|
377
|
+
</div>
|
|
378
|
+
<p-datatable :value="displayData" paginator :rows="12" filterDisplay="menu" :filters="filters"
|
|
379
|
+
v-model:filters="filters" :sortMode="'multiple'" @filter="onFilter" scrollable scrollHeight="800px"
|
|
380
|
+
class="datatable fixed-height-table">
|
|
381
|
+
<template #header>
|
|
382
|
+
<div class="tableHeaderDiv">
|
|
383
|
+
<div style="margin-right: 5px">
|
|
384
|
+
<p-multiselect v-model="selectedColumns" :options="allColumns"
|
|
385
|
+
optionLabel="header" dataKey="field" placeholder="Select Columns" :maxSelectedLabels="3"
|
|
386
|
+
class="columnSelector w-full md:w-20rem" display="chip">
|
|
387
|
+
|
|
388
|
+
<template #option="slotProps">
|
|
389
|
+
<div style="display: flex; align-items: center;">
|
|
390
|
+
<div class="custom-checkbox">
|
|
391
|
+
<div :class="['custom-checkbox-box', {'selected': isSelected(slotProps.option)}]">
|
|
392
|
+
<i v-if="isSelected(slotProps.option)" class="pi pi-check custom-checkbox-icon"></i>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<span>{{ slotProps.option.header }}</span>
|
|
396
|
+
</div>
|
|
397
|
+
</template>
|
|
398
|
+
|
|
399
|
+
<template #value="slotProps">
|
|
400
|
+
<template v-if="slotProps.value && slotProps.value.length > 0">
|
|
401
|
+
<template v-if="slotProps.value.length <= 3">
|
|
402
|
+
<div class="p-multiselect-token" v-for="(item, index) in slotProps.value" :key="item.field">
|
|
403
|
+
<span class="p-multiselect-token-label">{{ item.header }}</span>
|
|
404
|
+
<span v-if="index < slotProps.value.length - 1">, </span>
|
|
405
|
+
</div>
|
|
406
|
+
</template>
|
|
407
|
+
<template v-else>
|
|
408
|
+
<div class="p-multiselect-token-label">{{ slotProps.value.length }} items selected</div>
|
|
409
|
+
</template>
|
|
410
|
+
</template>
|
|
411
|
+
<span v-else class="p-multiselect-placeholder">{{ 'Select Columns' }}</span>
|
|
412
|
+
</template>
|
|
413
|
+
</p-multiselect>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div style="width: 100%">
|
|
417
|
+
<i class="pi pi-search searchBarContainer"></i>
|
|
418
|
+
<p-input-text class="searchBar" type="text" pInputText v-model="filters['global'].value"
|
|
419
|
+
placeholder="Search"></p-input-text>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
</template>
|
|
424
|
+
<p-column v-for="col in visibleColumns" :key="col.field" :field="col.field" :header="col.header" sortable
|
|
425
|
+
filter></p-column>
|
|
426
|
+
<template #paginatorstart>
|
|
427
|
+
</template>
|
|
428
|
+
<template #paginatorend="slotProps">
|
|
429
|
+
<span class="">
|
|
430
|
+
<span class="statBlockValue">{{displayedRowsCount}} results</span>
|
|
431
|
+
</span>
|
|
432
|
+
</template>
|
|
433
|
+
</p-datatable>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
\`;
|
|
437
|
+
|
|
438
|
+
// Mount the app
|
|
439
|
+
app.mount('#app');
|
|
440
|
+
});
|
|
441
|
+
`;
|
|
442
|
+
|
|
443
|
+
newDoc.body.appendChild(initScript);
|
|
444
|
+
|
|
445
|
+
// Add styles directly from the current document
|
|
446
|
+
const stylesText = await extractAllStyles();
|
|
447
|
+
const style = newDoc.createElement("style");
|
|
448
|
+
style.textContent = stylesText;
|
|
449
|
+
newDoc.head.appendChild(style);
|
|
450
|
+
|
|
451
|
+
// Return the complete HTML document
|
|
452
|
+
return "<!DOCTYPE html>" + newDoc.documentElement.outerHTML;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Add all required dependencies to the document
|
|
457
|
+
* @param {Document} doc - The HTML document
|
|
458
|
+
*/
|
|
459
|
+
function addDependencies(doc) {
|
|
460
|
+
// Get all scripts from current document
|
|
461
|
+
const scripts = document.querySelectorAll('script');
|
|
462
|
+
|
|
463
|
+
// Get Vue script
|
|
464
|
+
const vueScriptSrc = Array.from(scripts)
|
|
465
|
+
.find(script => script.src.includes('vue') && script.src.includes('global.prod.js'))?.src
|
|
466
|
+
|| "resources/@bitpoolos/edge-bacnet/vue3513.global.prod.js";
|
|
467
|
+
|
|
468
|
+
// Add Vue 3
|
|
469
|
+
const vueScript = doc.createElement("script");
|
|
470
|
+
vueScript.src = vueScriptSrc;
|
|
471
|
+
doc.head.appendChild(vueScript);
|
|
472
|
+
|
|
473
|
+
// Get PrimeVue script
|
|
474
|
+
const primeVueScriptSrc = Array.from(scripts)
|
|
475
|
+
.find(script => script.src.includes('primevue.min.js'))?.src
|
|
476
|
+
|| "resources/@bitpoolos/edge-bacnet/primevue.min.js";
|
|
477
|
+
|
|
478
|
+
// Add PrimeVue UMD
|
|
479
|
+
const primeVueScript = doc.createElement("script");
|
|
480
|
+
primeVueScript.src = primeVueScriptSrc;
|
|
481
|
+
doc.head.appendChild(primeVueScript);
|
|
482
|
+
|
|
483
|
+
// Get all stylesheets from current document
|
|
484
|
+
const stylesheets = document.querySelectorAll('link[rel="stylesheet"]');
|
|
485
|
+
|
|
486
|
+
// Map of stylesheet types we want to include
|
|
487
|
+
const stylesheetTypes = [
|
|
488
|
+
{ name: 'primevue-saga-blue-theme', src: 'resources/@bitpoolos/edge-bacnet/primevue-saga-blue-theme.css' },
|
|
489
|
+
{ name: 'primevue.min', src: 'resources/@bitpoolos/edge-bacnet/primevue.min.css' },
|
|
490
|
+
{ name: 'primeflex.min', src: 'resources/@bitpoolos/edge-bacnet/primeflex.min.css' },
|
|
491
|
+
{ name: 'primeicons', src: 'resources/@bitpoolos/edge-bacnet/primeicons.css' },
|
|
492
|
+
{ name: 'inspectorStyles', src: 'resources/@bitpoolos/edge-bacnet/inspectorStyles.css' }
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
// Add each stylesheet
|
|
496
|
+
stylesheetTypes.forEach(styleType => {
|
|
497
|
+
// Try to find in document first
|
|
498
|
+
const stylesheet = Array.from(stylesheets)
|
|
499
|
+
.find(link => link.href.includes(styleType.name));
|
|
500
|
+
|
|
501
|
+
const cssLink = doc.createElement("link");
|
|
502
|
+
cssLink.rel = "stylesheet";
|
|
503
|
+
cssLink.href = stylesheet ? stylesheet.href : styleType.src;
|
|
504
|
+
doc.head.appendChild(cssLink);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Extract all styles from the current document
|
|
510
|
+
* @returns {Promise<string>} - Combined CSS content
|
|
511
|
+
*/
|
|
512
|
+
async function extractAllStyles() {
|
|
513
|
+
let cssText = "";
|
|
514
|
+
|
|
515
|
+
// Get inline styles
|
|
516
|
+
document.querySelectorAll("style").forEach((style) => {
|
|
517
|
+
cssText += style.textContent + "\n";
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Try to get linked stylesheets
|
|
521
|
+
const styleSheets = document.querySelectorAll('link[rel="stylesheet"]');
|
|
522
|
+
for (const sheet of styleSheets) {
|
|
523
|
+
try {
|
|
524
|
+
const response = await fetch(sheet.href);
|
|
525
|
+
if (response.ok) {
|
|
526
|
+
const text = await response.text();
|
|
527
|
+
cssText += text + "\n";
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.warn(`Could not fetch stylesheet ${sheet.href}:`, error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return cssText;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Extract the current application state
|
|
539
|
+
* @returns {Object} - The application state
|
|
540
|
+
*/
|
|
541
|
+
function extractAppState() {
|
|
542
|
+
// Try to find the Vue app instance
|
|
543
|
+
const appElement = document.getElementById("app");
|
|
544
|
+
if (!appElement || !appElement.__vue_app__) {
|
|
545
|
+
console.warn("Vue app instance not found");
|
|
546
|
+
return {};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Get the Vue instance
|
|
550
|
+
const vueApp = appElement.__vue_app__;
|
|
551
|
+
|
|
552
|
+
// Get component instance
|
|
553
|
+
let componentInstance = null;
|
|
554
|
+
if (vueApp._instance && vueApp._instance.data) {
|
|
555
|
+
componentInstance = vueApp._instance;
|
|
556
|
+
} else if (vueApp._container && vueApp._container.__vue__) {
|
|
557
|
+
componentInstance = vueApp._container.__vue__;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!componentInstance) {
|
|
561
|
+
console.warn("Vue component instance not found");
|
|
562
|
+
return {};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Extract data from component
|
|
566
|
+
const appData = {};
|
|
567
|
+
|
|
568
|
+
// Try different ways to access the component data
|
|
569
|
+
if (componentInstance.data) {
|
|
570
|
+
// Get key data properties
|
|
571
|
+
const data = componentInstance.data;
|
|
572
|
+
|
|
573
|
+
// Copy important properties
|
|
574
|
+
appData.tableData = data.tableData;
|
|
575
|
+
appData.siteName = data.siteName;
|
|
576
|
+
appData.statCounts = data.statCounts;
|
|
577
|
+
appData.displayedRowsCount = data.displayedRowsCount;
|
|
578
|
+
appData.filters = data.filters;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return appData;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function getIconsAsBase64() {
|
|
585
|
+
const icons = {
|
|
586
|
+
ok: 'points-ok-icon.svg',
|
|
587
|
+
error: 'points-error-icon.svg',
|
|
588
|
+
missing: 'points-missing-icon.svg',
|
|
589
|
+
warning: 'points-warning-icon.svg',
|
|
590
|
+
deviceIdChange: 'device-id-change-icon.svg',
|
|
591
|
+
unmapped: 'points-unmapped-icon.svg',
|
|
592
|
+
deviceIdConflict: 'device-id-conflict-icon.svg'
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const iconBase64Map = {};
|
|
596
|
+
|
|
597
|
+
for (const [key, filename] of Object.entries(icons)) {
|
|
598
|
+
try {
|
|
599
|
+
const response = await fetch(`resources/@bitpoolos/edge-bacnet/icons/${filename}`);
|
|
600
|
+
if (!response.ok) continue;
|
|
601
|
+
|
|
602
|
+
const blob = await response.blob();
|
|
603
|
+
const base64 = await new Promise((resolve) => {
|
|
604
|
+
const reader = new FileReader();
|
|
605
|
+
reader.onloadend = () => resolve(reader.result.split(',')[1]);
|
|
606
|
+
reader.readAsDataURL(blob);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
iconBase64Map[key] = base64;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error(`Error converting ${filename} to base64:`, error);
|
|
612
|
+
iconBase64Map[key] = '';
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return iconBase64Map;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Get the Bitpool logo as base64 to embed it in the HTML
|
|
621
|
+
* @returns {Promise<string>} - Base64 encoded logo
|
|
622
|
+
*/
|
|
623
|
+
async function getLogoAsBase64() {
|
|
624
|
+
try {
|
|
625
|
+
// Find the logo in the current document
|
|
626
|
+
const logoImg = document.querySelector(".logo");
|
|
627
|
+
if (!logoImg) {
|
|
628
|
+
return ""; // No logo found
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Fetch the logo image
|
|
632
|
+
const response = await fetch(logoImg.src);
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
return ""; // Failed to fetch
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Get the image as blob
|
|
638
|
+
const blob = await response.blob();
|
|
639
|
+
|
|
640
|
+
// Convert blob to Base64
|
|
641
|
+
return new Promise((resolve) => {
|
|
642
|
+
const reader = new FileReader();
|
|
643
|
+
reader.onloadend = () => {
|
|
644
|
+
// Strip the data URL prefix (data:image/svg+xml;base64,)
|
|
645
|
+
const base64 = reader.result.split(",")[1];
|
|
646
|
+
resolve(base64);
|
|
647
|
+
};
|
|
648
|
+
reader.readAsDataURL(blob);
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error("Error converting logo to base64:", error);
|
|
652
|
+
return "";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M14 8.19876L11 5.19876M2.375 16.8238L4.91328 16.5417C5.22339 16.5073 5.37845 16.49 5.52338 16.4431C5.65197 16.4015 5.77434 16.3427 5.88717 16.2683C6.01434 16.1844 6.12466 16.0741 6.34529 15.8535L16.25 5.94876C17.0784 5.12033 17.0784 3.77719 16.25 2.94876C15.4216 2.12033 14.0784 2.12033 13.25 2.94876L3.3453 12.8535C3.12466 13.0741 3.01434 13.1844 2.93048 13.3116C2.85607 13.4244 2.79726 13.5468 2.75564 13.6754C2.70872 13.8203 2.69149 13.9754 2.65703 14.2855L2.375 16.8238Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M14 10.3889V14M14 14H10.3889M14 14L9.66667 9.66667M1 1L5.33333 5.33333M10.3889 1H14M14 1V4.61111M14 1L1 14" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M11.75 7.44876L7.25 11.9488M7.25 7.44876L11.75 11.9488M17 9.69876C17 13.8409 13.6421 17.1988 9.5 17.1988C5.35786 17.1988 2 13.8409 2 9.69876C2 5.55663 5.35786 2.19876 9.5 2.19876C13.6421 2.19876 17 5.55663 17 9.69876Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M10.25 5.94876L9.41334 4.27545C9.17255 3.79386 9.05215 3.55306 8.87253 3.37713C8.71368 3.22156 8.52224 3.10324 8.31206 3.03074C8.07437 2.94876 7.80516 2.94876 7.26672 2.94876H4.4C3.55992 2.94876 3.13988 2.94876 2.81901 3.11225C2.53677 3.25606 2.3073 3.48553 2.16349 3.76778C2 4.08864 2 4.50868 2 5.34876V5.94876M2 5.94876H13.4C14.6601 5.94876 15.2902 5.94876 15.7715 6.194C16.1948 6.40971 16.539 6.75392 16.7548 7.17728C17 7.65858 17 8.28864 17 9.54876V12.8488C17 14.1089 17 14.7389 16.7548 15.2202C16.539 15.6436 16.1948 15.9878 15.7715 16.2035C15.2902 16.4488 14.6601 16.4488 13.4 16.4488H5.6C4.33988 16.4488 3.70982 16.4488 3.22852 16.2035C2.80516 15.9878 2.46095 15.6436 2.24524 15.2202C2 14.7389 2 14.1089 2 12.8488V5.94876ZM7.88757 9.51294C8.01972 9.13728 8.28055 8.82051 8.62387 8.61874C8.96719 8.41697 9.37085 8.34321 9.76334 8.41054C10.1558 8.47786 10.5118 8.68192 10.7683 8.98657C11.0247 9.29121 11.1651 9.6768 11.1645 10.075C11.1645 11.1992 9.47826 11.7613 9.47826 11.7613M9.5 14.0113H9.5075" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|