UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 171 kB
{"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-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 {\n DeviceRegistrationBulkService,\n FeatureService,\n IDeviceRegistrationBulkResult\n} from '@c8y/client';\nimport { C8yJSONSchema, C8yStepper, GainsightService, gettext } 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';\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})\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 = this.featureService\n .detail('certificate-authority')\n .then(({ data }) => data.active);\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 featureService: FeatureService\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, 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} from '@c8y/ngx-components';\nimport { 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 FeatureService,\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})\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 = this.featureService\n .detail('certificate-authority')\n .then(({ data }) => data.active);\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').value as Array<{ id: string }>\n ).filter(el => el.id === control.value);\n return found.length === 0;\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 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 featureService: FeatureService\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 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 csvRows = dataToSend\n .map(row => fullCsvHeaders.map(header => 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 [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","import { Component } from '@angular/core';\nimport { BsModalService } from 'ngx-bootstrap/modal';\nimport { GeneralDeviceRegistrationComponent } from './general-device-registration.component';\n\n@Component({\n selector: 'c8y-general-device-registration-button',\n templateUrl: 'general-device-registration-button.component.html'\n})\nexport class GeneralDeviceRegistrationButtonComponent {\n constructor(private modalService: BsModalService) {}\n\n async open() {\n this.modalService.show(GeneralDeviceRegistrationComponent, {\n class: 'modal-md',\n ariaDescribedby: 'modal-body',\n ariaLabelledBy: 'modal-title',\n ignoreBackdropClick: true\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})\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>\n </ul>\n\n <!-- fake dropdown for mobile screen sizes. *dropdownMenu is missing by design! -->\n <ul class=\"dropdown-menu dropdown-menu visible-xs\">\n <ng-container *ngTemplateOutlet=\"dropdown\"></ng-container>\n </ul>\n\n <ng-template #dropdown>\n <ng-container *ngIf=\"single$ | async as single\">\n <li class=\"dropdown-header\" *ngIf=\"single.length > 0\" translate data-cy=\"single-group\">Single registration</li>\n <li *ngFor=\"let item of single\">\n <ng-container *c8yOutlet=\"item.template\"></ng-container>\n </li>\n </ng-container>\n <ng-container *ngIf=\"bulk$ | async as bulk\">\n <li class=\"dropdown-header\" *ngIf=\"bulk.length > 0\" translate data-cy=\"bulk-group\">Bulk registration</li>\n <li *ngFor=\"let item of bulk\">\n <ng-container *c8yOutlet=\"item.template\"></ng-container>\n </li>\n </ng-container>\n </ng-template>\n</div>\n","import { Component, OnDestroy, OnInit } from '@angular/core';\nimport {\n DeviceRegistrationSecurityMode,\n DeviceRegistrationStatus,\n IDeviceRegistration,\n IDeviceRegistrationLimit,\n Paging\n} from '@c8y/client';\nimport { BehaviorSubject, Observable, Subject } from 'rxjs';\nimport { filter, map, switchMap, takeUntil } from 'rxjs/operators';\nimport {\n DeviceBootstrapRealtimeService,\n IRealtimeDeviceBootstrap,\n TenantUiService,\n ModalService,\n Status,\n gettext,\n OptionsService\n} from '@c8y/ngx-components';\nimport { RegisterDeviceService } from './register-device.service';\nimport { sortBy } from 'lodash-es';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Component({\n selector: 'c8y-device-registration-view',\n templateUrl: 'device-registration-view.component.html'\n})\nexport class DeviceRegistrationViewComponent implements OnInit, OnDestroy {\n deviceRequests$: Observable<{\n data: IDeviceRegistration[];\n paging?: Paging<IDeviceRegistration>;\n }>;\n limit$: Observable<IDeviceRegistrationLimit>;\n limitReachedInfo$: Observable<string>;\n requireSecurityToken = false;\n isManagementTenant = false;\n isLoading = false;\n gridOrList: 'interact-list' | 'interact-grid' = 'interact-grid';\n status = DeviceRegistrationStatus;\n\n readonly statusProps = {\n [DeviceRegistrationStatus.WAITING_FOR_CONNECTION]: {\n label: gettext('Waiting for connection'),\n icon: 'unlink',\n cls: 'text-danger'\n },\n [DeviceRegistrationStatus.PENDING_ACCEPTA