@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
598 lines (592 loc) • 170 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, ChangeDetectionStrategy, Component, inject, InjectionToken, Input, ViewChild, NgModule } from '@angular/core';
import * as i4 from 'ngx-bootstrap/modal';
import { BsModalService } from 'ngx-bootstrap/modal';
import { __decorate, __metadata } from 'tslib';
import * as i5 from '@ngx-formly/core';
import { FormlyModule } from '@ngx-formly/core';
import { gettext } from '@c8y/ngx-components/gettext';
import * as i1$2 from '@c8y/ngx-components';
import { ModalComponent, IconDirective, C8yStepper, C8yTranslateDirective, C8yStepperButtons, OperationResultComponent, ListGroupComponent, ListItemComponent, ListItemIconComponent, ListItemCollapseComponent, C8yTranslatePipe, memoize, hookGeneric, ExtensionPointWithoutStateForPlugins, fromTriggerOnce, OutletDirective, Status, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, ActionBarItemComponent, ListDisplaySwitchComponent, IfAllowedDirective, HelpComponent, RequiredInputPlaceholderDirective, LoadMoreComponent, DatePipe, Permissions, NavigatorNode, FormGroupComponent, LoadingComponent, CommonModule, CoreModule, StepperModule, DynamicFormsModule, hookNavigator, DeviceBootstrapRealtimeService } from '@c8y/ngx-components';
import * as i6 from '@angular/forms';
import { FormGroup, FormsModule } from '@angular/forms';
import { Subject, BehaviorSubject, from, forkJoin, defer, Observable } from 'rxjs';
import { takeUntil, finalize, mergeMap, takeLast, map, tap, filter, switchMap, shareReplay, startWith } from 'rxjs/operators';
import * as i1$1 from '@c8y/client';
import { DeviceRegistrationStatus, DeviceRegistrationSecurityMode, ApplicationType } from '@c8y/client';
import * as i1 from '@angular/router';
import { RouterModule } from '@angular/router';
import { get, pick, sortBy, flatMap } from 'lodash-es';
import { CdkStep, STEP_STATE } from '@angular/cdk/stepper';
import { NgClass, AsyncPipe, JsonPipe, NgIf, NgTemplateOutlet, NgFor } from '@angular/common';
import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover';
import * as i3 from '@ngx-translate/core';
import { flatten } from 'lodash';
import { BsDropdownDirective, BsDropdownToggleDirective, BsDropdownMenuDirective, BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { saveAs } from 'file-saver';
class RegisterDeviceService {
constructor(router, deviceRegService, alertService) {
this.router = router;
this.deviceRegService = deviceRegService;
this.alertService = alertService;
this._loading = new Subject();
this._limit = new BehaviorSubject({
isReached: false
});
this._deviceRegistrationRequests = new BehaviorSubject({ data: [] });
this.deviceRegistrationRequests$ = this._deviceRegistrationRequests.asObservable();
this.loading$ = this._loading.asObservable();
this.limit$ = this._limit.asObservable();
this.deviceRegUrl = '/deviceregistration';
this.endSubscriptions = new Subject();
}
isDeviceRegistration() {
return get(this.router, 'url') === this.deviceRegUrl;
}
internalListUpdate(deviceRequests, pagingObject) {
let { paging, data } = this._deviceRegistrationRequests.getValue();
if (pagingObject) {
paging = pagingObject;
}
data = [...data, ...deviceRequests].filter(deviceReq => deviceReq.type !== 'c8y_DataBroker');
this._deviceRegistrationRequests.next({ data, paging });
}
onDeviceBootstrap(bsData) {
const { id, status } = bsData;
this._deviceRegistrationRequests.next({
data: this.updateStatusById(id, status)
});
}
list(pageSize = 100) {
this._loading.next(true);
this._deviceRegistrationRequests.next({ data: [], paging: undefined });
from(this.deviceRegService.list({ pageSize, withTotalPages: true }))
.pipe(takeUntil(this.endSubscriptions), finalize(() => this.limit()))
.subscribe(res => {
const { data, paging } = res;
this.internalListUpdate(data, paging);
this._loading.next(false);
}, err => {
this._loading.next(false);
this.alertService.addServerFailure(err);
});
}
createMultiple(newDeviceRequests) {
if (newDeviceRequests && newDeviceRequests.length > 0) {
this._loading.next(true);
const newRequests$ = newDeviceRequests.map(element => {
return from(this.deviceRegService.create(element).catch((err) => ({
res: err.res,
data: { ...err.data, id: element.id }
})));
});
const groupedRequests = {
success: [],
failed: []
};
return forkJoin(newRequests$).pipe(mergeMap(resp => resp.map(el => {
el.res.ok
? groupedRequests.success.push(el.data)
: groupedRequests.failed.push(el.data);
return groupedRequests;
})), takeLast(1), finalize(() => {
this.internalListUpdate(groupedRequests.success);
this._loading.next(false);
}));
}
}
remove(id) {
this._loading.next(true);
from(this.deviceRegService.delete(id))
.pipe(takeUntil(this.endSubscriptions))
.subscribe(() => {
this._deviceRegistrationRequests.next({
data: this.removeDeviceRegistrationRequestById(id)
});
this._loading.next(false);
this.alertService.success(gettext('Device registration cancelled.'));
}, err => {
this._loading.next(false);
this.alertService.addServerFailure(err);
});
}
accept(request) {
this._loading.next(true);
const payload = pick(request, ['id', 'securityToken']);
from(this.deviceRegService.accept(payload))
.pipe(takeUntil(this.endSubscriptions))
.subscribe(() => {
this._deviceRegistrationRequests.next({
data: this.removeDeviceRegistrationRequestById(payload.id)
});
this.limit();
this._loading.next(false);
this.alertService.success(gettext('Device registration accepted.'));
}, err => {
this._loading.next(false);
this.alertService.addServerFailure(err);
});
}
acceptAll() {
const acceptedDeviceRequests = [];
const failedDeviceRequests = [];
this._loading.next(true);
from(this.deviceRegService.acceptAll())
.pipe(takeUntil(this.endSubscriptions), map(({ data }) => {
data.map(deviceRegistrationRequest => {
if (deviceRegistrationRequest.successful) {
acceptedDeviceRequests.push(deviceRegistrationRequest);
this.removeDeviceRegistrationRequestById(deviceRegistrationRequest.id);
}
else {
failedDeviceRequests.push(deviceRegistrationRequest);
}
});
return data;
}), finalize(() => {
// update rendered list with successful accepted device registrations
// see: this.updateStatusById(...)
this.internalListUpdate([]);
this.limit();
this._loading.next(false);
if (failedDeviceRequests.length > 0) {
this.alertService.warning(gettext('Could not accept all pending registration requests.'), JSON.stringify({
failedDeviceRequests,
acceptedDeviceRequests
}, undefined, 2));
}
else {
this.alertService.success(gettext('Accepted all pending registration requests.'));
}
}))
.subscribe(() => {
// empty by design
}, err => {
this._loading.next(false);
this.alertService.addServerFailure(err);
});
}
limit() {
from(this.deviceRegService.limit())
.pipe(takeUntil(this.endSubscriptions))
.subscribe(res => this._limit.next(res.data), err => this.alertService.addServerFailure(err));
}
getRequestByStatus(status) {
return this._deviceRegistrationRequests.getValue().data.filter(req => req.status === status);
}
ngOnDestroy() {
this.endSubscriptions.next();
this.endSubscriptions.complete();
}
updateStatusById(id, status) {
const items = this._deviceRegistrationRequests.getValue().data;
const matchingElementIndex = items.findIndex(element => element.id === id);
if (matchingElementIndex >= 0) {
items[matchingElementIndex].status = status;
}
return items;
}
removeDeviceRegistrationRequestById(id) {
const items = this._deviceRegistrationRequests.getValue().data;
const matchingElementIndex = items.findIndex(element => element.id === id);
if (matchingElementIndex >= 0) {
items.splice(matchingElementIndex, 1);
}
this._loading.next(false);
return items;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceService, deps: [{ token: i1.Router }, { token: i1$1.DeviceRegistrationService }, { token: i1$2.AlertService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i1.Router }, { type: i1$1.DeviceRegistrationService }, { type: i1$2.AlertService }] });
class GeneralDeviceRegistrationComponent {
constructor(tenantUIService, tenantService, registerDeviceService, inventoryService, cd, bsModalRef, gainsightService, featureCacheService) {
this.tenantUIService = tenantUIService;
this.tenantService = tenantService;
this.registerDeviceService = registerDeviceService;
this.inventoryService = inventoryService;
this.cd = cd;
this.bsModalRef = bsModalRef;
this.gainsightService = gainsightService;
this.featureCacheService = featureCacheService;
this.MANAGEMENT = 'management';
this.FILTER = {
withTotalPages: true,
pageSize: 25
};
this.useEST$ = new BehaviorSubject(false);
this.certificateAuthorityFeatureEnabled = this.featureCacheService.getFeatureState('certificate-authority');
this.form = new FormGroup({});
this.model = {
devicesToCreate: [{}]
};
this.options = {
formState: {
canLoadTenants: true,
useEST: this.useEST$.getValue()
}
};
this.PRODUCT_EXPERIENCE = {
EVENT: 'deviceRegistration',
COMPONENT: 'single-general-registration',
RESULT: { SUCCESS: 'registrationSuccess', FAILURE: 'registrationFailure' }
};
this.success = [];
this.failed = [];
this.fields = [
{
type: 'array',
key: 'devicesToCreate',
props: {
addText: gettext('Add device'),
addTextDataCy: 'add-device'
},
fieldArray: {
fieldGroup: [
{
key: 'id',
type: 'string',
focus: true,
props: {
placeholder: '0123ab32fcd',
label: gettext('Device ID'),
required: true
},
validators: {
unique: {
expression: (control) => {
const found = control.root.get('devicesToCreate').controls
.map(el => el.controls.id)
.find(el => el !== control && el.value === control.value);
return !found;
},
message: () => gettext('Device ID duplicates are not allowed')
}
}
},
{
key: 'tenant',
type: 'typeahead',
expressions: {
hide: field => {
const formState = field.options?.formState;
if (!formState?.canLoadTenants) {
field.formControl.setValue(null);
}
return !formState?.canLoadTenants || false;
}
},
defaultValue: { id: this.MANAGEMENT },
props: {
label: gettext('Add to tenant'),
required: true,
c8yForOptions: this.canLoadTenants$().pipe(filter(canLoad => canLoad), switchMap(() => this.getTenants$())),
container: 'body',
displayProperty: 'id',
valueProperties: ['id']
},
hooks: {
onInit: _field => this.canLoadTenants$().pipe(tap(canLoad => {
this.options.formState.canLoadTenants = canLoad;
this.cd.detectChanges();
}))
}
},
{
key: 'group',
type: 'typeahead',
expressions: {
'props.disabled': (field) => {
const formState = field.options?.formState;
const model = field.model;
if (formState?.canLoadTenants) {
if (model?.tenant?.id !== this.MANAGEMENT) {
field.formControl.setValue(null);
}
return !(model?.tenant?.id === this.MANAGEMENT);
}
delete field?.props?.description;
return false;
}
},
props: {
disabled: false,
label: gettext('Add to group'),
description: gettext('You can add device to specific group for management tenant only.'),
container: 'body',
displayProperty: 'name',
valueProperties: ['id'],
c8yForOptions: this.getGroups$()
},
hooks: {
onInit: _field => this.canLoadTenants$().pipe(tap(canLoad => {
this.options.formState.canLoadTenants = canLoad;
this.cd.detectChanges();
}))
}
},
{
key: 'oneTimePassword',
type: 'password',
expressions: {
hide: field => !field.options?.formState?.useEST
},
props: {
placeholder: 'TruDN3H45L0',
label: gettext('One-time password'),
required: true
},
hooks: {
onInit: _field => this.useEST$.pipe(tap(useEST => {
this.options.formState.useEST = useEST;
this.cd.detectChanges();
}))
}
}
]
}
}
];
this.result = new Promise((resolve, reject) => {
this.onSuccessfulClosing = resolve;
this.onCancel = reject;
});
this.destroy$ = new Subject();
this.lastCreatedDevices = [];
this.isLoading$ = this.registerDeviceService.loading$;
}
ngAfterViewInit() {
this.cd.detectChanges();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
registerDevice(eventObject) {
this.create(eventObject);
}
fixErrors(event, failedRequests) {
if (failedRequests && failedRequests.length > 0) {
this.options.resetModel({
devicesToCreate: [
...this.lastCreatedDevices.filter(el => failedRequests.map(data => data.id).includes(el.id))
]
});
this.cd.detectChanges();
}
event?.stepper.previous();
}
close() {
this.bsModalRef.hide();
this.onSuccessfulClosing();
}
cancel() {
this.bsModalRef.hide();
this.onCancel();
}
create(eventObject) {
if (this.model?.devicesToCreate?.length > 0) {
this.lastCreatedDevices = [...this.model.devicesToCreate];
const dataToSend = this.model.devicesToCreate.map((el) => {
const { id, tenant, group, oneTimePassword } = el;
let data = { id };
if (tenant?.id) {
data = { ...data, tenantId: tenant.id };
}
if (group?.id) {
data = { ...data, groupId: group.id };
}
if (oneTimePassword) {
data = { ...data, enrollmentToken: oneTimePassword };
}
return data;
});
this.registerDeviceService
.createMultiple(dataToSend)
.pipe(takeUntil(this.destroy$))
.subscribe(requests => {
this.success = requests.success ?? [];
if (this.success.length > 0) {
this.gainsightService.triggerEvent(this.PRODUCT_EXPERIENCE.EVENT, {
result: this.PRODUCT_EXPERIENCE.RESULT.SUCCESS,
component: this.PRODUCT_EXPERIENCE.COMPONENT
});
}
this.failed = requests.failed ?? [];
if (this.failed.length > 0) {
this.gainsightService.triggerEvent(this.PRODUCT_EXPERIENCE.EVENT, {
result: this.PRODUCT_EXPERIENCE.RESULT.FAILURE,
component: this.PRODUCT_EXPERIENCE.COMPONENT
});
}
if (eventObject) {
eventObject.stepper.next();
}
});
}
}
canLoadTenants$() {
return defer(() => from(this.tenantUIService.isManagementTenant())).pipe(shareReplay(1));
}
getTenants$() {
return defer(() => from(this.tenantService.list(this.FILTER))).pipe(shareReplay(1));
}
getGroups$() {
return defer(() => from(this.inventoryService.listQuery({ __filter: { __has: 'c8y_IsDeviceGroup' }, __orderby: [{ name: 1 }] }, { ...this.FILTER }))).pipe(shareReplay(1));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationComponent, deps: [{ token: i1$2.TenantUiService }, { token: i1$1.TenantService }, { token: RegisterDeviceService }, { token: i1$1.InventoryService }, { token: i0.ChangeDetectorRef }, { token: i4.BsModalRef }, { token: i1$2.GainsightService }, { token: i1$2.FeatureCacheService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: GeneralDeviceRegistrationComponent, isStandalone: true, selector: "c8y-general-device-registration", ngImport: i0, template: "<c8y-modal\n [title]=\"'Register devices' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [customFooter]=\"true\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'c8y-device-connect'\"></span>\n </ng-container>\n <c8y-stepper\n [hideStepProgress]=\"true\"\n linear\n c8y-modal-body\n >\n <cdk-step [stepControl]=\"form\">\n <div class=\"text-center sticky-top bg-component\">\n <p\n class=\"text-medium text-16 separator-bottom p-16\"\n translate\n >\n Register general devices\n </p>\n @if (certificateAuthorityFeatureEnabled | async) {\n <label\n class=\"c8y-switch m-24 a-i-center\"\n title=\"{{ 'Create device certificates during device registration' | translate }}\"\n for=\"useEST\"\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 }\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 @if (success.length === 1 && failed.length === 0) {\n <c8y-operation-result\n class=\"lead\"\n type=\"success\"\n text=\"{{ 'Device registered' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n } @else if (success.length === 0 && failed.length === 1) {\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\n text=\"{{ 'Failed to register device' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n } @else if (success.length > 1 && failed.length === 0) {\n <c8y-operation-result\n class=\"lead\"\n type=\"success\"\n [text]=\"\n '{{ successfulDevicesCount }} devices registered'\n | translate: { successfulDevicesCount: success.length }\n \"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n } @else if (success.length === 0 && failed.length > 1) {\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\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 } @else if (success.length > 0 && failed.length > 0) {\n <div\n class=\"p-l-24 p-r-24 text-center\"\n data-cy=\"device-registration-failure-message\"\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\n @if (success.length > 0) {\n <div\n class=\"m-b-8 p-l-24 p-r-24\"\n data-cy=\"device-registration-success-message\"\n >\n @if (!(useEST$ | async)) {\n <span translate>\n Turn on the registered devices and wait for connections to be established. Once a\n device is connected, its status will change to \"Pending acceptance\". You will need\n to approve it by clicking on the \"Accept\" button.\n </span>\n } @else {\n <span translate>\n The successfully enrolled devices can now request signed certificates and use them\n to connect and authenticate to the platform via certificate-based authentication.\n </span>\n }\n </div>\n }\n\n <c8y-list-group class=\"separator-top m-t-16\">\n @for (fail of failed; track $index) {\n <c8y-li>\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\n @for (s of success; track $index) {\n <c8y-li>\n <c8y-li-icon\n class=\"text-success\"\n [icon]=\"'check-circle'\"\n ></c8y-li-icon>\n {{ s?.id }}\n </c8y-li>\n }\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", dependencies: [{ kind: "component", type: ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: C8yStepper, selector: "c8y-stepper", inputs: ["disableDefaultIcons", "disableProgressButtons", "customClasses", "hideStepProgress", "useStepLabelsAsTitlesOnly"], outputs: ["onStepChange"] }, { kind: "component", type: CdkStep, selector: "cdk-step", inputs: ["stepControl", "label", "errorMessage", "aria-label", "aria-labelledby", "state", "editable", "optional", "completed", "hasError"], outputs: ["interacted"], exportAs: ["cdkStep"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i6.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i6.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i6.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "ngmodule", type: FormlyModule }, { kind: "component", type: i5.FormlyForm, selector: "formly-form", inputs: ["form", "model", "fields", "options"], outputs: ["modelChange"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: C8yStepperButtons, selector: "c8y-stepper-buttons", inputs: ["labels", "pending", "disabled", "showButtons"], outputs: ["onCancel", "onNext", "onBack", "onCustom"] }, { kind: "component", type: OperationResultComponent, selector: "c8y-operation-result", inputs: ["text", "vertical", "size", "type"] }, { kind: "component", type: ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: ListItemCollapseComponent, selector: "c8y-list-item-collapse, c8y-li-collapse", inputs: ["collapseWay"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
__decorate([
memoize(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Observable)
], GeneralDeviceRegistrationComponent.prototype, "canLoadTenants$", null);
__decorate([
memoize(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Observable)
], GeneralDeviceRegistrationComponent.prototype, "getTenants$", null);
__decorate([
memoize(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Observable)
], GeneralDeviceRegistrationComponent.prototype, "getGroups$", null);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-general-device-registration', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
ModalComponent,
IconDirective,
C8yStepper,
CdkStep,
C8yTranslateDirective,
FormsModule,
PopoverDirective,
FormlyModule,
NgClass,
C8yStepperButtons,
OperationResultComponent,
ListGroupComponent,
ListItemComponent,
ListItemIconComponent,
ListItemCollapseComponent,
C8yTranslatePipe,
AsyncPipe,
JsonPipe
], template: "<c8y-modal\n [title]=\"'Register devices' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [customFooter]=\"true\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'c8y-device-connect'\"></span>\n </ng-container>\n <c8y-stepper\n [hideStepProgress]=\"true\"\n linear\n c8y-modal-body\n >\n <cdk-step [stepControl]=\"form\">\n <div class=\"text-center sticky-top bg-component\">\n <p\n class=\"text-medium text-16 separator-bottom p-16\"\n translate\n >\n Register general devices\n </p>\n @if (certificateAuthorityFeatureEnabled | async) {\n <label\n class=\"c8y-switch m-24 a-i-center\"\n title=\"{{ 'Create device certificates during device registration' | translate }}\"\n for=\"useEST\"\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 }\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 @if (success.length === 1 && failed.length === 0) {\n <c8y-operation-result\n class=\"lead\"\n type=\"success\"\n text=\"{{ 'Device registered' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n } @else if (success.length === 0 && failed.length === 1) {\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\n text=\"{{ 'Failed to register device' | translate }}\"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n } @else if (success.length > 1 && failed.length === 0) {\n <c8y-operation-result\n class=\"lead\"\n type=\"success\"\n [text]=\"\n '{{ successfulDevicesCount }} devices registered'\n | translate: { successfulDevicesCount: success.length }\n \"\n [size]=\"84\"\n [vertical]=\"true\"\n ></c8y-operation-result>\n } @else if (success.length === 0 && failed.length > 1) {\n <c8y-operation-result\n class=\"lead\"\n type=\"error\"\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 } @else if (success.length > 0 && failed.length > 0) {\n <div\n class=\"p-l-24 p-r-24 text-center\"\n data-cy=\"device-registration-failure-message\"\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\n @if (success.length > 0) {\n <div\n class=\"m-b-8 p-l-24 p-r-24\"\n data-cy=\"device-registration-success-message\"\n >\n @if (!(useEST$ | async)) {\n <span translate>\n Turn on the registered devices and wait for connections to be established. Once a\n device is connected, its status will change to \"Pending acceptance\". You will need\n to approve it by clicking on the \"Accept\" button.\n </span>\n } @else {\n <span translate>\n The successfully enrolled devices can now request signed certificates and use them\n to connect and authenticate to the platform via certificate-based authentication.\n </span>\n }\n </div>\n }\n\n <c8y-list-group class=\"separator-top m-t-16\">\n @for (fail of failed; track $index) {\n <c8y-li>\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\n @for (s of success; track $index) {\n <c8y-li>\n <c8y-li-icon\n class=\"text-success\"\n [icon]=\"'check-circle'\"\n ></c8y-li-icon>\n {{ s?.id }}\n </c8y-li>\n }\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" }]
}], ctorParameters: () => [{ type: i1$2.TenantUiService }, { type: i1$1.TenantService }, { type: RegisterDeviceService }, { type: i1$1.InventoryService }, { type: i0.ChangeDetectorRef }, { type: i4.BsModalRef }, { type: i1$2.GainsightService }, { type: i1$2.FeatureCacheService }], propDecorators: { canLoadTenants$: [], getTenants$: [], getGroups$: [] } });
class GeneralDeviceRegistrationService {
constructor() {
this.modalService = inject(BsModalService);
}
async open(initialState) {
const modalRef = this.modalService.show(GeneralDeviceRegistrationComponent, {
class: 'modal-lg',
ariaDescribedby: 'modal-body',
ariaLabelledBy: 'modal-title',
ignoreBackdropClick: true,
initialState: {
...initialState
}
});
return await modalRef.content.result;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class GeneralDeviceRegistrationButtonComponent {
constructor(registrationService) {
this.registrationService = registrationService;
}
async open() {
try {
await this.registrationService.open();
}
catch {
// modal was closed
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationButtonComponent, deps: [{ token: GeneralDeviceRegistrationService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: GeneralDeviceRegistrationButtonComponent, isStandalone: true, selector: "c8y-general-device-registration-button", ngImport: i0, template: "<button title=\"{{ 'General' | translate }}\" type=\"button\" (click)=\"open()\">\n <i c8yIcon=\"c8y-device-connect\"></i>\n {{ 'General' | translate }}\n</button>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: GeneralDeviceRegistrationButtonComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-general-device-registration-button', imports: [IconDirective, C8yTranslatePipe], template: "<button title=\"{{ 'General' | translate }}\" type=\"button\" (click)=\"open()\">\n <i c8yIcon=\"c8y-device-connect\"></i>\n {{ 'General' | translate }}\n</button>\n" }]
}], ctorParameters: () => [{ type: GeneralDeviceRegistrationService }] });
/**
* A hook to use for Multi Provider extension.
* @deprecated Consider using the `hookDeviceRegistration` function instead.
*/
const HOOK_DEVICE_REGISTRATION = new InjectionToken('HOOK_DEVICE_REGISTRATION');
/**
* You can either provide a single `RegisterDeviceExtension` as parameter:
* ```typescript
* hookDeviceRegistration(...)
* ```
*
* Or an array to directly register multiple:
* ```typescript
* hookDeviceRegistration([...])
* ```
*
* Or you provide an Service that implements `ExtensionFactory<RegisterDeviceExtension>`
* ```typescript
* export class MyDeviceRegistrationFactory implements ExtensionFactory<RegisterDeviceExtension> {...}
* ...
* hookDeviceRegistration(MyDeviceRegistrationFactory)
* ```
* A typed alternative to `HOOK_DEVICE_REGISTRATION`.
* @param registration The `RegisterDeviceExtension`'s or `ExtensionFactory` to be provided.
* @returns An `Provider` to be provided in your module.
*/
function hookDeviceRegistration(registration, options) {
return hookGeneric(registration, HOOK_DEVICE_REGISTRATION, options);
}
/**
* A service which defines device registration options.
*/
class RegisterDeviceExtensionService extends ExtensionPointWithoutStateForPlugins {
constructor(rootInjector, router, plugins) {
super(rootInjector, plugins);
this.router = router;
this.items$ = this.setupItemsObservable();
}
setupItemsObservable() {
return fromTriggerOnce(this.router, this.refresh$, [
() => flatten(this.injectors.map(injector => injector.get(HOOK_DEVICE_REGISTRATION, [], { self: true }))),
() => this.factories
]).pipe(startWith([]), shareReplay(1));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceExtensionService, deps: [{ token: i0.Injector }, { token: i1.Router }, { token: i1$2.PluginsResolveService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceExtensionService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceExtensionService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i0.Injector }, { type: i1.Router }, { type: i1$2.PluginsResolveService }] });
class RegisterDeviceDropdownComponent {
constructor(registerDeviceExtensionService, registerDeviceService) {
this.registerDeviceExtensionService = registerDeviceExtensionService;
this.registerDeviceService = registerDeviceService;
this.single$ = this.registerDeviceExtensionService.items$.pipe(map(items => items.filter(item => item.category === 'single').sort((a, b) => b.priority - a.priority)));
this.bulk$ = this.registerDeviceExtensionService.items$.pipe(map(items => items.filter(item => item.category === 'bulk').sort((a, b) => b.priority - a.priority)));
this.limit$ = this.registerDeviceService.limit$.pipe(map(limit => limit.isReached));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterDeviceDropdownComponent, deps: [{ token: RegisterDeviceExtensionService }, { token: RegisterDeviceService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: RegisterDeviceDropdownComponent, isStandalone: true, selector: "c8y-register-device-dropdown", ngImport: i0, template: "<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", dependencies: [{ kind: "directive", type: BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: NgTemplateOu