@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 174 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-register-device.mjs","sources":["../../register-device/register-device.service.ts","../../register-device/extensible/base-device-registration.model.ts","../../register-device/bulk/bulk-device-registration-modal.component.ts","../../register-device/bulk/bulk-device-registration-modal.component.html","../../register-device/general/general-device-registration.component.ts","../../register-device/general/general-device-registration.component.html","../../register-device/general/general-device-registration.service.ts","../../register-device/general/general-device-registration-button.component.ts","../../register-device/general/general-device-registration-button.component.html","../../register-device/dropdown/register-device-extension.service.ts","../../register-device/dropdown/register-device-dropdown.component.ts","../../register-device/dropdown/register-device-dropdown.component.html","../../register-device/device-registration-view.component.ts","../../register-device/device-registration-view.component.html","../../register-device/register-device-navigation.factory.ts","../../register-device/extensible/base-extensible-device-registration.service.ts","../../register-device/extensible/single/extensible-device-registration.service.ts","../../register-device/extensible/single/extensible-device-registration-stepper.component.ts","../../register-device/extensible/single/extensible-device-registration-stepper.component.html","../../register-device/extensible/single/extensible-device-registration-modal.component.ts","../../register-device/extensible/single/extensible-device-registration-modal.component.html","../../register-device/extensible/single/extensible-device-registration-button.component.ts","../../register-device/extensible/single/extensible-device-registration-button.component.html","../../register-device/extensible/bulk/extensible-bulk-device-registration.service.ts","../../register-device/extensible/bulk/extensible-bulk-device-registration-modal.component.ts","../../register-device/extensible/bulk/extensible-bulk-device-registration-modal.component.html","../../register-device/extensible/bulk/extensible-bulk-device-registration-button.component.ts","../../register-device/extensible/bulk/extensible-bulk-device-registration-button.component.html","../../register-device/bulk/bulk-device-registration-button.component.ts","../../register-device/bulk/bulk-device-registration-button.component.html","../../register-device/register-device.factory.ts","../../register-device/register-device.module.ts","../../register-device/c8y-ngx-components-register-device.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport {\n DeviceRegistrationService,\n DeviceRegistrationStatus,\n IDeviceRegistration,\n IDeviceRegistrationAccept,\n IDeviceRegistrationCreate,\n IDeviceRegistrationLimit,\n IResult,\n Paging\n} from '@c8y/client';\nimport { get, pick } from 'lodash-es';\nimport { BehaviorSubject, forkJoin, from, Observable, Subject } from 'rxjs';\nimport { AlertService, gettext, IRealtimeDeviceBootstrap } from '@c8y/ngx-components';\nimport { finalize, map, mergeMap, takeLast, takeUntil } from 'rxjs/operators';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class RegisterDeviceService {\n readonly _loading: Subject<boolean> = new Subject();\n readonly _limit: BehaviorSubject<IDeviceRegistrationLimit> = new BehaviorSubject({\n isReached: false\n });\n readonly _deviceRegistrationRequests: BehaviorSubject<{\n data: IDeviceRegistration[];\n paging?: Paging<IDeviceRegistration>;\n }> = new BehaviorSubject({ data: [] });\n readonly deviceRegistrationRequests$: Observable<{\n data: IDeviceRegistration[];\n paging?: Paging<IDeviceRegistration>;\n }> = this._deviceRegistrationRequests.asObservable();\n readonly loading$: Observable<boolean> = this._loading.asObservable();\n readonly limit$: Observable<IDeviceRegistrationLimit> = this._limit.asObservable();\n paging: Paging<IDeviceRegistration>;\n\n private readonly deviceRegUrl = '/deviceregistration';\n private endSubscriptions: Subject<void> = new Subject();\n\n constructor(\n private router: Router,\n private deviceRegService: DeviceRegistrationService,\n private alertService: AlertService\n ) {}\n\n isDeviceRegistration(): boolean {\n return get(this.router, 'url') === this.deviceRegUrl;\n }\n\n internalListUpdate(\n deviceRequests: IDeviceRegistration[],\n pagingObject?: Paging<IDeviceRegistration>\n ) {\n let { paging, data } = this._deviceRegistrationRequests.getValue();\n if (pagingObject) {\n paging = pagingObject;\n }\n data = [...data, ...deviceRequests].filter(deviceReq => deviceReq.type !== 'c8y_DataBroker');\n this._deviceRegistrationRequests.next({ data, paging });\n }\n\n onDeviceBootstrap(bsData: IRealtimeDeviceBootstrap) {\n const { id, status } = bsData;\n this._deviceRegistrationRequests.next({\n data: this.updateStatusById(id, status)\n });\n }\n\n list(pageSize = 100) {\n this._loading.next(true);\n this._deviceRegistrationRequests.next({ data: [], paging: undefined });\n\n from(this.deviceRegService.list({ pageSize, withTotalPages: true }))\n .pipe(\n takeUntil(this.endSubscriptions),\n finalize(() => this.limit())\n )\n .subscribe(\n res => {\n const { data, paging } = res;\n this.internalListUpdate(data, paging);\n this._loading.next(false);\n },\n err => {\n this._loading.next(false);\n this.alertService.addServerFailure(err);\n }\n );\n }\n\n createMultiple(newDeviceRequests: IDeviceRegistrationCreate[]) {\n if (newDeviceRequests && newDeviceRequests.length > 0) {\n this._loading.next(true);\n const newRequests$ = newDeviceRequests.map(element => {\n return from(\n this.deviceRegService.create(element).catch((err: IResult<IDeviceRegistration>) => ({\n res: err.res,\n data: { ...err.data, id: element.id }\n }))\n );\n });\n\n const groupedRequests: {\n success: IDeviceRegistration[];\n failed: IDeviceRegistration[];\n } = {\n success: [],\n failed: []\n };\n\n return forkJoin(newRequests$).pipe(\n mergeMap(resp =>\n resp.map(el => {\n el.res.ok\n ? groupedRequests.success.push(el.data)\n : groupedRequests.failed.push(el.data);\n return groupedRequests;\n })\n ),\n takeLast(1),\n finalize(() => {\n this.internalListUpdate(groupedRequests.success);\n this._loading.next(false);\n })\n );\n }\n }\n\n remove(id: string) {\n this._loading.next(true);\n from(this.deviceRegService.delete(id))\n .pipe(takeUntil(this.endSubscriptions))\n .subscribe(\n () => {\n this._deviceRegistrationRequests.next({\n data: this.removeDeviceRegistrationRequestById(id)\n });\n this._loading.next(false);\n this.alertService.success(gettext('Device registration cancelled.'));\n },\n err => {\n this._loading.next(false);\n this.alertService.addServerFailure(err);\n }\n );\n }\n\n accept(request: IDeviceRegistration) {\n this._loading.next(true);\n const payload = pick(request, ['id', 'securityToken']);\n from(this.deviceRegService.accept(payload))\n .pipe(takeUntil(this.endSubscriptions))\n .subscribe(\n () => {\n this._deviceRegistrationRequests.next({\n data: this.removeDeviceRegistrationRequestById(payload.id)\n });\n this.limit();\n this._loading.next(false);\n this.alertService.success(gettext('Device registration accepted.'));\n },\n err => {\n this._loading.next(false);\n this.alertService.addServerFailure(err);\n }\n );\n }\n\n acceptAll() {\n const acceptedDeviceRequests: IDeviceRegistrationAccept[] = [];\n const failedDeviceRequests: IDeviceRegistrationAccept[] = [];\n this._loading.next(true);\n\n from(this.deviceRegService.acceptAll())\n .pipe(\n takeUntil(this.endSubscriptions),\n map(({ data }) => {\n data.map(deviceRegistrationRequest => {\n if (deviceRegistrationRequest.successful) {\n acceptedDeviceRequests.push(deviceRegistrationRequest);\n this.removeDeviceRegistrationRequestById(deviceRegistrationRequest.id);\n } else {\n failedDeviceRequests.push(deviceRegistrationRequest);\n }\n });\n return data;\n }),\n finalize(() => {\n // update rendered list with successful accepted device registrations\n // see: this.updateStatusById(...)\n this.internalListUpdate([]);\n this.limit();\n this._loading.next(false);\n if (failedDeviceRequests.length > 0) {\n this.alertService.warning(\n gettext('Could not accept all pending registration requests.'),\n JSON.stringify(\n {\n failedDeviceRequests,\n acceptedDeviceRequests\n },\n undefined,\n 2\n )\n );\n } else {\n this.alertService.success(gettext('Accepted all pending registration requests.'));\n }\n })\n )\n .subscribe(\n () => {\n // empty by design\n },\n err => {\n this._loading.next(false);\n this.alertService.addServerFailure(err);\n }\n );\n }\n\n limit() {\n from(this.deviceRegService.limit())\n .pipe(takeUntil(this.endSubscriptions))\n .subscribe(\n res => this._limit.next(res.data),\n err => this.alertService.addServerFailure(err)\n );\n }\n\n getRequestByStatus(status: DeviceRegistrationStatus): IDeviceRegistration[] {\n return this._deviceRegistrationRequests.getValue().data.filter(req => req.status === status);\n }\n\n ngOnDestroy(): void {\n this.endSubscriptions.next();\n this.endSubscriptions.complete();\n }\n\n private updateStatusById(id: string, status: DeviceRegistrationStatus) {\n const items = this._deviceRegistrationRequests.getValue().data;\n const matchingElementIndex = items.findIndex(element => element.id === id);\n if (matchingElementIndex >= 0) {\n items[matchingElementIndex].status = status;\n }\n return items;\n }\n\n private removeDeviceRegistrationRequestById(id: string) {\n const items = this._deviceRegistrationRequests.getValue().data;\n const matchingElementIndex = items.findIndex(element => element.id === id);\n if (matchingElementIndex >= 0) {\n items.splice(matchingElementIndex, 1);\n }\n this._loading.next(false);\n return items;\n }\n}\n","export interface ApplicationExtension {\n name: string;\n description: string;\n type: string;\n}\n\nexport interface ExtensibleDeviceRegistrationProvider extends ApplicationExtension {\n contextPath: string;\n}\n\nexport const PRODUCT_EXPERIENCE_BASE_REGISTRATION = {\n EVENT: 'deviceRegistration',\n COMPONENT: {\n BULK: 'bulk-registration',\n EXTENSIBLE_BULK: 'bulk-extensible-registration',\n EXTENSIBLE_SINGLE: 'single-extensible-registration'\n },\n RESULT: { SUCCESS: 'registrationSuccess', FAILURE: 'registrationFailure' }\n} as const;\n","import { Component, ViewChild } from '@angular/core';\nimport { FormGroup } from '@angular/forms';\nimport { DeviceRegistrationBulkService, IDeviceRegistrationBulkResult } from '@c8y/client';\nimport {\n C8yJSONSchema,\n C8yStepper,\n FeatureCacheService,\n GainsightService,\n gettext\n} from '@c8y/ngx-components';\nimport { FormlyFieldConfig } from '@ngx-formly/core';\nimport { saveAs } from 'file-saver';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport { PRODUCT_EXPERIENCE_BASE_REGISTRATION } from '../extensible/base-device-registration.model';\nimport { BulkFailedResult } from '../extensible/bulk/extensible-bulk-device-registration.model';\nimport { RegisterDeviceService } from '../register-device.service';\nimport { TranslateService } from '@ngx-translate/core';\n\nconst registerDeviceBulkSchema: object = {\n $schema: 'https://json-schema.org/draft/2019-09/schema',\n type: 'object',\n properties: {\n csvBulkFile: {\n type: 'array',\n title: gettext('CSV file upload'),\n description: gettext(\n 'You can use file upload component to let users send files. This input accepts only a single CSV file.'\n ),\n contentMediaType: 'csv'\n }\n },\n required: ['csvBulkFile'],\n additionalProperties: false\n};\n\nconst simpleCsvHeaders: string[] = ['ID', 'PATH'];\nconst csvHeaders: string[] = [\n 'ID',\n 'TYPE',\n 'NAME',\n 'ICCID',\n 'IDTYPE',\n 'PATH',\n 'SHELL',\n 'AUTH_TYPE'\n];\nconst fullCsvHeaders: string[] = [...csvHeaders, 'CREDENTIALS'];\nexport const ESTCsvHeaders: string[] = [...csvHeaders, 'ENROLLMENT_OTP'];\n\n@Component({\n selector: 'bulk-device-registration',\n templateUrl: 'bulk-device-registration-modal.component.html',\n standalone: false\n})\nexport class BulkDeviceRegistrationModalComponent {\n @ViewChild(C8yStepper, { static: true }) stepper: C8yStepper;\n message: string;\n success: boolean;\n pending: boolean;\n result: IDeviceRegistrationBulkResult;\n failedResult: BulkFailedResult;\n form = new FormGroup({});\n model = {};\n template: FormlyFieldConfig[];\n certificateAuthorityFeatureEnabled =\n this.featureCacheService.getFeatureState('certificate-authority');\n\n constructor(\n private jsonschema: C8yJSONSchema,\n private deviceRegistrationService: DeviceRegistrationBulkService,\n private registerDeviceService: RegisterDeviceService,\n private bsModalRef: BsModalRef,\n private gainsightService: GainsightService,\n private featureCacheService: FeatureCacheService,\n private translateService: TranslateService\n ) {}\n\n ngOnInit() {\n this.template = [this.jsonschema.toFieldConfig(registerDeviceBulkSchema)];\n }\n\n upload() {\n this.pending = true;\n const file = this.getFile(this.model);\n this.deviceRegistrationService\n .create(file)\n .then(({ res, data }) => {\n if (res.status < 400) {\n this.result = data;\n this.success = data.numberOfFailed === 0 && data.numberOfSuccessful === data.numberOfAll;\n this.message = this.success\n ? gettext('Device registration created.')\n : (this.message = gettext('Device registration failed.'));\n if (this.success) {\n this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_BASE_REGISTRATION.EVENT, {\n result: PRODUCT_EXPERIENCE_BASE_REGISTRATION.RESULT.SUCCESS,\n component: PRODUCT_EXPERIENCE_BASE_REGISTRATION.COMPONENT.BULK\n });\n } else {\n this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_BASE_REGISTRATION.EVENT, {\n result: PRODUCT_EXPERIENCE_BASE_REGISTRATION.RESULT.FAILURE,\n component: PRODUCT_EXPERIENCE_BASE_REGISTRATION.COMPONENT.BULK\n });\n }\n } else {\n this.failedResult = data as unknown as BulkFailedResult;\n this.message = gettext('Device registration failed.');\n this.gainsightService.triggerEvent(PRODUCT_EXPERIENCE_BASE_REGISTRATION.EVENT, {\n result: PRODUCT_EXPERIENCE_BASE_REGISTRATION.RESULT.FAILURE,\n component: PRODUCT_EXPERIENCE_BASE_REGISTRATION.COMPONENT.BULK\n });\n }\n this.model = {};\n this.pending = false;\n this.stepper.next();\n })\n .catch(() => {\n this.message = gettext('Error occurred while processing the uploaded file.');\n this.pending = false;\n this.stepper.next();\n });\n }\n\n downloadSimple() {\n return this.download(simpleCsvHeaders, gettext('Simple bulk registration - template.csv'));\n }\n\n downloadFull() {\n return this.download(fullCsvHeaders, gettext('Full bulk registration - template.csv'));\n }\n\n downloadEst() {\n return this.download(ESTCsvHeaders, gettext('EST registration - template.csv'));\n }\n\n download(headers: string[], fileName: string) {\n const headerRaw = headers.map(header => `\"${header}\"`).join(';');\n const binaryFile = new Blob([headerRaw], { type: 'text/csv' });\n saveAs(binaryFile, this.translateService.instant(fileName));\n }\n\n complete() {\n this.registerDeviceService.list();\n this.bsModalRef.hide();\n }\n\n cancel() {\n this.bsModalRef.hide();\n }\n\n private getFile(model): File {\n const csvBulkFile = (model as any)?.csvBulkFile;\n return csvBulkFile ? csvBulkFile[0]?.file : undefined;\n }\n}\n","<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","import {\n AfterViewInit,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n OnDestroy\n} from '@angular/core';\nimport { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';\nimport {\n TenantUiService,\n gettext,\n C8yStepper,\n memoize,\n GainsightService,\n FeatureCacheService\n} from '@c8y/ngx-components';\nimport { FormArray, FormControl, FormGroup } from '@angular/forms';\nimport { from, Observable, Subject, defer, BehaviorSubject } from 'rxjs';\nimport { filter, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';\nimport {\n DeviceRegistrationBulkService,\n IManagedObject,\n InventoryService,\n IResultList,\n ITenant,\n TenantService\n} from '@c8y/client';\nimport { RegisterDeviceService } from '../register-device.service';\nimport { CdkStep } from '@angular/cdk/stepper';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\nimport { ESTCsvHeaders } from '../bulk/bulk-device-registration-modal.component';\n\ninterface GeneralDeviceRegistrationModelType {\n id: string;\n tenant?: { id: string };\n group?: { id: string; name?: string };\n oneTimePassword?: string;\n}\n\n@Component({\n selector: 'c8y-general-device-registration',\n templateUrl: 'general-device-registration.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false\n})\nexport class GeneralDeviceRegistrationComponent implements AfterViewInit, OnDestroy {\n readonly MANAGEMENT = 'management';\n readonly FILTER: object = {\n withTotalPages: true,\n pageSize: 25\n };\n\n useEST$ = new BehaviorSubject<boolean>(false);\n certificateAuthorityFeatureEnabled =\n this.featureCacheService.getFeatureState('certificate-authority');\n\n form = new FormGroup({});\n model = {\n devicesToCreate: [{} as GeneralDeviceRegistrationModelType]\n };\n options: FormlyFormOptions = {\n formState: {\n canLoadTenants: true,\n useEST: this.useEST$.getValue()\n }\n };\n\n PRODUCT_EXPERIENCE = {\n EVENT: 'deviceRegistration',\n COMPONENT: 'single-general-registration',\n RESULT: { SUCCESS: 'registrationSuccess', FAILURE: 'registrationFailure' }\n };\n\n isLoading$: Observable<boolean>;\n success: { id: string }[] = [];\n failed: { id: string; message?: string; details?: any }[] = [];\n\n fields: FormlyFieldConfig[] = [\n {\n type: 'array',\n key: 'devicesToCreate',\n props: {\n addText: gettext('Add device'),\n addTextDataCy: 'add-device'\n },\n fieldArray: {\n fieldGroup: [\n {\n key: 'id',\n type: 'string',\n focus: true,\n props: {\n placeholder: '0123ab32fcd',\n label: gettext('Device ID'),\n required: true\n },\n validators: {\n unique: {\n expression: (control: FormControl) => {\n const found = (\n control.root.get('devicesToCreate') as FormArray<\n FormGroup<{ id: FormControl<string> }>\n >\n ).controls\n .map(el => el.controls.id)\n .find(el => el !== control && el.value === control.value);\n return !found;\n },\n message: () => gettext('Device ID duplicates are not allowed')\n }\n }\n },\n {\n key: 'tenant',\n type: 'typeahead',\n expressions: {\n hide: field => {\n const formState = field.options?.formState;\n if (!formState?.canLoadTenants) {\n field.formControl.setValue(null);\n }\n return !formState?.canLoadTenants || false;\n }\n },\n defaultValue: { id: this.MANAGEMENT },\n props: {\n label: gettext('Add to tenant'),\n required: true,\n c8yForOptions: this.canLoadTenants$().pipe(\n filter(canLoad => canLoad),\n switchMap(() => this.getTenants$())\n ) as Observable<IResultList<ITenant>>,\n container: 'body',\n displayProperty: 'id',\n valueProperties: ['id']\n },\n hooks: {\n onInit: _field =>\n this.canLoadTenants$().pipe(\n tap(canLoad => {\n this.options.formState.canLoadTenants = canLoad;\n this.cd.detectChanges();\n })\n )\n }\n },\n {\n key: 'group',\n type: 'typeahead',\n expressions: {\n 'props.disabled': (field: FormlyFieldConfig) => {\n const formState = field.options?.formState;\n const model = field.model;\n if (formState?.canLoadTenants) {\n if (model?.tenant?.id !== this.MANAGEMENT) {\n field.formControl.setValue(null);\n }\n return !(model?.tenant?.id === this.MANAGEMENT);\n }\n delete field?.props?.description;\n return false;\n }\n },\n props: {\n disabled: false,\n label: gettext('Add to group'),\n description: gettext(\n 'You can add device to specific group for management tenant only.'\n ),\n container: 'body',\n displayProperty: 'name',\n valueProperties: ['id'],\n c8yForOptions: this.getGroups$()\n },\n hooks: {\n onInit: _field =>\n this.canLoadTenants$().pipe(\n tap(canLoad => {\n this.options.formState.canLoadTenants = canLoad;\n this.cd.detectChanges();\n })\n )\n }\n },\n {\n key: 'oneTimePassword',\n type: 'string',\n expressions: {\n hide: field => !field.options?.formState?.useEST\n },\n props: {\n placeholder: 'TruDN3H45L0',\n label: gettext('One-time password'),\n required: true\n },\n hooks: {\n onInit: _field =>\n this.useEST$.pipe(\n tap(useEST => {\n this.options.formState.useEST = useEST;\n this.cd.detectChanges();\n })\n )\n }\n }\n ]\n }\n }\n ];\n\n result = new Promise<void>((resolve, reject) => {\n this.onSuccessfulClosing = resolve;\n this.onCancel = reject;\n });\n\n private onSuccessfulClosing: () => void;\n private onCancel: () => void;\n\n private destroy$: Subject<void> = new Subject();\n private lastCreatedDevices: GeneralDeviceRegistrationModelType[] = [];\n\n constructor(\n private tenantUIService: TenantUiService,\n private tenantService: TenantService,\n private registerDeviceService: RegisterDeviceService,\n private inventoryService: InventoryService,\n private cd: ChangeDetectorRef,\n public bsModalRef: BsModalRef,\n private gainsightService: GainsightService,\n private deviceRegistrationService: DeviceRegistrationBulkService,\n private featureCacheService: FeatureCacheService\n ) {\n this.isLoading$ = this.registerDeviceService.loading$;\n }\n\n ngAfterViewInit() {\n this.cd.detectChanges();\n }\n\n ngOnDestroy() {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n registerDevice(eventObject: { stepper: C8yStepper; step: CdkStep }) {\n !this.useEST$.getValue() ? this.create(eventObject) : this.registerByEst(eventObject);\n }\n\n fixErrors(event: { stepper: C8yStepper; step: CdkStep }, failedRequests: any) {\n if (failedRequests && failedRequests.length > 0) {\n this.options.resetModel({\n devicesToCreate: [\n ...this.lastCreatedDevices.filter(el =>\n failedRequests.map(data => data.id).includes(el.id)\n )\n ]\n });\n this.cd.detectChanges();\n }\n event?.stepper.previous();\n }\n\n close() {\n this.bsModalRef.hide();\n this.onSuccessfulClosing();\n }\n\n cancel() {\n this.bsModalRef.hide();\n this.onCancel();\n }\n\n private create(eventObject: { stepper: C8yStepper; step: CdkStep }) {\n if (this.model?.devicesToCreate?.length > 0) {\n this.lastCreatedDevices = [...this.model.devicesToCreate];\n\n const dataToSend = this.model.devicesToCreate.map(\n (el: GeneralDeviceRegistrationModelType) => {\n const { id, tenant, group } = el;\n let data: { id: string; tenantId?: string; groupId?: string } = { id };\n\n if (tenant?.id) {\n data = { ...data, tenantId: tenant.id };\n }\n\n if (group?.id) {\n data = { ...data, groupId: group.id };\n }\n\n return data;\n }\n );\n\n this.registerDeviceService\n .createMultiple(dataToSend)\n .pipe(takeUntil(this.destroy$))\n .subscribe(requests => {\n this.success = requests.success ?? [];\n if (this.success.length > 0) {\n this.gainsightService.triggerEvent(this.PRODUCT_EXPERIENCE.EVENT, {\n result: this.PRODUCT_EXPERIENCE.RESULT.SUCCESS,\n component: this.PRODUCT_EXPERIENCE.COMPONENT\n });\n }\n\n this.failed = requests.failed ?? [];\n if (this.failed.length > 0) {\n this.gainsightService.triggerEvent(this.PRODUCT_EXPERIENCE.EVENT, {\n result: this.PRODUCT_EXPERIENCE.RESULT.FAILURE,\n component: this.PRODUCT_EXPERIENCE.COMPONENT\n });\n }\n\n if (eventObject) {\n eventObject.stepper.next();\n }\n });\n }\n }\n\n private registerByEst(eventObject: { stepper: C8yStepper; step: CdkStep }) {\n this.lastCreatedDevices = [...this.model.devicesToCreate];\n this.deviceRegistrationService\n .create(this.convertObjectToCSVFile(this.model.devicesToCreate))\n .then(({ res, data }) => {\n if (res.status < 400) {\n this.failed = data.failedCreationList.map(value => {\n return {\n id: value.deviceId,\n message: value.failureReason\n };\n });\n const failedIds = new Set(this.failed.map(item => item.id));\n this.success = this.model.devicesToCreate.filter(item => !failedIds.has(item.id));\n }\n eventObject.stepper.next();\n })\n .catch(() => {\n eventObject.stepper.next();\n });\n }\n\n private convertObjectToCSVFile(data: any): File {\n const fullCsvHeaders = ESTCsvHeaders;\n\n const csvHeaders = fullCsvHeaders.join(';') + '\\n';\n const dataToSend = data.map(el => {\n return {\n ID: el.id,\n AUTH_TYPE: 'CERTIFICATES',\n ENROLLMENT_OTP: el.oneTimePassword,\n PATH: el.group?.id || '',\n TENANT: el.tenant?.id || ''\n };\n });\n\n const escapeValue = (value: string) => {\n if (value.includes(';') || value.includes('\"')) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n }\n return value;\n };\n\n const csvRows = dataToSend\n .map(row => fullCsvHeaders.map(header => escapeValue(row[header] ?? '')).join(';'))\n .join('\\n');\n\n const csvContent = csvHeaders + csvRows;\n const blob = new Blob([csvContent], { type: 'text/csv' });\n return new File([blob], `ESTRegistrationFile.csv`, { type: 'text/csv' });\n }\n\n @memoize()\n private canLoadTenants$(): Observable<boolean> {\n return defer(() => from(this.tenantUIService.isManagementTenant())).pipe(shareReplay(1));\n }\n\n @memoize()\n private getTenants$(): Observable<IResultList<ITenant>> {\n return defer(() => from(this.tenantService.list(this.FILTER))).pipe(shareReplay(1));\n }\n\n @memoize()\n private getGroups$(): Observable<IResultList<IManagedObject>> {\n return defer(() =>\n from(\n this.inventoryService.listQuery(\n { __filter: { __has: 'c8y_IsDeviceGroup' }, __orderby: [{ name: 1 }] },\n { ...this.FILTER }\n )\n )\n ).pipe(shareReplay(1));\n }\n}\n","<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\">\n <c8y-li *ngFor=\"let fail of failed\">\n <c8y-li-icon\n class=\"text-danger\"\n [icon]=\"'ban'\"\n ></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\n class=\"text-success\"\n [icon]=\"'check-circle'\"\n ></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)=\"close()\"\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","import { inject, Injectable } from '@angular/core';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport { GeneralDeviceRegistrationComponent } from './general-device-registration.component';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class GeneralDeviceRegistrationService {\n private modalService = inject(BsModalService);\n\n async open(initialState?: Partial<GeneralDeviceRegistrationComponent>) {\n const modalRef = this.modalService.show(GeneralDeviceRegistrationComponent, {\n class: 'modal-lg',\n ariaDescribedby: 'modal-body',\n ariaLabelledBy: 'modal-title',\n ignoreBackdropClick: true,\n initialState: {\n ...initialState\n }\n });\n\n return await modalRef.content.result;\n }\n}\n","import { Component } from '@angular/core';\nimport { GeneralDeviceRegistrationService } from './general-device-registration.service';\n\n@Component({\n selector: 'c8y-general-device-registration-button',\n templateUrl: 'general-device-registration-button.component.html',\n standalone: false\n})\nexport class GeneralDeviceRegistrationButtonComponent {\n constructor(private registrationService: GeneralDeviceRegistrationService) {}\n\n async open() {\n try {\n await this.registrationService.open();\n } catch {\n // modal was closed\n }\n }\n}\n","<button title=\"{{ 'General' | translate }}\" type=\"button\" (click)=\"open()\">\n <i c8yIcon=\"c8y-device-connect\"></i>\n {{ 'General' | translate }}\n</button>\n","import { Injectable, InjectionToken, Injector } from '@angular/core';\nimport { Router } from '@angular/router';\nimport {\n ExtensionFactory,\n ExtensionPointWithoutStateForPlugins,\n fromTriggerOnce,\n GenericHookType,\n hookGeneric,\n PluginsResolveService,\n GenericHookOptions\n} from '@c8y/ngx-components';\nimport { flatten } from 'lodash';\nimport { Observable } from 'rxjs';\nimport { shareReplay, startWith } from 'rxjs/operators';\nimport { RegisterDeviceItem } from './RegisterDeviceItem';\n\n/**\n * An extension HOOK can use either a pure value:\n * ```typescript\n * { provide: HOOK_X, useValue: { ...hookValue }, multi: true }\n * ```\n *\n * Or an array to directly register multiple:\n * ```typescript\n * { provide: HOOK_X, useValue: [{ ...hookValues }], multi: true }\n * ```\n *\n * Or an ExtensionFactory which allows to define a get() function. This function\n * gets called on each navigation with the current route and can return values\n * async (observable or promise).\n * ```typescript\n * { provide: HOOK_X, useFactory: { get: (route) => doSomethingAsync(route) }, multi: true }\n * ```\n */\nexport type RegisterDeviceExtension =\n | RegisterDeviceItem\n | RegisterDeviceItem[]\n | ExtensionFactory<RegisterDeviceItem>;\n/**\n * A hook to use for Multi Provider extension.\n * @deprecated Consider using the `hookDeviceRegistration` function instead.\n */\nexport const HOOK_DEVICE_REGISTRATION = new InjectionToken<RegisterDeviceExtension>(\n 'HOOK_DEVICE_REGISTRATION'\n);\n\n/**\n * You can either provide a single `RegisterDeviceExtension` as parameter:\n * ```typescript\n * hookDeviceRegistration(...)\n * ```\n *\n * Or an array to directly register multiple:\n * ```typescript\n * hookDeviceRegistration([...])\n * ```\n *\n * Or you provide an Service that implements `ExtensionFactory<RegisterDeviceExtension>`\n * ```typescript\n * export class MyDeviceRegistrationFactory implements ExtensionFactory<RegisterDeviceExtension> {...}\n * ...\n * hookDeviceRegistration(MyDeviceRegistrationFactory)\n * ```\n * A typed alternative to `HOOK_DEVICE_REGISTRATION`.\n * @param registration The `RegisterDeviceExtension`'s or `ExtensionFactory` to be provided.\n * @returns An `Provider` to be provided in your module.\n */\nexport function hookDeviceRegistration(\n registration: GenericHookType<RegisterDeviceExtension>,\n options?: Partial<GenericHookOptions>\n) {\n return hookGeneric<RegisterDeviceExtension>(registration, HOOK_DEVICE_REGISTRATION, options);\n}\n\n/**\n * A service which defines device registration options.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class RegisterDeviceExtensionService extends ExtensionPointWithoutStateForPlugins<RegisterDeviceItem> {\n constructor(\n rootInjector: Injector,\n private router: Router,\n plugins: PluginsResolveService\n ) {\n super(rootInjector, plugins);\n this.items$ = this.setupItemsObservable();\n }\n\n protected setupItemsObservable(): Observable<RegisterDeviceItem[]> {\n return fromTriggerOnce<RegisterDeviceItem>(this.router, this.refresh$, [\n () =>\n flatten(\n this.injectors.map(injector => injector.get(HOOK_DEVICE_REGISTRATION, [], { self: true }))\n ),\n () => this.factories\n ]).pipe(startWith([]), shareReplay(1));\n }\n}\n","import { Component } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\nimport { RegisterDeviceService } from '../register-device.service';\nimport { RegisterDeviceExtensionService } from './register-device-extension.service';\n\n@Component({\n selector: 'c8y-register-device-dropdown',\n templateUrl: './register-device-dropdown.component.html',\n standalone: false\n})\nexport class RegisterDeviceDropdownComponent {\n single$ = this.registerDeviceExtensionService.items$.pipe(\n map(items =>\n items.filter(item => item.category === 'single').sort((a, b) => b.priority - a.priority)\n )\n );\n\n bulk$ = this.registerDeviceExtensionService.items$.pipe(\n map(items =>\n items.filter(item => item.category === 'bulk').sort((a, b) => b.priority - a.priority)\n )\n );\n\n limit$: Observable<boolean> = this.registerDeviceService.limit$.pipe(\n map(limit => limit.isReached)\n );\n\n constructor(\n private registerDeviceExtensionService: RegisterDeviceExtensionService,\n private registerDeviceService: RegisterDeviceService\n ) {}\n}\n","<div class=\"dropdown\" dropdown>\n <button\n *ngIf=\"!(limit$ | async); else disable\"\n title=\"{{ 'Register device' | translate }}\"\n type=\"button\"\n class=\"dropdown-toggle c8y-dropdown d-flex a-i-center\"\n dropdownToggle\n aria-haspopup=\"true\"\n data-cy=\"register-device--dropdown-button\"\n >\n <span class=\"text-truncate\" translate>Register device</span>\n <i [c8yIcon]=\"'caret-down'\" class=\"m-l-4 text-primary\"></i>\n </button>\n <ng-template #disable>\n <button\n title=\"{{ 'Device registration disabled' | translate }}\"\n type=\"button\"\n class=\"btn btn-clean d-flex p-l-8\"\n disabled\n >\n <span class=\"text-truncate\" translate>Register device</span>\n <i [c8yIcon]=\"'caret-down'\"></i>\n </button>\n </ng-template>\n\n <!-- dropdown for normal screen sizes -->\n <ul class=\"dropdown-menu dropdown-menu-right hidden-xs\" data-cy=\"register-device--dropdown\" *dropdownMenu>\n <ng-container *ngTemplateOutlet=\"dropdown\"></ng-container>