UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines • 172 kB
{"version":3,"file":"c8y-ngx-components-register-device.mjs","sources":["../../register-device/register-device.service.ts","../../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/base-device-registration.model.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/bulk/bulk-device-registration-modal.component.ts","../../register-device/bulk/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","import {\n AfterViewInit,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n OnDestroy\n} from '@angular/core';\nimport { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core';\nimport {\n TenantUiService,\n gettext,\n C8yStepper,\n memoize,\n GainsightService,\n FeatureCacheService,\n ModalComponent,\n IconDirective,\n C8yTranslateDirective,\n C8yStepperButtons,\n OperationResultComponent,\n ListGroupComponent,\n ListItemComponent,\n ListItemIconComponent,\n ListItemCollapseComponent,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { FormArray, FormControl, FormGroup, FormsModule } from '@angular/forms';\nimport { from, Observable, Subject, defer, BehaviorSubject } from 'rxjs';\nimport { filter, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';\nimport { IManagedObject, InventoryService, IResultList, ITenant, TenantService } from '@c8y/client';\nimport { RegisterDeviceService } from '../register-device.service';\nimport { CdkStep } from '@angular/cdk/stepper';\nimport { BsModalRef } from 'ngx-bootstrap/modal';\n\nimport { NgIf, NgClass, NgFor, AsyncPipe, JsonPipe } from '@angular/common';\nimport { PopoverDirective } from 'ngx-bootstrap/popover';\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 imports: [\n ModalComponent,\n IconDirective,\n C8yStepper,\n CdkStep,\n C8yTranslateDirective,\n NgIf,\n FormsModule,\n PopoverDirective,\n FormlyModule,\n NgClass,\n C8yStepperButtons,\n OperationResultComponent,\n ListGroupComponent,\n NgFor,\n ListItemComponent,\n ListItemIconComponent,\n ListItemCollapseComponent,\n C8yTranslatePipe,\n AsyncPipe,\n JsonPipe\n ]\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 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.create(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, oneTimePassword } = el;\n let data: { id: string; tenantId?: string; groupId?: string; enrollmentToken?: string } =\n { 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 if (oneTimePassword) {\n data = { ...data, enrollmentToken: oneTimePassword };\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 @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';\nimport { IconDirective, C8yTranslatePipe } from '@c8y/ngx-components';\n\n@Component({\n selector: 'c8y-general-device-registration-button',\n templateUrl: 'general-device-registration-button.component.html',\n imports: [IconDirective, C8yTranslatePipe]\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';\nimport {\n BsDropdownDirective,\n BsDropdownToggleDirective,\n BsDropdownMenuDirective\n} from 'ngx-bootstrap/dropdown';\nimport { NgIf, NgTemplateOutlet, NgFor, AsyncPipe } from '@angular/common';\nimport {\n C8yTranslateDirective,\n IconDirective,\n OutletDirective,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\n\n@Component({\n selector: 'c8y-register-device-dropdown',\n templateUrl: './register-device-dropdown.component.html',\n imports: [\n BsDropdownDirective,\n NgIf,\n BsDropdownToggleDirective,\n C8yTranslateDirective,\n IconDirective,\n BsDropdownMenuDirective,\n NgTemplateOutlet,\n NgFor,\n OutletDirective,\n C8yTranslatePipe,\n AsyncPipe\n ]\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 TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n ActionBarItemComponent,\n ListDisplaySwitchComponent,\n IfAllowedDirective,\n IconDirective,\n HelpComponent,\n C8yTranslateDirective,\n RequiredInputPlaceholderDirective,\n LoadMoreComponent,\n C8yTranslatePipe,\n DatePipe\n} from '@c8y/ngx-components';\nimport { RegisterDeviceService } from './register-device.service';\nimport { sortBy } from 'lodash-es';\nimport { TranslateService } from '@ngx-translate/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { GeneralDeviceRegistrationService } from './general/general-device-registration.service';\nimport { NgIf, NgClass, NgFor, AsyncPipe } from '@angular/common';\nimport { RegisterDeviceDropdownComponent } from './dropdown/register-device-dropdown.component';\nimport { PopoverDirective } from 'ngx-bootstrap/popover';\nimport { FormsModule } from '@angular/forms';\n\n@Component({\n selector: 'c8y-device-registration-view',\n templateUrl: 'device-registration-view.component.html',\n imports: [\n NgIf,\n TitleComponent,\n BreadcrumbComponent,\n BreadcrumbItemComponent,\n ActionBarItemComponent,\n ListDisplaySwitchComponent,\n IfAllowedDirective,\n IconDirective,\n NgClass,\n RegisterDeviceDropdownComponent,\n HelpComponent,\n C8yTranslateDirective,\n PopoverDirective,\n NgFor,\n FormsModule,\n RequiredInputPlaceholderDirective,\n LoadMoreComponent,\n C8yTranslatePipe,\n AsyncPipe,\n DatePipe\n ]\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_ACCEPTANCE]: {\n label: gettext('Pending acceptance'),\n icon: 'circle',\n cls: 'text-info'\n },\n [DeviceRegistrationStatus.ACCEPTED]: {\n label: gettext('Accepted'),\n icon: 'check-circle',\n cls: 'text-success'\n },\n [DeviceRegistrationStatus.BLOCKED]: {\n label: gettext('Blocked'),\n icon: 'ban',\n cls: 'text-danger'\n }\n };\n\n private unsubscribe$: Subject<void> = new Subject();\n private readonly _securityTokenPolicy: BehaviorSubject<DeviceRegistrationSecurityMode> =\n new BehaviorSubject(DeviceRegistrationSecurityMode.OPTIONAL);\n\n constructor(\n private registerDeviceService: RegisterDeviceService,\n private bootstrapRealtimeService: DeviceBootstrapRealtimeService,\n private tenantUiService: TenantUiService,\n private modalService: ModalService,\n private translateService: TranslateService,\n private optionsService: OptionsService,\n private activatedRoute: ActivatedRoute,\n private generalRegistration: GeneralDeviceRegistrationService\n ) {}\n\n ngOnInit() {\n this.loadAll();\n this.setIsManagementTenant();\n this.setRequireSecurityToken();\n\n this.deviceRequests$ = this.registerDeviceService.deviceRegistrationRequests$.pipe(\n map(req => ({\n data: sortBy(req.data, [\n ({ status }) => (status === DeviceRegistrationStatus.PENDING_ACCEPTANCE ? 0 : 1),\n '-creationTime'\n ]),\n paging: req.paging\n }))\n );\n this.limit$ = this.registerDeviceService.limit$;\n this.limitReachedInfo$ = this.limit$.pipe(\n filter(deviceRegistrationLimit => deviceRegistrationLimit.isReached),\n switchMap(({ limit }) =>\n this.translateService.stream(\n gettext(\n 'You reached the limit of {{ maxDevices }} devices. No more devices can be registered.'\n ),\n { maxDevices: limit }\n )\n )\n );\n this.registerDeviceService.loading$\n .pipe(takeUntil(this.unsubscribe$))\n .subscribe(value => (this.isLoading = value));\n\n this.bootstrapRealtimeService\n .onUpdate$()\n .pipe(takeUntil(this.unsubscribe$))\n .subscribe((bootstrap: IRealtimeDeviceBootstrap) => {\n this.registerDeviceService.onDeviceBootstrap(bootstrap);\n });\n\n this.handleQueryParams();\n }\n\n ngOnDestroy() {\n this.unsubscribe$.next();\n this.unsubscribe$.complete();\n }\n\n updateList(data) {\n this.registerDeviceService.internalListUpdate(data);\n }\n\n async handleQueryParams() {\n const { externalId, 'one-time-password': oneTimePassword } =\n this.activatedRoute.snapshot.queryParams;\n\n if (!externalId) {\n return;\n }\n try {\n await this.generalRegistration.open({\n useEST$: new BehaviorSubject<boolean>(!!oneTimePassword),\n model: {\n devicesToCreate: [\n {\n id: externalId,\n oneTimePassword\n }\n ]\n }\n });\n this.loadAll();\n } catch (e) {\n // modal closed\n }\n }\n\n async delete(id: string) {\n const confirmed = await this.modalService.confirm(\n gettext('Cancel device registration'),\n this.translateService.instant(\n gettext(\n 'You are about to cancel device registration for ID \"{{id}}\". Do you want to proceed?'\n ),\n { id }\n ),\n Status.DANGER,\n {\n ok: gettext('Cancel registration'),\n cancel: gettext('Close')\n }\n );\n\n if (confirmed) {\n this.registerDeviceService.remove(id);\n }\n }\n\n accept(request: IDeviceRegistration) {\n this.registerDeviceService.accept(request);\n }\n\n acceptAll() {\n this.registerDeviceService.acceptAll();\n }\n\n canAcceptAll() {\n const pendingRequests = this.registerDeviceService.getRequestByStatus(\n DeviceRegistrationStatus.PENDING_ACCEPTANCE\n );\n return !(pendingRequests.length > 0 && !this.requireSecurityToken);\n }\n\n loadAll() {\n this.registerDeviceService.list();\n }\n\n displayMode(listClass: 'interact-list' | 'interact-grid') {\n this.gridOrList = listClass;\n }\n\n async setRequireSecurityToken() {\n const mode: DeviceRegistrationSecurityMode =\n (await this.optionsService.getTenantOption<DeviceRegistrationSecurityMode>(\n 'device-registration',\n 'security-token.policy',\n DeviceRegistrationSecurityMode.OPTIONAL\n )) as DeviceRegistrationSecurityMode;\n this._securityTokenPolicy.next(mode);\n this.requireSecurityToken = mode === DeviceRegistrationSecurityMode.REQUIRED;\n }\n\n async setIsManagementTenant() {\n this.isManagementTenant = await this.tenantUiService.isManagementTenant();\n }\n\n shouldShowSecurityTokenInput(data: IDeviceRegistration) {\n return (\n data &&\n data.status === DeviceRegistrationStatus.PENDING_ACCEPTANCE &&\n this.showTokenInputBasedOnSecurityMode()\n );\n }\n\n showTokenInputBasedOnSecurityMode() {\n return this._securityTokenPolicy.getValue() !== DeviceRegistrationSecurityMode.IGNORED;\n }\n}\n","<ng-container *ngIf=\"deviceRequests$ | async as deviceRequestList\">\n <c8y-title>\n {{ 'Device registration' | translate }}\n <small *ngIf=\"deviceRequestList.data.length === 1\">1 {{ 'new device' | translate }}</small>\n <small *ngIf=\"deviceRequestList.data.length > 1\">\n {{ deviceRequestList.data.length }} {{ 'new devices' | translate }}\n </small>\n </c8y-title>\n\n <c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'exchange'\"\n [label]=\"'Devices' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-device-connect'\"\n [label]=\"'Device registration' | translate\"\n ></c8y-breadcrumb-item>\n </c8y-breadcrumb>\n\n <c8y-action-bar-item\n [placement]=\"'left'\"\n itemClass=\"navbar-form hidden-xs\"\n >\n <c8y-list-display-switch (onListClassChange)=\"displayMode($event)\"></c8y-list-display-switch>\n </c8y-action-bar-item>\n\n <ng-container *ngIf=\"limit$ | async as limitStatus\">\n <c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"10\"\n >\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Accept all' | translate }}\"\n type=\"button\"\n *c8yIfAllowed=\"['ROLE_DEVICE_CONTROL_ADMIN']\"\n (click)=\"acceptAll()\"\n [disabled]=\"canAcceptAll() || limitStatus?.isReached\"\n >\n <i [c8yIcon]=\"'check'\"></i>\n {{ 'Accept all' | translate }}\n </button>\n </c8y-action-bar-item>\n\n <c8y-action-bar-item\n [placement]=\"'right'\"\n [priority]=\"9\"\n >\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n type=\"button\"\n (click)=\"loadAll()\"\n [disabled]=\"isLoading\"\n >\n <i\n [c8yIcon]=\"'refresh'\"\n [ngClass]=\"{ 'icon-spin': isLoading }\"\n ></i>\n {{ 'Reload' | translate }}\n </button>\n </c8y-action-bar-item>\n\n <c8y-action-bar-item\n [placement]=\"'right'\"\n *c8yIfAllowed=\"['ROLE_DEVICE_CONTROL_ADMIN']\"\n >\n <c8y-register-device-dropdown></c8y-register-device-dropdown>\n </c8y-action-bar-item>\n\n <c8y-help\n src=\"/docs/device-management-application/registering-devices/#registering-devices\"\n ></c8y-help>\n\n <ng-container *ngIf=\"deviceRequestList.data.length > 0; else noData\">\n <div\n class=\"card-group\"\n [ngClass]=\"gridOrList\"\n >\n <!-- START interact-list sticky header START -->\n <div\n class=\"page-sticky-header hidden-xs\"\n *ngIf=\"gridOrList === 'interact-list'\"\n >\n <div class=\"d-flex\">\n <div class=\"card-header p-l-40\">\n <p translate>Device</p>\n </div>\n <div class=\"card-block card-column-30 p-l-0 m-l-8\">\n <p translate>Status</p>\n </div>\n <div\n class=\"card-block card-column-30 p-0\"\n *ngIf=\"showTokenInputBasedOnSecurityMode()\"\n >\n <p translate>Security token</p>\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{\n 'Security token is required if the connected device uses it.' | translate\n }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </div>\n <div class=\"card-footer card-column-50\">\n <div\n class=\"d-contents\"\n *ngIf=\"isManagementTenant; else noManagement\"\n >\n <div class=\"card-column-50\">\n {{ 'Created' | translate }}\n </div>\n <div class=\"card-column-30\">\n {{ 'By`user`' | translate }}\n </div>\n