@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
622 lines (618 loc) • 167 kB
JavaScript
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\">