UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

654 lines (646 loc) 138 kB
import * as i0 from '@angular/core'; import { Injectable, InjectionToken, Optional, Inject, Component, Input, ViewChild, NgModule } from '@angular/core'; import * as i3 from '@angular/router'; import { RouterLink, RouterModule } from '@angular/router'; import { BsDatepickerInputDirective, BsDatepickerDirective, BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1 from '@c8y/ngx-components'; import { NavigatorNode, FormGroupComponent, C8yTranslatePipe, DatePipe, Permissions, BuiltInActionType, Status, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, ActionBarItemComponent, IconDirective, HelpComponent, DataGridComponent, LoadingComponent, EmptyStateContextDirective, EmptyStateComponent, GuideDocsComponent, C8yTranslateDirective, GuideHrefDirective, ColumnDirective, CellRendererDefDirective, ValidationPattern, validateInternationalPhoneNumber, CommonModule, RequiredInputPlaceholderDirective, NewPasswordComponent, FormsModule as FormsModule$1, DateTimePickerModule, CoreModule, hookNavigator, hookRoute, ViewContext } from '@c8y/ngx-components'; import * as i4 from '@angular/common'; import { NgIf, AsyncPipe, NgFor, NgTemplateOutlet, CommonModule as CommonModule$1 } from '@angular/common'; import * as i2 from '@ngx-translate/core'; import { saveAs } from 'file-saver'; import { BehaviorSubject, from } from 'rxjs'; import { expand, takeWhile, reduce, shareReplay, take } from 'rxjs/operators'; import * as i1$1 from '@c8y/client'; import { TenantStatus } from '@c8y/client'; import * as i2$1 from '@angular/forms'; import { FormsModule, Validators, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { find, cloneDeep, assign } from 'lodash-es'; import * as i1$2 from '@c8y/ngx-components/upgrade/upgraded-services'; class TenantListGuard { constructor(tenantUiService) { this.tenantUiService = tenantUiService; } /** * Checks if tenant list should be active, * i.e. whether the current tenant can read other tenants. * **Note: the check is executed only once in the runtime.** * * @returns True, if the feature should be active. */ canActivate() { if (this.active === undefined) { this.active = this.tenantUiService.canReadTenants(); } return this.active; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantListGuard, deps: [{ token: i1.TenantUiService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantListGuard }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantListGuard, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.TenantUiService }] }); const TENANTS_MODULE_CONFIG = new InjectionToken('TenantsModuleConfig'); const PRODUCT_EXPERIENCE_TENANT_MANAGEMENT = { EVENTS: { TENANT_MANAGEMENT: 'tenantManagement' }, COMPONENTS: { TENANT_LIST: 'tenant-list', TENANT_FORM: 'tenant-form', TENANT_CUSTOM_PROPERTIES: 'tenant-custom-properties', TENANT_LIMITS: 'tenant-limits' }, ACTIONS: { TENANT_CREATION_INITIALIZED: 'tenantCreationInitialized', TENANT_CREATION_STARTED_FILLING: 'tenantCreationStartedFilling', TENANT_CREATION_SAVED: 'tenantCreationSaved', TENANT_PROPERTIES_OPENED: 'tenantPropertiesOpened', TENANT_PROPERTIES_STARTED_CHANGING: 'tenantPropertiesStartedChanging', TENANT_PROPERTIES_SAVED: 'tenantPropertiesSaved', TENANT_CUSTOM_PROPERTIES_OPENED: 'tenantCustomPropertiesOpened', TENANT_CUSTOM_PROPERTIES_STARTED_CHANGING: 'tenantCustomPropertiesStartedChanging', TENANT_CUSTOM_PROPERTIES_SAVED: 'tenantCustomPropertiesSaved', TENANT_LIMITS_OPENED: 'tenantLimitsOpened', TENANT_LIMITS_STARTED_CHANGING: 'tenantLimitsStartedChanging', TENANT_LIMITS_SAVED: 'tenantLimitsSaved' }, RESULTS: { SUCCESS: 'success', FAILURE: 'failure' } }; class TenantsNavigationFactory { constructor(tenantListGuard, config) { this.tenantListGuard = tenantListGuard; this.config = config; this.navs = []; } async get() { const canActivateTenantList = await this.tenantListGuard.canActivate(); if (!this.navs.length) { const subtenantsNavigatorNode = this.config?.subtenantsNavigatorNode ?? true; if (subtenantsNavigatorNode !== false) { this.navs.push(new NavigatorNode({ parent: { label: gettext('Tenants'), icon: 'c8y-layers' }, label: gettext('Subtenants'), icon: 'c8y-sub-tenants', path: 'tenants', routerLinkExact: false, priority: 4000, hidden: !canActivateTenantList, ...(subtenantsNavigatorNode === true ? {} : subtenantsNavigatorNode) })); } } return this.navs; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantsNavigationFactory, deps: [{ token: TenantListGuard }, { token: TENANTS_MODULE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantsNavigationFactory }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantsNavigationFactory, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: TenantListGuard }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [TENANTS_MODULE_CONFIG] }] }] }); class CreationTimeFilteringFormRendererComponent { constructor(context, c8yDate, translateService) { this.context = context; this.c8yDate = c8yDate; this.translateService = translateService; this.model = (this.context.property.externalFilterQuery || {}).model || {}; } applyFilter() { this.context.applyFilter({ externalFilterQuery: { model: this.model, chips: this.getChipsForModel(this.model) }, filterPredicate: (tenant) => { const creationTime = new Date(tenant.creationTime); let dateFrom; let dateTo; if (this.model.dateFrom) { dateFrom = this.model.dateFrom; dateFrom.setHours(0, 0, 0, 0); } if (this.model.dateTo) { dateTo = this.model.dateTo; dateTo.setHours(23, 59, 59, 999); } return Boolean((!dateFrom && !dateTo) || (dateFrom && !dateTo && dateFrom <= creationTime) || (!dateFrom && dateTo && creationTime <= dateTo) || (dateFrom && dateTo && dateFrom <= creationTime && creationTime <= dateTo)); } }); } getChipsForModel(model) { const updateChips = externalFilterQuery => { externalFilterQuery.chips = this.getChipsForModel(externalFilterQuery.model); }; const createChip = (key, displayValueTpl) => { return { columnName: this.context.property.name, path: ['model', key], displayValue: this.translateService.instant(displayValueTpl, { date: this.c8yDate.transform(this.model[key], 'mediumDate') }), value: this.model[key], remove() { delete this.externalFilterQuery.model[key]; updateChips(this.externalFilterQuery); return { columnName: this.columnName, externalFilterQuery: this.externalFilterQuery }; } }; }; const chips = []; if (model.dateFrom) { chips.push(createChip('dateFrom', gettext('from: {{ date }}'))); } if (model.dateTo) { chips.push(createChip('dateTo', gettext('to: {{ date }}'))); } return chips; } resetFilter() { this.context.resetFilter(); } // this method is only used for workaround for ngx-bootstrap issue (preventing the datepicker from closing dropdown when clicking inside the datepicker container) // TODO: remove this method when the issue is resolved https://github.com/valor-software/ngx-bootstrap/issues/6736 datepickerShown({ _element }) { const datepickerContainerElement = _element.nativeElement; datepickerContainerElement.addEventListener('click', (e) => { e.stopPropagation(); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CreationTimeFilteringFormRendererComponent, deps: [{ token: i1.FilteringFormRendererContext }, { token: i1.DatePipe }, { token: i2.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: CreationTimeFilteringFormRendererComponent, isStandalone: true, selector: "c8y-creation-time-filtering-form-renderer", providers: [DatePipe], ngImport: i0, template: "<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 (onShown)=\"datepickerShown($event)\"\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 (onShown)=\"datepickerShown($event)\"\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", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: BsDatepickerInputDirective, selector: "input[bsDatepicker]" }, { kind: "directive", type: BsDatepickerDirective, selector: "[bsDatepicker]", inputs: ["placement", "triggers", "outsideClick", "container", "outsideEsc", "isDisabled", "minDate", "maxDate", "ignoreMinMaxErrors", "minMode", "daysDisabled", "datesDisabled", "datesEnabled", "dateCustomClasses", "dateTooltipTexts", "isOpen", "bsValue", "bsConfig"], outputs: ["onShown", "onHidden", "bsValueChange"], exportAs: ["bsDatepicker"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: CreationTimeFilteringFormRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-creation-time-filtering-form-renderer', imports: [ FormsModule, FormGroupComponent, BsDatepickerInputDirective, BsDatepickerDirective, C8yTranslatePipe ], providers: [DatePipe], template: "<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 (onShown)=\"datepickerShown($event)\"\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 (onShown)=\"datepickerShown($event)\"\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" }] }], ctorParameters: () => [{ type: i1.FilteringFormRendererContext }, { type: i1.DatePipe }, { type: i2.TranslateService }] }); class StatusFilteringFormRendererComponent { constructor(context) { this.context = context; this.model = (this.context.property.externalFilterQuery || {}).model || {}; } applyFilter() { this.context.applyFilter({ externalFilterQuery: { model: this.model }, filterPredicate: (tenant) => Boolean((!this.model.active && !this.model.suspended) || (this.model.active && tenant.status === TenantStatus.ACTIVE) || (this.model.suspended && tenant.status === TenantStatus.SUSPENDED)) }); } resetFilter() { this.context.resetFilter(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: StatusFilteringFormRendererComponent, deps: [{ token: i1.FilteringFormRendererContext }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: StatusFilteringFormRendererComponent, isStandalone: true, selector: "c8y-status-filtering-form-renderer", ngImport: i0, template: "<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", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: StatusFilteringFormRendererComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-status-filtering-form-renderer', imports: [FormsModule, FormGroupComponent, C8yTranslatePipe], template: "<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" }] }], ctorParameters: () => [{ type: i1.FilteringFormRendererContext }] }); const { EVENTS: EVENTS$3, COMPONENTS: COMPONENTS$3, ACTIONS: ACTIONS$3 } = PRODUCT_EXPERIENCE_TENANT_MANAGEMENT; class TenantListComponent { constructor(appState, alertService, modalService, translateService, tenantService, tenantUiService, location, passwordService, userService, permissionsService, gainsightService) { this.appState = appState; this.alertService = alertService; this.modalService = modalService; this.translateService = translateService; this.tenantService = tenantService; this.tenantUiService = tenantUiService; this.location = location; this.passwordService = passwordService; this.userService = userService; this.permissionsService = permissionsService; this.gainsightService = gainsightService; this.tenants$ = new BehaviorSubject(undefined); this.isPermittedToCreateTenanant = this.permissionsService.hasAnyRole([ Permissions.ROLE_TENANT_MANAGEMENT_ADMIN, Permissions.ROLE_TENANT_MANAGEMENT_CREATE ]); this.TOP_TENANT_NAME = 'management'; this.title = null; this.loadMoreItemsLabel = gettext('Load more tenants'); this.loadingItemsLabel = gettext('Loading tenants…'); this.displayOptions = { bordered: false, striped: true, filter: true, gridHeader: true, hover: true }; this.columns = this.getColumns(); this.pagination = this.getPagination(); this.showSearch = true; this.actionControls = this.getActionControls(); this.noResultsMessage = gettext('No tenants to display.'); this.noDataMessage = gettext('There are no tenants defined.'); this.noResultsSubtitle = gettext('Refine your search terms or check your spelling.'); this.noDataSubtitle = gettext('Create the first tenant.'); this.TenantStatus = TenantStatus; } async ngOnInit() { this.currentTenant = this.appState.currentTenant.value; this.isManagementTenant = await this.tenantUiService.isManagementTenant(); this.loadTenants(); } loadTenants() { this.tenants$.next(undefined); from(this.tenantService.list({ pageSize: 2000, withTotalPages: true, withApps: false })) .pipe(expand(resultList => resultList.paging.nextPage !== null && resultList.paging.next()), takeWhile(resultList => resultList.paging.nextPage !== null, true), reduce((tenants, resultList) => [ ...tenants, ...resultList.data ], []), shareReplay(1)) .subscribe(tenants => this.tenants$.next(tenants)); } getColumns() { return [ { name: 'company', header: gettext('Tenant'), path: 'company', filterable: true, sortable: true, sortOrder: 'asc' }, { name: 'id', header: gettext('ID'), path: 'id', filterable: true, sortable: true }, { name: 'domain', header: gettext('Domain'), path: 'domain', filterable: true, sortable: true }, { name: 'parent', header: gettext('Parent tenant'), path: 'parent', filterable: true, sortable: true }, { name: 'contactName', header: gettext('Contact name'), path: 'contactName', filterable: true, sortable: true }, { name: 'creationTime', header: gettext('Created'), path: 'creationTime', filterable: true, filteringFormRendererComponent: CreationTimeFilteringFormRendererComponent, sortable: true }, { name: 'externalReference', header: gettext('External reference'), path: 'customProperties.externalReference', filterable: true, sortable: true }, { name: 'status', header: gettext('Status'), path: 'status', filterable: true, filteringFormRendererComponent: StatusFilteringFormRendererComponent, sortable: true, resizable: false } ]; } getPagination() { return { pageSize: 10, currentPage: 1 }; } getActionControls() { return [ { type: BuiltInActionType.Edit, text: gettext('Edit`tenant`'), callback: tenant => this.goToDetails(tenant) }, { type: 'activateTenantAction', icon: 'power-off', text: gettext('Activate`tenant`'), callback: async (tenant) => { await this.activateTenant(tenant); this.loadTenants(); }, showIf: (tenant) => this.isSuspended(tenant) }, { type: 'suspendTenantAction', icon: 'power-off', text: gettext('Suspend`tenant`'), callback: async (tenant) => { await this.suspendTenant(tenant); this.loadTenants(); }, showIf: (tenant) => this.isActive(tenant) }, { type: BuiltInActionType.Delete, text: gettext('Delete`tenant`'), callback: tenant => this.delete(tenant), showIf: () => this.isManagementTenant } ]; } createTenant(options = { sendGainsightEvent: true }) { if (options.sendGainsightEvent) { this.gainsightService.triggerEvent(EVENTS$3.TENANT_MANAGEMENT, { component: COMPONENTS$3.TENANT_LIST, action: ACTIONS$3.TENANT_CREATION_INITIALIZED }); } this.location.go('/tenants/new'); } goToDetails(tenant) { this.location.go(`/tenants/${tenant.id}`); } async activateTenant(tenant) { try { const { data: savedTenant } = await this.tenantService.update({ id: tenant.id, status: TenantStatus.ACTIVE }); Object.assign(tenant, savedTenant); this.alertService.success(gettext('Tenant activated.')); } catch (ex) { if (ex) { this.alertService.addServerFailure(ex); } } } async suspendTenant(tenant) { const title = gettext('Suspend tenant'); const confirmationText = gettext('You are about to suspend tenant "{{ company }}" (ID "{{ id }}").'); const proceed = gettext('Do you want to proceed?'); const body = [ this.translateService.instant(confirmationText, { company: tenant.company, id: tenant.id }), this.translateService.instant(proceed) ].join(' '); const labels = { ok: gettext('Suspend`tenant`') }; try { await this.modalService.confirm(title, body, Status.DANGER, labels); const confirmed = await this.passwordService.confirmPassword().toPromise(); if (confirmed === true) { const { data: savedTenant } = await this.tenantService.update({ id: tenant.id, status: TenantStatus.SUSPENDED }); Object.assign(tenant, savedTenant); this.alertService.success(gettext('Tenant suspended.')); } } catch (ex) { if (ex) { this.alertService.addServerFailure(ex); } } } async delete(tenant) { const title = gettext('Delete tenant'); const confirmationText = gettext('You are about to delete tenant "{{ company }}" (ID "{{ id }}").'); const hint = gettext('This operation is irreversible.'); const proceed = gettext('Do you want to proceed?'); const body = [ this.translateService.instant(confirmationText, { company: tenant.company, id: tenant.id }), this.translateService.instant(hint), this.translateService.instant(proceed) ].join(' '); const labels = { ok: gettext('Delete`tenant`') }; try { await this.modalService.confirm(title, body, Status.DANGER, labels); const confirmed = await this.passwordService.confirmPassword().toPromise(); if (confirmed === true) { await this.tenantService.delete(tenant); const tenantsWithoutRemovedOne = this.tenants$.value.filter(t => t !== tenant); this.tenants$.next(tenantsWithoutRemovedOne); this.alertService.success(gettext('Tenant is being deleted in the background. This might take a while…')); } } catch (ex) { if (ex) { this.alertService.addServerFailure(ex); } } } isActive(tenant) { return tenant.status === TenantStatus.ACTIVE; } isSuspended(tenant) { return tenant.status === TenantStatus.SUSPENDED; } async downloadNewsletterEmails() { const { res, data } = await this.userService.getNewsletterEmails(); const contentType = res.headers.get('content-type'); const contentDisposition = res.headers.get('content-disposition'); const filename = /filename="(.*)"/.exec(contentDisposition)[1]; const blob = new Blob([data], { type: contentType }); saveAs(blob, filename); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantListComponent, deps: [{ token: i1.AppStateService }, { token: i1.AlertService }, { token: i1.ModalService }, { token: i2.TranslateService }, { token: i1$1.TenantService }, { token: i1.TenantUiService }, { token: i4.Location }, { token: i1.PasswordService }, { token: i1$1.UserService }, { token: i1.Permissions }, { token: i1.GainsightService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantListComponent, isStandalone: true, selector: "c8y-tenant-list", ngImport: i0, template: "<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({ sendGainsightEvent: false })\"\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", dependencies: [{ kind: "component", type: TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "component", type: BreadcrumbComponent, selector: "c8y-breadcrumb" }, { kind: "component", type: BreadcrumbItemComponent, selector: "c8y-breadcrumb-item", inputs: ["icon", "translate", "label", "path", "injector"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ActionBarItemComponent, selector: "c8y-action-bar-item", inputs: ["placement", "priority", "itemClass", "injector", "groupId", "inGroupPriority"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: HelpComponent, selector: "c8y-help", inputs: ["src", "isCollapsed", "priority", "icon"] }, { kind: "component", type: DataGridComponent, selector: "c8y-data-grid", inputs: ["title", "loadMoreItemsLabel", "loadingItemsLabel", "showSearch", "refresh", "columns", "rows", "pagination", "childNodePagination", "infiniteScroll", "serverSideDataCallback", "selectable", "singleSelection", "selectionPrimaryKey", "displayOptions", "actionControls", "bulkActionControls", "headerActionControls", "searchText", "configureColumnsEnabled", "showCounterWarning", "activeClassName", "expandableRows", "treeGrid", "hideReload", "childNodesProperty", "parentNodeLabelProperty"], outputs: ["rowMouseOver", "rowMouseLeave", "rowClick", "onConfigChange", "onBeforeFilter", "onBeforeSearch", "onFilter", "itemsSelect", "onReload", "onAddCustomColumn", "onRemoveCustomColumn", "onColumnFilterReset", "onSort", "onPageSizeChange", "onColumnReordered", "onColumnVisibilityChange"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "directive", type: EmptyStateContextDirective, selector: "[emptyStateContext]" }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "component", type: GuideDocsComponent, selector: "[c8y-guide-docs]" }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: GuideHrefDirective, selector: "[c8y-guide-href]", inputs: ["c8y-guide-href"] }, { kind: "directive", type: ColumnDirective, selector: "c8y-column", inputs: ["name"] }, { kind: "directive", type: CellRendererDefDirective, selector: "[c8yCellRendererDef]" }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantListComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-tenant-list', imports: [ TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, NgIf, ActionBarItemComponent, IconDirective, HelpComponent, DataGridComponent, LoadingComponent, EmptyStateContextDirective, EmptyStateComponent, GuideDocsComponent, C8yTranslateDirective, GuideHrefDirective, ColumnDirective, CellRendererDefDirective, RouterLink, C8yTranslatePipe, AsyncPipe, DatePipe ], template: "<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({ sendGainsightEvent: false })\"\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" }] }], ctorParameters: () => [{ type: i1.AppStateService }, { type: i1.AlertService }, { type: i1.ModalService }, { type: i2.TranslateService }, { type: i1$1.TenantService }, { type: i1.TenantUiService }, { type: i4.Location }, { type: i1.PasswordService }, { type: i1$1.UserService }, { type: i1.Permissions }, { type: i1.GainsightService }] }); /** * Define all hardTyped tenant form fields, checkboxes and select elements. * * while still benefiting from hard typing, thanks to "satisfies" keyword */ const tenantPropertiesDefinitions = { tenantID: { id: 'tenantId', validators: [], defaultValue: null, type: 'text', label: gettext('ID') }, domain: { id: 'domain', validators: [ Validators.required, Validators.maxLength(32), // TODO: To be consistent, pattern should be change for domain. It is tenantId, to avoid breaking tests, and be consistent with previous implementation. Validators.pattern(ValidationPattern.rules.tenantId.pattern) ], defaultValue: null, type: 'text', label: gettext('Domain/URL'), placeholder: gettext('e.g. my-tenant`used in URL`') }, companyName: { id: 'companyName', validators: [ Validators.required, Validators.maxLength(256), Validators.pattern(ValidationPattern.rules.noLeadingOrTrailingWhitespaces.pattern) ], defaultValue: null, type: 'text', label: gettext('Name'), placeholder: gettext('e.g. Company A') }, contactName: { id: 'contactName', validators: [Validators.maxLength(30)], defaultValue: null, type: 'text', label: gettext('Contact name'), placeholder: gettext('e.g. Joe Doe`LOCALIZE`') }, contactPhone: { id: 'contactPhone', validators: [Validators.required, validateInternationalPhoneNumber(), Validators.maxLength(20)], defaultValue: null, type: 'text', label: gettext('Contact phone'), placeholder: gettext('e.g. +49 9 876 543 210`LOCALIZE`') }, administratorEmail: { id: 'administratorEmail', validators: [Validators.required, Validators.email, Validators.maxLength(256)], defaultValue: null, type: 'text', label: gettext(`Administrator's email`), placeholder: gettext('e.g. joe.doe@example.com`LOCALIZE`') }, administratorUsername: { id: 'administratorUsername', validators: [ Validators.required, Validators.pattern(ValidationPattern.rules.user.pattern), Validators.maxLength(256) ], defaultValue: null, type: 'text', label: gettext(`Administrator's username`), placeholder: gettext('e.g. joe`LOCALIZE`') }, externalReference: { id: 'externalReference', validators: [], defaultValue: null, type: 'text', label: gettext('External reference'), placeholder: gettext('e.g. REF12345`reference number`') }, sendPasswordResetEmail: { id: 'sendPasswordResetEmail', validators: [], defaultValue: true, type: 'checkbox', label: gettext('Send password reset link as email') }, tenantPolicy: { id: 'tenantPolicy', validators: [], defaultValue: null, type: 'select', label: gettext('Tenant policy') }, allowCreateTenants: { id: 'allowCreateTenants', validators: [], defaultValue: null, type: 'checkbox', label: gettext('Allow creation of subtenants') }, gainsightEnabled: { id: 'gainsightEnabled', validators: [], defaultValue: false, type: 'checkbox', label: gettext('Enable Gainsight product experience tracking') } }; const tenantFormInputsDefinitions = tenantPropertiesDefinitions; class SupportUserAccessComponent { constructor(tenantUiService, appState, permissions) { this.tenantUiService = tenantUiService; this.appState = appState; this.permissions = permissions; } async ngOnInit() { this.isTopTenant = await this.tenantUiService.isManagementTenant(); this.getSupportUserLogin(); } async getSupportUserLogin() { const currentUser = (await this.appState.currentUser).value; if (this.permissions.hasAnyRole([Permissions.ROLE_SUPPORT_ADMIN, Permissions.ROLE_SUPPORT_READ])) { this.supportUserLogin = encodeURIComponent(`${currentUser.id}$`); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SupportUserAccessComponent, deps: [{ token: i1.TenantUiService }, { token: i1.AppStateService }, { token: i1.Permissions }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i