@bitpoolos/edge-bacnet 1.5.3 → 1.6.1

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,656 @@
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
+ ok: 0,
129
+ error: 0,
130
+ missing: 0,
131
+ warnings: 0,
132
+ deviceIdChange: 0,
133
+ deviceIdConflict: 0,
134
+ unmapped: 0
135
+ },
136
+ activeFilter: null,
137
+ };
138
+ },
139
+ setup() {
140
+ return {};
141
+ },
142
+ mounted() {
143
+ let app = this;
144
+ this.selectedColumns = JSON.parse(JSON.stringify(this.allColumns));
145
+
146
+ // Calculate percentages
147
+ const total = this.tableData.length || 1; // Avoid division by zero
148
+ this.statPercentages = {
149
+ readCount: Math.round((this.statCounts.readCount / total) * 100),
150
+ ok: Math.round((this.statCounts.statBlock.ok / total) * 100),
151
+ error: Math.round((this.statCounts.statBlock.error / total) * 100),
152
+ missing: Math.round((this.statCounts.statBlock.missing / total) * 100),
153
+ warnings: Math.round((this.statCounts.statBlock.warnings / total) * 100),
154
+ deviceIdChange: Math.round((this.statCounts.statBlock.deviceIdChange / total) * 100),
155
+ deviceIdConflict: Math.round((this.statCounts.statBlock.deviceIdConflict / total) * 100 || 0),
156
+ unmapped: Math.round((this.statCounts.statBlock.unmapped / total) * 100)
157
+ };
158
+ },
159
+ computed: {
160
+ visibleColumns() {
161
+ return this.selectedColumns || []; // Handle null case
162
+ },
163
+ displayData() {
164
+ if (!this.tableData || this.tableData.length === 0) {
165
+ // Create empty rows to maintain height
166
+ return Array(20).fill({}).map(() => ({}));
167
+ }
168
+ return this.tableData;
169
+ }
170
+ },
171
+ methods: {
172
+ statusItemClicked(e) {
173
+ // Get the filter category from the clicked item's text content
174
+ const clickedItem = e.currentTarget;
175
+ const statusType = clickedItem.querySelector('.statBlockKey').textContent.trim();
176
+
177
+ // Clear previous active status styling
178
+ document.querySelectorAll('.statusItem').forEach(item => {
179
+ item.classList.remove('active-filter');
180
+ });
181
+
182
+ // If clicking the already active filter, clear it
183
+ if (this.activeFilter === statusType) {
184
+ this.activeFilter = null;
185
+ this.filters['dataModelStatus'].value = null;
186
+ return;
187
+ }
188
+
189
+ // Set this item as active
190
+ clickedItem.classList.add('active-filter');
191
+ this.activeFilter = statusType;
192
+
193
+ // Apply the appropriate filter based on which status item was clicked
194
+ let filterValue = '';
195
+ switch (statusType) {
196
+ case 'Points OK':
197
+ filterValue = 'Point Ok';
198
+ break;
199
+ case 'Points Error':
200
+ filterValue = 'Point Error';
201
+ break;
202
+ case 'Points Missing':
203
+ filterValue = 'Point Missing';
204
+ break;
205
+ case 'Points Warnings':
206
+ filterValue = 'Point Warning';
207
+ break;
208
+ case 'Points Unmapped':
209
+ filterValue = 'Point Unmapped';
210
+ break;
211
+ case "Changed Device ID's":
212
+ filterValue = 'Device ID Changed';
213
+ break;
214
+ case "Conflicting Device ID's":
215
+ filterValue = 'Device ID Conflict';
216
+ break;
217
+ default:
218
+ filterValue = '';
219
+ }
220
+
221
+ // Apply the filter
222
+ this.filters['dataModelStatus'].value = filterValue;
223
+ },
224
+ enforceTableHeight() {
225
+ // Force minimum height on table elements
226
+ const tableWrapper = document.querySelector('.p-datatable-wrapper');
227
+ if (tableWrapper) {
228
+ tableWrapper.style.minHeight = '800px';
229
+ }
230
+
231
+ const tableBody = document.querySelector('.p-datatable-tbody');
232
+ if (tableBody) {
233
+ tableBody.style.minHeight = '760px';
234
+ }
235
+ },
236
+ isSelected(option) {
237
+ if (!this.selectedColumns) return false;
238
+ return this.selectedColumns.some(item => item.field === option.field);
239
+ },
240
+ getRowClass(rowData) {
241
+ console.log("rowData", rowData);
242
+ if (rowData.dataModelStatus.includes("Point OK")) return "row-ok";
243
+ if (rowData.dataModelStatus.includes("Point Error")) return "row-error";
244
+ if (rowData.dataModelStatus.includes("Point Warning")) return "row-warning";
245
+ if (rowData.dataModelStatus.includes("Point Missing")) return "row-missing";
246
+ return ""; // Default, no class
247
+ },
248
+ onFilter(e) {
249
+ // e.filteredValue contains the new filtered dataset
250
+ if (e.filteredValue) {
251
+ this.displayedRowsCount = e.filteredValue.length;
252
+ } else {
253
+ // if no filter is applied, revert to full tableData length
254
+ this.displayedRowsCount = this.tableData ? this.tableData.length : 0;
255
+ }
256
+
257
+ this.$nextTick(() => {
258
+ this.enforceTableHeight();
259
+ });
260
+ },
261
+ },
262
+ components: {
263
+ "p-button": PrimeVue.Button,
264
+ "p-card": PrimeVue.Card,
265
+ "p-input-text": PrimeVue.InputText,
266
+ "p-datatable": PrimeVue.DataTable,
267
+ "p-column": PrimeVue.Column,
268
+ "p-icon-field": PrimeVue.IconField,
269
+ "p-input-icon": PrimeVue.InputIcon,
270
+ "p-multiselect": PrimeVue.MultiSelect
271
+ },
272
+ });
273
+
274
+ app.use(PrimeVue.Config);
275
+
276
+ // Initialize the app template
277
+ document.getElementById('app').innerHTML = \`
278
+ <div class="card header-content">
279
+ <div class="header">
280
+ <div class="dividerRight">
281
+ <img src="data:image/svg+xml;base64,${await getLogoAsBase64()}" class="logo" alt="Bitpool" />
282
+ </div>
283
+ <div class="status">
284
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
285
+ <span class="status-icon-wrapper" style="background-color: #10B981;">
286
+ <img src="data:image/svg+xml;base64,${iconBase64Map.ok}" class="status-icon ok-icon" alt="Points OK" />
287
+ </span>
288
+ <div class="status-text">
289
+ <span class="statBlockValue">
290
+ {{statCounts?.statBlock.ok}}
291
+ <span class="stat-percentage">{{statPercentages.ok}}%</span>
292
+ </span>
293
+ <span class="statBlockKey">Points OK</span>
294
+ </div>
295
+ </span>
296
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
297
+ <span class="status-icon-wrapper" style="background-color: #F1707B;">
298
+ <img src="data:image/svg+xml;base64,${iconBase64Map.error}" class="status-icon error-icon" alt="Points Error" />
299
+ </span>
300
+ <div class="status-text">
301
+ <span class="statBlockValue">
302
+ {{statCounts?.statBlock.error}}
303
+ <span class="stat-percentage">{{statPercentages.error}}%</span>
304
+ </span>
305
+ <span class="statBlockKey">Points Error</span>
306
+ </div>
307
+ </span>
308
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
309
+ <span class="status-icon-wrapper" style="background-color: #133547;">
310
+ <img src="data:image/svg+xml;base64,${iconBase64Map.missing}" class="status-icon missing-icon" alt="Points Missing" />
311
+ </span>
312
+ <div class="status-text">
313
+ <span class="statBlockValue">
314
+ {{statCounts?.statBlock.missing}}
315
+ <span class="stat-percentage">{{statPercentages.missing}}%</span>
316
+ </span>
317
+ <span class="statBlockKey">Points Missing</span>
318
+ </div>
319
+ </span>
320
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
321
+ <span class="status-icon-wrapper" style="background-color: #F59E0B;">
322
+ <img src="data:image/svg+xml;base64,${iconBase64Map.warning}" class="status-icon warning-icon" alt="Points Warning" />
323
+ </span>
324
+ <div class="status-text">
325
+ <span class="statBlockValue">
326
+ {{statCounts?.statBlock.warnings}}
327
+ <span class="stat-percentage">{{statPercentages.warnings}}%</span>
328
+ </span>
329
+ <span class="statBlockKey">Points Warnings</span>
330
+ </div>
331
+ </span>
332
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
333
+ <span class="status-icon-wrapper" style="background-color: #00ADEF;">
334
+ <img src="data:image/svg+xml;base64,${iconBase64Map.unmapped}" class="status-icon unmapped-icon" alt="Points Unmapped" />
335
+ </span>
336
+ <div class="status-text">
337
+ <span class="statBlockValue">
338
+ {{statCounts?.statBlock.unmapped}}
339
+ <span class="stat-percentage">{{statPercentages.unmapped}}%</span>
340
+ </span>
341
+ <span class="statBlockKey">Points Unmapped</span>
342
+ </div>
343
+ </span>
344
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
345
+ <span class="status-icon-wrapper" style="background-color: #0689BC;">
346
+ <img src="data:image/svg+xml;base64,${iconBase64Map.deviceIdChange}" class="status-icon device-id-change-icon" alt="Device ID Change" />
347
+ </span>
348
+ <div class="status-text">
349
+ <span class="statBlockValue">
350
+ {{statCounts?.statBlock.deviceIdChange}}
351
+ <span class="stat-percentage">{{statPercentages.deviceIdChange}}%</span>
352
+ </span>
353
+ <span class="statBlockKey">Changed Device ID's</span>
354
+ </div>
355
+ </span>
356
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
357
+ <span class="status-icon-wrapper" style="background-color: #406C7D;">
358
+ <img src="data:image/svg+xml;base64,${iconBase64Map.deviceIdConflict}"
359
+ class="status-icon device-id-conflict-icon" alt="Device ID Conflict" />
360
+ </span>
361
+ <div class="status-text">
362
+ <span class="statBlockValue">
363
+ {{statCounts?.statBlock.deviceIdConflict}}
364
+ <span class="stat-percentage">{{statPercentages.deviceIdConflict}}%</span>
365
+ </span>
366
+ <span class="statBlockKey">Conflicting Device ID's</span>
367
+ </div>
368
+ </span>
369
+ </div>
370
+ <div class="actionButtons">
371
+ </div>
372
+ </div>
373
+
374
+ </div>
375
+ <div class="content-wrapper">
376
+ <div class="card datatable-card">
377
+ <div class="center">
378
+ <p class="headertext">{{siteName}} - Point Status Display</p>
379
+ </div>
380
+ <p-datatable :value="displayData" paginator :rows="12" filterDisplay="menu" :filters="filters"
381
+ v-model:filters="filters" :sortMode="'multiple'" @filter="onFilter" scrollable scrollHeight="800px"
382
+ class="datatable fixed-height-table">
383
+ <template #header>
384
+ <div class="tableHeaderDiv">
385
+ <div style="margin-right: 5px">
386
+ <p-multiselect v-model="selectedColumns" :options="allColumns"
387
+ optionLabel="header" dataKey="field" placeholder="Select Columns" :maxSelectedLabels="3"
388
+ class="columnSelector w-full md:w-20rem" display="chip">
389
+
390
+ <template #option="slotProps">
391
+ <div style="display: flex; align-items: center;">
392
+ <div class="custom-checkbox">
393
+ <div :class="['custom-checkbox-box', {'selected': isSelected(slotProps.option)}]">
394
+ <i v-if="isSelected(slotProps.option)" class="pi pi-check custom-checkbox-icon"></i>
395
+ </div>
396
+ </div>
397
+ <span>{{ slotProps.option.header }}</span>
398
+ </div>
399
+ </template>
400
+
401
+ <template #value="slotProps">
402
+ <template v-if="slotProps.value && slotProps.value.length > 0">
403
+ <template v-if="slotProps.value.length <= 3">
404
+ <div class="p-multiselect-token" v-for="(item, index) in slotProps.value" :key="item.field">
405
+ <span class="p-multiselect-token-label">{{ item.header }}</span>
406
+ <span v-if="index < slotProps.value.length - 1">, </span>
407
+ </div>
408
+ </template>
409
+ <template v-else>
410
+ <div class="p-multiselect-token-label">{{ slotProps.value.length }} items selected</div>
411
+ </template>
412
+ </template>
413
+ <span v-else class="p-multiselect-placeholder">{{ 'Select Columns' }}</span>
414
+ </template>
415
+ </p-multiselect>
416
+ </div>
417
+
418
+ <div style="width: 100%">
419
+ <i class="pi pi-search searchBarContainer"></i>
420
+ <p-input-text class="searchBar" type="text" pInputText v-model="filters['global'].value"
421
+ placeholder="Search"></p-input-text>
422
+ </div>
423
+ </div>
424
+
425
+ </template>
426
+ <p-column v-for="col in visibleColumns" :key="col.field" :field="col.field" :header="col.header" sortable
427
+ filter></p-column>
428
+ <template #paginatorstart>
429
+ </template>
430
+ <template #paginatorend="slotProps">
431
+ <span class="">
432
+ <span class="statBlockValue">{{displayedRowsCount}} results</span>
433
+ </span>
434
+ </template>
435
+ </p-datatable>
436
+ </div>
437
+ </div>
438
+ \`;
439
+
440
+ // Mount the app
441
+ app.mount('#app');
442
+ });
443
+ `;
444
+
445
+ newDoc.body.appendChild(initScript);
446
+
447
+ // Add styles directly from the current document
448
+ const stylesText = await extractAllStyles();
449
+ const style = newDoc.createElement("style");
450
+ style.textContent = stylesText;
451
+ newDoc.head.appendChild(style);
452
+
453
+ // Return the complete HTML document
454
+ return "<!DOCTYPE html>" + newDoc.documentElement.outerHTML;
455
+ }
456
+
457
+ /**
458
+ * Add all required dependencies to the document
459
+ * @param {Document} doc - The HTML document
460
+ */
461
+ function addDependencies(doc) {
462
+ // Get all scripts from current document
463
+ const scripts = document.querySelectorAll('script');
464
+
465
+ // Get Vue script
466
+ const vueScriptSrc = Array.from(scripts)
467
+ .find(script => script.src.includes('vue') && script.src.includes('global.prod.js'))?.src
468
+ || "resources/@bitpoolos/edge-bacnet/vue3513.global.prod.js";
469
+
470
+ // Add Vue 3
471
+ const vueScript = doc.createElement("script");
472
+ vueScript.src = vueScriptSrc;
473
+ doc.head.appendChild(vueScript);
474
+
475
+ // Get PrimeVue script
476
+ const primeVueScriptSrc = Array.from(scripts)
477
+ .find(script => script.src.includes('primevue.min.js'))?.src
478
+ || "resources/@bitpoolos/edge-bacnet/primevue.min.js";
479
+
480
+ // Add PrimeVue UMD
481
+ const primeVueScript = doc.createElement("script");
482
+ primeVueScript.src = primeVueScriptSrc;
483
+ doc.head.appendChild(primeVueScript);
484
+
485
+ // Get all stylesheets from current document
486
+ const stylesheets = document.querySelectorAll('link[rel="stylesheet"]');
487
+
488
+ // Map of stylesheet types we want to include
489
+ const stylesheetTypes = [
490
+ { name: 'primevue-saga-blue-theme', src: 'resources/@bitpoolos/edge-bacnet/primevue-saga-blue-theme.css' },
491
+ { name: 'primevue.min', src: 'resources/@bitpoolos/edge-bacnet/primevue.min.css' },
492
+ { name: 'primeflex.min', src: 'resources/@bitpoolos/edge-bacnet/primeflex.min.css' },
493
+ { name: 'primeicons', src: 'resources/@bitpoolos/edge-bacnet/primeicons.css' },
494
+ { name: 'inspectorStyles', src: 'resources/@bitpoolos/edge-bacnet/inspectorStyles.css' }
495
+ ];
496
+
497
+ // Add each stylesheet
498
+ stylesheetTypes.forEach(styleType => {
499
+ // Try to find in document first
500
+ const stylesheet = Array.from(stylesheets)
501
+ .find(link => link.href.includes(styleType.name));
502
+
503
+ const cssLink = doc.createElement("link");
504
+ cssLink.rel = "stylesheet";
505
+ cssLink.href = stylesheet ? stylesheet.href : styleType.src;
506
+ doc.head.appendChild(cssLink);
507
+ });
508
+ }
509
+
510
+ /**
511
+ * Extract all styles from the current document
512
+ * @returns {Promise<string>} - Combined CSS content
513
+ */
514
+ async function extractAllStyles() {
515
+ let cssText = "";
516
+
517
+ // Get inline styles
518
+ document.querySelectorAll("style").forEach((style) => {
519
+ cssText += style.textContent + "\n";
520
+ });
521
+
522
+ // Try to get linked stylesheets
523
+ const styleSheets = document.querySelectorAll('link[rel="stylesheet"]');
524
+ for (const sheet of styleSheets) {
525
+ try {
526
+ const response = await fetch(sheet.href);
527
+ if (response.ok) {
528
+ const text = await response.text();
529
+ cssText += text + "\n";
530
+ }
531
+ } catch (error) {
532
+ console.warn(`Could not fetch stylesheet ${sheet.href}:`, error);
533
+ }
534
+ }
535
+
536
+ return cssText;
537
+ }
538
+
539
+ /**
540
+ * Extract the current application state
541
+ * @returns {Object} - The application state
542
+ */
543
+ function extractAppState() {
544
+ // Try to find the Vue app instance
545
+ const appElement = document.getElementById("app");
546
+ if (!appElement || !appElement.__vue_app__) {
547
+ console.warn("Vue app instance not found");
548
+ return {};
549
+ }
550
+
551
+ // Get the Vue instance
552
+ const vueApp = appElement.__vue_app__;
553
+
554
+ // Get component instance
555
+ let componentInstance = null;
556
+ if (vueApp._instance && vueApp._instance.data) {
557
+ componentInstance = vueApp._instance;
558
+ } else if (vueApp._container && vueApp._container.__vue__) {
559
+ componentInstance = vueApp._container.__vue__;
560
+ }
561
+
562
+ if (!componentInstance) {
563
+ console.warn("Vue component instance not found");
564
+ return {};
565
+ }
566
+
567
+ // Extract data from component
568
+ const appData = {};
569
+
570
+ // Try different ways to access the component data
571
+ if (componentInstance.data) {
572
+ // Get key data properties
573
+ const data = componentInstance.data;
574
+
575
+ // Copy important properties
576
+ appData.tableData = data.tableData;
577
+ appData.siteName = data.siteName;
578
+ appData.statCounts = data.statCounts;
579
+ appData.displayedRowsCount = data.displayedRowsCount;
580
+ appData.filters = data.filters;
581
+ }
582
+
583
+ return appData;
584
+ }
585
+
586
+ async function getIconsAsBase64() {
587
+ const icons = {
588
+ ok: 'points-ok-icon.svg',
589
+ error: 'points-error-icon.svg',
590
+ missing: 'points-missing-icon.svg',
591
+ warning: 'points-warning-icon.svg',
592
+ deviceIdChange: 'device-id-change-icon.svg',
593
+ unmapped: 'points-unmapped-icon.svg',
594
+ deviceIdConflict: 'device-id-conflict-icon.svg'
595
+ };
596
+
597
+ const iconBase64Map = {};
598
+
599
+ for (const [key, filename] of Object.entries(icons)) {
600
+ try {
601
+ const response = await fetch(`resources/@bitpoolos/edge-bacnet/icons/${filename}`);
602
+ if (!response.ok) continue;
603
+
604
+ const blob = await response.blob();
605
+ const base64 = await new Promise((resolve) => {
606
+ const reader = new FileReader();
607
+ reader.onloadend = () => resolve(reader.result.split(',')[1]);
608
+ reader.readAsDataURL(blob);
609
+ });
610
+
611
+ iconBase64Map[key] = base64;
612
+ } catch (error) {
613
+ console.error(`Error converting ${filename} to base64:`, error);
614
+ iconBase64Map[key] = '';
615
+ }
616
+ }
617
+
618
+ return iconBase64Map;
619
+ }
620
+
621
+ /**
622
+ * Get the Bitpool logo as base64 to embed it in the HTML
623
+ * @returns {Promise<string>} - Base64 encoded logo
624
+ */
625
+ async function getLogoAsBase64() {
626
+ try {
627
+ // Find the logo in the current document
628
+ const logoImg = document.querySelector(".logo");
629
+ if (!logoImg) {
630
+ return ""; // No logo found
631
+ }
632
+
633
+ // Fetch the logo image
634
+ const response = await fetch(logoImg.src);
635
+ if (!response.ok) {
636
+ return ""; // Failed to fetch
637
+ }
638
+
639
+ // Get the image as blob
640
+ const blob = await response.blob();
641
+
642
+ // Convert blob to Base64
643
+ return new Promise((resolve) => {
644
+ const reader = new FileReader();
645
+ reader.onloadend = () => {
646
+ // Strip the data URL prefix (data:image/svg+xml;base64,)
647
+ const base64 = reader.result.split(",")[1];
648
+ resolve(base64);
649
+ };
650
+ reader.readAsDataURL(blob);
651
+ });
652
+ } catch (error) {
653
+ console.error("Error converting logo to base64:", error);
654
+ return "";
655
+ }
656
+ }
@@ -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>