Browse Source

refactor(iot-hub): use blob object URLs for device package zip images

Switch from base64 data URLs to URL.createObjectURL for images extracted from the device package zip. Revokes them in ngOnDestroy to avoid leaking memory, and avoids embedding multi-megabyte data URLs in the DOM.
pull/15787/head
Igor Kulikov 4 months ago
parent
commit
402732377a
  1. 32
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts

32
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, Inject, OnInit, Type, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { import {
@ -40,9 +40,6 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { generateSecret } from '@core/utils'; import { generateSecret } from '@core/utils';
import { import {
installMethodIcons as INSTALL_METHOD_ICONS,
installMethodLabels as INSTALL_METHOD_LABELS,
peOnlyInstallMethods,
DeviceInstallStep, DeviceInstallStep,
DevicePackageInfo, DevicePackageInfo,
ENTITY_STEP_TYPES, ENTITY_STEP_TYPES,
@ -50,7 +47,10 @@ import {
EntityStepProgress, EntityStepProgress,
FormFieldDefinition, FormFieldDefinition,
FormFieldType, FormFieldType,
installMethodIcons as INSTALL_METHOD_ICONS,
installMethodLabels as INSTALL_METHOD_LABELS,
InstallStepType, InstallStepType,
peOnlyInstallMethods,
stepTypeAliasMap stepTypeAliasMap
} from '@shared/models/iot-hub/device-package.models'; } from '@shared/models/iot-hub/device-package.models';
import { mergeMap } from 'rxjs/operators'; import { mergeMap } from 'rxjs/operators';
@ -89,7 +89,7 @@ const DEFAULT_RANDOM_SIZE = 20;
templateUrl: './device-install-dialog.component.html', templateUrl: './device-install-dialog.component.html',
styleUrls: ['./device-install-dialog.component.scss'] styleUrls: ['./device-install-dialog.component.scss']
}) })
export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInstallDialogComponent> implements OnInit { export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInstallDialogComponent> implements OnInit, OnDestroy {
@ViewChild('installStepper', {static: false}) stepper: MatStepper; @ViewChild('installStepper', {static: false}) stepper: MatStepper;
@ -148,9 +148,9 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
if (!entry.dir) { if (!entry.dir) {
const ext = path.split('.').pop()?.toLowerCase(); const ext = path.split('.').pop()?.toLowerCase();
if (imageExtensions.has(ext)) { if (imageExtensions.has(ext)) {
const base64 = await entry.async('base64'); const blob = await entry.async('blob');
const mimeType = ext === 'jpg' ? 'image/jpeg' : ext === 'svg' ? 'image/svg+xml' : `image/${ext}`; const blobUrl = URL.createObjectURL(blob);
this.zipImages.set(path, `data:${mimeType};base64,${base64}`); this.zipImages.set(path, blobUrl);
} else { } else {
const content = await entry.async('string'); const content = await entry.async('string');
this.zipFiles.set(path, content); this.zipFiles.set(path, content);
@ -200,6 +200,13 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
this.cdr.detectChanges(); this.cdr.detectChanges();
} }
ngOnDestroy() {
for (const url of this.zipImages.values()) {
URL.revokeObjectURL(url);
}
super.ngOnDestroy();
}
// --- Connectivity --- // --- Connectivity ---
selectConnectivity(ct: string): void { selectConnectivity(ct: string): void {
@ -237,8 +244,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
const ws = this.wizardSteps[index]; const ws = this.wizardSteps[index];
if (!ws) return; if (!ws) return;
if (ws.type === 'instruction' && !ws.markdown) { if (ws.type === 'instruction' && !ws.markdown) {
const raw = this.zipFiles.get(ws.rawSteps[0].file) || ''; ws.markdown = this.zipFiles.get(ws.rawSteps[0].file) || '';
ws.markdown = raw;
} else if (ws.type === 'progress' && !ws.progressDone) { } else if (ws.type === 'progress' && !ws.progressDone) {
this.showCompletedEntitySteps(ws); this.showCompletedEntitySteps(ws);
} }
@ -546,8 +552,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
// Pre-activate all steps for tab mode (tabs render lazily on first select) // Pre-activate all steps for tab mode (tabs render lazily on first select)
for (const ws of this.wizardSteps) { for (const ws of this.wizardSteps) {
if (ws.type === 'instruction') { if (ws.type === 'instruction') {
const raw = this.zipFiles.get(ws.rawSteps[0].file) || ''; ws.markdown = this.zipFiles.get(ws.rawSteps[0].file) || '';
ws.markdown = raw;
} else if (ws.type === 'progress') { } else if (ws.type === 'progress') {
this.showCompletedEntitySteps(ws); this.showCompletedEntitySteps(ws);
} }
@ -677,8 +682,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
return; return;
} }
if (step.type === 'instruction') { if (step.type === 'instruction') {
const raw = this.zipFiles.get(step.rawSteps[0].file) || ''; step.markdown = this.zipFiles.get(step.rawSteps[0].file) || '';
step.markdown = raw;
} else if (step.type === 'progress' && !step.progressDone) { } else if (step.type === 'progress' && !step.progressDone) {
if (this.reviewMode) { if (this.reviewMode) {
this.showCompletedEntitySteps(step); this.showCompletedEntitySteps(step);

Loading…
Cancel
Save