UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

604 lines (600 loc) 162 kB
import * as i0 from '@angular/core'; import { Injectable, Component, ViewChild, ChangeDetectionStrategy, InjectionToken, Input, NgModule } from '@angular/core'; import * as i4 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 i6$1 from '@angular/forms'; import { FormGroup } from '@angular/forms'; import { Subject, BehaviorSubject, from, forkJoin, defer, Observable } from 'rxjs'; import { takeUntil, finalize, mergeMap, takeLast, map, filter, switchMap, tap, 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 '@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$1 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i3$1 from '@ngx-translate/core'; 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: "18.2.13", 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: "18.2.13", ngImport: i0, type: RegisterDeviceService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", 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, featureService) { this.jsonschema = jsonschema; this.deviceRegistrationService = deviceRegistrationService; this.registerDeviceService = registerDeviceService; this.bsModalRef = bsModalRef; this.gainsightService = gainsightService; this.featureService = featureService; this.form = new FormGroup({}); this.model = {}; this.certificateAuthorityFeatureEnabled = this.featureService .detail('certificate-authority') .then(({ data }) => data.active); } 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, 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: "18.2.13", ngImport: i0, type: BulkDeviceRegistrationModalComponent, deps: [{ token: i1$1.C8yJSONSchema }, { token: i2.DeviceRegistrationBulkService }, { token: RegisterDeviceService }, { token: i4.BsModalRef }, { token: i1$1.GainsightService }, { token: i2.FeatureService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: BulkDeviceRegistrationModalComponent, 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.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.AsyncPipe, name: "async" }, { kind: "pipe", type: i5.JsonPipe, name: "json" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: BulkDeviceRegistrationModalComponent, decorators: [{ type: Component, args: [{ selector: 'bulk-device-registration', 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: i2.FeatureService }], propDecorators: { stepper: [{ type: ViewChild, args: [C8yStepper, { static: true }] }] } }); class GeneralDeviceRegistrationComponent { constructor(tenantUIService, tenantService, registerDeviceService, inventoryService, cd, bsModalRef, gainsightService, deviceRegistrationService, featureService) { 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.featureService = featureService; this.MANAGEMENT = 'management'; this.FILTER = { withTotalPages: true, pageSize: 25 }; this.useEST$ = new BehaviorSubject(false); this.certificateAuthorityFeatureEnabled = this.featureService .detail('certificate-authority') .then(({ data }) => data.active); 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').value.filter(el => el.id === control.value); return found.length === 0; }, 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.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(); } 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 csvRows = dataToSend .map(row => fullCsvHeaders.map(header => 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: "18.2.13", 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: i2.FeatureService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: GeneralDeviceRegistrationComponent, 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 [hideStepProgress]=\"true\" linear c8y-modal-body>\n <cdk-step [stepControl]=\"form\">\n <div class=\"text-center sticky-top bg-component\">\n <p class=\"text-medium text-16 separator-bottom p-16\" translate>Register general devices</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 type=\"checkbox\"\n name=\"useEST\"\n id=\"useEST\"\n [ngModel]=\"useEST$.getValue()\"\n (ngModelChange)=\"useEST$.next($event)\"\n />\n <span></span>\n <span class=\"control-label\">{{ 'Create device certificates during device registration' | translate }}</span>\n <button\n type=\"button\"\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ 'The device registration process includes creating device certificates, which are issued by the tenant\\'s Certificate Authority (CA).' | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n </div>\n <div>\n <formly-form\n [form]=\"form\"\n [fields]=\"fields\"\n [model]=\"model\"\n [options]=\"options\"\n class=\"formly-group-array-cols d-block p-l-24 p-b-24 min-height-fit p-r-8\"\n [ngClass]=\"{'p-t-24' : !(certificateAuthorityFeatureEnabled | async)}\"\n ></formly-form>\n </div>\n <c8y-stepper-buttons\n (onNext)=\"registerDevice($event)\"\n (onCancel)=\"bsModalRef.hide()\"\n [showButtons]=\"{ cancel: true, next: true }\"\n [disabled]=\"!form?.valid\"\n [pending]=\"isLoading$ | async\"\n class=\"sticky-bottom d-block p-t-16 p-b-16 separator-top bg-level-0\"\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 *ngIf=\"success.length === 1 && failed.length === 0\"\n text=\"{{ 'Device registered' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"success\"\n class=\"lead\"\n ></c8y-operation-result>\n <c8y-operation-result\n *ngIf=\"success.length === 0 && failed.length === 1\"\n text=\"{{ 'Failed to register device' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"error\"\n class=\"lead\"\n ></c8y-operation-result>\n\n <ng-container *ngIf=\"success.length > 1 || failed.length > 1\">\n <c8y-operation-result\n *ngIf=\"failed.length === 0\"\n [text]=\"\n '{{ successfulDevicesCount }} devices registered'\n | translate: { successfulDevicesCount: success.length }\n \"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"success\"\n class=\"lead\"\n ></c8y-operation-result>\n <c8y-operation-result\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 type=\"error\"\n class=\"lead\"\n ></c8y-operation-result>\n </ng-container>\n\n <div *ngIf=\"success.length > 0 && failed.length > 0\" class=\"p-l-24 p-r-24 text-center\">\n <c8y-operation-result\n text=\"{{ 'Several devices failed to register' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n type=\"error\"\n class=\"lead\"\n ></c8y-operation-result>\n <p\n ngNonBindable\n translate\n [translateParams]=\"{ count: failed.length, total: failed.length + success.length }\"\n class=\"p-b-16 text-danger\"\n >\n Registration failed for {{ count }} devices out of {{ total }}.\n </p>\n </div>\n\n <div class=\"m-b-8 p-l-24 p-r-24\" *ngIf=\"success.length > 0\" translate>\n Turn on the registered device(s) and wait for connection(s) to be established. Once a\n device is connected, its status will change to \"Pending acceptance\". You will need to\n approve it by clicking on the \"Accept\" button.\n </div>\n\n <c8y-list-group class=\"separator-top m-t-16\">\n <c8y-li *ngFor=\"let fail of failed\">\n <c8y-li-icon class=\"text-danger\" [icon]=\"'ban'\"></c8y-li-icon>\n <p>{{ fail?.id }}</p>\n <small>{{ fail?.message | translate }}</small>\n <c8y-li-collapse>\n <pre><code>{{ fail?.details | json }}</code></pre>\n </c8y-li-collapse>\n </c8y-li>\n\n <c8y-li *ngFor=\"let s of success\">\n <c8y-li-icon class=\"text-success\" [icon]=\"'check-circle'\"></c8y-li-icon>\n {{ s?.id }}\n </c8y-li>\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 (onCustom)=\"bsModalRef.hide()\"\n (onBack)=\"fixErrors($event, failed)\"\n [showButtons]=\"{ back: failed.length > 0, custom: true }\"\n [labels]=\"{ back: 'Fix errors', custom: 'Close' }\"\n ></c8y-stepper-buttons>\n </cdk-step>\n </c8y-stepper>\n</c8y-modal>\n", dependencies: [{ kind: "directive", type: i5$1.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "directive", type: i1$1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i1$1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive",