UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

710 lines (696 loc) 112 kB
import * as i0 from '@angular/core'; import { inject, Injectable, LOCALE_ID, Pipe, input, computed, Component, Input, DestroyRef, EventEmitter, ViewChild, NgModule } from '@angular/core'; import * as i1$2 from '@c8y/ngx-components'; import { AlertService, Permissions, NavigatorNode, AppStateService, NavigatorService, IconDirective, C8yTranslatePipe, C8yTranslateModule, HeaderModule, HelpModule, BreadcrumbModule, ActionBarItemComponent, LoadingComponent, BaseColumn, DataGridModule, EmptyStateComponent, EmptyStateContextDirective, BytesPipe, ModalService, Status, C8yTranslateDirective, RelativeTimePipe, DataGridComponent, hookRoute, hookTab, hookNavigator, hookPreview } from '@c8y/ngx-components'; import { gettext } from '@c8y/ngx-components/gettext'; import * as i1 from '@c8y/client'; import { Service, UserService, FeatureService, ApplicationService, Paging } from '@c8y/client'; import { filter, map, take, switchMap, catchError, shareReplay, tap } from 'rxjs/operators'; import { from, BehaviorSubject, combineLatest, firstValueFrom } from 'rxjs'; import * as i1$1 from '@angular/common'; import { formatNumber, NgIf, NgClass, PercentPipe, CommonModule, DecimalPipe, AsyncPipe } from '@angular/common'; import * as i1$4 from '@angular/router'; import { RouterLink, ActivatedRoute } from '@angular/router'; import * as i1$3 from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { omit } from 'lodash-es'; /** * Currently known namespaces and their properties. */ const NAMESPACE_PROPS = { mqtt: { icon: 'c8y-device-protocols', label: gettext('MQTT Service') }, 'data-broker-fwd': { icon: 'c8y-data-broker', label: gettext('Data Broker') }, relnotif: { icon: 'c8y-notification', label: gettext('Notifications 2.0') }, 'streaming-analytics': { icon: 'c8y-streaming-analytics', label: gettext('Streaming Analytics') } }; class MessagingNamespacesService extends Service { constructor(client) { super(client); this.baseUrl = '/service/messaging-management'; this.listUrl = 'tenants'; this.alertService = inject(AlertService); } /** * Get namespace list for a tenant * * @param tenant Tenant id * @return Namespaces list */ async getNamespaces(tenant) { const namespacesUrl = `/${this.listUrl}/${tenant}/namespaces`; return this.fetch(namespacesUrl).then(res => res.json()); } /** * Get namespace. * * @param tenant Tenant ID. * @param namespace Name of namespace. * @return Namespace. */ async getNamespace(tenant, namespace) { const namespaceUrl = `/${this.listUrl}/${tenant}/namespaces/${namespace}`; return this.fetch(namespaceUrl).then(res => res.json()); } /** * Get namespace policies * * @param tenant Tenant id * @param namespace Name of namespace * @return Namespaces policies */ async getNamespacePolicies(tenant, namespace) { const policiesUrl = `/${this.listUrl}/${tenant}/namespaces/${namespace}/policies`; return this.fetch(policiesUrl).then(res => res.json()); } /** * Get single namespace details * @param tenant Tenant ID * @param namespaceName Namespace name * @return Namespace with details */ async getNamespaceDetails(tenant, namespaceName) { const namespace = await this.getNamespace(tenant, namespaceName); const policies = await this.getNamespacePolicies(tenant, namespaceName); return { id: namespaceName, namespace, policies }; } /** * Get namespaces with details * * @param tenant Tenant ID * @return Namespaces with details */ async getNamespacesDetails(tenant) { try { const { namespaces } = await this.getNamespaces(tenant); return Promise.all(namespaces.map(async (namespace) => this.getNamespaceDetails(tenant, namespace.name))); } catch (error) { this.alertService.addServerFailure(error); return []; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingNamespacesService, deps: [{ token: i1.FetchClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingNamespacesService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingNamespacesService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.FetchClient }] }); const basePath = 'monitoring/messaging-service'; const MESSAGING_MANAGEMENT_FEATURE_KEY = 'messaging-management.api'; const MESSAGING_MANAGEMENT_MICROSERVICE_NAME = 'messaging-management'; class MessagingManagementGuard { constructor() { this.userService = inject(UserService); this.featureService = inject(FeatureService); this.applicationService = inject(ApplicationService); this.cachedResult = null; } async canActivate() { if (this.cachedResult !== null) { return this.cachedResult; } const currentUser = (await this.userService.current()).data; const hasRequiredRoles = this.userService.hasAnyRole(currentUser, [ Permissions.ROLE_TENANT_STATISTICS_READ, Permissions.ROLE_TENANT_MANAGEMENT_ADMIN ]); const featureEnabled = await this.featureService .detail(MESSAGING_MANAGEMENT_FEATURE_KEY) .then(({ data }) => data.active) .catch(() => false); const microserviceSubscribed = await this.applicationService .isAvailable(MESSAGING_MANAGEMENT_MICROSERVICE_NAME) .then(({ data: subscribed }) => subscribed) .catch(() => false); this.cachedResult = hasRequiredRoles && featureEnabled && microserviceSubscribed; return this.cachedResult; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingManagementGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingManagementGuard, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingManagementGuard, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const baseNode = new NavigatorNode({ label: gettext('Messaging service'), icon: 'arrows-dotted-left-right', path: 'monitoring/messaging-service', priority: 100, parent: { label: gettext('Monitoring'), icon: 'monitoring' } }); class MessagingNavigatorNodeFactory { constructor() { this.appState = inject(AppStateService); this.navigatorService = inject(NavigatorService); this.namespacesService = inject(MessagingNamespacesService); this.guard = inject(MessagingManagementGuard); this.currentTenantId = this.appState.currentTenant.pipe(filter(currentTenant => !!currentTenant), map(currentTenant => currentTenant.name), take(1)); this.navigatorNode$ = from(this.guard.canActivate()).pipe(filter(allowed => allowed), switchMap(() => this.currentTenantId), switchMap(currentTenantId => this.namespacesService.getNamespaces(currentTenantId)), map(({ namespaces }) => { if (!namespaces?.length) { return []; } namespaces.forEach(namespace => { const label = NAMESPACE_PROPS[namespace.name]?.label || namespace.name; const childNode = new NavigatorNode({ label, path: `monitoring/messaging-service/namespace/${namespace.name}`, icon: NAMESPACE_PROPS[namespace.name]?.icon, routerLinkExact: false }); baseNode.add(childNode); }); return baseNode; }), catchError(() => { return []; }), shareReplay(1)); } get() { return this.navigatorNode$; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingNavigatorNodeFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingNavigatorNodeFactory, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingNavigatorNodeFactory, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * In case of limits in Messaging management, value of '-1' means that there is no limit. */ class BacklogQuotaLimitPipe { constructor() { this.translateService = inject(TranslateService); this.locale = inject(LOCALE_ID); } transform(value) { if (value == null || isNaN(value)) { return '-'; } else if (value === -1) { return this.translateService.instant(gettext('Unlimited` backlog quota`')); } else { return formatNumber(value, this.locale); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: BacklogQuotaLimitPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: BacklogQuotaLimitPipe, isStandalone: true, name: "backlogQuotaLimit" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: BacklogQuotaLimitPipe, decorators: [{ type: Pipe, args: [{ name: 'backlogQuotaLimit', standalone: true }] }] }); /** * Usage component displays usage information in a form of e.g. "51% used". * It can be used in two ways: * 1. By providing `count` and `limit` inputs, it will calculate the usage percentage. * 2. By providing `percentage` input, it will use the provided percentage value. * Note: `percentage` input takes precedence over `count` and `limit` inputs. */ class UsageComponent { constructor() { this.count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : [])); this.limit = input(null, ...(ngDevMode ? [{ debugName: "limit" }] : [])); /** * Percentage of usage. Value range is from 0 to 100 (or more). * For example, if 10% is used, this value should be provided as 10 (not 0.1). */ this.percentage = input(null, ...(ngDevMode ? [{ debugName: "percentage" }] : [])); /** * Usage as a fraction (e.g. if 50% is used, usage value will be 0.5) */ this.usage = computed(() => this.percentage() != null ? this.percentage() / 100 : this.getUsage(this.count(), this.limit()), ...(ngDevMode ? [{ debugName: "usage" }] : [])); this.status = computed(() => this.getStatus(this.usage()), ...(ngDevMode ? [{ debugName: "status" }] : [])); this.usageToDisplay = gettext('{{ percentageOfQuota }} used'); this.statusMap = { danger: ['tag--danger'], warning: ['tag--warning'], success: ['tag--success'] }; } /** * Get usage as fraction of count and limit. * E.g. if count is 5 and limit is 10, returned usage will be 0.5 * @param count Usage count * @param limit Usage limit * @returns Count divided by limit or null if count or limit is null or limit is -1 (indicates no limit) */ getUsage(count, limit) { if (count == null || limit == null || limit === -1) { return null; } return count / limit; } getStatus(usage) { if (usage == null) { return null; } const percentage = usage * 100; if (percentage >= 80) { return 'danger'; } else if (percentage >= 50) { return 'warning'; } else { return 'success'; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: UsageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.19", type: UsageComponent, isStandalone: true, selector: "app-usage", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, percentage: { classPropertyName: "percentage", publicName: "percentage", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "<div\n class=\"tag no-pointer\"\n [ngClass]=\"statusMap[status()]\"\n *ngIf=\"usage() !== null\"\n>\n <i\n class=\"text-danger m-r-4 text-12\"\n c8yIcon=\"exclamation-circle\"\n *ngIf=\"status() === 'danger'\"\n ></i>\n <span>\n {{ usageToDisplay | translate: { percentageOfQuota: (usage() | percent: '1.0-2') } }}\n </span>\n</div>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: PercentPipe, name: "percent" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: UsageComponent, decorators: [{ type: Component, args: [{ selector: 'app-usage', standalone: true, imports: [IconDirective, NgIf, NgClass, IconDirective, C8yTranslatePipe, PercentPipe], host: { class: 'd-contents' }, template: "<div\n class=\"tag no-pointer\"\n [ngClass]=\"statusMap[status()]\"\n *ngIf=\"usage() !== null\"\n>\n <i\n class=\"text-danger m-r-4 text-12\"\n c8yIcon=\"exclamation-circle\"\n *ngIf=\"status() === 'danger'\"\n ></i>\n <span>\n {{ usageToDisplay | translate: { percentageOfQuota: (usage() | percent: '1.0-2') } }}\n </span>\n</div>\n" }] }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], percentage: [{ type: i0.Input, args: [{ isSignal: true, alias: "percentage", required: false }] }] } }); const DataType = { publishers: 'publishers', subscribers: 'subscribers', topics: 'topics' }; class NamespaceItemCardComponent { constructor() { this.DATA_TYPE = DataType; this.ITEM_DETAILS = { publishers: { icon: 'output', title: gettext('Publishers') }, subscribers: { icon: 'input', title: gettext('Subscribers') }, topics: { icon: 'day-view', title: gettext('Topics') } }; this.topicsLimitLabel = gettext('Limit: {{ backlogQuotaLimit }}'); /** * The label of the service (already translated). */ this.serviceLabel = ''; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NamespaceItemCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: NamespaceItemCardComponent, isStandalone: true, selector: "app-namespace-item-card", inputs: { serviceLabel: "serviceLabel", limit: "limit", dataType: "dataType", value: "value" }, host: { classAttribute: "card m-b-0 fit-w" }, ngImport: i0, template: "<div class=\"card-block text-default visible-xs text-center p-b-0\">\n <span class=\"text-12 text-uppercase text-muted\">\n {{ serviceLabel | translate }}\n </span>\n</div>\n<div\n class=\"card-block text-default p-t-sm-48\"\n [ngClass]=\"{\n 'p-b-sm-48': limit === 0\n }\"\n>\n <div class=\"d-flex fit-w a-i-center gap-8 j-c-center\">\n <i\n class=\"icon-32 c8y-icon-duocolor\"\n [c8yIcon]=\"ITEM_DETAILS[dataType].icon\"\n ></i>\n <span\n class=\"h1\"\n data-cy=\"namespace-item-card--value\"\n >\n {{ value | number }}\n </span>\n <span\n class=\"a-s-baseline text-14 text-medium text-truncate\"\n title=\"{{ ITEM_DETAILS[dataType].title | translate }}\"\n data-cy=\"namespace-item-card--label\"\n >\n {{ ITEM_DETAILS[dataType].title | translate }}\n </span>\n </div>\n</div>\n@if (dataType === DATA_TYPE.topics && limit !== 0) {\n <div class=\"card-footer d-flex gap-16 j-c-center a-i-center\">\n <span\n class=\"tag tag--default no-pointer\"\n data-cy=\"namespace-item-card--limit\"\n >\n {{ topicsLimitLabel | translate: { backlogQuotaLimit: limit | backlogQuotaLimit } }}\n </span>\n <app-usage\n data-cy=\"namespace-item-card--usage\"\n [count]=\"value\"\n [limit]=\"limit\"\n ></app-usage>\n </div>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: UsageComponent, selector: "app-usage", inputs: ["count", "limit", "percentage"] }, { kind: "pipe", type: i1$2.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "pipe", type: BacklogQuotaLimitPipe, name: "backlogQuotaLimit" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NamespaceItemCardComponent, decorators: [{ type: Component, args: [{ selector: 'app-namespace-item-card', standalone: true, imports: [IconDirective, C8yTranslateModule, CommonModule, BacklogQuotaLimitPipe, UsageComponent], host: { class: 'card m-b-0 fit-w' }, template: "<div class=\"card-block text-default visible-xs text-center p-b-0\">\n <span class=\"text-12 text-uppercase text-muted\">\n {{ serviceLabel | translate }}\n </span>\n</div>\n<div\n class=\"card-block text-default p-t-sm-48\"\n [ngClass]=\"{\n 'p-b-sm-48': limit === 0\n }\"\n>\n <div class=\"d-flex fit-w a-i-center gap-8 j-c-center\">\n <i\n class=\"icon-32 c8y-icon-duocolor\"\n [c8yIcon]=\"ITEM_DETAILS[dataType].icon\"\n ></i>\n <span\n class=\"h1\"\n data-cy=\"namespace-item-card--value\"\n >\n {{ value | number }}\n </span>\n <span\n class=\"a-s-baseline text-14 text-medium text-truncate\"\n title=\"{{ ITEM_DETAILS[dataType].title | translate }}\"\n data-cy=\"namespace-item-card--label\"\n >\n {{ ITEM_DETAILS[dataType].title | translate }}\n </span>\n </div>\n</div>\n@if (dataType === DATA_TYPE.topics && limit !== 0) {\n <div class=\"card-footer d-flex gap-16 j-c-center a-i-center\">\n <span\n class=\"tag tag--default no-pointer\"\n data-cy=\"namespace-item-card--limit\"\n >\n {{ topicsLimitLabel | translate: { backlogQuotaLimit: limit | backlogQuotaLimit } }}\n </span>\n <app-usage\n data-cy=\"namespace-item-card--usage\"\n [count]=\"value\"\n [limit]=\"limit\"\n ></app-usage>\n </div>\n}\n" }] }], propDecorators: { serviceLabel: [{ type: Input }], limit: [{ type: Input }], dataType: [{ type: Input }], value: [{ type: Input }] } }); class NamespaceItemComponent { constructor() { this.translateService = inject(TranslateService); this.namespaceName = ''; this.namespaceLabel = ''; this.icon = ''; this.namespace = {}; this.policies = {}; } set _namespaceName(name) { this.namespaceName = name; this.icon = NAMESPACE_PROPS[name]?.icon; this.namespaceLabel = this.translateService.instant(NAMESPACE_PROPS[name]?.label) || name; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NamespaceItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NamespaceItemComponent, isStandalone: true, selector: "app-namespace-item", inputs: { _namespaceName: ["namespaceName", "_namespaceName"], namespace: "namespace", policies: "policies" }, ngImport: i0, template: "<div class=\"d-flex-sm d-col-xs p-t-24 p-l-16 p-r-16 m-0 bg-level-1\">\n <div\n class=\"col-sm-3 m-b-24 col-xs-12 d-flex gap-16 text-default a-i-center j-c-center a-s-stretch\"\n >\n <div class=\"text-center d-col\">\n <i\n class=\"m-b-8 icon-40 c8y-icon-duocolor\"\n [c8yIcon]=\"icon\"\n ></i>\n <span class=\"tag tag--info\">{{ 'Service' | translate }}</span>\n </div>\n <span\n class=\"h4\"\n data-cy=\"namespace-item--namespace-label\"\n >\n {{ namespaceLabel }}\n </span>\n </div>\n <div class=\"col-sm-3 m-b-24 col-xs-12 a-i-stretch d-flex\">\n <app-namespace-item-card\n [serviceLabel]=\"namespaceLabel\"\n [dataType]=\"'topics'\"\n [limit]=\"namespace?.topics?.limit\"\n [value]=\"namespace?.topics?.count\"\n ></app-namespace-item-card>\n </div>\n <div class=\"col-sm-3 m-b-24 col-xs-12 a-i-stretch d-flex\">\n <app-namespace-item-card\n [serviceLabel]=\"namespaceLabel\"\n [dataType]=\"'publishers'\"\n [value]=\"namespace?.publishers?.count\"\n ></app-namespace-item-card>\n </div>\n <div class=\"col-sm-3 m-b-24 col-xs-12 a-i-stretch d-flex\">\n <app-namespace-item-card\n [serviceLabel]=\"namespaceLabel\"\n [dataType]=\"'subscribers'\"\n [value]=\"namespace?.subscribers?.count\"\n ></app-namespace-item-card>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: NamespaceItemCardComponent, selector: "app-namespace-item-card", inputs: ["serviceLabel", "limit", "dataType", "value"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NamespaceItemComponent, decorators: [{ type: Component, args: [{ selector: 'app-namespace-item', standalone: true, imports: [CommonModule, IconDirective, NamespaceItemCardComponent, C8yTranslatePipe], template: "<div class=\"d-flex-sm d-col-xs p-t-24 p-l-16 p-r-16 m-0 bg-level-1\">\n <div\n class=\"col-sm-3 m-b-24 col-xs-12 d-flex gap-16 text-default a-i-center j-c-center a-s-stretch\"\n >\n <div class=\"text-center d-col\">\n <i\n class=\"m-b-8 icon-40 c8y-icon-duocolor\"\n [c8yIcon]=\"icon\"\n ></i>\n <span class=\"tag tag--info\">{{ 'Service' | translate }}</span>\n </div>\n <span\n class=\"h4\"\n data-cy=\"namespace-item--namespace-label\"\n >\n {{ namespaceLabel }}\n </span>\n </div>\n <div class=\"col-sm-3 m-b-24 col-xs-12 a-i-stretch d-flex\">\n <app-namespace-item-card\n [serviceLabel]=\"namespaceLabel\"\n [dataType]=\"'topics'\"\n [limit]=\"namespace?.topics?.limit\"\n [value]=\"namespace?.topics?.count\"\n ></app-namespace-item-card>\n </div>\n <div class=\"col-sm-3 m-b-24 col-xs-12 a-i-stretch d-flex\">\n <app-namespace-item-card\n [serviceLabel]=\"namespaceLabel\"\n [dataType]=\"'publishers'\"\n [value]=\"namespace?.publishers?.count\"\n ></app-namespace-item-card>\n </div>\n <div class=\"col-sm-3 m-b-24 col-xs-12 a-i-stretch d-flex\">\n <app-namespace-item-card\n [serviceLabel]=\"namespaceLabel\"\n [dataType]=\"'subscribers'\"\n [value]=\"namespace?.subscribers?.count\"\n ></app-namespace-item-card>\n </div>\n</div>\n" }] }], propDecorators: { _namespaceName: [{ type: Input, args: ['namespaceName'] }], namespace: [{ type: Input }], policies: [{ type: Input }] } }); class NamespaceListComponent { constructor() { this.alertService = inject(AlertService); this.appState = inject(AppStateService); this.namespacesService = inject(MessagingNamespacesService); this.loading = true; } async ngOnInit() { await this.reload(); } async reload() { this.loading = true; try { const currentTenantId = this.appState.currentTenant.value.name; this.namespacesDetails = await this.namespacesService.getNamespacesDetails(currentTenantId); } catch (e) { this.alertService.addServerFailure(e); } finally { this.loading = false; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NamespaceListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NamespaceListComponent, isStandalone: true, selector: "app-namespace-list", ngImport: i0, template: "<c8y-title>{{ 'Messaging service' | translate }}</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'monitoring'\"\n [label]=\"'Monitoring' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"'Messaging service' | translate\"></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <li>\n <a\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n (click)=\"reload()\"\n >\n <i\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': loading }\"\n ></i>\n {{ 'Reload' | translate }}\n </a>\n </li>\n</c8y-action-bar-item>\n\n<c8y-help src=\"/docs/standard-tenant/monitoring/#messaging-service\"></c8y-help>\n\n<div\n class=\"interact-grid\"\n *ngIf=\"!loading; else loadingTemplate\"\n>\n <a\n class=\"card\"\n *ngFor=\"let namespace of namespacesDetails\"\n [routerLink]=\"['namespace', namespace.id]\"\n >\n <app-namespace-item\n [namespaceName]=\"namespace.id\"\n [namespace]=\"namespace.namespace\"\n ></app-namespace-item>\n </a>\n</div>\n\n<ng-template #loadingTemplate>\n <c8y-loading></c8y-loading>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: HeaderModule }, { kind: "component", type: i1$2.TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "ngmodule", type: HelpModule }, { kind: "component", type: i1$2.HelpComponent, selector: "c8y-help", inputs: ["src", "isCollapsed", "priority", "icon"] }, { kind: "ngmodule", type: C8yTranslateModule }, { kind: "component", type: NamespaceItemComponent, selector: "app-namespace-item", inputs: ["namespaceName", "namespace", "policies"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: BreadcrumbModule }, { kind: "component", type: i1$2.BreadcrumbComponent, selector: "c8y-breadcrumb" }, { kind: "component", type: i1$2.BreadcrumbItemComponent, selector: "c8y-breadcrumb-item", inputs: ["icon", "translate", "label", "path", "injector"] }, { 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: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "pipe", type: i1$2.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NamespaceListComponent, decorators: [{ type: Component, args: [{ selector: 'app-namespace-list', imports: [ CommonModule, HeaderModule, HelpModule, C8yTranslateModule, NamespaceItemComponent, RouterLink, BreadcrumbModule, ActionBarItemComponent, IconDirective, LoadingComponent ], standalone: true, template: "<c8y-title>{{ 'Messaging service' | translate }}</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'monitoring'\"\n [label]=\"'Monitoring' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"'Messaging service' | translate\"></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <li>\n <a\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n (click)=\"reload()\"\n >\n <i\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': loading }\"\n ></i>\n {{ 'Reload' | translate }}\n </a>\n </li>\n</c8y-action-bar-item>\n\n<c8y-help src=\"/docs/standard-tenant/monitoring/#messaging-service\"></c8y-help>\n\n<div\n class=\"interact-grid\"\n *ngIf=\"!loading; else loadingTemplate\"\n>\n <a\n class=\"card\"\n *ngFor=\"let namespace of namespacesDetails\"\n [routerLink]=\"['namespace', namespace.id]\"\n >\n <app-namespace-item\n [namespaceName]=\"namespace.id\"\n [namespace]=\"namespace.namespace\"\n ></app-namespace-item>\n </a>\n</div>\n\n<ng-template #loadingTemplate>\n <c8y-loading></c8y-loading>\n</ng-template>\n" }] }] }); class TimeToLivePipe { constructor(translateService) { this.translateService = translateService; } /** * Transform time in seconds to human readable format. * @param seconds time in seconds * @returns human readable time period */ transform(seconds) { if (seconds == null || isNaN(seconds)) { return '-'; } if (seconds === -1) { return this.translateService.instant(gettext('Unlimited` time-to-live period`')); } const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days >= 1) { const remainingHours = hours % 24; if (days === 1) { if (remainingHours === 0) { return this.translateService.instant(gettext('1 day')); } if (remainingHours === 1) { return this.translateService.instant(gettext('1 day 1 hour')); } return this.translateService.instant(gettext('1 day {{hours}} hours'), { hours: remainingHours }); } else { if (remainingHours === 0) { return this.translateService.instant(gettext('{{days}} days'), { days }); } if (remainingHours === 1) { return this.translateService.instant(gettext('{{days}} days 1 hour'), { days }); } return this.translateService.instant(gettext('{{days}} days {{hours}} hours'), { days, hours: remainingHours }); } } if (hours > 0) { const remainingMinutes = minutes % 60; if (hours === 1) { if (remainingMinutes === 0) { return this.translateService.instant(gettext('1 hour')); } if (remainingMinutes === 1) { return this.translateService.instant(gettext('1 hour 1 minute')); } return this.translateService.instant(gettext('1 hour {{minutes}} minutes'), { minutes: remainingMinutes }); } else { if (remainingMinutes === 0) { return this.translateService.instant(gettext('{{hours}} hours'), { hours }); } if (remainingMinutes === 1) { return this.translateService.instant(gettext('{{hours}} hours 1 minute'), { hours }); } return this.translateService.instant(gettext('{{hours}} hours {{minutes}} minutes'), { hours, minutes: remainingMinutes }); } } if (minutes > 0) { const remainingSeconds = seconds % 60; if (minutes === 1) { if (remainingSeconds === 0) { return this.translateService.instant(gettext('1 minute')); } if (remainingSeconds === 1) { return this.translateService.instant(gettext('1 minute 1 second')); } return this.translateService.instant(gettext('1 minute {{seconds}} seconds'), { seconds: remainingSeconds }); } else { if (remainingSeconds === 0) { return this.translateService.instant(gettext('{{minutes}} minutes'), { minutes }); } if (remainingSeconds === 1) { return this.translateService.instant(gettext('{{minutes}} minutes 1 second'), { minutes }); } return this.translateService.instant(gettext('{{minutes}} minutes {{seconds}} seconds'), { minutes, seconds: remainingSeconds }); } } if (seconds === 1) { return this.translateService.instant(gettext('1 second')); } else { return this.translateService.instant(gettext('{{seconds}} seconds'), { seconds }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: TimeToLivePipe, deps: [{ token: i1$3.TranslateService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: TimeToLivePipe, isStandalone: true, name: "timeToLive" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: TimeToLivePipe, decorators: [{ type: Pipe, args: [{ name: 'timeToLive', standalone: true }] }], ctorParameters: () => [{ type: i1$3.TranslateService }] }); class MessagingTopicsService extends Service { constructor(client) { super(client); this.baseUrl = '/service/messaging-management'; this.listUrl = 'tenants'; } async list(filter) { const headers = { accept: 'application/json' }; const { tenant, namespace, ...params } = filter; const url = `/${this.listUrl}/${tenant}/namespaces/${namespace}/topics`; const res = await this.fetch(url, this.changeFetchOptions({ headers, params }, url)); const topicList = (await res.json()); const data = topicList.topics; const paging = this.getPaging(topicList, filter); return { res, data, paging }; } async detail(filter) { const headers = { accept: 'application/json' }; const { tenant, namespace, topic, type } = filter; const url = `/${this.listUrl}/${tenant}/namespaces/${namespace}/topics/${topic}/types/${type}`; const res = await this.fetch(url, this.changeFetchOptions({ headers }, url)); const data = await res.json(); return { res, data }; } getPaging(topicList, filter) { if (topicList.pageStatistics) { const { currentPage, totalPages } = topicList.pageStatistics; const statistics = { ...topicList.pageStatistics, nextPage: currentPage < totalPages ? currentPage + 1 : null, prevPage: currentPage > 1 ? currentPage - 1 : null }; return new Paging(this, statistics, filter); } return null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingTopicsService, deps: [{ token: i1.FetchClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingTopicsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: MessagingTopicsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.FetchClient }] }); class TopicsDataGridService { constructor() { this.topicsService = inject(MessagingTopicsService); } getColumns() { return [ this.createColumn({ name: 'name', header: gettext('Name'), path: 'name', filterable: true, filteringConfig: { fields: [ { key: 'name', type: 'input', props: { label: gettext('Filter topics by partial name'), placeholder: 'myTopic', required: true } } ], getFilter(model) { return model.name; } } }), this.createColumn({ name: 'msgRateIn', header: gettext('Message rate in (msg/s)'), path: 'msgRateIn' }), this.createColumn({ name: 'msgRateOut', header: gettext('Message rate out (msg/s)'), path: 'msgRateOut' }), this.createColumn({ name: 'subscribers', header: gettext('Subscribers'), path: 'subscribers' }), this.createColumn({ name: 'backlogSize', header: gettext('Message backlog'), path: 'backlogSize' }), this.createColumn({ name: 'backlogUsagePercentage', header: gettext('Used backlog'), path: 'backlogUsagePercentage', sortOrder: 'desc' }) ]; } async getServerSideData(tenantId, namespaceId, dataSourceModifier) { const topicFilters = this.getTopicFilters(tenantId, namespaceId, dataSourceModifier); const { res, data, paging } = await this.topicsService.list(topicFilters); const filteredSize = paging.totalElements; const size = (await this.topicsService.list({ ...topicFilters, currentPage: 1, pageSize: 1 })).paging.totalPages; return { res, data, paging, size, filteredSize }; } createColumn(columnProps) { const column = new BaseColumn(); Object.assign(column, columnProps); return column; } getTopicFilters(tenantId, namespaceId, dataSourceModifier) { const topicFilters = { tenant: tenantId, namespace: namespaceId, currentPage: dataSourceModifier.pagination.currentPage, pageSize: dataSourceModifier.pagination.pageSize }; return dataSourceModifier.columns.reduce((topicFilters, column) => { if (column.filterable) { if (column.filterPredicate) { topicFilters[column.path] = column.filterPredicate; } if (column.externalFilterQuery) { topicFilters[column.path] = column.filteringConfig.getFilter(column.externalFilterQuery); } } if (column.sortable && column.sortOrder) { const sortPath = column.sortingConfig?.pathSortingConfigs?.[0]?.path || column.path; topicFilters.sort = `${sortPath},${column.sortOrder}`; } return topicFilters; }, topicFilters); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: TopicsDataGridService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: TopicsDataGridService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: TopicsDataGridService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class TopicListViewComponent { constructor() { this.route = inject(ActivatedRoute); this.appState = inject(AppStateService); this.namespacesService = inject(MessagingNamespacesService); this.topicsDataGridService = inject(TopicsDataGridService); this.translateService = inject(TranslateService); this.destroyRef = inject(DestroyRef); this.loading$ = new BehaviorSubject(false); this.refresh = new EventEmitter(); this.tenantId$ = this.appState.currentTenant.pipe(map(tenant => tenant.name)); this.namespaceId$ = this.route.params.pipe(map(params => params['namespace'])); this.namespaceLabel$ = this.namespaceId$.pipe(map(namespaceId => this.translateService.instant(NAMESPACE_PROPS[namespaceId].label))); this.icon$ = this.namespaceId$.pipe(map(namespaceId => NAMESPACE_PROPS[namespaceId].icon)); this.namespaceDetails$ = combineLatest([this.tenantId$, this.namespaceId$, this.refresh]).pipe(tap(() => this.loading$.next(true)), switchMap(([tenantId, namespaceId]) => this.namespacesService.getNamespaceDetails(tenantId, namespaceId)), tap(() => this.loading$.next(false)), shareReplay(1)); this.tableTitle = gettext('Topics'); this.loadingItemsLabel = gettext('Loading topics...'); this.loadMoreItemsLabel = gettext('Load more topics'); this.noResultsMessage = gettext('No matching topics found.'); this.noResultsSubtitle = gettext('Refine your search terms or check your spelling.'); this.noDataMessage = gettext('No topics to display.'); this.noDataSubtitle = gettext('Create new topics to monitor them here.'); this.columns = this.topicsDataGridService.getColumns(); this.pagination = { pageSize: 20, currentPage: 1 }; this.serverSideDataCallback = this.onDataSourceModifier.bind(this); } async onDataSourceModifier(dataSourceModifier) { return firstValueFrom(combineLatest([this.tenantId$, this.namespaceId$]).pipe(switchMap(([tenantId, namespaceId]) => this.topicsDataGridService.getServerSideData(tenantId, namespaceId, dataSourceModifier)))); } ngAfterViewInit() { this.route.params .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => this.refresh.emit()); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: TopicListViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: TopicListViewComponent, isStandalone: true, selector: "app-topic-list-view", ngImport: i0, template: "<c8y-title>{{ namespaceLabel$ | async }}</c8y-title>\n\n<c8y-breadcrumb>\n <c8y-breadcrumb-item\n [icon]=\"'monitoring'\"\n [label]=\"'Monitoring' | translate\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item\n [label]=\"'Messaging service' | translate\"\n [path]=\"'/monitoring/messaging-service'\"\n ></c8y-breadcrumb-item>\n <c8y-breadcrumb-item [label]=\"namespaceLabel$ | async\"></c8y-breadcrumb-item>\n</c8y-breadcrumb>\n\n<c8y-action-bar-item [placement]=\"'right'\">\n <a\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n (click)=\"refresh.emit()\"\n >\n <i\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': loading$ | async }\"\n ></i>\n {{ 'Reload' | translate }}\n </a>\n</c8y-action-bar-item>\n\n<div class=\"card content-fullpage d-flex d-col\">\n <div class=\"bg-level-1 separator-bottom flex-no-shrink\">\n <div\n class=\"card-block\"\n style=\"min-height: 172px\"\n >\n <div\n class=\"col-md-4 m-b-24 col-xs-12 d-flex p-t-24 gap-16 text-default a-i-center a-s-stretch\"\n >\n <div class=\"text-center d-col\">\n <i\n class=\"m-b-8 icon-40 c8y-icon-duocolor\"\n [c8yIcon]=\"icon$ | async\"\n ></i>\n <span class=\"tag tag--info\">{{ 'Service' | translate }}</span>\n </div>\n <span class=\"h4 text-break-all\">{{ namespaceLabel$ | async }}</span>\n </div>\n <div class=\"col-md-4\">\n <fieldset class=\"c8y-fieldset c8y-fieldset--lg\">\n <legend>\n {{ 'Service usage/limits' | translate }}\n </legend>\n\n @if (loading$ | async) {\n <c8y-loading></c8y-loading>\n } @else {\n @let namespace = (namespaceDetails$ | async)?.namespace;\n @if (namespace) {\n <ul class=\"list-unstyled small animated fadeIn\">\n <li\n class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\"\n data-cy=\"topic-list-view--topics\"\n >\n <label class=\"small m-b-0 m-r-auto\">{{ 'Topics' | translate }}</label>\n @if (namespace?.topics?.limit !== 0) {\n <app-usage\n [count]=\"namespace?.topics?.count\"\n [limit]=\"namespace?.topics?.limit\"\n ></app-usage>\n <span class=\"m-l-16\">\n {{ namespace?.topics?.count | number }} /\n {{ namespace?.topics?.limit | backlogQuotaLimit }}\n </span>\n } @else {\n <span class=\"m-l-16\">{{ namespace?.topics?.count | number }}</span>\n }\n </li>\n\n <li\n class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\"\n data-cy=\"topic-list-view--subscribers\"\n >\n <label class=\"small m-b-0 m-r-auto\">{{ 'Subscribers' | translate }}</label>\n <span class=\"m-l-16\">\n {{ namespace?.subscribers?.count | number }}\n </span>\n </li>\n\n <li\n class=\"p-t-4 p-b-4 d-flex text-nowrap\"\n data-cy=\"topic-list-view--publishers\"\n >\n <label class=\"small m-b-0 m-r-auto\">{{ 'Publishers' | translate }}</label>\n <span class=\"m-l-16\">\n {{ namespace?.publishers?.count | number }}\n </span>\n </li>\n </ul>\n }\n }\n </fieldset>\n </div>\n <div class=\"col-md-4\">\n <fieldset class=\"c8y-fieldset c8y-fieldset--lg\">\n <legend>{{ 'Service message backlog limits' | translate }}</legend>\n\n @if (loading$ | async) {\n <c8y-loading></c8y-loading>\n } @else {\n @let policies = (namespaceDetails$ | async)?.policies;\n @if (policies) {\n <ul class=\"list-unstyled small animated fadeIn\">\n <li\n class=\"p-t-4 p-b-4 d-flex separator-bottom text-nowrap\"\n data-cy=\"topic-list-view--backlog-quota\"\n >\n <label class=\"small m-b-0 m-r-auto\">\n {{ 'Backlog quota (per topic)' | translate }}\n </label>\n <span\n title=\"{{\n policies?.backlogQuota?.limi