UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

1 lines 93.3 kB
{"version":3,"file":"c8y-ngx-components-tenants.mjs","sources":["../../tenants/tenant-list/tenant-list.guard.ts","../../tenants/tenants.model.ts","../../tenants/tenants-navigation.factory.ts","../../tenants/tenant-list/creation-time.filtering-form-renderer.component.ts","../../tenants/tenant-list/creation-time.filtering-form-renderer.component.html","../../tenants/tenant-list/status.filtering-form-renderer.component.ts","../../tenants/tenant-list/status.filtering-form-renderer.component.html","../../tenants/tenant-list/tenant-list.component.ts","../../tenants/tenant-list/tenant-list.component.html","../../tenants/custom-properties/custom-properties.service.ts","../../tenants/custom-properties/custom-property-field/custom-property-field.component.ts","../../tenants/custom-properties/custom-property-field/custom-property-field.component.html","../../tenants/custom-properties/custom-properties.component.ts","../../tenants/custom-properties/custom-properties.component.html","../../tenants/tenant-limits/tenant-limits-definitions.ts","../../tenants/tenant-limits/tenant-limits.component.ts","../../tenants/tenant-limits/tenant-limits.component.html","../../tenants/tenants.module.ts","../../tenants/c8y-ngx-components-tenants.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\nimport { TenantUiService } from '@c8y/ngx-components';\n\n@Injectable()\nexport class TenantListGuard {\n private active: boolean;\n\n constructor(private tenantUiService: TenantUiService) {}\n\n /**\n * Checks if tenant list should be active,\n * i.e. whether the current tenant can read other tenants.\n * **Note: the check is executed only once in the runtime.**\n *\n * @returns True, if the feature should be active.\n */\n canActivate(): boolean {\n if (this.active === undefined) {\n this.active = this.tenantUiService.canReadTenants();\n }\n return this.active;\n }\n}\n","import { InjectionToken } from '@angular/core';\nimport { NavigatorNode } from '@c8y/ngx-components';\n\nexport const TENANTS_MODULE_CONFIG = new InjectionToken<TenantsModuleConfig>('TenantsModuleConfig');\n\n/**\n * Configuration object for `TenantsModule`.\n */\nexport interface TenantsModuleConfig {\n /**\n * Allows for hiding or customizing \"Subtenants\" navigator node:\n * - `subtenantsNavigatorNode: true` - shows the default navigator node (default),\n * - `subtenantsNavigatorNode: false` - hides the navigator node,\n * - `subtenantsNavigatorNode: { label: 'My subtenants' }` - overrides default navigator node,\n * - `subtenantsNavigatorNode: new NavigatorNode({ label: 'My subtenants' })` - overrides default navigator node.\n */\n subtenantsNavigatorNode?: boolean | Partial<NavigatorNode>;\n}\n","import { Inject, Injectable, Optional } from '@angular/core';\nimport { gettext, NavigatorNode, NavigatorNodeFactory } from '@c8y/ngx-components';\nimport { TenantListGuard } from './tenant-list/tenant-list.guard';\nimport { TenantsModuleConfig, TENANTS_MODULE_CONFIG } from './tenants.model';\n\n@Injectable()\nexport class TenantsNavigationFactory implements NavigatorNodeFactory {\n private navs: NavigatorNode[] = [];\n\n constructor(\n private tenantListGuard: TenantListGuard,\n @Optional() @Inject(TENANTS_MODULE_CONFIG) private config: TenantsModuleConfig\n ) {}\n\n async get(): Promise<NavigatorNode[]> {\n const canActivateTenantList = await this.tenantListGuard.canActivate();\n if (!this.navs.length) {\n const subtenantsNavigatorNode = this.config?.subtenantsNavigatorNode ?? true;\n if (subtenantsNavigatorNode !== false) {\n this.navs.push(\n new NavigatorNode({\n parent: {\n label: gettext('Tenants'),\n icon: 'c8y-layers'\n },\n label: gettext('Subtenants'),\n icon: 'c8y-sub-tenants',\n path: 'tenants',\n routerLinkExact: false,\n priority: 4000,\n hidden: !canActivateTenantList,\n ...(subtenantsNavigatorNode === true ? {} : subtenantsNavigatorNode)\n })\n );\n }\n }\n return this.navs;\n }\n}\n","import { Component } from '@angular/core';\nimport { ITenant } from '@c8y/client';\nimport { FilterChip, FilteringFormRendererContext, gettext } from '@c8y/ngx-components';\nimport { DatePipe } from '@angular/common';\nimport { TranslateService } from '@ngx-translate/core';\n\nexport interface Model {\n dateFrom: Date;\n dateTo: Date;\n}\n\n@Component({\n templateUrl: './creation-time.filtering-form-renderer.component.html',\n selector: 'c8y-creation-time-filtering-form-renderer'\n})\nexport class CreationTimeFilteringFormRendererComponent {\n model: Model;\n\n constructor(\n public context: FilteringFormRendererContext,\n private c8yDate: DatePipe,\n private translateService: TranslateService\n ) {\n this.model = (this.context.property.externalFilterQuery || {}).model || {};\n }\n\n applyFilter() {\n this.context.applyFilter({\n externalFilterQuery: {\n model: this.model,\n chips: this.getChipsForModel(this.model)\n },\n filterPredicate: (tenant: ITenant) => {\n const creationTime = new Date(tenant.creationTime);\n let dateFrom;\n let dateTo;\n\n if (this.model.dateFrom) {\n dateFrom = this.model.dateFrom;\n dateFrom.setHours(0, 0, 0, 0);\n }\n\n if (this.model.dateTo) {\n dateTo = this.model.dateTo;\n dateTo.setHours(23, 59, 59, 999);\n }\n\n return Boolean(\n (!dateFrom && !dateTo) ||\n (dateFrom && !dateTo && dateFrom <= creationTime) ||\n (!dateFrom && dateTo && creationTime <= dateTo) ||\n (dateFrom && dateTo && dateFrom <= creationTime && creationTime <= dateTo)\n );\n }\n });\n }\n\n getChipsForModel(model: Model): FilterChip[] {\n const updateChips = externalFilterQuery => {\n externalFilterQuery.chips = this.getChipsForModel(externalFilterQuery.model);\n };\n const createChip = (key: keyof Model, displayValueTpl: string): FilterChip => {\n return {\n columnName: this.context.property.name,\n path: ['model', key],\n displayValue: this.translateService.instant(displayValueTpl, {\n date: this.c8yDate.transform(this.model[key])\n }),\n value: this.model[key],\n remove() {\n delete this.externalFilterQuery.model[key];\n updateChips(this.externalFilterQuery);\n return {\n columnName: this.columnName,\n externalFilterQuery: this.externalFilterQuery\n };\n }\n };\n };\n\n const chips = [];\n if (model.dateFrom) {\n chips.push(createChip('dateFrom', gettext('from: {{ date }}')));\n }\n if (model.dateTo) {\n chips.push(createChip('dateTo', gettext('to: {{ date }}')));\n }\n return chips;\n }\n\n resetFilter() {\n this.context.resetFilter();\n }\n}\n","<form #filterForm=\"ngForm\">\n <div class=\"m-b-8 p-t-8\">\n <label>{{ 'Filter by creation time' | translate }}</label>\n <c8y-form-group\n class=\"datepicker d-block m-b-16\"\n style=\"max-height: 32px\"\n >\n <input\n class=\"form-control fit-w text-left\"\n placeholder=\"{{ 'Created from`date`' | translate }}\"\n name=\"dateFrom\"\n [(ngModel)]=\"model.dateFrom\"\n bsDatepicker\n [bsConfig]=\"{ customTodayClass: 'today', returnFocusToInput: true }\"\n [maxDate]=\"model.dateTo\"\n />\n </c8y-form-group>\n <c8y-form-group\n class=\"datepicker m-l-0 d-block\"\n style=\"max-height: 32px\"\n >\n <input\n class=\"form-control fit-w text-left\"\n placeholder=\"{{ 'Created to`date`' | translate }}\"\n name=\"dateTo\"\n [(ngModel)]=\"model.dateTo\"\n bsDatepicker\n [bsConfig]=\"{ customTodayClass: 'today', returnFocusToInput: true }\"\n [minDate]=\"model.dateFrom\"\n />\n </c8y-form-group>\n </div>\n</form>\n\n<div class=\"data-grid__dropdown__footer d-flex separator-top\">\n <button\n class=\"btn btn-default btn-sm m-r-8 flex-grow\"\n title=\"{{ 'Reset' | translate }}\"\n (click)=\"resetFilter()\"\n >\n {{ 'Reset' | translate }}\n </button>\n <button\n class=\"btn btn-primary btn-sm flex-grow\"\n title=\"{{ 'Apply' | translate }}\"\n [disabled]=\"filterForm.invalid || !(model.dateFrom || model.dateTo)\"\n (click)=\"applyFilter()\"\n >\n {{ 'Apply' | translate }}\n </button>\n</div>\n","import { Component } from '@angular/core';\nimport { ITenant, TenantStatus } from '@c8y/client';\nimport { FilteringFormRendererContext } from '@c8y/ngx-components';\n\n@Component({\n templateUrl: './status.filtering-form-renderer.component.html',\n selector: 'c8y-status-filtering-form-renderer'\n})\nexport class StatusFilteringFormRendererComponent {\n model: {\n active: boolean;\n suspended: boolean;\n };\n\n constructor(public context: FilteringFormRendererContext) {\n this.model = (this.context.property.externalFilterQuery || {}).model || {};\n }\n\n applyFilter() {\n this.context.applyFilter({\n externalFilterQuery: {\n model: this.model\n },\n filterPredicate: (tenant: ITenant) =>\n Boolean(\n (!this.model.active && !this.model.suspended) ||\n (this.model.active && tenant.status === TenantStatus.ACTIVE) ||\n (this.model.suspended && tenant.status === TenantStatus.SUSPENDED)\n )\n });\n }\n\n resetFilter() {\n this.context.resetFilter();\n }\n}\n","<form #filterForm=\"ngForm\">\n <div class=\"m-b-8 p-t-8\">\n <label>{{ 'Filter by status' | translate }}</label>\n <c8y-form-group class=\"m-b-0\">\n <label class=\"c8y-checkbox\">\n <input type=\"checkbox\" name=\"active\" [(ngModel)]=\"model.active\" />\n <span></span>\n <span>{{ 'Active`tenant`' | translate }}</span>\n </label>\n </c8y-form-group>\n <c8y-form-group class=\"m-b-0\">\n <label class=\"c8y-checkbox\">\n <input type=\"checkbox\" name=\"suspended\" [(ngModel)]=\"model.suspended\" />\n <span></span>\n <span>{{ 'Suspended`tenant`' | translate }}</span>\n </label>\n </c8y-form-group>\n </div>\n</form>\n\n<div class=\"data-grid__dropdown__footer d-flex separator-top\">\n <button\n class=\"btn btn-default btn-sm m-r-8 flex-grow\"\n (click)=\"resetFilter()\"\n title=\"{{ 'Reset' | translate }}\"\n >\n {{ 'Reset' | translate }}\n </button>\n <button\n class=\"btn btn-primary btn-sm flex-grow\"\n [disabled]=\"filterForm.invalid\"\n (click)=\"applyFilter()\"\n title=\"{{ 'Apply' | translate }}\"\n >\n {{ 'Apply' | translate }}\n </button>\n</div>\n","import { Location } from '@angular/common';\nimport { Component, OnInit } from '@angular/core';\nimport { TranslateService } from '@ngx-translate/core';\nimport { saveAs } from 'file-saver';\nimport { BehaviorSubject, from } from 'rxjs';\nimport { expand, reduce, shareReplay, takeWhile } from 'rxjs/operators';\n\nimport {\n ICurrentTenant,\n IResultList,\n ITenant,\n TenantService,\n TenantStatus,\n UserService\n} from '@c8y/client';\nimport {\n ActionControl,\n AlertService,\n AppStateService,\n BuiltInActionType,\n Column,\n DisplayOptions,\n gettext,\n ModalService,\n Pagination,\n PasswordService,\n Permissions,\n SortOrder,\n Status,\n TenantUiService\n} from '@c8y/ngx-components';\n\nimport { CreationTimeFilteringFormRendererComponent } from './creation-time.filtering-form-renderer.component';\nimport { StatusFilteringFormRendererComponent } from './status.filtering-form-renderer.component';\n\n@Component({\n selector: 'c8y-tenant-list',\n templateUrl: './tenant-list.component.html'\n})\nexport class TenantListComponent implements OnInit {\n tenants$: BehaviorSubject<ITenant[]> = new BehaviorSubject(undefined);\n isPermittedToCreateTenanant = this.permissionsService.hasAnyRole([\n Permissions.ROLE_TENANT_MANAGEMENT_ADMIN,\n Permissions.ROLE_TENANT_MANAGEMENT_CREATE\n ]);\n currentTenant: ICurrentTenant;\n isManagementTenant: boolean;\n TOP_TENANT_NAME = 'management';\n\n title: string = null;\n loadMoreItemsLabel: string = gettext('Load more tenants');\n loadingItemsLabel: string = gettext('Loading tenants…');\n\n displayOptions: DisplayOptions = {\n bordered: false,\n striped: true,\n filter: true,\n gridHeader: true,\n hover: true\n };\n\n columns: Column[] = this.getColumns();\n pagination: Pagination = this.getPagination();\n showSearch = true;\n actionControls: ActionControl[] = this.getActionControls();\n\n noResultsMessage = gettext('No tenants to display.');\n noDataMessage = gettext('There are no tenants defined.');\n noResultsSubtitle = gettext('Refine your search terms or check your spelling.');\n noDataSubtitle = gettext('Create the first tenant.');\n\n TenantStatus = TenantStatus;\n\n constructor(\n public appState: AppStateService,\n private alertService: AlertService,\n private modalService: ModalService,\n private translateService: TranslateService,\n private tenantService: TenantService,\n private tenantUiService: TenantUiService,\n private location: Location,\n private passwordService: PasswordService,\n private userService: UserService,\n private permissionsService: Permissions\n ) {}\n\n async ngOnInit() {\n this.currentTenant = this.appState.currentTenant.value;\n this.isManagementTenant = await this.tenantUiService.isManagementTenant();\n this.loadTenants();\n }\n\n loadTenants() {\n this.tenants$.next(undefined);\n from(this.tenantService.list({ pageSize: 2000, withTotalPages: true, withApps: false }))\n .pipe(\n expand(resultList => resultList.paging.nextPage !== null && resultList.paging.next()),\n takeWhile(resultList => resultList.paging.nextPage !== null, true),\n reduce(\n (tenants: ITenant[], resultList: IResultList<ITenant>) => [\n ...tenants,\n ...resultList.data\n ],\n []\n ),\n shareReplay(1)\n )\n .subscribe(tenants => this.tenants$.next(tenants));\n }\n\n getColumns(): Column[] {\n return [\n {\n name: 'company',\n header: gettext('Tenant'),\n path: 'company',\n filterable: true,\n sortable: true,\n sortOrder: 'asc' as SortOrder\n },\n {\n name: 'id',\n header: gettext('ID'),\n path: 'id',\n filterable: true,\n sortable: true\n },\n {\n name: 'domain',\n header: gettext('Domain'),\n path: 'domain',\n filterable: true,\n sortable: true\n },\n {\n name: 'parent',\n header: gettext('Parent tenant'),\n path: 'parent',\n filterable: true,\n sortable: true\n },\n {\n name: 'contactName',\n header: gettext('Contact name'),\n path: 'contactName',\n filterable: true,\n sortable: true\n },\n {\n name: 'creationTime',\n header: gettext('Created'),\n path: 'creationTime',\n filterable: true,\n filteringFormRendererComponent: CreationTimeFilteringFormRendererComponent,\n sortable: true\n },\n {\n name: 'externalReference',\n header: gettext('External reference'),\n path: 'customProperties.externalReference',\n filterable: true,\n sortable: true\n },\n {\n name: 'status',\n header: gettext('Status'),\n path: 'status',\n filterable: true,\n filteringFormRendererComponent: StatusFilteringFormRendererComponent,\n sortable: true,\n resizable: false\n }\n ];\n }\n\n getPagination(): Pagination {\n return {\n pageSize: 10,\n currentPage: 1\n };\n }\n\n getActionControls(): ActionControl[] {\n return [\n {\n type: BuiltInActionType.Edit,\n text: gettext('Edit`tenant`'),\n callback: tenant => this.goToDetails(tenant)\n },\n {\n type: 'activateTenantAction',\n icon: 'power-off',\n text: gettext('Activate`tenant`'),\n callback: (tenant: ITenant) => this.activateTenant(tenant),\n showIf: (tenant: ITenant) => this.isSuspended(tenant)\n },\n {\n type: 'suspendTenantAction',\n icon: 'power-off',\n text: gettext('Suspend`tenant`'),\n callback: (tenant: ITenant) => this.suspendTenant(tenant),\n showIf: (tenant: ITenant) => this.isActive(tenant)\n },\n {\n type: BuiltInActionType.Delete,\n text: gettext('Delete`tenant`'),\n callback: tenant => this.delete(tenant),\n showIf: () => this.isManagementTenant\n }\n ];\n }\n\n createTenant() {\n this.location.go('/tenants/new');\n }\n\n goToDetails(tenant: ITenant) {\n this.location.go(`/tenants/${tenant.id}`);\n }\n\n async activateTenant(tenant) {\n try {\n const { data: savedTenant } = await this.tenantService.update({\n id: tenant.id,\n status: TenantStatus.ACTIVE\n });\n Object.assign(tenant, savedTenant);\n this.alertService.success(gettext('Tenant activated.'));\n } catch (ex) {\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n async suspendTenant(tenant) {\n const title = gettext('Suspend tenant');\n const confirmationText = gettext(\n 'You are about to suspend tenant \"{{ company }}\" (ID \"{{ id }}\").'\n );\n const proceed = gettext('Do you want to proceed?');\n const body = [\n this.translateService.instant(confirmationText, {\n company: tenant.company,\n id: tenant.id\n }),\n this.translateService.instant(proceed)\n ].join(' ');\n const labels = {\n ok: gettext('Suspend`tenant`')\n };\n try {\n await this.modalService.confirm(title, body, Status.DANGER, labels);\n const confirmed = await this.passwordService.confirmPassword().toPromise();\n if (confirmed === true) {\n const { data: savedTenant } = await this.tenantService.update({\n id: tenant.id,\n status: TenantStatus.SUSPENDED\n });\n Object.assign(tenant, savedTenant);\n this.alertService.success(gettext('Tenant suspended.'));\n }\n } catch (ex) {\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n async delete(tenant) {\n const title = gettext('Delete tenant');\n const confirmationText = gettext(\n 'You are about to delete tenant \"{{ company }}\" (ID \"{{ id }}\").'\n );\n const hint = gettext('This operation is irreversible.');\n const proceed = gettext('Do you want to proceed?');\n const body = [\n this.translateService.instant(confirmationText, {\n company: tenant.company,\n id: tenant.id\n }),\n this.translateService.instant(hint),\n this.translateService.instant(proceed)\n ].join(' ');\n const labels = {\n ok: gettext('Delete`tenant`')\n };\n try {\n await this.modalService.confirm(title, body, Status.DANGER, labels);\n const confirmed = await this.passwordService.confirmPassword().toPromise();\n if (confirmed === true) {\n await this.tenantService.delete(tenant);\n const tenantsWithoutRemovedOne = this.tenants$.value.filter(t => t !== tenant);\n this.tenants$.next(tenantsWithoutRemovedOne);\n this.alertService.success(\n gettext('Tenant is being deleted in the background. This might take a while…')\n );\n }\n } catch (ex) {\n if (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n }\n\n isActive(tenant: ITenant) {\n return tenant.status === TenantStatus.ACTIVE;\n }\n\n isSuspended(tenant: ITenant) {\n return tenant.status === TenantStatus.SUSPENDED;\n }\n\n async downloadNewsletterEmails() {\n const { res, data } = await this.userService.getNewsletterEmails();\n const contentType = res.headers.get('content-type');\n const contentDisposition = res.headers.get('content-disposition');\n const filename = /filename=\"(.*)\"/.exec(contentDisposition)[1];\n const blob = new Blob([data], { type: contentType });\n saveAs(blob, filename);\n }\n}\n","<c8y-title>\n {{ 'Subtenants' | translate }}\n</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n icon=\"c8y-layers\"\n label=\"{{ 'Tenants' | translate }}\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-layers'\"\n [label]=\"'Subtenants' | translate\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item\n *ngIf=\"!!(appState.state$ | async).newsletter\"\n [placement]=\"'right'\"\n>\n <button\n class=\"btn btn-link\"\n title=\"{{\n 'Downloads the list of emails of users subscribed for newsletter on the current tenant and its subtenants.'\n | translate\n }}\"\n type=\"button\"\n (click)=\"downloadNewsletterEmails()\"\n >\n <i c8yIcon=\"download\"></i>\n {{ 'Email addresses' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Create tenant' | translate }}\"\n type=\"button\"\n (click)=\"createTenant()\"\n [disabled]=\"!isPermittedToCreateTenanant\"\n >\n <i c8yIcon=\"plus-circle\"></i>\n {{ 'Create tenant' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<c8y-help src=\"/docs/enterprise-tenant/managing-tenants/#managing-subtenants\"></c8y-help>\n\n<div class=\"content-fullpage border-top border-bottom\">\n <c8y-data-grid\n [title]=\"title\"\n [loadMoreItemsLabel]=\"loadMoreItemsLabel\"\n [loadingItemsLabel]=\"loadingItemsLabel\"\n [displayOptions]=\"displayOptions\"\n [columns]=\"columns\"\n [rows]=\"tenants$ | async\"\n [pagination]=\"pagination\"\n [showSearch]=\"showSearch\"\n [actionControls]=\"actionControls\"\n (onReload)=\"loadTenants()\"\n >\n <ng-container *ngIf=\"!(tenants$ | async); else empty\">\n <c8y-loading></c8y-loading>\n </ng-container>\n <ng-template #empty>\n <c8y-ui-empty-state\n [icon]=\"stats?.size > 0 ? 'search' : 'c8y-layers'\"\n [title]=\"stats?.size > 0 ? (noResultsMessage | translate) : (noDataMessage | translate)\"\n [subtitle]=\"\n stats?.size > 0 ? (noResultsSubtitle | translate) : (noDataSubtitle | translate)\n \"\n *emptyStateContext=\"let stats\"\n [horizontal]=\"stats?.size > 0\"\n >\n <ng-container *ngIf=\"stats?.size === 0\">\n <div>\n <button\n class=\"btn btn-primary\"\n title=\"{{ 'Create tenant' | translate }}\"\n (click)=\"createTenant()\"\n [disabled]=\"!isPermittedToCreateTenanant\"\n >\n {{ 'Create tenant' | translate }}\n </button>\n </div>\n <p c8y-guide-docs>\n <small\n translate\n ngNonBindable\n >\n Find out more in the\n <a c8y-guide-href=\"/docs/enterprise-tenant/managing-tenants\">user documentation</a>\n .\n </small>\n </p>\n </ng-container>\n </c8y-ui-empty-state>\n </ng-template>\n\n <c8y-column name=\"company\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value }}\">\n <a [routerLink]=\"['/tenants', context.item.id]\">{{ context.value }}</a>\n </span>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"parent\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value || currentTenant.name }}\">\n {{ context.value || currentTenant.name }}\n </span>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"creationTime\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value | c8yDate }}\">\n {{ context.value | c8yDate }}\n </span>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"status\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span\n title=\"{{ 'Active`tenant`' | translate }}\"\n *ngIf=\"context.item.status === TenantStatus.ACTIVE\"\n >\n <i\n class=\"text-success\"\n c8yIcon=\"check-circle\"\n ></i>\n </span>\n <span\n title=\"{{ 'Suspended`tenant`' | translate }}\"\n *ngIf=\"context.item.status === TenantStatus.SUSPENDED\"\n >\n <i\n class=\"text-danger\"\n c8yIcon=\"ban\"\n ></i>\n </span>\n </ng-container>\n </c8y-column>\n </c8y-data-grid>\n</div>\n","import { Injectable } from '@angular/core';\nimport { InventoryService } from '@c8y/client';\nimport { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';\nimport { JSONSchema7, JSONSchema7TypeName } from 'json-schema';\nimport { ValidationPattern } from '@c8y/ngx-components';\n\ntype PropertiesType = { [key: string]: JSONSchema7 };\n\n// Model for individual tenant custom property field\nexport interface TenantCustomPropertyField {\n id: string;\n label: string;\n type: JSONSchema7TypeName | JSONSchema7TypeName[];\n format?: 'datetime' | string;\n formControlReference: AbstractControl;\n}\n\nconst defaultFilters = {\n query: \"(type eq 'c8y_JsonSchema') and (appliesTo.TENANTS eq true)\",\n pageSize: 1000,\n withTotalPages: true\n};\n\n@Injectable({\n providedIn: 'root'\n})\nexport class CustomPropertiesService {\n constructor(private inventoryService: InventoryService) {}\n\n async getFormAndFieldList(): Promise<{ form: FormGroup; fields: TenantCustomPropertyField[] }> {\n const schema = await this.getCustomProperties();\n const formGroup = this.buildFormGroup(schema);\n const fields = this.buildFieldList(formGroup, schema);\n return { form: formGroup, fields };\n }\n\n private async getCustomProperties(): Promise<PropertiesType> {\n let customFieldsSchema: PropertiesType = {};\n const customProperties = await this.inventoryService.list(defaultFilters);\n\n customProperties.data.forEach(item => {\n const fieldSchema: PropertiesType = item['c8y_JsonSchema'].properties;\n customFieldsSchema = { ...customFieldsSchema, ...(fieldSchema as object) };\n });\n return customFieldsSchema;\n }\n\n private buildFormGroup(schema: PropertiesType): FormGroup {\n const fg = new FormGroup({});\n\n for (const [key, value] of Object.entries(schema)) {\n const control = new FormControl(value.default, []);\n this.applyValidators(control, value);\n fg.addControl(key, control);\n }\n return fg;\n }\n\n private applyValidators(control: FormControl, props: JSONSchema7): void {\n const validatorsMap = {\n required: Validators.required,\n minimum: Validators.min(props.minimum),\n maximum: Validators.max(props.maximum),\n minLength: Validators.minLength(props.minLength),\n maxLength: Validators.maxLength(props.maxLength),\n pattern: Validators.pattern(props.pattern)\n };\n\n Object.entries(validatorsMap).forEach(([key, validator]) => {\n if (props[key as keyof JSONSchema7] !== undefined) {\n control.addValidators(validator);\n }\n });\n\n if (props.type === 'integer') {\n control.addValidators(Validators.pattern(ValidationPattern.rules.integer.pattern));\n }\n }\n\n private buildFieldList(form: FormGroup, schema: PropertiesType): TenantCustomPropertyField[] {\n const fieldList: TenantCustomPropertyField[] = [];\n Object.entries(schema).forEach(([key, value]) => {\n fieldList.push({\n id: key,\n label: value.title,\n type: value.type,\n format: value.format,\n formControlReference: form.get(key) as FormControl\n });\n });\n\n return fieldList;\n }\n}\n","import { Component, Input } from '@angular/core';\nimport { TenantCustomPropertyField } from '../custom-properties.service';\nimport { CommonModule } from '@angular/common';\nimport { C8yTranslatePipe, DateTimePickerModule, FormGroupComponent } from '@c8y/ngx-components';\nimport { FormsModule } from '@c8y/ngx-components';\nimport { FormGroup, ReactiveFormsModule } from '@angular/forms';\n\n@Component({\n selector: 'c8y-custom-property-field',\n standalone: true,\n imports: [\n CommonModule,\n C8yTranslatePipe,\n FormGroupComponent,\n FormsModule,\n ReactiveFormsModule,\n DateTimePickerModule\n ],\n templateUrl: './custom-property-field.component.html'\n})\nexport class CustomPropertyFieldComponent {\n @Input()\n fieldDefinition: TenantCustomPropertyField;\n\n @Input()\n form: FormGroup;\n}\n","<ng-container *ngIf=\"form && fieldDefinition\">\n <ng-container [ngSwitch]=\"fieldDefinition.type\">\n <ng-container *ngSwitchCase=\"'boolean'\">\n <c8y-form-group [formGroup]=\"form\">\n <label\n class=\"c8y-checkbox\"\n [title]=\"fieldDefinition.label | translate\"\n [for]=\"fieldDefinition.id\"\n >\n <input\n type=\"checkbox\"\n [id]=\"fieldDefinition.id\"\n [formControlName]=\"fieldDefinition.id\"\n />\n <span></span>\n <span>{{ fieldDefinition.label | translate }}</span>\n </label>\n </c8y-form-group>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'number'\">\n <c8y-form-group [formGroup]=\"form\">\n <label [for]=\"fieldDefinition.id\">\n {{ fieldDefinition.label | translate }}\n </label>\n <input\n class=\"form-control\"\n type=\"number\"\n [id]=\"fieldDefinition.id\"\n [formControlName]=\"fieldDefinition.id\"\n />\n </c8y-form-group>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'integer'\">\n <c8y-form-group [formGroup]=\"form\">\n <label [for]=\"fieldDefinition.id\">\n {{ fieldDefinition.label | translate }}\n </label>\n <input\n class=\"form-control\"\n type=\"number\"\n [id]=\"fieldDefinition.id\"\n [formControlName]=\"fieldDefinition.id\"\n />\n </c8y-form-group>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'string'\">\n <c8y-form-group\n *ngIf=\"!fieldDefinition.format\"\n [formGroup]=\"form\"\n >\n <label [for]=\"fieldDefinition.id\">\n {{ fieldDefinition.label | translate }}\n </label>\n <input\n class=\"form-control\"\n type=\"text\"\n [id]=\"fieldDefinition.id\"\n [formControlName]=\"fieldDefinition.id\"\n />\n </c8y-form-group>\n\n <c8y-form-group\n *ngIf=\"fieldDefinition.format === 'datetime'\"\n [formGroup]=\"form\"\n >\n <label [for]=\"fieldDefinition.id\">\n {{ fieldDefinition.label | translate }}\n </label>\n <c8y-date-time-picker\n [id]=\"fieldDefinition.id\"\n [formControlName]=\"fieldDefinition.id\"\n data-cy=\"c8y-custom-property-field--date-time-picker\"\n ></c8y-date-time-picker>\n </c8y-form-group>\n </ng-container>\n </ng-container>\n</ng-container>\n","import { Component, OnInit } from '@angular/core';\nimport { FormGroup, ReactiveFormsModule } from '@angular/forms';\nimport { CommonModule } from '@angular/common';\nimport { IResult, ITenant, TenantService } from '@c8y/client';\nimport { ActivatedRoute, RouterLink } from '@angular/router';\nimport { AlertService, CoreModule, FormsModule, gettext } from '@c8y/ngx-components';\nimport {\n CustomPropertiesService,\n TenantCustomPropertyField\n} from '../custom-properties/custom-properties.service';\nimport { CustomPropertyFieldComponent } from '../custom-properties/custom-property-field/custom-property-field.component';\n\n@Component({\n selector: 'c8y-custom-properties',\n standalone: true,\n imports: [\n CommonModule,\n ReactiveFormsModule,\n FormsModule,\n CoreModule,\n RouterLink,\n CustomPropertyFieldComponent\n ],\n templateUrl: './custom-properties.component.html'\n})\nexport class CustomPropertiesComponent implements OnInit {\n fieldDefinitions: TenantCustomPropertyField[];\n customPropsForm: FormGroup = new FormGroup({});\n tenant: ITenant | null = null;\n initialized = false;\n\n constructor(\n private tenantService: TenantService,\n private alertService: AlertService,\n private activatedRoute: ActivatedRoute,\n private customPropertiesService: CustomPropertiesService\n ) {}\n\n async ngOnInit() {\n await this.loadTenantDetails();\n const { form, fields } = await this.customPropertiesService.getFormAndFieldList();\n this.customPropsForm = form;\n this.fieldDefinitions = fields;\n this.applyValuesFromTenant();\n this.initialized = true;\n }\n\n async onSubmit(): Promise<void> {\n if (this.customPropsForm.invalid || !this.tenant) {\n return;\n }\n\n const updatedTenant: ITenant = {\n ...this.tenant,\n customProperties: {\n ...this.tenant.customProperties,\n ...this.getDirtyValues()\n }\n };\n\n try {\n await this.tenantService.update(updatedTenant);\n this.alertService.success(gettext('Custom properties values saved.'));\n } catch (error) {\n this.alertService.addServerFailure(error);\n }\n }\n\n private async loadTenantDetails() {\n try {\n const result: IResult<ITenant> = await this.tenantService.detail(\n this.activatedRoute.snapshot.parent.data.contextData.id\n );\n this.tenant = result.data;\n } catch (error) {\n this.alertService.addServerFailure(error);\n }\n }\n\n private applyValuesFromTenant(): void {\n const customProps = this.tenant?.customProperties || {};\n this.customPropsForm.patchValue(customProps);\n }\n\n private getDirtyValues(): { [key: string]: unknown } {\n const dirtyValues: { [key: string]: unknown } = {};\n Object.keys(this.customPropsForm.controls).forEach(key => {\n const control = this.customPropsForm.controls[key];\n if (control && control.dirty && control.value !== null) {\n dirtyValues[key] = control.value;\n }\n });\n\n return dirtyValues;\n }\n}\n","<c8y-title *ngIf=\"tenant\">\n {{ tenant.company }}\n</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-layers'\"\n [label]=\"'Tenants' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-layers'\"\n [label]=\"'Subtenants' | translate\"\n [path]=\"'/tenants'\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<ng-container>\n <form\n [formGroup]=\"customPropsForm\"\n (ngSubmit)=\"onSubmit()\"\n >\n <div class=\"card card--fullpage m-b-0\">\n <div class=\"card-header separator\">\n <div\n class=\"card-title\"\n translate\n >\n Custom properties\n </div>\n </div>\n\n <div class=\"inner-scroll\">\n <div\n class=\"card-block\"\n *ngIf=\"!initialized\"\n >\n <c8y-loading></c8y-loading>\n </div>\n\n <c8y-help src=\"/docs/enterprise-tenant/managing-tenants/#custom-properties\"></c8y-help>\n\n <!-- empty state -->\n <c8y-ui-empty-state\n [icon]=\"'property-script'\"\n [title]=\"'No custom properties to display.' | translate\"\n [subtitle]=\"'Add a new tenant property in Properties library.' | translate\"\n *ngIf=\"fieldDefinitions?.length === 0 && initialized\"\n >\n <p c8y-guide-docs>\n <small translate>\n Find out more in the\n <a c8y-guide-href=\"/docs/enterprise-tenant/managing-tenants/#custom-properties\">\n User guide\n </a>\n .\n </small>\n </p>\n </c8y-ui-empty-state>\n\n <div\n class=\"card-block\"\n *ngIf=\"initialized\"\n >\n <ng-container *ngFor=\"let field of fieldDefinitions\">\n <c8y-custom-property-field\n [fieldDefinition]=\"field\"\n [form]=\"customPropsForm\"\n ></c8y-custom-property-field>\n </ng-container>\n </div>\n </div>\n\n <div\n class=\"card-footer separator\"\n *ngIf=\"initialized\"\n >\n <button\n class=\"btn btn-default\"\n type=\"button\"\n [routerLink]=\"['/tenants']\"\n translate\n data-cy=\"custom-properties--cancel-button\"\n >\n Cancel\n </button>\n <button\n class=\"btn btn-primary\"\n type=\"submit\"\n [disabled]=\"!(!customPropsForm.invalid && customPropsForm.dirty)\"\n translate\n data-cy=\"custom-properties--save-button\"\n >\n Save\n </button>\n </div>\n </div>\n </form>\n</ng-container>\n","import { ValidatorFn, Validators } from '@angular/forms';\nimport { gettext } from '@c8y/ngx-components/gettext';\n\n// Model for individual tenant limit field\nexport interface TenantLimit {\n id: string;\n validators: ValidatorFn[];\n defaultValue: number | string | boolean | null;\n type: 'text' | 'checkbox' | 'number';\n label: string;\n labelArgs?: object;\n placeholder?: string;\n placeholderArgs?: object;\n}\n\n/**\n * Define all hardTyped custom properties, and their configuration in one place.\n *\n * This exported const utilizes typescript inferring,\n * while still benefiting from hard typing, thanks to \"satisfies\" keyword\n */\nconst tenantLimitsCustomPropertiesDefinition = {\n //TODO: External reference - going to be moved to the Properties tab.\n externalReference: {\n id: 'externalReference',\n validators: [],\n defaultValue: null,\n type: 'text',\n label: gettext('External reference'),\n placeholder: gettext('e.g. REF12345`reference number`')\n },\n limitDevicesNumber: {\n id: 'limit.devices.number',\n validators: [Validators.min(0)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit number of devices'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 1000 }\n },\n limitHttpRequests: {\n id: 'limit.http.requests',\n validators: [Validators.min(-1)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit HTTP requests'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 10000 }\n },\n limitHttpQueue: {\n id: 'limit.http.queue',\n validators: [Validators.min(-1)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit HTTP queue'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 100 }\n },\n limitStreamRequests: {\n id: 'limit.stream.requests',\n validators: [Validators.min(-1)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit stream requests'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 100 }\n },\n limitStreamQueue: {\n id: 'limit.stream.queue',\n validators: [Validators.min(-1)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit stream queue'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 100 }\n },\n cepServerQueueLimit: {\n id: 'cepServer.queue.limit',\n validators: [Validators.min(-1)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit CEP server queue'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 100 }\n },\n dataBrokerQueueLimit: {\n id: 'data-broker.queue.limit',\n validators: [Validators.min(0)],\n defaultValue: null,\n type: 'number',\n label: gettext('Limit data broker queue'),\n placeholder: gettext('e.g. {{ example }}'),\n placeholderArgs: { example: 100 }\n },\n // TODO: Gainsight checkbox - going to be moved to the Properties tab.\n gainsightEnabled: {\n id: 'gainsightEnabled',\n validators: [],\n defaultValue: false,\n type: 'checkbox',\n label: gettext('Enable Gainsight product experience tracking')\n }\n} satisfies Record<string, TenantLimit>;\n\ntype Keys = keyof typeof tenantLimitsCustomPropertiesDefinition;\n\nexport const tenantLimitsCustomProperties: { [K in Keys]: TenantLimit } =\n tenantLimitsCustomPropertiesDefinition;\n","import { Component, OnInit } from '@angular/core';\nimport { FormGroup, ReactiveFormsModule, FormControl } from '@angular/forms';\nimport { CommonModule } from '@angular/common';\nimport {\n ApplicationService,\n IResult,\n ITenant,\n TenantOptionsService,\n TenantService\n} from '@c8y/client';\nimport { ActivatedRoute, RouterLink } from '@angular/router';\nimport { AlertService, CoreModule, FormsModule, gettext } from '@c8y/ngx-components';\nimport { tenantLimitsCustomProperties } from './tenant-limits-definitions';\nimport { get } from 'lodash-es';\n\n@Component({\n selector: 'c8y-tenant-limits',\n standalone: true,\n imports: [CommonModule, ReactiveFormsModule, FormsModule, CoreModule, RouterLink],\n templateUrl: './tenant-limits.component.html'\n})\nexport class TenantLimitsComponent implements OnInit {\n fieldDefinitions = { ...tenantLimitsCustomProperties };\n fieldKeys: string[];\n limitsForm: FormGroup = new FormGroup({});\n tenant: ITenant | null = null;\n initialized = false;\n\n constructor(\n private tenantService: TenantService,\n private tenantOptionsService: TenantOptionsService,\n private alertService: AlertService,\n private activatedRoute: ActivatedRoute,\n private applicationService: ApplicationService\n ) {}\n\n async ngOnInit() {\n await this.loadTenantDetails();\n await this.setupConditionalFields();\n this.generateForm();\n this.initialized = true;\n }\n\n async onSubmit() {\n if (this.limitsForm.invalid || !this.tenant) {\n return;\n }\n\n const updatedTenant: ITenant = {\n ...this.tenant,\n customProperties: {\n ...this.tenant.customProperties,\n ...this.getDirtyValues()\n }\n };\n\n try {\n await this.tenantService.update(updatedTenant);\n this.alertService.success(gettext('Limit values saved.'));\n } catch (error) {\n this.alertService.addServerFailure(error);\n }\n }\n\n private async loadTenantDetails() {\n try {\n const result: IResult<ITenant> = await this.tenantService.detail(\n this.activatedRoute.snapshot.parent.data.contextData.id\n );\n this.tenant = result.data;\n } catch (error) {\n this.alertService.addServerFailure(error);\n }\n }\n\n private async setupConditionalFields() {\n try {\n const apps = (\n await this.applicationService.listByUser(undefined, {\n dropOverwrittenApps: true,\n noPaging: true\n })\n ).data;\n\n const cepModuleEnabled = apps.some(app => app.name === 'cep' || app.contextPath === 'cep');\n const dataBrokerModuleEnabled = apps.some(\n app => app.name === 'feature-broker' || app.contextPath === 'feature-broker'\n );\n const gainsightAvailable = await this.isGainsightAvailable();\n\n if (!cepModuleEnabled) {\n delete this.fieldDefinitions.cepServerQueueLimit;\n }\n if (!dataBrokerModuleEnabled) {\n delete this.fieldDefinitions.dataBrokerQueueLimit;\n }\n if (!gainsightAvailable) {\n delete this.fieldDefinitions.gainsightEnabled;\n }\n this.fieldKeys = Object.keys(this.fieldDefinitions);\n } catch (ex) {\n this.alertService.addServerFailure(ex);\n }\n }\n\n private async isGainsightAvailable() {\n if (get(window, 'C8Y_APP.gainsightKey')) {\n return true;\n }\n\n try {\n const res = await this.tenantOptionsService.detail({\n category: 'configuration',\n key: 'system.gainsight.api.key'\n });\n return !!res.data.value;\n } catch (error) {\n return false;\n }\n }\n\n private generateForm() {\n for (const field of Object.values(tenantLimitsCustomProperties)) {\n this.limitsForm.addControl(field.id, new FormControl(field.defaultValue, field.validators));\n }\n const customProps = this.tenant?.customProperties || {};\n this.limitsForm.patchValue(customProps);\n }\n\n private getDirtyValues(): any {\n const dirtyValues: { [key: string]: any } = {};\n Object.keys(this.limitsForm.controls).forEach(key => {\n const control = this.limitsForm.controls[key];\n if (control && control.dirty) {\n dirtyValues[key] = control.value;\n }\n });\n return dirtyValues;\n }\n}\n","<c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-layers'\"\n [label]=\"'Tenants' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [icon]=\"'c8y-layers'\"\n [label]=\"'Subtenants' | translate\"\n [path]=\"'/tenants'\"\n ></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<form\n [formGroup]=\"limitsForm\"\n (ngSubmit)=\"onSubmit()\"\n>\n <div class=\"card card--fullpage m-b-0\">\n <div class=\"card-header separator\">\n <div\n class=\"card-title\"\n translate\n >\n Limits\n </div>\n </div>\n\n <c8y-help src=\"/docs/enterprise-tenant/managing-tenants/#setting-limits\"></c8y-help>\n\n <div class=\"inner-scroll\">\n <div\n class=\"card-block\"\n *ngIf=\"!initialized\"\n >\n <c8y-loading></c8y-loading>\n </div>\n\n <div\n class=\"card-block\"\n *ngIf=\"initialized\"\n >\n <ng-container *ngFor=\"let key of fieldKeys\">\n <ng-container *ngIf=\"fieldDefinitions[key].type === 'text'\">\n <ng-container\n *ngTemplateOutlet=\"textField; context: { $implicit: fieldDefinitions[key] }\"\n ></ng-container>\n </ng-container>\n <ng-container *ngIf=\"fieldDefinitions[key].type === 'number'\">\n <ng-container\n *ngTemplateOutlet=\"numberField; context: { $implicit: fieldDefinitions[key] }\"\n ></ng-container>\n </ng-container>\n <ng-container *ngIf=\"fieldDefinitions[key].type === 'checkbox'\">\n <ng-container\n *ngTemplateOutlet=\"checkboxField; context: { $implicit: fieldDefinitions[key] }\"\n ></ng-container>\n </ng-container>\n </ng-container>\n </div>\n </div>\n\n <div\n class=\"card-footer separator\"\n *ngIf=\"initialized\"\n >\n <button\n class=\"btn btn-default\"\n type=\"button\"\n [routerLink]=\"['/tenants']\"\n translate\n >\n Cancel\n </button>\n <button\n class=\"btn btn-primary\"\n type=\"submit\"\n [disabled]=\"limitsForm.invalid\"\n translate\n >\n Save\n </button>\n </div>\n </div>\n\n <ng-template\n #textField\n let-fieldDefinition\n >\n <c8y-form-group>\n <label [for]=\"fieldDefinition.id\">\n {{ fieldDefinition.label | translate: fieldDefinition.labelArgs }}\n </label>\n <input\n class=\"form-control\"\n type=\"text\"\n [id]=\"fieldDefinition.id\"\n [placeholder]=\"fieldDefinition.placeholder | translate: fieldDefinition.placeholderArgs\"\n [formControlName]=\"fieldDefinition.id\"\n />\n </c8y-form-group>\n </ng-template>\n\n <ng-template\n #numberField\n let-fieldDefinition\n >\n <c8y-form-group>\n <label [for]=\"fieldDefinition.id\">\n {{ fieldDefinition.label | translate: fieldDefinition.labelArgs }}\n </label>\n <input\n class=\"form-control\"\n type=\"number\"\n [id]=\"fieldDefinition.id\"\n [placeholder]=\"fieldDefinition.placeholder | translate: fieldDefinition.placeholderArgs\"\n [formControlName]=\"fieldDefinition.id\"\n />\n </c8y-form-group>\n </ng-template>\n\n <ng-template\n #checkboxField\n let-fieldDefinition\n >\n <c8y-form-group>\n <label\n class=\"c8y-checkbox\"\n [title]=\"fieldDefinition.label | translate\"\n [for]=\"fieldDefinition.id\"\n >\n <input\n type=\"checkbox\"\n [id]=\"fieldDefinition.id\"\n [formControlName]=\"fieldDefinition.id\"\n />\n <span></span>\n <span>{{ fieldDefinition.label | translate }}</span>\n </label>\n </c8y-form-group>\n </ng-template>\n</form>\n","import { ModuleWithProviders, NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\nimport { BsDatepickerModule } from 'ngx-bootstrap/datepicker';\n\nimport { CoreModule, gettext, hookNavigator, hookRoute, ViewContext } from '@c8y/ngx-components';\nimport { TenantsNavigationFactory } from './tenants-navigation.factory';\nimport { TenantListComponent } from './tenant-list/tenant-list.component';\nimport { CreationTimeFilteringFormRendererComponent } from './tenant-list/creation-time.filtering-form-renderer.component';\nimport { StatusFilteringFormRendererComponent } from './tenant-list/status.filtering-form-renderer.component';\nimport { TenantsModuleConfig, TENANTS_MODULE_CONFIG } from './tenants.model';\nimport { TenantListGuard } from './tenant-list/tenant-list.guard';\nimport { CustomPr