@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.
@@ -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>