@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
710 lines (696 loc) • 112 kB
JavaScript
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