@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 126 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-messaging-management.mjs","sources":["../../messaging-management/utils/namespace-props.ts","../../messaging-management/api/services/messaging-namespaces.service.ts","../../messaging-management/constants.ts","../../messaging-management/messaging-management.guard.ts","../../messaging-management/navigator/messaging-navigator-factory.ts","../../messaging-management/utils/backlog-quota-limit.pipe.ts","../../messaging-management/messaging/shared/usage/usage.component.ts","../../messaging-management/messaging/shared/usage/usage.component.html","../../messaging-management/messaging/namespace-list/namespace-item/namespace-item-card/namespace-item-card.component.ts","../../messaging-management/messaging/namespace-list/namespace-item/namespace-item-card/namespace-item-card.component.html","../../messaging-management/messaging/namespace-list/namespace-item/namespace-item.component.ts","../../messaging-management/messaging/namespace-list/namespace-item/namespace-item.component.html","../../messaging-management/messaging/namespace-list/namespace-list.component.ts","../../messaging-management/messaging/namespace-list/namespace-list.component.html","../../messaging-management/utils/time-to-live.pipe.ts","../../messaging-management/api/services/messaging-topics.service.ts","../../messaging-management/messaging/topic/topics-data-grid.service.ts","../../messaging-management/messaging/topic/topic-list-view.component.ts","../../messaging-management/messaging/topic/topic-list-view.component.html","../../messaging-management/api/model/topicType.ts","../../messaging-management/api/services/messaging-subscribers.service.ts","../../messaging-management/messaging/topic/topic-subscribers-view/topic-subscribers-data-grid.service.ts","../../messaging-management/messaging/topic/topic-subscribers-view/topic-subscribers-view.component.ts","../../messaging-management/messaging/topic/topic-subscribers-view/topic-subscribers-view.component.html","../../messaging-management/navigator/topic-details-tab.factory.ts","../../messaging-management/messaging-management-preview.factory.ts","../../messaging-management/messaging-management.module.ts","../../messaging-management/c8y-ngx-components-messaging-management.ts"],"sourcesContent":["import { gettext } from '@c8y/ngx-components/gettext';\n\ntype NamespaceProps = {\n icon: string;\n label: string;\n};\n\n/**\n * Currently known namespaces and their properties.\n */\nexport const NAMESPACE_PROPS: Record<string, NamespaceProps> = {\n mqtt: {\n icon: 'c8y-device-protocols',\n label: gettext('MQTT Service')\n },\n 'data-broker-fwd': {\n icon: 'c8y-data-broker',\n label: gettext('Data Broker')\n },\n relnotif: {\n icon: 'c8y-notification',\n label: gettext('Notifications 2.0')\n },\n 'streaming-analytics': {\n icon: 'c8y-streaming-analytics',\n label: gettext('Streaming Analytics')\n }\n};\n","import { inject, Injectable } from '@angular/core';\nimport { MessagingNamespaceList } from '../model/namespaceList';\nimport { MessagingNamespacePolicies } from '../model/namespacePolicies';\nimport { MessagingNamespace } from '../model/namespace';\nimport { FetchClient, Service } from '@c8y/client';\nimport { MessagingNamespaceDetails } from '../model/namespaceDetails';\nimport { AlertService } from '@c8y/ngx-components';\n\n@Injectable({ providedIn: 'root' })\nexport class MessagingNamespacesService extends Service<any> {\n protected baseUrl = '/service/messaging-management';\n protected listUrl = 'tenants';\n\n private alertService = inject(AlertService);\n\n constructor(client: FetchClient) {\n super(client);\n }\n\n /**\n * Get namespace list for a tenant\n *\n * @param tenant Tenant id\n * @return Namespaces list\n */\n async getNamespaces(tenant: string): Promise<MessagingNamespaceList> {\n const namespacesUrl = `/${this.listUrl}/${tenant}/namespaces`;\n return this.fetch(namespacesUrl).then(res => res.json());\n }\n\n /**\n * Get namespace.\n *\n * @param tenant Tenant ID.\n * @param namespace Name of namespace.\n * @return Namespace.\n */\n async getNamespace(tenant: string, namespace: string): Promise<MessagingNamespace> {\n const namespaceUrl = `/${this.listUrl}/${tenant}/namespaces/${namespace}`;\n return this.fetch(namespaceUrl).then(res => res.json());\n }\n\n /**\n * Get namespace policies\n *\n * @param tenant Tenant id\n * @param namespace Name of namespace\n * @return Namespaces policies\n */\n async getNamespacePolicies(\n tenant: string,\n namespace: string\n ): Promise<MessagingNamespacePolicies> {\n const policiesUrl = `/${this.listUrl}/${tenant}/namespaces/${namespace}/policies`;\n return this.fetch(policiesUrl).then(res => res.json());\n }\n\n /**\n * Get single namespace details\n * @param tenant Tenant ID\n * @param namespaceName Namespace name\n * @return Namespace with details\n */\n async getNamespaceDetails(\n tenant: string,\n namespaceName: string\n ): Promise<MessagingNamespaceDetails> {\n const namespace = await this.getNamespace(tenant, namespaceName);\n const policies = await this.getNamespacePolicies(tenant, namespaceName);\n return {\n id: namespaceName,\n namespace,\n policies\n };\n }\n\n /**\n * Get namespaces with details\n *\n * @param tenant Tenant ID\n * @return Namespaces with details\n */\n async getNamespacesDetails(tenant: string): Promise<MessagingNamespaceDetails[]> {\n try {\n const { namespaces } = await this.getNamespaces(tenant);\n return Promise.all(\n namespaces.map(async namespace => this.getNamespaceDetails(tenant, namespace.name))\n );\n } catch (error) {\n this.alertService.addServerFailure(error);\n return [];\n }\n }\n}\n","export const basePath = 'monitoring/messaging-service';\nexport const MESSAGING_MANAGEMENT_FEATURE_KEY = 'messaging-management.api';\nexport const MESSAGING_MANAGEMENT_MICROSERVICE_NAME = 'messaging-management';\n","import { inject, Injectable } from '@angular/core';\nimport { CanActivate } from '@angular/router';\nimport { Permissions } from '@c8y/ngx-components';\nimport { ApplicationService, FeatureService, UserService } from '@c8y/client';\nimport {\n MESSAGING_MANAGEMENT_FEATURE_KEY,\n MESSAGING_MANAGEMENT_MICROSERVICE_NAME\n} from './constants';\n\n@Injectable({ providedIn: 'root' })\nexport class MessagingManagementGuard implements CanActivate {\n private userService = inject(UserService);\n private featureService = inject(FeatureService);\n private applicationService = inject(ApplicationService);\n\n private cachedResult: boolean | null = null;\n\n async canActivate(): Promise<boolean> {\n if (this.cachedResult !== null) {\n return this.cachedResult;\n }\n\n const currentUser = (await this.userService.current()).data;\n const hasRequiredRoles = this.userService.hasAnyRole(currentUser, [\n Permissions.ROLE_TENANT_STATISTICS_READ,\n Permissions.ROLE_TENANT_MANAGEMENT_ADMIN\n ]);\n\n const featureEnabled = await this.featureService\n .detail(MESSAGING_MANAGEMENT_FEATURE_KEY)\n .then(({ data }) => data.active)\n .catch(() => false);\n\n const microserviceSubscribed = await this.applicationService\n .isAvailable(MESSAGING_MANAGEMENT_MICROSERVICE_NAME)\n .then(({ data: subscribed }) => subscribed)\n .catch(() => false);\n\n this.cachedResult = hasRequiredRoles && featureEnabled && microserviceSubscribed;\n\n return this.cachedResult;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n AppStateService,\n ExtensionFactory,\n NavigatorNode,\n NavigatorService\n} from '@c8y/ngx-components';\nimport { NAMESPACE_PROPS } from '../utils/namespace-props';\nimport { MessagingNamespacesService } from '../api/services/messaging-namespaces.service';\nimport { catchError, filter, map, shareReplay, switchMap, take } from 'rxjs/operators';\nimport { from, Observable } from 'rxjs';\nimport { MessagingManagementGuard } from '../messaging-management.guard';\n\nconst baseNode = new NavigatorNode({\n label: gettext('Messaging service'),\n icon: 'arrows-dotted-left-right',\n path: 'monitoring/messaging-service',\n priority: 100,\n parent: {\n label: gettext('Monitoring'),\n icon: 'monitoring'\n }\n});\n\n@Injectable({ providedIn: 'root' })\nexport class MessagingNavigatorNodeFactory implements ExtensionFactory<NavigatorNode> {\n appState = inject(AppStateService);\n navigatorService = inject(NavigatorService);\n namespacesService = inject(MessagingNamespacesService);\n guard = inject(MessagingManagementGuard);\n\n currentTenantId = this.appState.currentTenant.pipe(\n filter(currentTenant => !!currentTenant),\n map(currentTenant => currentTenant.name),\n take(1)\n );\n\n navigatorNode$: Observable<NavigatorNode | NavigatorNode[]>;\n\n constructor() {\n this.navigatorNode$ = from(this.guard.canActivate()).pipe(\n filter(allowed => allowed),\n switchMap(() => this.currentTenantId),\n switchMap(currentTenantId => this.namespacesService.getNamespaces(currentTenantId)),\n map(({ namespaces }) => {\n if (!namespaces?.length) {\n return [];\n }\n namespaces.forEach(namespace => {\n const label = NAMESPACE_PROPS[namespace.name]?.label || namespace.name;\n const childNode = new NavigatorNode({\n label,\n path: `monitoring/messaging-service/namespace/${namespace.name}`,\n icon: NAMESPACE_PROPS[namespace.name]?.icon,\n routerLinkExact: false\n });\n baseNode.add(childNode);\n });\n return baseNode;\n }),\n catchError(() => {\n return [];\n }),\n shareReplay(1)\n );\n }\n\n get(): Observable<NavigatorNode | NavigatorNode[]> {\n return this.navigatorNode$;\n }\n}\n","import { formatNumber } from '@angular/common';\nimport { inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\n\n/**\n * In case of limits in Messaging management, value of '-1' means that there is no limit.\n */\n@Pipe({ name: 'backlogQuotaLimit', standalone: true })\nexport class BacklogQuotaLimitPipe implements PipeTransform {\n private translateService = inject(TranslateService);\n private locale = inject(LOCALE_ID);\n\n transform(value: number): number | string {\n if (value == null || isNaN(value)) {\n return '-';\n } else if (value === -1) {\n return this.translateService.instant(gettext('Unlimited` backlog quota`'));\n } else {\n return formatNumber(value, this.locale);\n }\n }\n}\n","import { Component, computed, input } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { C8yTranslatePipe, IconDirective } from '@c8y/ngx-components';\nimport { NgClass, NgIf, PercentPipe } from '@angular/common';\n\n/**\n * Usage component displays usage information in a form of e.g. \"51% used\".\n * It can be used in two ways:\n * 1. By providing `count` and `limit` inputs, it will calculate the usage percentage.\n * 2. By providing `percentage` input, it will use the provided percentage value.\n * Note: `percentage` input takes precedence over `count` and `limit` inputs.\n */\n@Component({\n selector: 'app-usage',\n standalone: true,\n imports: [IconDirective, NgIf, NgClass, IconDirective, C8yTranslatePipe, PercentPipe],\n templateUrl: './usage.component.html',\n host: { class: 'd-contents' }\n})\nexport class UsageComponent {\n count = input<number | null>(null);\n limit = input<number | null>(null);\n /**\n * Percentage of usage. Value range is from 0 to 100 (or more).\n * For example, if 10% is used, this value should be provided as 10 (not 0.1).\n */\n percentage = input<number | null>(null);\n\n /**\n * Usage as a fraction (e.g. if 50% is used, usage value will be 0.5)\n */\n usage = computed(() =>\n this.percentage() != null ? this.percentage() / 100 : this.getUsage(this.count(), this.limit())\n );\n status = computed(() => this.getStatus(this.usage()));\n usageToDisplay = gettext('{{ percentageOfQuota }} used');\n\n statusMap = {\n danger: ['tag--danger'],\n warning: ['tag--warning'],\n success: ['tag--success']\n };\n\n /**\n * Get usage as fraction of count and limit.\n * E.g. if count is 5 and limit is 10, returned usage will be 0.5\n * @param count Usage count\n * @param limit Usage limit\n * @returns Count divided by limit or null if count or limit is null or limit is -1 (indicates no limit)\n */\n private getUsage(count: number, limit: number): number | null {\n if (count == null || limit == null || limit === -1) {\n return null;\n }\n return count / limit;\n }\n\n private getStatus(usage: number | null) {\n if (usage == null) {\n return null;\n }\n const percentage = usage * 100;\n\n if (percentage >= 80) {\n return 'danger';\n } else if (percentage >= 50) {\n return 'warning';\n } else {\n return 'success';\n }\n }\n}\n","<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","import { Component, Input } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { C8yTranslateModule, IconDirective } from '@c8y/ngx-components';\nimport { CommonModule } from '@angular/common';\nimport { BacklogQuotaLimitPipe } from '../../../../utils/backlog-quota-limit.pipe';\nimport { UsageComponent } from '../../../shared/usage/usage.component';\n\nconst DataType = {\n publishers: 'publishers',\n subscribers: 'subscribers',\n topics: 'topics'\n} as const;\n\n@Component({\n selector: 'app-namespace-item-card',\n standalone: true,\n imports: [IconDirective, C8yTranslateModule, CommonModule, BacklogQuotaLimitPipe, UsageComponent],\n templateUrl: './namespace-item-card.component.html',\n host: { class: 'card m-b-0 fit-w' }\n})\nexport class NamespaceItemCardComponent {\n readonly DATA_TYPE = DataType;\n readonly ITEM_DETAILS: Record<keyof typeof DataType, { icon: string; title: string }> = {\n publishers: { icon: 'output', title: gettext('Publishers') },\n subscribers: { icon: 'input', title: gettext('Subscribers') },\n topics: { icon: 'day-view', title: gettext('Topics') }\n } as const;\n topicsLimitLabel = gettext('Limit: {{ backlogQuotaLimit }}');\n\n /**\n * The label of the service (already translated).\n */\n @Input() serviceLabel = '';\n @Input() limit?: number;\n @Input() dataType: keyof typeof DataType;\n @Input() value: number | undefined;\n}\n","<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","import { Component, inject, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { C8yTranslatePipe, IconDirective } from '@c8y/ngx-components';\nimport { NamespaceItemCardComponent } from './namespace-item-card/namespace-item-card.component';\nimport { NAMESPACE_PROPS } from '../../../utils/namespace-props';\nimport { MessagingNamespace } from '../../../api/model/namespace';\nimport { MessagingNamespacePolicies } from '../../../api/model/namespacePolicies';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Component({\n selector: 'app-namespace-item',\n standalone: true,\n imports: [CommonModule, IconDirective, NamespaceItemCardComponent, C8yTranslatePipe],\n templateUrl: './namespace-item.component.html'\n})\nexport class NamespaceItemComponent {\n private translateService = inject(TranslateService);\n\n @Input('namespaceName')\n set _namespaceName(name: string) {\n this.namespaceName = name;\n this.icon = NAMESPACE_PROPS[name]?.icon;\n this.namespaceLabel = this.translateService.instant(NAMESPACE_PROPS[name]?.label) || name;\n }\n namespaceName = '';\n namespaceLabel = '';\n icon = '';\n\n @Input() namespace: MessagingNamespace = {};\n @Input() policies: MessagingNamespacePolicies = {};\n}\n","<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","import { Component, inject, OnInit } from '@angular/core';\nimport {\n ActionBarItemComponent,\n AlertService,\n AppStateService,\n BreadcrumbModule,\n C8yTranslateModule,\n HeaderModule,\n HelpModule,\n IconDirective,\n LoadingComponent\n} from '@c8y/ngx-components';\nimport { CommonModule } from '@angular/common';\nimport { RouterLink } from '@angular/router';\nimport { NamespaceItemComponent } from './namespace-item/namespace-item.component';\nimport { MessagingNamespacesService } from '../../api/services/messaging-namespaces.service';\nimport { MessagingNamespaceDetails } from '../../api/model/namespaceDetails';\n\n@Component({\n selector: 'app-namespace-list',\n templateUrl: './namespace-list.component.html',\n imports: [\n CommonModule,\n HeaderModule,\n HelpModule,\n C8yTranslateModule,\n NamespaceItemComponent,\n RouterLink,\n BreadcrumbModule,\n ActionBarItemComponent,\n IconDirective,\n LoadingComponent\n ],\n standalone: true\n})\nexport class NamespaceListComponent implements OnInit {\n alertService = inject(AlertService);\n appState = inject(AppStateService);\n namespacesService = inject(MessagingNamespacesService);\n\n namespacesDetails: MessagingNamespaceDetails[];\n loading = true;\n\n async ngOnInit() {\n await this.reload();\n }\n\n async reload() {\n this.loading = true;\n try {\n const currentTenantId = this.appState.currentTenant.value.name;\n this.namespacesDetails = await this.namespacesService.getNamespacesDetails(currentTenantId);\n } catch (e) {\n this.alertService.addServerFailure(e);\n } finally {\n this.loading = false;\n }\n }\n}\n","<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","import { Pipe, PipeTransform } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\n\n@Pipe({\n name: 'timeToLive',\n standalone: true\n})\nexport class TimeToLivePipe implements PipeTransform {\n constructor(private translateService: TranslateService) {}\n\n /**\n * Transform time in seconds to human readable format.\n * @param seconds time in seconds\n * @returns human readable time period\n */\n transform(seconds: number | null): string {\n if (seconds == null || isNaN(seconds)) {\n return '-';\n }\n if (seconds === -1) {\n return this.translateService.instant(gettext('Unlimited` time-to-live period`'));\n }\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days >= 1) {\n const remainingHours = hours % 24;\n if (days === 1) {\n if (remainingHours === 0) {\n return this.translateService.instant(gettext('1 day'));\n }\n if (remainingHours === 1) {\n return this.translateService.instant(gettext('1 day 1 hour'));\n }\n return this.translateService.instant(gettext('1 day {{hours}} hours'), {\n hours: remainingHours\n });\n } else {\n if (remainingHours === 0) {\n return this.translateService.instant(gettext('{{days}} days'), { days });\n }\n if (remainingHours === 1) {\n return this.translateService.instant(gettext('{{days}} days 1 hour'), { days });\n }\n return this.translateService.instant(gettext('{{days}} days {{hours}} hours'), {\n days,\n hours: remainingHours\n });\n }\n }\n\n if (hours > 0) {\n const remainingMinutes = minutes % 60;\n if (hours === 1) {\n if (remainingMinutes === 0) {\n return this.translateService.instant(gettext('1 hour'));\n }\n if (remainingMinutes === 1) {\n return this.translateService.instant(gettext('1 hour 1 minute'));\n }\n return this.translateService.instant(gettext('1 hour {{minutes}} minutes'), {\n minutes: remainingMinutes\n });\n } else {\n if (remainingMinutes === 0) {\n return this.translateService.instant(gettext('{{hours}} hours'), { hours });\n }\n if (remainingMinutes === 1) {\n return this.translateService.instant(gettext('{{hours}} hours 1 minute'), { hours });\n }\n return this.translateService.instant(gettext('{{hours}} hours {{minutes}} minutes'), {\n hours,\n minutes: remainingMinutes\n });\n }\n }\n\n if (minutes > 0) {\n const remainingSeconds = seconds % 60;\n if (minutes === 1) {\n if (remainingSeconds === 0) {\n return this.translateService.instant(gettext('1 minute'));\n }\n if (remainingSeconds === 1) {\n return this.translateService.instant(gettext('1 minute 1 second'));\n }\n return this.translateService.instant(gettext('1 minute {{seconds}} seconds'), {\n seconds: remainingSeconds\n });\n } else {\n if (remainingSeconds === 0) {\n return this.translateService.instant(gettext('{{minutes}} minutes'), { minutes });\n }\n if (remainingSeconds === 1) {\n return this.translateService.instant(gettext('{{minutes}} minutes 1 second'), {\n minutes\n });\n }\n return this.translateService.instant(gettext('{{minutes}} minutes {{seconds}} seconds'), {\n minutes,\n seconds: remainingSeconds\n });\n }\n }\n\n if (seconds === 1) {\n return this.translateService.instant(gettext('1 second'));\n } else {\n return this.translateService.instant(gettext('{{seconds}} seconds'), { seconds });\n }\n }\n}\n","import { Injectable } from '@angular/core';\nimport { FetchClient, IResult, Paging, Service } from '@c8y/client';\nimport { MessagingTopicList } from '../model/topicList';\nimport { MessagingTopicListFilters } from '../model/topicListFilters';\nimport { MessagingTopic } from '../model/topic';\nimport { MessagingTopicDetailFilters } from '../model/topicDetailFilters';\n\n@Injectable({ providedIn: 'root' })\nexport class MessagingTopicsService extends Service<MessagingTopic> {\n protected baseUrl = '/service/messaging-management';\n protected listUrl = 'tenants';\n\n constructor(client: FetchClient) {\n super(client);\n }\n\n async list(filter: MessagingTopicListFilters) {\n const headers = { accept: 'application/json' };\n const { tenant, namespace, ...params } = filter;\n const url = `/${this.listUrl}/${tenant}/namespaces/${namespace}/topics`;\n const res = await this.fetch(url, this.changeFetchOptions({ headers, params }, url));\n const topicList = (await res.json()) as MessagingTopicList;\n const data = topicList.topics;\n const paging = this.getPaging(topicList, filter);\n return { res, data, paging };\n }\n\n async detail(filter: MessagingTopicDetailFilters): Promise<IResult<MessagingTopic>> {\n const headers = { accept: 'application/json' };\n const { tenant, namespace, topic, type } = filter;\n const url = `/${this.listUrl}/${tenant}/namespaces/${namespace}/topics/${topic}/types/${type}`;\n const res = await this.fetch(url, this.changeFetchOptions({ headers }, url));\n const data = await res.json();\n return { res, data };\n }\n\n protected getPaging(\n topicList: MessagingTopicList,\n filter: MessagingTopicListFilters\n ): Paging<MessagingTopic> {\n if (topicList.pageStatistics) {\n const { currentPage, totalPages } = topicList.pageStatistics;\n const statistics = {\n ...topicList.pageStatistics,\n nextPage: currentPage < totalPages ? currentPage + 1 : null,\n prevPage: currentPage > 1 ? currentPage - 1 : null\n };\n return new Paging<MessagingTopic>(this, statistics, filter);\n }\n return null;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { BaseColumn, Column, DataSourceModifier, ServerSideDataResult } from '@c8y/ngx-components';\nimport { MessagingTopicListFilters } from '../../api/model/topicListFilters';\nimport { MessagingTopicsService } from '../../api/services/messaging-topics.service';\n\n@Injectable({ providedIn: 'root' })\nexport class TopicsDataGridService {\n protected topicsService = inject(MessagingTopicsService);\n\n getColumns(): Column[] {\n return [\n this.createColumn({\n name: 'name',\n header: gettext('Name'),\n path: 'name',\n filterable: true,\n filteringConfig: {\n fields: [\n {\n key: 'name',\n type: 'input',\n props: {\n label: gettext('Filter topics by partial name'),\n placeholder: 'myTopic',\n required: true\n }\n }\n ],\n getFilter(model: { name: string }): string {\n return model.name;\n }\n }\n }),\n this.createColumn({\n name: 'msgRateIn',\n header: gettext('Message rate in (msg/s)'),\n path: 'msgRateIn'\n }),\n this.createColumn({\n name: 'msgRateOut',\n header: gettext('Message rate out (msg/s)'),\n path: 'msgRateOut'\n }),\n this.createColumn({\n name: 'subscribers',\n header: gettext('Subscribers'),\n path: 'subscribers'\n }),\n this.createColumn({\n name: 'backlogSize',\n header: gettext('Message backlog'),\n path: 'backlogSize'\n }),\n this.createColumn({\n name: 'backlogUsagePercentage',\n header: gettext('Used backlog'),\n path: 'backlogUsagePercentage',\n sortOrder: 'desc'\n })\n ];\n }\n\n async getServerSideData(\n tenantId: string,\n namespaceId: string,\n dataSourceModifier: DataSourceModifier\n ): Promise<ServerSideDataResult> {\n const topicFilters = this.getTopicFilters(tenantId, namespaceId, dataSourceModifier);\n const { res, data, paging } = await this.topicsService.list(topicFilters);\n const filteredSize = paging.totalElements;\n const size = (\n await this.topicsService.list({\n ...topicFilters,\n currentPage: 1,\n pageSize: 1\n })\n ).paging.totalPages;\n\n return {\n res,\n data,\n paging,\n size,\n filteredSize\n };\n }\n\n private createColumn(columnProps: Partial<BaseColumn>): BaseColumn {\n const column = new BaseColumn();\n Object.assign(column, columnProps);\n return column;\n }\n\n private getTopicFilters(\n tenantId: string,\n namespaceId: string,\n dataSourceModifier: DataSourceModifier\n ): MessagingTopicListFilters {\n const topicFilters: MessagingTopicListFilters = {\n tenant: tenantId,\n namespace: namespaceId,\n currentPage: dataSourceModifier.pagination.currentPage,\n pageSize: dataSourceModifier.pagination.pageSize\n };\n\n return dataSourceModifier.columns.reduce((topicFilters, column) => {\n if (column.filterable) {\n if (column.filterPredicate) {\n topicFilters[column.path] = column.filterPredicate;\n }\n\n if (column.externalFilterQuery) {\n topicFilters[column.path] = column.filteringConfig.getFilter(column.externalFilterQuery);\n }\n }\n\n if (column.sortable && column.sortOrder) {\n const sortPath = column.sortingConfig?.pathSortingConfigs?.[0]?.path || column.path;\n topicFilters.sort = `${sortPath},${column.sortOrder}`;\n }\n\n return topicFilters;\n }, topicFilters);\n }\n}\n","import { AfterViewInit, Component, DestroyRef, EventEmitter, inject } from '@angular/core';\nimport { ActivatedRoute, RouterLink } from '@angular/router';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n ActionBarItemComponent,\n AppStateService,\n BreadcrumbModule,\n BytesPipe,\n C8yTranslatePipe,\n Column,\n DataGridModule,\n DataSourceModifier,\n EmptyStateComponent,\n EmptyStateContextDirective,\n HeaderModule,\n IconDirective,\n LoadingComponent,\n Pagination,\n ServerSideDataCallback,\n ServerSideDataResult\n} from '@c8y/ngx-components';\nimport { AsyncPipe, DecimalPipe, NgClass, NgIf, PercentPipe } from '@angular/common';\nimport { MessagingNamespacesService } from '../../api/services/messaging-namespaces.service';\nimport { BacklogQuotaLimitPipe } from '../../utils/backlog-quota-limit.pipe';\nimport { UsageComponent } from '../shared/usage/usage.component';\nimport { TimeToLivePipe } from '../../utils/time-to-live.pipe';\nimport { NAMESPACE_PROPS } from '../../utils/namespace-props';\nimport { TranslateService } from '@ngx-translate/core';\nimport { TopicsDataGridService } from './topics-data-grid.service';\nimport { map, shareReplay, switchMap, tap } from 'rxjs/operators';\nimport { BehaviorSubject, combineLatest, firstValueFrom } from 'rxjs';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\n\n@Component({\n imports: [\n RouterLink,\n HeaderModule,\n DataGridModule,\n DecimalPipe,\n C8yTranslatePipe,\n BacklogQuotaLimitPipe,\n UsageComponent,\n BytesPipe,\n TimeToLivePipe,\n BreadcrumbModule,\n LoadingComponent,\n NgIf,\n ActionBarItemComponent,\n NgClass,\n IconDirective,\n PercentPipe,\n EmptyStateComponent,\n EmptyStateContextDirective,\n AsyncPipe\n ],\n selector: 'app-topic-list-view',\n standalone: true,\n templateUrl: './topic-list-view.component.html'\n})\nexport class TopicListViewComponent implements AfterViewInit {\n route = inject(ActivatedRoute);\n appState = inject(AppStateService);\n namespacesService = inject(MessagingNamespacesService);\n topicsDataGridService = inject(TopicsDataGridService);\n translateService = inject(TranslateService);\n destroyRef = inject(DestroyRef);\n\n loading$ = new BehaviorSubject<boolean>(false);\n refresh = new EventEmitter<void>();\n\n tenantId$ = this.appState.currentTenant.pipe(map(tenant => tenant.name));\n namespaceId$ = this.route.params.pipe(map(params => params['namespace'] as string));\n namespaceLabel$ = this.namespaceId$.pipe(\n map(namespaceId => this.translateService.instant(NAMESPACE_PROPS[namespaceId].label))\n );\n icon$ = this.namespaceId$.pipe(map(namespaceId => NAMESPACE_PROPS[namespaceId].icon));\n\n namespaceDetails$ = combineLatest([this.tenantId$, this.namespaceId$, this.refresh]).pipe(\n tap(() => this.loading$.next(true)),\n switchMap(([tenantId, namespaceId]) =>\n this.namespacesService.getNamespaceDetails(tenantId, namespaceId)\n ),\n tap(() => this.loading$.next(false)),\n shareReplay(1)\n );\n\n tableTitle = gettext('Topics');\n loadingItemsLabel = gettext('Loading topics...');\n loadMoreItemsLabel = gettext('Load more topics');\n noResultsMessage = gettext('No matching topics found.');\n noResultsSubtitle = gettext('Refine your search terms or check your spelling.');\n noDataMessage = gettext('No topics to display.');\n noDataSubtitle = gettext('Create new topics to monitor them here.');\n\n columns: Column[] = this.topicsDataGridService.getColumns();\n pagination: Pagination = {\n pageSize: 20,\n currentPage: 1\n };\n\n serverSideDataCallback: ServerSideDataCallback = this.onDataSourceModifier.bind(this);\n async onDataSourceModifier(\n dataSourceModifier: DataSourceModifier\n ): Promise<ServerSideDataResult> {\n return firstValueFrom(\n combineLatest([this.tenantId$, this.namespaceId$]).pipe(\n switchMap(([tenantId, namespaceId]) =>\n this.topicsDataGridService.getServerSideData(tenantId, namespaceId, dataSourceModifier)\n )\n )\n );\n }\n\n ngAfterViewInit() {\n this.route.params\n .pipe(takeUntilDestroyed(this.destroyRef))\n .subscribe(() => this.refresh.emit());\n }\n}\n","<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?.limit > 0\n ? (policies.backlogQuota.limit | bytes: 0 : true)\n : '-'\n }}\"\n >\n {{\n policies?.backlogQuota?.limit > 0\n ? (policies.backlogQuota.limit | bytes: 0)\n : '-'\n }}\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--backlog-ttl\"\n >\n <label class=\"small m-b-0 m-r-auto\">\n {{ 'Backlog time to live (TTL)' | translate }}\n </label>\n <span>{{ policies?.messageTTL | timeToLive }}</span>\n </li>\n </ul>\n }\n }\n </fieldset>\n </div>\n </div>\n </div>\n\n <c8y-data-grid\n class=\"d-contents\"\n [title]=\"tableTitle | translate\"\n [loadingItemsLabel]=\"loadingItemsLabel | translate\"\n [loadMoreItemsLabel]=\"loadMoreItemsLabel | translate\"\n [columns]=\"columns\"\n [pagination]=\"pagination\"\n [serverSideDataCallback]=\"serverSideDataCallback\"\n [refresh]=\"refresh\"\n [hideReload]=\"true\"\n >\n <c8y-ui-empty-state\n [icon]=\"stats?.size > 0 ? 'search' : 'day-view'\"\n [title]=\"stats?.size > 0 ? (noResultsMessage | translate) : (noDataMessage | translate)\"\n [subtitle]=\"stats?.size > 0 ? (noResultsSubtitle | translate) : (noDataSubtitle | translate)\"\n *emptyStateContext=\"let stats\"\n [horizontal]=\"stats?.size > 0\"\n ></c8y-ui-empty-state>\n\n <c8y-column name=\"name\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <a\n title=\"{{ context.value }}\"\n [routerLink]=\"['topic', context.item.id]\"\n >\n {{ context.value }}\n </a>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"msgRateIn\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value | number }}\">\n {{ context.value | number }}\n </span>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"msgRateOut\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value | number }}\">\n {{ context.value | number }}\n </span>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"subscribers\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <a\n title=\"{{ context.value | number }}\"\n [routerLink]=\"['topic', context.item.id, 'subscribers']\"\n >\n {{ context.value | number }}\n </a>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"backlogSize\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value | bytes: 2 : true }}\">\n {{ context.value | bytes: 2 }}\n </span>\n </ng-container>\n </c8y-column>\n\n <c8y-column name=\"backlogUsagePercentage\">\n <ng-container *c8yCellRendererDef=\"let context\">\n <span title=\"{{ context.value / 100 | percent: '1.0-2' }}\">\n {{ context.value / 100 | percent: '1.0-2' }}\n </span>\n </ng-container>\n </c8y-column>\n </c8y-data-grid>\n</div>\n","/**\n * If the topic is persistent or not. If false, the topic is not saved and will be deleted if there are no clients.\n */\nexport enum MessagingTopicType {\n Persistent = 'persistent',\n NonPersistent = 'non-persistent'\n}\n","import { Injectable } from '@angular/core';\nimport { FetchClient, Paging, Service } from '@c8y/client';\nimport { MessagingSubscriberList } from '../model/subscriberList';\nimport { MessagingSubscriber } from '../model/subscriber';\nimport { MessagingTopic } from '../model/topic';\nimport { MessagingSubscriberFilters } from '../model/subscriberFilters';\nimport { MessagingSubscriberToDelete } from '../model/subscriberToDelete';\nimport { MessagingTopicType } from '../model/topicType';\nimport { omit } from 'lodash-es';\n\n@Injectable({ providedIn: 'root' })\nexport class MessagingSubscribersService extends Service<MessagingSubscriber> {\n protected baseUrl = '/service/messaging-management';\n protected listUrl = 'tenants';\n\n constructor(client: FetchClient) {\n super(client);\n }\n\n /**\n * Get the list of subscribers for a topic.\n *\n * @param filter Subscriber filters.\n */\n async list(filter: MessagingSubscriberFilters) {\n const headers = { accept: 'application/json' };\n const params = omit(filter, ['tenant', 'namespace', 'topic', 'type']);\n const url = this.getBaseUrl(filter);\n const res = await this.fetch(url, this.changeFetchOptions({ headers, params }, url));\n const subscriberList = (await res.json()) as MessagingSubscriberList;\n const data = subscriberList.subscribers;\n const paging = this.getPaging(subscriberList, filter);\n return { res, data, paging };\n }\n\n async delete(subscriberToDelete: MessagingSubscriberToDelete) {\n const method = 'DELETE';\n const url = this.getBaseUrl(subscriberToDelete) + `/${subscriberToDelete.name}`;\n const res = await this.fetch(url, this.changeFetchOptions({ method }, url));\n return { res, data: null };\n }\n\n protected getPaging(json: MessagingSubscriberList, filter: object): Paging<MessagingSubscriber> {\n if (json.pageStatistics) {\n const { currentPage, totalPages } = json.pageStatistics;\n const statistics = {\n ...json.pageStatistics,\n nextPage: currentPage < totalPages ? currentPage + 1 : null,\n prevPage: currentPage > 1 ? currentPage - 1 : null\n };\n return new Paging<MessagingTopic>(this, statistics, filter);\n }\n return null;\n }\n\n private getBaseUrl({\n tenant,\n namespace,\n topic,\n type\n }: {\n tenant: string;\n namespace: string;\n topic: string;\n type: MessagingTopicType;\n }) {\n return `/${this.listUrl}/${tenant}/namespaces/${namespace}/topics/${topic}/types/${type}/subscribers`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n AlertService,\n BaseColumn,\n Column,\n DataSourceModifier,\n ModalService,\n ServerSideDataResult,\n Status\n} from '@c8y/ngx-components';\nimport { MessagingSubscribersService } from '../../../api/services/messaging-subscribers.service';\nimport { MessagingTopicType } from '../../../api/model/topicType';\nimport { MessagingSubscriberFilters } from '../../../api/model/subscriberFilters';\nimport { MessagingSubscriber } from '../../../api/model/subscriber';\nimport { TranslateService } from '@ngx-translate/core';\nimport { MessagingSubscriberToDelete } from '../../../api/model/subscriberToDelete';\n\n@Injectable({ providedIn: 'root' })\nexport class TopicSubscribersDataGridService {\n protected subscribersService = inject(MessagingSubscribersService);\n private alertService = inject(AlertService);\n private modalService = inject(ModalService);\n private translateService = inject(TranslateService);\n\n getColumns(): Column[] {\n return [\n this.createColumn({\n name: 'name',\n header: gettext('Name'),\n path: 'name',\n filterable: true,\n filteringConfig: {\n fields: [\n {\n key: 'name',\n type: 'input',\n props: {\n label: gettext('Filter subscribers by partial name'),\n placeholder: 'mySubscriber',\n required: true\n }\n }\n ],\n getFilter(model: { name: string }): string {\n return model.name;\n }\n }\n }),\n this.createColumn({\n name: 'activeClients',\n header: gettext('Connected clients'),\n path: 'activeClients'\n }),\n this.createColumn({\n name: 'messageAckRate',\n header: gettext('Acknowledgement rate (msg/s)'),\n path: 'messageAckRate'\n }),\n this.createColumn({\n name: 'lastAcknowledgeDate',\n header: gettext('Last acknowledged'),\n path: 'lastAcknowledgeDate',\n sortingConfig: {\n pathSortingConfigs: [\n {\n path: 'lastAcknowledgeTimestamp'\n }\n ]\n }\n }),\n this.createColumn({\n name: 'unackMsgBacklog',\n header: gettext('Unacknowledged messages'),\n path: 'unackMsgBacklog'\n }),\n this.createColumn({\n name: 'backlogUsagePercentage',\n header: gettext('Used backlog'),\n path: 'backlogUsagePercentage',\n sortOrder: 'desc'\n })\n ];\n }\n\n async getServerSideData(\n tenantId: string,\n namespaceId: string,\n topicId: string,\n topicType: MessagingTopicType,\n dataSourceModifier: DataSourceModifier\n ): Promise<ServerSideDataResult> {\n const subscriberFilters = this.getSubscriberFilters(\n tenantId,\n