UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

622 lines (618 loc) 167 kB
import * as i0 from '@angular/core'; import { Injectable, ViewChild, Component, ChangeDetectionStrategy, inject, InjectionToken, Input, NgModule } from '@angular/core'; import * as i4 from 'ngx-bootstrap/modal'; import { BsModalService } from 'ngx-bootstrap/modal'; import { __decorate, __metadata } from 'tslib'; import * as i1$1 from '@c8y/ngx-components'; import { gettext, C8yStepper, memoize, hookGeneric, ExtensionPointWithoutStateForPlugins, fromTriggerOnce, Status, Permissions, NavigatorNode, CommonModule, CoreModule, StepperModule, DynamicFormsModule, hookNavigator, DeviceBootstrapRealtimeService } from '@c8y/ngx-components'; import * as i8 from '@angular/forms'; import { FormGroup } from '@angular/forms'; import { Subject, BehaviorSubject, from, forkJoin, defer, Observable } from 'rxjs'; import { takeUntil, finalize, mergeMap, takeLast, map, tap, filter, switchMap, shareReplay, startWith } from 'rxjs/operators'; import * as i2 from '@c8y/client'; import { DeviceRegistrationStatus, DeviceRegistrationSecurityMode, ApplicationType } from '@c8y/client'; import * as i1 from '@angular/router'; import { RouterModule } from '@angular/router'; import { get, pick, sortBy, flatMap } from 'lodash-es'; import { saveAs } from 'file-saver'; import * as i5 from '@ngx-translate/core'; import * as i5$1 from '@angular/common'; import * as i6 from '@angular/cdk/stepper'; import { STEP_STATE } from '@angular/cdk/stepper'; import * as i7 from '@ngx-formly/core'; import * as i5$2 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { flatten } from 'lodash'; import * as i3 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; class RegisterDeviceService { constructor(router, deviceRegService, alertService) { this.router = router; this.deviceRegService = deviceRegService; this.alertService = alertService; this._loading = new Subject(); this._limit = new BehaviorSubject({ isReached: false }); this._deviceRegistrationRequests = new BehaviorSubject({ data: [] }); this.deviceRegistrationRequests$ = this._deviceRegistrationRequests.asObservable(); this.loading$ = this._loading.asObservable(); this.limit$ = this._limit.asObservable(); this.deviceRegUrl = '/deviceregistration'; this.endSubscriptions = new Subject(); } isDeviceRegistration() { return get(this.router, 'url') === this.deviceRegUrl; } internalListUpdate(deviceRequests, pagingObject) { let { paging, data } = this._deviceRegistrationRequests.getValue(); if (pagingObject) { paging = pagingObject; } data = [...data, ...deviceRequests].filter(deviceReq => deviceReq.type !== 'c8y_DataBroker'); this._deviceRegistrationRequests.next({ data, paging }); } onDeviceBootstrap(bsData) { const { id, status } = bsData; this._deviceRegistrationRequests.next({ data: this.updateStatusById(id, status) }); } list(pageSize = 100) { this._loading.next(true); this._deviceRegistrationRequests.next({ data: [], paging: undefined }); from(this.deviceRegService.list({ pageSize, withTotalPages: true })) .pipe(takeUntil(this.endSubscriptions), finalize(() => this.limit())) .subscribe(res => { const { data, paging } = res; this.internalListUpdate(data, paging); this._loading.next(false); }, err => { this._loading.next(false); this.alertService.addServerFailure(err); }); } createMultiple(newDeviceRequests) { if (newDeviceRequests && newDeviceRequests.length > 0) { this._loading.next(true); const newRequests$ = newDeviceRequests.map(element => { return from(this.deviceRegService.create(element).catch((err) => ({ res: err.res, data: { ...err.data, id: element.id } }))); }); const groupedRequests = { success: [], failed: [] }; return forkJoin(newRequests$).pipe(mergeMap(resp => resp.map(el => { el.res.ok ? groupedRequests.success.push(el.data) : groupedRequests.failed.push(el.data); return groupedRequests; })), takeLast(1), finalize(() => { this.internalListUpdate(groupedRequests.success); this._loading.next(false); })); } } remove(id) { this._loading.next(true); from(this.deviceRegService.delete(id)) .pipe(takeUntil(this.endSubscriptions)) .subscribe(() => { this._deviceRegistrationRequests.next({ data: this.removeDeviceRegistrationRequestById(id) }); this._loading.next(false); this.alertService.success(gettext('Device registration cancelled.')); }, err => { this._loading.next(false); this.alertService.addServerFailure(err); }); } accept(request) { this._loading.next(true); const payload = pick(request, ['id', 'securityToken']); from(this.deviceRegService.accept(payload)) .pipe(takeUntil(this.endSubscriptions)) .subscribe(() => { this._deviceRegistrationRequests.next({ data: this.removeDeviceRegistrationRequestById(payload.id) }); this.limit(); this._loading.next(false); this.alertService.success(gettext('Device registration accepted.')); }, err => { this._loading.next(false); this.alertService.addServerFailure(err); }); } acceptAll() { const acceptedDeviceRequests = []; const failedDeviceRequests = []; this._loading.next(true); from(this.deviceRegService.acceptAll()) .pipe(takeUntil(this.endSubscriptions), map(({ data }) => { data.map(deviceRegistrationRequest => { if (deviceRegistrationRequest.successful) { acceptedDeviceRequests.push(deviceRegistrationRequest); this.removeDeviceRegistrationRequestById(deviceRegistrationRequest.id); } else { failedDeviceRequests.push(deviceRegistrationRequest); } }); return data; }), finalize(() => { // update rendered list with successful accepted device registrations // see: this.updateStatusById(...) this.internalListUpdate([]); this.limit(); this._loading.next(false); if (failedDeviceRequests.length > 0) { this.alertService.warning(gettext('Could not accept all pending registration requests.'), JSON.stringify({ failedDeviceRequests, acceptedDeviceRequests }, undefined, 2)); } else { this.alertService.success(gettext('Accepted all pending registration requests.')); } })) .subscribe(() => { // empty by design }, err => { this._loading.next(false); this.alertService.addServerFailure(err); }); } limit() { from(this.deviceRegService.limit()) .pipe(takeUntil(this.endSubscriptions)) .subscribe(res => this._limit.next(res.data), err => this.alertService.addServerFailure(err)); } getRequestByStatus(status) { return this._deviceRegistrationRequests.getValue().data.filter(req => req.status === status); } ngOnDestroy() { this.endSubscriptions.next(); this.endSubscriptions.complete(); } updateStatusById(id, status) { const items = this._deviceRegistrationRequests.getValue().data; const matchingElementIndex = items.findIndex(element => element.id === id); if (matchingElementIndex >= 0) { items[matchingElementIndex].status = status; } return items; } removeDeviceRegistrationRequestById(id) { const items = this._deviceRegistrationRequests.getValue().data; const matchingElementIndex = items.findIndex(element => element.id === id); if (matchingElementIndex >= 0) { items.splice(matchingElementIndex, 1); } this._loading.next(false); return items; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RegisterDeviceService, deps: [{ token: i1.Router }, { token: i2.DeviceRegistrationService }, { token: i1$1.AlertService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RegisterDeviceService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: RegisterDeviceService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.Router }, { type: i2.DeviceRegistrationService }, { type: i1$1.AlertService }] }); const PRODUCT_EXPERIENCE_BASE_REGISTRATION = { EVENT: 'deviceRegistration', COMPONENT: { BULK: 'bulk-registration', EXTENSIBLE_BULK: 'bulk-extensible-registration', EXTENSIBLE_SINGLE: 'single-extensible-registration' }, RESULT: { SUCCESS: 'registrationSuccess', FAILURE: 'registrationFailure' } }; const registerDeviceBulkSchema = { $schema: 'https://json-schema.org/draft/2019-09/schema', type: 'object', properties: { csvBulkFile: { type: 'array', title: gettext('CSV file upload'), description: gettext('You can use file upload component to let users send files. This input accepts only a single CSV file.'), contentMediaType: 'csv' } }, required: ['csvBulkFile'], additionalProperties: false }; const simpleCsvHeaders = ['ID', 'PATH']; const csvHeaders = [ 'ID', 'TYPE', 'NAME', 'ICCID', 'IDTYPE', 'PATH', 'SHELL', 'AUTH_TYPE' ]; const fullCsvHeaders = [...csvHeaders, 'CREDENTIALS']; const ESTCsvHeaders = [...csvHeaders, 'ENROLLMENT_OTP']; class BulkDeviceRegistrationModalComponent { constructor(jsonschema, deviceRegistrationService, registerDeviceService, bsModalRef, gainsightService, featureCacheService, translateService) { this.jsonschema = jsonschema; this.deviceRegistrationService = deviceRegistrationService; this.registerDeviceService = registerDeviceService; this.bsModalRef = bsModalRef; this.gainsightService = gainsightService; this.featureCacheService = featureCacheService; this.translateService = translateService; this.form = new FormGroup({}); this.model = {}; this.certificateAuthorityFeatureEnabled = this.featureCacheService.getFeatureState('certificate-authority'); } ngOnInit() { this.template = [this.jsonschema.toFieldConfig(registerDeviceBulkSchema)]; } upload() { this.pending = true; const file = this.getFile(this.model); this.deviceRegistrationService .create(file) .then(({ res, data }) => { if (res.status < 400) { this.result = data; this.success = data.numberOfFailed === 0 && data.numberOfSuccessful === data.numberOfAll; this.message = this.success ? gettext('Device registration created.') : (this.message = gettext('Device registration failed.')); if (this.success) { this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_BASE_REGISTRATION.EVENT, { result: PRODUCT_EXPERIENCE_BASE_REGISTRATION.RESULT.SUCCESS, component: PRODUCT_EXPERIENCE_BASE_REGISTRATION.COMPONENT.BULK }); } else { this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_BASE_REGISTRATION.EVENT, { result: PRODUCT_EXPERIENCE_BASE_REGISTRATION.RESULT.FAILURE, component: PRODUCT_EXPERIENCE_BASE_REGISTRATION.COMPONENT.BULK }); } } else { this.failedResult = data; this.message = gettext('Device registration failed.'); this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_BASE_REGISTRATION.EVENT, { result: PRODUCT_EXPERIENCE_BASE_REGISTRATION.RESULT.FAILURE, component: PRODUCT_EXPERIENCE_BASE_REGISTRATION.COMPONENT.BULK }); } this.model = {}; this.pending = false; this.stepper.next(); }) .catch(() => { this.message = gettext('Error occurred while processing the uploaded file.'); this.pending = false; this.stepper.next(); }); } downloadSimple() { return this.download(simpleCsvHeaders, gettext('Simple bulk registration - template.csv')); } downloadFull() { return this.download(fullCsvHeaders, gettext('Full bulk registration - template.csv')); } downloadEst() { return this.download(ESTCsvHeaders, gettext('EST registration - template.csv')); } download(headers, fileName) { const headerRaw = headers.map(header => `"${header}"`).join(';'); const binaryFile = new Blob([headerRaw], { type: 'text/csv' }); saveAs(binaryFile, this.translateService.instant(fileName)); } complete() { this.registerDeviceService.list(); this.bsModalRef.hide(); } cancel() { this.bsModalRef.hide(); } getFile(model) { const csvBulkFile = model?.csvBulkFile; return csvBulkFile ? csvBulkFile[0]?.file : undefined; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BulkDeviceRegistrationModalComponent, deps: [{ token: i1$1.C8yJSONSchema }, { token: i2.DeviceRegistrationBulkService }, { token: RegisterDeviceService }, { token: i4.BsModalRef }, { token: i1$1.GainsightService }, { token: i1$1.FeatureCacheService }, { token: i5.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: BulkDeviceRegistrationModalComponent, isStandalone: false, selector: "bulk-device-registration", viewQueries: [{ propertyName: "stepper", first: true, predicate: C8yStepper, descendants: true, static: true }], ngImport: i0, template: "<c8y-modal\n [title]=\"'Bulk device registration' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [customFooter]=\"true\"\n>\n <ng-container c8y-modal-title>\n <i c8yIcon=\"upload\"></i>\n </ng-container>\n\n <c8y-stepper [hideStepProgress]=\"true\" linear id=\"modal-body\">\n <cdk-step>\n <p class=\"modal-subtitle sticky-top\" translate>Register devices in bulk</p>\n\n <c8y-form-group class=\"d-block p-24 p-t-16 p-b-0 m-b-0\">\n <formly-form [form]=\"form\" [fields]=\"template\" [model]=\"model\"></formly-form>\n </c8y-form-group>\n\n <div class=\"p-24 m-t-0 bg-level-1\">\n <div class=\"bg-gray-white separator-bottom p-t-16 p-b-16 p-l-24 p-r-24\">\n <div>\n <p class=\"m-b-8 text-medium\">\n <strong translate>Simple registration</strong>\n </p>\n <small class=\"text-muted\" translate>\n Creates all registration requests at once, then each one needs to go through regular\n acceptance process.\n </small>\n </div>\n <div class=\"m-b-16 m-t-16\">\n <a\n title=\"{{ 'Download template' | translate }}\"\n class=\"btn btn-default btn-sm\"\n target=\"_self\"\n (click)=\"downloadSimple()\"\n >\n <i c8yIcon=\"download\" translate></i>\n {{ 'Download template' | translate }}\n </a>\n </div>\n </div>\n <div class=\"bg-gray-white separator-bottom p-t-16 p-b-16 p-l-24 p-r-24\">\n <div>\n <p class=\"m-b-8 text-medium\">\n <strong translate>Full registration</strong>\n </p>\n <small class=\"text-muted\" translate>\n Creates all device credentials and devices using provided list of property values.\n Devices can start communicating with the platform immediately.\n </small>\n </div>\n <div class=\"m-b-16 m-t-16\">\n <a\n title=\"{{ 'Download template' | translate }}\"\n class=\"btn btn-default btn-sm\"\n target=\"_self\"\n (click)=\"downloadFull()\"\n >\n <i c8yIcon=\"download\" translate></i>\n {{ 'Download template' | translate }}\n </a>\n </div>\n </div>\n <div class=\"bg-gray-white separator-bottom p-t-16 p-b-16 p-l-24 p-r-24\" *ngIf=\"certificateAuthorityFeatureEnabled | async\">\n <div>\n <p class=\"m-b-8 text-medium\">\n <strong translate>Full registration with device certificate creation</strong>\n </p>\n <small class=\"text-muted\" translate>\n Creates device certificates and devices using the provided list of property values. Once the certificates are provisioned, the devices can immediately start communicating with the platform\n </small>\n </div>\n <div class=\"m-b-16 m-t-16\">\n <a\n title=\"{{ 'Download template' | translate }}\"\n class=\"btn btn-default btn-sm\"\n target=\"_self\"\n (click)=\"downloadEst()\"\n >\n <i c8yIcon=\"download\"></i>\n {{ 'Download template' | translate }}\n </a>\n </div>\n </div>\n </div>\n\n <c8y-stepper-buttons\n class=\"sticky-bottom d-block p-t-16 p-b-16 separator-top bg-level-0\"\n [showButtons]=\"{ cancel: true, next: true }\"\n [disabled]=\"form.invalid\"\n [pending]=\"pending\"\n (onCancel)=\"cancel()\"\n (onNext)=\"upload()\"\n [labels]=\"{ next: 'Upload' }\"\n ></c8y-stepper-buttons>\n </cdk-step>\n\n <cdk-step state=\"final\">\n <div class=\"m-24\">\n <div *ngIf=\"success; else warning\">\n <c8y-operation-result\n text=\"{{ message | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"success\"\n class=\"lead\"\n ></c8y-operation-result>\n </div>\n <ng-template #warning>\n <c8y-operation-result\n text=\"{{ message | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"error\"\n class=\"lead\"\n ></c8y-operation-result>\n </ng-template>\n <c8y-list-group class=\"separator-top m-t-16\">\n <ng-container *ngIf=\"result; else failedResponse\">\n <c8y-li *ngIf=\"success; else fail\">\n <c8y-li-icon class=\"text-success\" icon=\"check-circle\"></c8y-li-icon>\n <p>{{ 'All devices have been processed.' | translate }}</p>\n <c8y-li-collapse>\n <pre><code>{{ result | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n <ng-template #fail>\n <c8y-li>\n <c8y-li-icon class=\"text-danger\" icon=\"ban\"></c8y-li-icon>\n <p\n ngNonBindable\n [translateParams]=\"{ count: result?.numberOfFailed, total: result?.numberOfAll }\"\n translate\n >\n Failed to process {{ count }} out of {{ total }}.\n </p>\n <c8y-li-collapse>\n <pre><code>{{ result | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n </ng-template>\n </ng-container>\n <ng-template #failedResponse>\n <c8y-li>\n <c8y-li-icon class=\"text-danger\" [icon]=\"'ban'\"></c8y-li-icon>\n <small>{{ failedResult?.message | translate }}</small>\n <c8y-li-collapse>\n <pre><code>{{ failedResult | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n </ng-template>\n </c8y-list-group>\n </div>\n <c8y-stepper-buttons\n class=\"sticky-bottom d-block p-t-16 p-b-16 separator-top bg-level-0\"\n [showButtons]=\"{ next: true }\"\n (onNext)=\"complete()\"\n [labels]=\"{ next: success ? 'Close' : 'Cancel' }\"\n ></c8y-stepper-buttons>\n </cdk-step>\n </c8y-stepper>\n</c8y-modal>\n", dependencies: [{ kind: "directive", type: i1$1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i1$1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i5$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i1$1.OperationResultComponent, selector: "c8y-operation-result", inputs: ["text", "vertical", "size", "type"] }, { kind: "component", type: i1$1.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "component", type: i1$1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: i1$1.C8yStepper, selector: "c8y-stepper", inputs: ["disableDefaultIcons", "disableProgressButtons", "customClasses", "hideStepProgress", "useStepLabelsAsTitlesOnly"], outputs: ["onStepChange"] }, { kind: "component", type: i6.CdkStep, selector: "cdk-step", inputs: ["stepControl", "label", "errorMessage", "aria-label", "aria-labelledby", "state", "editable", "optional", "completed", "hasError"], outputs: ["interacted"], exportAs: ["cdkStep"] }, { kind: "component", type: i1$1.C8yStepperButtons, selector: "c8y-stepper-buttons", inputs: ["labels", "pending", "disabled", "showButtons"], outputs: ["onCancel", "onNext", "onBack", "onCustom"] }, { kind: "component", type: i1$1.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i1$1.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i1$1.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i1$1.ListItemCollapseComponent, selector: "c8y-list-item-collapse, c8y-li-collapse", inputs: ["collapseWay"] }, { kind: "component", type: i7.FormlyForm, selector: "formly-form", inputs: ["form", "model", "fields", "options"], outputs: ["modelChange"] }, { kind: "pipe", type: i1$1.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i5$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i5$1.JsonPipe, name: "json" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BulkDeviceRegistrationModalComponent, decorators: [{ type: Component, args: [{ selector: 'bulk-device-registration', standalone: false, template: "<c8y-modal\n [title]=\"'Bulk device registration' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [customFooter]=\"true\"\n>\n <ng-container c8y-modal-title>\n <i c8yIcon=\"upload\"></i>\n </ng-container>\n\n <c8y-stepper [hideStepProgress]=\"true\" linear id=\"modal-body\">\n <cdk-step>\n <p class=\"modal-subtitle sticky-top\" translate>Register devices in bulk</p>\n\n <c8y-form-group class=\"d-block p-24 p-t-16 p-b-0 m-b-0\">\n <formly-form [form]=\"form\" [fields]=\"template\" [model]=\"model\"></formly-form>\n </c8y-form-group>\n\n <div class=\"p-24 m-t-0 bg-level-1\">\n <div class=\"bg-gray-white separator-bottom p-t-16 p-b-16 p-l-24 p-r-24\">\n <div>\n <p class=\"m-b-8 text-medium\">\n <strong translate>Simple registration</strong>\n </p>\n <small class=\"text-muted\" translate>\n Creates all registration requests at once, then each one needs to go through regular\n acceptance process.\n </small>\n </div>\n <div class=\"m-b-16 m-t-16\">\n <a\n title=\"{{ 'Download template' | translate }}\"\n class=\"btn btn-default btn-sm\"\n target=\"_self\"\n (click)=\"downloadSimple()\"\n >\n <i c8yIcon=\"download\" translate></i>\n {{ 'Download template' | translate }}\n </a>\n </div>\n </div>\n <div class=\"bg-gray-white separator-bottom p-t-16 p-b-16 p-l-24 p-r-24\">\n <div>\n <p class=\"m-b-8 text-medium\">\n <strong translate>Full registration</strong>\n </p>\n <small class=\"text-muted\" translate>\n Creates all device credentials and devices using provided list of property values.\n Devices can start communicating with the platform immediately.\n </small>\n </div>\n <div class=\"m-b-16 m-t-16\">\n <a\n title=\"{{ 'Download template' | translate }}\"\n class=\"btn btn-default btn-sm\"\n target=\"_self\"\n (click)=\"downloadFull()\"\n >\n <i c8yIcon=\"download\" translate></i>\n {{ 'Download template' | translate }}\n </a>\n </div>\n </div>\n <div class=\"bg-gray-white separator-bottom p-t-16 p-b-16 p-l-24 p-r-24\" *ngIf=\"certificateAuthorityFeatureEnabled | async\">\n <div>\n <p class=\"m-b-8 text-medium\">\n <strong translate>Full registration with device certificate creation</strong>\n </p>\n <small class=\"text-muted\" translate>\n Creates device certificates and devices using the provided list of property values. Once the certificates are provisioned, the devices can immediately start communicating with the platform\n </small>\n </div>\n <div class=\"m-b-16 m-t-16\">\n <a\n title=\"{{ 'Download template' | translate }}\"\n class=\"btn btn-default btn-sm\"\n target=\"_self\"\n (click)=\"downloadEst()\"\n >\n <i c8yIcon=\"download\"></i>\n {{ 'Download template' | translate }}\n </a>\n </div>\n </div>\n </div>\n\n <c8y-stepper-buttons\n class=\"sticky-bottom d-block p-t-16 p-b-16 separator-top bg-level-0\"\n [showButtons]=\"{ cancel: true, next: true }\"\n [disabled]=\"form.invalid\"\n [pending]=\"pending\"\n (onCancel)=\"cancel()\"\n (onNext)=\"upload()\"\n [labels]=\"{ next: 'Upload' }\"\n ></c8y-stepper-buttons>\n </cdk-step>\n\n <cdk-step state=\"final\">\n <div class=\"m-24\">\n <div *ngIf=\"success; else warning\">\n <c8y-operation-result\n text=\"{{ message | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"success\"\n class=\"lead\"\n ></c8y-operation-result>\n </div>\n <ng-template #warning>\n <c8y-operation-result\n text=\"{{ message | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"error\"\n class=\"lead\"\n ></c8y-operation-result>\n </ng-template>\n <c8y-list-group class=\"separator-top m-t-16\">\n <ng-container *ngIf=\"result; else failedResponse\">\n <c8y-li *ngIf=\"success; else fail\">\n <c8y-li-icon class=\"text-success\" icon=\"check-circle\"></c8y-li-icon>\n <p>{{ 'All devices have been processed.' | translate }}</p>\n <c8y-li-collapse>\n <pre><code>{{ result | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n <ng-template #fail>\n <c8y-li>\n <c8y-li-icon class=\"text-danger\" icon=\"ban\"></c8y-li-icon>\n <p\n ngNonBindable\n [translateParams]=\"{ count: result?.numberOfFailed, total: result?.numberOfAll }\"\n translate\n >\n Failed to process {{ count }} out of {{ total }}.\n </p>\n <c8y-li-collapse>\n <pre><code>{{ result | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n </ng-template>\n </ng-container>\n <ng-template #failedResponse>\n <c8y-li>\n <c8y-li-icon class=\"text-danger\" [icon]=\"'ban'\"></c8y-li-icon>\n <small>{{ failedResult?.message | translate }}</small>\n <c8y-li-collapse>\n <pre><code>{{ failedResult | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n </ng-template>\n </c8y-list-group>\n </div>\n <c8y-stepper-buttons\n class=\"sticky-bottom d-block p-t-16 p-b-16 separator-top bg-level-0\"\n [showButtons]=\"{ next: true }\"\n (onNext)=\"complete()\"\n [labels]=\"{ next: success ? 'Close' : 'Cancel' }\"\n ></c8y-stepper-buttons>\n </cdk-step>\n </c8y-stepper>\n</c8y-modal>\n" }] }], ctorParameters: () => [{ type: i1$1.C8yJSONSchema }, { type: i2.DeviceRegistrationBulkService }, { type: RegisterDeviceService }, { type: i4.BsModalRef }, { type: i1$1.GainsightService }, { type: i1$1.FeatureCacheService }, { type: i5.TranslateService }], propDecorators: { stepper: [{ type: ViewChild, args: [C8yStepper, { static: true }] }] } }); class GeneralDeviceRegistrationComponent { constructor(tenantUIService, tenantService, registerDeviceService, inventoryService, cd, bsModalRef, gainsightService, deviceRegistrationService, featureCacheService) { this.tenantUIService = tenantUIService; this.tenantService = tenantService; this.registerDeviceService = registerDeviceService; this.inventoryService = inventoryService; this.cd = cd; this.bsModalRef = bsModalRef; this.gainsightService = gainsightService; this.deviceRegistrationService = deviceRegistrationService; this.featureCacheService = featureCacheService; this.MANAGEMENT = 'management'; this.FILTER = { withTotalPages: true, pageSize: 25 }; this.useEST$ = new BehaviorSubject(false); this.certificateAuthorityFeatureEnabled = this.featureCacheService.getFeatureState('certificate-authority'); this.form = new FormGroup({}); this.model = { devicesToCreate: [{}] }; this.options = { formState: { canLoadTenants: true, useEST: this.useEST$.getValue() } }; this.PRODUCT_EXPERIENCE = { EVENT: 'deviceRegistration', COMPONENT: 'single-general-registration', RESULT: { SUCCESS: 'registrationSuccess', FAILURE: 'registrationFailure' } }; this.success = []; this.failed = []; this.fields = [ { type: 'array', key: 'devicesToCreate', props: { addText: gettext('Add device'), addTextDataCy: 'add-device' }, fieldArray: { fieldGroup: [ { key: 'id', type: 'string', focus: true, props: { placeholder: '0123ab32fcd', label: gettext('Device ID'), required: true }, validators: { unique: { expression: (control) => { const found = control.root.get('devicesToCreate').controls .map(el => el.controls.id) .find(el => el !== control && el.value === control.value); return !found; }, message: () => gettext('Device ID duplicates are not allowed') } } }, { key: 'tenant', type: 'typeahead', expressions: { hide: field => { const formState = field.options?.formState; if (!formState?.canLoadTenants) { field.formControl.setValue(null); } return !formState?.canLoadTenants || false; } }, defaultValue: { id: this.MANAGEMENT }, props: { label: gettext('Add to tenant'), required: true, c8yForOptions: this.canLoadTenants$().pipe(filter(canLoad => canLoad), switchMap(() => this.getTenants$())), container: 'body', displayProperty: 'id', valueProperties: ['id'] }, hooks: { onInit: _field => this.canLoadTenants$().pipe(tap(canLoad => { this.options.formState.canLoadTenants = canLoad; this.cd.detectChanges(); })) } }, { key: 'group', type: 'typeahead', expressions: { 'props.disabled': (field) => { const formState = field.options?.formState; const model = field.model; if (formState?.canLoadTenants) { if (model?.tenant?.id !== this.MANAGEMENT) { field.formControl.setValue(null); } return !(model?.tenant?.id === this.MANAGEMENT); } delete field?.props?.description; return false; } }, props: { disabled: false, label: gettext('Add to group'), description: gettext('You can add device to specific group for management tenant only.'), container: 'body', displayProperty: 'name', valueProperties: ['id'], c8yForOptions: this.getGroups$() }, hooks: { onInit: _field => this.canLoadTenants$().pipe(tap(canLoad => { this.options.formState.canLoadTenants = canLoad; this.cd.detectChanges(); })) } }, { key: 'oneTimePassword', type: 'string', expressions: { hide: field => !field.options?.formState?.useEST }, props: { placeholder: 'TruDN3H45L0', label: gettext('One-time password'), required: true }, hooks: { onInit: _field => this.useEST$.pipe(tap(useEST => { this.options.formState.useEST = useEST; this.cd.detectChanges(); })) } } ] } } ]; this.result = new Promise((resolve, reject) => { this.onSuccessfulClosing = resolve; this.onCancel = reject; }); this.destroy$ = new Subject(); this.lastCreatedDevices = []; this.isLoading$ = this.registerDeviceService.loading$; } ngAfterViewInit() { this.cd.detectChanges(); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } registerDevice(eventObject) { !this.useEST$.getValue() ? this.create(eventObject) : this.registerByEst(eventObject); } fixErrors(event, failedRequests) { if (failedRequests && failedRequests.length > 0) { this.options.resetModel({ devicesToCreate: [ ...this.lastCreatedDevices.filter(el => failedRequests.map(data => data.id).includes(el.id)) ] }); this.cd.detectChanges(); } event?.stepper.previous(); } close() { this.bsModalRef.hide(); this.onSuccessfulClosing(); } cancel() { this.bsModalRef.hide(); this.onCancel(); } create(eventObject) { if (this.model?.devicesToCreate?.length > 0) { this.lastCreatedDevices = [...this.model.devicesToCreate]; const dataToSend = this.model.devicesToCreate.map((el) => { const { id, tenant, group } = el; let data = { id }; if (tenant?.id) { data = { ...data, tenantId: tenant.id }; } if (group?.id) { data = { ...data, groupId: group.id }; } return data; }); this.registerDeviceService .createMultiple(dataToSend) .pipe(takeUntil(this.destroy$)) .subscribe(requests => { this.success = requests.success ?? []; if (this.success.length > 0) { this.gainsightService.triggerEvent(this.PRODUCT_EXPERIENCE.EVENT, { result: this.PRODUCT_EXPERIENCE.RESULT.SUCCESS, component: this.PRODUCT_EXPERIENCE.COMPONENT }); } this.failed = requests.failed ?? []; if (this.failed.length > 0) { this.gainsightService.triggerEvent(this.PRODUCT_EXPERIENCE.EVENT, { result: this.PRODUCT_EXPERIENCE.RESULT.FAILURE, component: this.PRODUCT_EXPERIENCE.COMPONENT }); } if (eventObject) { eventObject.stepper.next(); } }); } } registerByEst(eventObject) { this.lastCreatedDevices = [...this.model.devicesToCreate]; this.deviceRegistrationService .create(this.convertObjectToCSVFile(this.model.devicesToCreate)) .then(({ res, data }) => { if (res.status < 400) { this.failed = data.failedCreationList.map(value => { return { id: value.deviceId, message: value.failureReason }; }); const failedIds = new Set(this.failed.map(item => item.id)); this.success = this.model.devicesToCreate.filter(item => !failedIds.has(item.id)); } eventObject.stepper.next(); }) .catch(() => { eventObject.stepper.next(); }); } convertObjectToCSVFile(data) { const fullCsvHeaders = ESTCsvHeaders; const csvHeaders = fullCsvHeaders.join(';') + '\n'; const dataToSend = data.map(el => { return { ID: el.id, AUTH_TYPE: 'CERTIFICATES', ENROLLMENT_OTP: el.oneTimePassword, PATH: el.group?.id || '', TENANT: el.tenant?.id || '' }; }); const escapeValue = (value) => { if (value.includes(';') || value.includes('"')) { return `"${value.replace(/"/g, '""')}"`; } return value; }; const csvRows = dataToSend .map(row => fullCsvHeaders.map(header => escapeValue(row[header] ?? '')).join(';')) .join('\n'); const csvContent = csvHeaders + csvRows; const blob = new Blob([csvContent], { type: 'text/csv' }); return new File([blob], `ESTRegistrationFile.csv`, { type: 'text/csv' }); } canLoadTenants$() { return defer(() => from(this.tenantUIService.isManagementTenant())).pipe(shareReplay(1)); } getTenants$() { return defer(() => from(this.tenantService.list(this.FILTER))).pipe(shareReplay(1)); } getGroups$() { return defer(() => from(this.inventoryService.listQuery({ __filter: { __has: 'c8y_IsDeviceGroup' }, __orderby: [{ name: 1 }] }, { ...this.FILTER }))).pipe(shareReplay(1)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: GeneralDeviceRegistrationComponent, deps: [{ token: i1$1.TenantUiService }, { token: i2.TenantService }, { token: RegisterDeviceService }, { token: i2.InventoryService }, { token: i0.ChangeDetectorRef }, { token: i4.BsModalRef }, { token: i1$1.GainsightService }, { token: i2.DeviceRegistrationBulkService }, { token: i1$1.FeatureCacheService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: GeneralDeviceRegistrationComponent, isStandalone: false, selector: "c8y-general-device-registration", ngImport: i0, template: "<c8y-modal\n [title]=\"'Register devices' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [customFooter]=\"true\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'c8y-device-connect'\"></span>\n </ng-container>\n <c8y-stepper\n [hideStepProgress]=\"true\"\n linear\n c8y-modal-body\n >\n <cdk-step [stepControl]=\"form\">\n <div class=\"text-center sticky-top bg-component\">\n <p\n class=\"text-medium text-16 separator-bottom p-16\"\n translate\n >\n Register general devices\n </p>\n <label\n class=\"c8y-switch m-24 a-i-center\"\n title=\"{{ 'Create device certificates during device registration' | translate }}\"\n for=\"useEST\"\n *ngIf=\"certificateAuthorityFeatureEnabled | async\"\n >\n <input\n id=\"useEST\"\n name=\"useEST\"\n type=\"checkbox\"\n [ngModel]=\"useEST$.getValue()\"\n (ngModelChange)=\"useEST$.next($event)\"\n />\n <span></span>\n <span class=\"control-label\">\n {{ 'Create device certificates during device registration' | translate }}\n </span>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{\n 'The device registration process includes creating device certificates, which are issued by the tenant\\'s Certificate Authority (CA).'\n | translate\n }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </label>\n </div>\n <div>\n <formly-form\n class=\"formly-group-array-cols d-block p-l-24 p-b-24 min-height-fit p-r-8\"\n [form]=\"form\"\n [fields]=\"fields\"\n [model]=\"model\"\n [options]=\"options\"\n [ngClass]=\"{ 'p-t-24': !(certificateAuthorityFeatureEnabled | async) }\"\n ></formly-form>\n </div>\n <c8y-stepper-buttons\n class=\"sticky-bottom d-block p-t-16 p-b-16 separator-top bg-level-0\"\n (onNext)=\"registerDevice($event)\"\n (onCancel)=\"cancel()\"\n [showButtons]=\"{ cancel: true, next: true }\"\n [disabled]=\"!form?.valid\"\n [pending]=\"isLoading$ | async\"\n ></c8y-stepper-buttons>\n </cdk-step>\n <cdk-step state=\"final\">\n <div class=\"p-24 min-height-fit\">\n <c8y-operation-result\n class=\"lead\"\n type=\"success\"\n *ngIf=\"success.length === 1 && failed.length === 0\"\n text=\"{{ 'Device registered' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\n *ngIf=\"success.length === 0 && failed.length === 1\"\n text=\"{{ 'Failed to register device' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n\n <ng-container *ngIf=\"success.length > 1 || failed.length > 1\">\n <c8y-operation-result\n class=\"lead\"\n type=\"success\"\n *ngIf=\"failed.length === 0\"\n [text]=\"\n '{{ successfulDevicesCount }} devices registered'\n | translate: { successfulDevicesCount: success.length }\n \"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\n *ngIf=\"success.length === 0\"\n [text]=\"\n '{{ failedDevicesCount }} devices failed to register'\n | translate: { failedDevicesCount: failed.length }\n \"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n </ng-container>\n\n <div\n class=\"p-l-24 p-r-24 text-center\"\n data-cy=\"device-registration-failure-message\"\n *ngIf=\"success.length > 0 && failed.length > 0\"\n >\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\n text=\"{{ 'Several devices failed to register' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n <p\n class=\"p-b-16 text-danger\"\n ngNonBindable\n translate\n [translateParams]=\"{ count: failed.length, total: failed.length + success.length }\"\n >\n Registration failed for {{ count }} devices out of {{ total }}.\n </p>\n </div>\n\n <div\n class=\"m-b-8 p-l-24 p-r-24\"\n data-cy=\"device-registration-success-message\"\n *ngIf=\"success.length > 0\"\n >\n <span\n *ngIf=\"!(useEST$ | async)\"\n translate\n >\n Turn on the registered devices and wait for connections to be established. Once a device\n is connected, its status will change to \"Pending acceptance\". You will need to approve\n it by clicking on the \"Accept\" button.\n </span>\n <span\n *ngIf=\"useEST$ | async\"\n translate\n >\n The successfully enrolled devices can now request signed certificates and use them to\n connect and authenticate to the platform via certificate-based authentication.\n </span>\n </div>\n\n <c8y-list-group class=\"separator-top m-t-16\">