@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
523 lines (515 loc) • 126 kB
JavaScript
import { JsonPipe, KeyValuePipe } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, Injectable, signal, computed, DestroyRef, ChangeDetectionStrategy, Component, Pipe, input, output, viewChild, effect, untracked, ElementRef, ViewChildren } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, RouterLink, Router, NavigationEnd, RouterLinkActive, RouterOutlet } from '@angular/router';
import * as i1 from '@c8y/client';
import { EventBinaryService, EventService, InventoryService } from '@c8y/client';
import { FilesService, GenericFileIconPipe, AlertService, ColorService, IconDirective, IconPanelComponent, LoadingComponent, AssetLinkPipe, C8yTranslatePipe, DatePipe, HumanizePipe, ViewContext, ContextRouteService, FormGroupComponent, C8yTranslateDirective, DateTimePickerComponent, MessagesComponent, MessageDirective, CountdownIntervalComponent, ForOfDirective, ListGroupComponent, ListItemBodyComponent, ListItemComponent, ListItemIconComponent, ListItemTimelineComponent, SplitViewAlertsComponent, SplitViewHeaderActionsComponent, SplitViewListComponent, EmptyStateComponent, TitleComponent, HelpComponent, ActionBarItemComponent, SplitViewComponent, SplitViewDetailsComponent } from '@c8y/ngx-components';
import { gettext } from '@c8y/ngx-components/gettext';
import { saveAs } from 'file-saver';
import { PopoverDirective } from 'ngx-bootstrap/popover';
import { pickBy, has, without, difference, keys, includes } from 'lodash-es';
import { INTERVAL_TITLES, INTERVALS, IntervalPickerComponent } from '@c8y/ngx-components/interval-picker';
import * as i2 from '@c8y/ngx-components/global-context';
import { Subject, take, timer, fromEvent } from 'rxjs';
import { filter, switchMap, map, distinctUntilChanged, debounceTime } from 'rxjs/operators';
import * as i1$1 from '@angular/forms';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BsDropdownDirective, BsDropdownToggleDirective, BsDropdownMenuDirective } from 'ngx-bootstrap/dropdown';
import { TooltipDirective } from 'ngx-bootstrap/tooltip';
import * as i1$2 from '@c8y/ngx-components/file-preview';
import { FilePreviewModule } from '@c8y/ngx-components/file-preview';
import { BsModalService } from 'ngx-bootstrap/modal';
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { AlarmEventSelectorService } from '@c8y/ngx-components/alarm-event-selector';
const EVENT_RESERVED_KEYS = [
'creationTime',
'id',
'lastUpdated',
'self',
'source',
'text',
'time',
'type',
'c8y_IsBinary'
];
const EVENT_STANDARD_KEYS = {
type: gettext('Type'),
text: gettext('Text'),
lastUpdated: gettext('Last updated')
};
const EVENTS_PATH = 'events';
/**
* Extended interval titles with an additional title for the case when no date is selected.
*/
const INTERVAL_TITLES_EXTENDED = {
...INTERVAL_TITLES,
none: gettext('No date filter')
};
const INTERVALS_EXTENDED = [
{
id: 'none',
title: gettext('No date filter')
},
...INTERVALS
];
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
class EventsService {
constructor() {
this.filesService = inject(FilesService);
this.fileIconPipe = new GenericFileIconPipe(this.filesService);
}
resolveFileIcon(fileName) {
const extension = this.filesService.getFileExtension(fileName);
if (!extension) {
return 'file-o';
}
const genericType = this.fileIconPipe.getGenericType({ name: fileName });
return this.fileIconPipe.getIcon(genericType);
}
getStandardKeys(event) {
return pickBy(EVENT_STANDARD_KEYS, (_, key) => has(event, key));
}
getNonStandardKeys(event, excluding = []) {
return without(difference(this.getKeys(event), keys(this.getStandardKeys(event))), ...excluding);
}
isImageBinary(binaryInfo) {
if (!binaryInfo?.name) {
return false;
}
const extension = this.filesService.getFileExtension(binaryInfo.name);
return extension ? IMAGE_EXTENSIONS.includes(extension) : false;
}
getCustomFragments(event) {
const nonStandardKeys = this.getNonStandardKeys(event);
if (nonStandardKeys.length === 0) {
return null;
}
const fragments = {};
for (const key of nonStandardKeys) {
fragments[key] = event[key];
}
return fragments;
}
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunks = [];
for (let i = 0; i < bytes.length; i += 8192) {
chunks.push(String.fromCharCode(...bytes.subarray(i, i + 8192)));
}
return btoa(chunks.join(''));
}
getKeys(managedObject) {
return Object.keys({ ...managedObject }).filter(key => !includes(EVENT_RESERVED_KEYS, key));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class EventDetailsComponent {
constructor() {
this.CREATION_TIME_HELP_TEXT = gettext("Time in which the event was created on the server. The time shown corresponds to the server's time.");
this.EVENT_ICON = 'online1';
this.TIME_HELP_TEXT = gettext('Time in which the event was created on the device. Device time can be different from server time.');
this.binaryPreviewLoading = signal(false, ...(ngDevMode ? [{ debugName: "binaryPreviewLoading" }] : []));
this.binaryPreviewUrl = signal(null, ...(ngDevMode ? [{ debugName: "binaryPreviewUrl" }] : []));
this.binaryPreviewFailed = signal(false, ...(ngDevMode ? [{ debugName: "binaryPreviewFailed" }] : []));
this.customFragments = signal(null, ...(ngDevMode ? [{ debugName: "customFragments" }] : []));
this.isDownloading = signal(false, ...(ngDevMode ? [{ debugName: "isDownloading" }] : []));
this.isLoading = signal(true, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
this.selectedEvent = signal(null, ...(ngDevMode ? [{ debugName: "selectedEvent" }] : []));
this.selectedEventSource = signal(null, ...(ngDevMode ? [{ debugName: "selectedEventSource" }] : []));
this.typeColor = signal('', ...(ngDevMode ? [{ debugName: "typeColor" }] : []));
this.binaryInfo = computed(() => this.selectedEvent()?.['c8y_IsBinary'] ?? null, ...(ngDevMode ? [{ debugName: "binaryInfo" }] : []));
this.hasBinary = computed(() => !!this.binaryInfo(), ...(ngDevMode ? [{ debugName: "hasBinary" }] : []));
this.canDownload = computed(() => this.binaryInfo()?.length !== undefined, ...(ngDevMode ? [{ debugName: "canDownload" }] : []));
this.binaryIcon = computed(() => {
const info = this.binaryInfo();
if (!info?.name) {
return 'file-o';
}
return this.eventsService.resolveFileIcon(info.name);
}, ...(ngDevMode ? [{ debugName: "binaryIcon" }] : []));
this.activatedRoute = inject(ActivatedRoute);
this.alertService = inject(AlertService);
this.colorService = inject(ColorService);
this.destroyRef = inject(DestroyRef);
this.eventBinaryService = inject(EventBinaryService);
this.eventService = inject(EventService);
this.eventsService = inject(EventsService);
this.inventoryService = inject(InventoryService);
}
ngOnInit() {
this.activatedRoute.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(params => {
if (params['id']) {
void this.loadEventDetails(params['id']);
}
});
}
isComplexValue(value) {
return typeof value === 'object' && value !== null;
}
async downloadBinary() {
const event = this.selectedEvent();
const binaryInfo = this.binaryInfo();
if (!event || !binaryInfo) {
return;
}
try {
this.isDownloading.set(true);
const response = await this.eventBinaryService.download(event);
const arrayBuffer = await response.arrayBuffer();
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
const blob = new Blob([arrayBuffer], { type: contentType });
saveAs(blob, binaryInfo.name || 'attachment');
}
catch (error) {
this.alertService.addServerFailure(error);
}
finally {
this.isDownloading.set(false);
}
}
async loadEventDetails(eventId) {
try {
this.isLoading.set(true);
this.binaryPreviewUrl.set(null);
this.binaryPreviewFailed.set(false);
const { data: event } = await this.eventService.detail(eventId);
this.selectedEvent.set(event);
const [source, color] = await Promise.all([
this.loadEventSource(event.source?.id),
this.colorService.generateColor(event.type)
]);
this.selectedEventSource.set(source);
this.typeColor.set(color);
this.customFragments.set(this.eventsService.getCustomFragments(event));
if (event['c8y_IsBinary'] && this.eventsService.isImageBinary(event['c8y_IsBinary'])) {
void this.loadBinaryPreview(event);
}
}
catch (error) {
this.alertService.addServerFailure(error);
}
finally {
this.isLoading.set(false);
}
}
async loadEventSource(sourceId) {
if (!sourceId) {
return null;
}
try {
const { data } = await this.inventoryService.detail(sourceId);
return data;
}
catch {
return null;
}
}
async loadBinaryPreview(event) {
try {
this.binaryPreviewLoading.set(true);
const response = await this.eventBinaryService.download(event);
const arrayBuffer = await response.arrayBuffer();
const contentType = response.headers.get('Content-Type') || 'image/png';
const base64 = this.eventsService.arrayBufferToBase64(arrayBuffer);
this.binaryPreviewUrl.set(`data:${contentType};base64,${base64}`);
}
catch {
this.binaryPreviewFailed.set(true);
}
finally {
this.binaryPreviewLoading.set(false);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventDetailsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: EventDetailsComponent, isStandalone: true, selector: "c8y-event-details", ngImport: i0, template: "@if (isLoading()) {\n <c8y-loading />\n} @else if (selectedEvent(); as event) {\n <!-- Header with event text -->\n <div\n class=\"card-header p-24 m-b-16 bg-component separator-bottom sticky-top\"\n style=\"margin: 0 -24px\"\n >\n <h4\n class=\"m-0\"\n data-cy=\"c8y-event-details-title\"\n >\n {{ event.text | translate }}\n </h4>\n </div>\n\n <c8y-icon-panel [sections]=\"[]\">\n <!-- Source section -->\n <div\n class=\"col-xs-12 col-md-6 d-flex p-b-8\"\n data-cy=\"c8y-event-details--source-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 m-t-4 stroked-icon status\"\n c8yIcon=\"contactless-payment\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 min-width-0\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Source' | translate }}</p>\n <p class=\"small\">\n @if (selectedEventSource(); as source) {\n <button\n class=\"btn-link p-0 m-r-8 text-left text-truncate\"\n [title]=\"source.name || event.source?.id\"\n type=\"button\"\n [routerLink]=\"source | assetLink\"\n >\n <i c8yIcon=\"exchange\"></i>\n {{ source.name || event.source?.id }}\n </button>\n } @else {\n <span class=\"text-muted\">{{ event.source?.id }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n\n <!-- Type section -->\n <div\n class=\"col-xs-12 col-md-6 d-flex p-b-8\"\n data-cy=\"c8y-event-details--type-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <span\n class=\"circle-icon-wrapper\"\n [style.background-color]=\"typeColor()\"\n >\n <i\n class=\"stroked-icon\"\n [c8yIcon]=\"EVENT_ICON\"\n ></i>\n </span>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 min-width-0\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Type' | translate }}</p>\n <p\n class=\"small text-truncate\"\n [title]=\"event.type\"\n >\n <code>{{ event.type }}</code>\n </p>\n </div>\n </div>\n </div>\n\n <!-- Time, Server creation time, and Last updated -->\n <div class=\"col-xs-12 col-md-12 p-b-16\">\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n c8yIcon=\"calendar\"\n data-cy=\"c8y-event-details--calendar-icon\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-0 p-r-8 flex-grow\">\n <div class=\"content-flex-50\">\n <!-- Device time -->\n <div\n class=\"col-4 p-b-8\"\n data-cy=\"c8y-event-details--time-wrapper\"\n >\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Time' | translate }}</p>\n <p class=\"small\">\n {{ event.time | c8yDate: 'medium' }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"TIME_HELP_TEXT | translate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </p>\n </div>\n <!-- Server creation time -->\n <div\n class=\"col-4 p-b-8\"\n data-cy=\"c8y-event-details--creation-time-wrapper\"\n >\n <p class=\"text-label-small m-b-0 m-r-8\">\n {{ 'Server creation time' | translate }}\n </p>\n <p class=\"small\">\n {{ event.creationTime | c8yDate: 'medium' }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"CREATION_TIME_HELP_TEXT | translate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </p>\n </div>\n <!-- Last updated -->\n @if (event['lastUpdated']) {\n <div\n class=\"col-4 p-b-8\"\n data-cy=\"c8y-event-details--last-updated-wrapper\"\n >\n <p class=\"text-label-small m-b-0 m-r-8\">\n {{ 'Last updated' | translate }}\n </p>\n <p class=\"small\">\n {{ event['lastUpdated'] | c8yDate: 'medium' }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Custom fragments section -->\n @if (customFragments(); as fragments) {\n <div\n class=\"col-xs-12 col-md-12 p-b-16\"\n data-cy=\"c8y-event-details--custom-fragments-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n c8yIcon=\"outgoing-data\"\n ></i>\n </div>\n <div\n class=\"p-t-8 p-b-0 p-r-8 flex-grow\"\n data-cy=\"event-details-custom-data\"\n >\n <ul class=\"list-unstyled small m-b-0\">\n @for (item of fragments | keyvalue; track item.key) {\n <li\n class=\"p-t-4 p-b-4 separator-bottom\"\n data-cy=\"event-details-custom-data-item\"\n >\n <label class=\"small m-b-4 text-label-small d-block\">\n {{ item.key | humanize }}\n </label>\n @if (isComplexValue(item.value)) {\n <pre class=\"m-b-0\"><code>{{ item.value | json }}</code></pre>\n } @else {\n <span>{{ item.value }}</span>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </div>\n }\n\n <!-- Attachment section -->\n @if (hasBinary()) {\n <div\n class=\"col-xs-12 col-md-12 p-b-16\"\n data-cy=\"c8y-event-details--attachment-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n [c8yIcon]=\"binaryIcon()\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 flex-grow\">\n <p class=\"text-label-small m-b-4 m-r-8\">{{ 'Attachment' | translate }}</p>\n\n <!-- Image preview -->\n @if (binaryPreviewLoading()) {\n <div class=\"m-b-16\">\n <c8y-loading></c8y-loading>\n </div>\n } @else if (binaryPreviewUrl(); as previewUrl) {\n @let imgAltText = 'Attachment preview' | translate;\n <div class=\"m-b-16\">\n <img\n class=\"max-width-100\"\n [src]=\"previewUrl\"\n [alt]=\"binaryInfo()?.name || imgAltText\"\n data-cy=\"c8y-event-details--attachment-preview\"\n />\n </div>\n } @else if (binaryPreviewFailed()) {\n <p\n class=\"text-muted small m-b-8\"\n translate\n >\n Preview unavailable.\n </p>\n }\n\n <!-- Download button -->\n @if (canDownload()) {\n <button\n class=\"btn btn-primary btn-xs\"\n [title]=\"'Download' | translate\"\n [attr.aria-busy]=\"isDownloading()\"\n type=\"button\"\n [disabled]=\"isDownloading()\"\n (click)=\"downloadBinary()\"\n data-cy=\"c8y-event-details--download-btn\"\n >\n <i [c8yIcon]=\"isDownloading() ? 'spinner' : 'download'\"></i>\n <span>{{ 'Download' | translate }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n }\n </c8y-icon-panel>\n}\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: IconPanelComponent, selector: "c8y-icon-panel", inputs: ["sections", "ariaLabel"] }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "directive", type: PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: AssetLinkPipe, name: "assetLink" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }, { kind: "pipe", type: HumanizePipe, name: "humanize" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventDetailsComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-event-details', imports: [
AssetLinkPipe,
C8yTranslatePipe,
DatePipe,
HumanizePipe,
IconDirective,
IconPanelComponent,
JsonPipe,
KeyValuePipe,
LoadingComponent,
PopoverDirective,
RouterLink
], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isLoading()) {\n <c8y-loading />\n} @else if (selectedEvent(); as event) {\n <!-- Header with event text -->\n <div\n class=\"card-header p-24 m-b-16 bg-component separator-bottom sticky-top\"\n style=\"margin: 0 -24px\"\n >\n <h4\n class=\"m-0\"\n data-cy=\"c8y-event-details-title\"\n >\n {{ event.text | translate }}\n </h4>\n </div>\n\n <c8y-icon-panel [sections]=\"[]\">\n <!-- Source section -->\n <div\n class=\"col-xs-12 col-md-6 d-flex p-b-8\"\n data-cy=\"c8y-event-details--source-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 m-t-4 stroked-icon status\"\n c8yIcon=\"contactless-payment\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 min-width-0\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Source' | translate }}</p>\n <p class=\"small\">\n @if (selectedEventSource(); as source) {\n <button\n class=\"btn-link p-0 m-r-8 text-left text-truncate\"\n [title]=\"source.name || event.source?.id\"\n type=\"button\"\n [routerLink]=\"source | assetLink\"\n >\n <i c8yIcon=\"exchange\"></i>\n {{ source.name || event.source?.id }}\n </button>\n } @else {\n <span class=\"text-muted\">{{ event.source?.id }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n\n <!-- Type section -->\n <div\n class=\"col-xs-12 col-md-6 d-flex p-b-8\"\n data-cy=\"c8y-event-details--type-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <span\n class=\"circle-icon-wrapper\"\n [style.background-color]=\"typeColor()\"\n >\n <i\n class=\"stroked-icon\"\n [c8yIcon]=\"EVENT_ICON\"\n ></i>\n </span>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 min-width-0\">\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Type' | translate }}</p>\n <p\n class=\"small text-truncate\"\n [title]=\"event.type\"\n >\n <code>{{ event.type }}</code>\n </p>\n </div>\n </div>\n </div>\n\n <!-- Time, Server creation time, and Last updated -->\n <div class=\"col-xs-12 col-md-12 p-b-16\">\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n c8yIcon=\"calendar\"\n data-cy=\"c8y-event-details--calendar-icon\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-0 p-r-8 flex-grow\">\n <div class=\"content-flex-50\">\n <!-- Device time -->\n <div\n class=\"col-4 p-b-8\"\n data-cy=\"c8y-event-details--time-wrapper\"\n >\n <p class=\"text-label-small m-b-0 m-r-8\">{{ 'Time' | translate }}</p>\n <p class=\"small\">\n {{ event.time | c8yDate: 'medium' }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"TIME_HELP_TEXT | translate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </p>\n </div>\n <!-- Server creation time -->\n <div\n class=\"col-4 p-b-8\"\n data-cy=\"c8y-event-details--creation-time-wrapper\"\n >\n <p class=\"text-label-small m-b-0 m-r-8\">\n {{ 'Server creation time' | translate }}\n </p>\n <p class=\"small\">\n {{ event.creationTime | c8yDate: 'medium' }}\n <button\n class=\"btn-help btn-help--sm\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"CREATION_TIME_HELP_TEXT | translate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n ></button>\n </p>\n </div>\n <!-- Last updated -->\n @if (event['lastUpdated']) {\n <div\n class=\"col-4 p-b-8\"\n data-cy=\"c8y-event-details--last-updated-wrapper\"\n >\n <p class=\"text-label-small m-b-0 m-r-8\">\n {{ 'Last updated' | translate }}\n </p>\n <p class=\"small\">\n {{ event['lastUpdated'] | c8yDate: 'medium' }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Custom fragments section -->\n @if (customFragments(); as fragments) {\n <div\n class=\"col-xs-12 col-md-12 p-b-16\"\n data-cy=\"c8y-event-details--custom-fragments-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n c8yIcon=\"outgoing-data\"\n ></i>\n </div>\n <div\n class=\"p-t-8 p-b-0 p-r-8 flex-grow\"\n data-cy=\"event-details-custom-data\"\n >\n <ul class=\"list-unstyled small m-b-0\">\n @for (item of fragments | keyvalue; track item.key) {\n <li\n class=\"p-t-4 p-b-4 separator-bottom\"\n data-cy=\"event-details-custom-data-item\"\n >\n <label class=\"small m-b-4 text-label-small d-block\">\n {{ item.key | humanize }}\n </label>\n @if (isComplexValue(item.value)) {\n <pre class=\"m-b-0\"><code>{{ item.value | json }}</code></pre>\n } @else {\n <span>{{ item.value }}</span>\n }\n </li>\n }\n </ul>\n </div>\n </div>\n </div>\n }\n\n <!-- Attachment section -->\n @if (hasBinary()) {\n <div\n class=\"col-xs-12 col-md-12 p-b-16\"\n data-cy=\"c8y-event-details--attachment-wrapper\"\n >\n <div class=\"border-all fit-w d-flex\">\n <div class=\"p-8\">\n <i\n class=\"icon-24 text-gray-dark m-t-4\"\n [c8yIcon]=\"binaryIcon()\"\n ></i>\n </div>\n <div class=\"p-t-8 p-b-8 p-r-8 flex-grow\">\n <p class=\"text-label-small m-b-4 m-r-8\">{{ 'Attachment' | translate }}</p>\n\n <!-- Image preview -->\n @if (binaryPreviewLoading()) {\n <div class=\"m-b-16\">\n <c8y-loading></c8y-loading>\n </div>\n } @else if (binaryPreviewUrl(); as previewUrl) {\n @let imgAltText = 'Attachment preview' | translate;\n <div class=\"m-b-16\">\n <img\n class=\"max-width-100\"\n [src]=\"previewUrl\"\n [alt]=\"binaryInfo()?.name || imgAltText\"\n data-cy=\"c8y-event-details--attachment-preview\"\n />\n </div>\n } @else if (binaryPreviewFailed()) {\n <p\n class=\"text-muted small m-b-8\"\n translate\n >\n Preview unavailable.\n </p>\n }\n\n <!-- Download button -->\n @if (canDownload()) {\n <button\n class=\"btn btn-primary btn-xs\"\n [title]=\"'Download' | translate\"\n [attr.aria-busy]=\"isDownloading()\"\n type=\"button\"\n [disabled]=\"isDownloading()\"\n (click)=\"downloadBinary()\"\n data-cy=\"c8y-event-details--download-btn\"\n >\n <i [c8yIcon]=\"isDownloading() ? 'spinner' : 'download'\"></i>\n <span>{{ 'Download' | translate }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n }\n </c8y-icon-panel>\n}\n" }]
}] });
const DEFAULT_EVENT_ICON = 'online1';
class EventIconPipe {
constructor() {
this.eventsService = inject(EventsService);
}
transform(event) {
const binaryInfo = event['c8y_IsBinary'];
return binaryInfo?.name
? this.eventsService.resolveFileIcon(binaryInfo.name)
: DEFAULT_EVENT_ICON;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventIconPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: EventIconPipe, isStandalone: true, name: "eventIcon" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventIconPipe, decorators: [{
type: Pipe,
args: [{
name: 'eventIcon',
standalone: true
}]
}] });
class EventIsImagePipe {
constructor() {
this.eventsService = inject(EventsService);
}
transform(event) {
return this.eventsService.isImageBinary(event['c8y_IsBinary']);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventIsImagePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: EventIsImagePipe, isStandalone: true, name: "eventIsImage" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventIsImagePipe, decorators: [{
type: Pipe,
args: [{
name: 'eventIsImage',
standalone: true
}]
}] });
class EventsViewService {
constructor(eventService, dateTimeContextPickerService) {
this.eventService = eventService;
this.dateTimeContextPickerService = dateTimeContextPickerService;
this.DEFAULT_PAGE_SIZE = 50;
this.DEFAULT_REFRESH_INTERVAL = 30_000;
this.reloadEventsList$ = new Subject();
}
/**
* Emits a subject to trigger events list reload.
*/
updateEventsList() {
this.reloadEventsList$.next();
}
/**
* Retrieves a list of events with optional filters.
*/
async retrieveEvents(filter) {
const queryFilter = {
pageSize: filter?.pageSize ?? this.DEFAULT_PAGE_SIZE,
withTotalPages: true
};
if (filter?.source) {
queryFilter['source'] = filter.source;
queryFilter['withSourceChildren'] = filter.withSourceChildren ?? true;
}
if (filter?.type) {
queryFilter['type'] = filter.type;
}
if (filter?.dateFrom) {
queryFilter['dateFrom'] = filter.dateFrom.toISOString();
}
if (filter?.dateTo) {
queryFilter['dateTo'] = filter.dateTo.toISOString();
}
return this.eventService.list(queryFilter);
}
/**
* Returns the correct link based on the provided context data.
*/
getRouterLink(contextData, event) {
let detailUrl = `/${EVENTS_PATH}`;
if (event) {
detailUrl = `/${EVENTS_PATH}/${event.id}`;
}
if (!contextData) {
return detailUrl;
}
switch (contextData.context) {
case ViewContext.Device:
return `/device/${contextData.contextData.id}${detailUrl}`;
case ViewContext.Group:
return `/group/${contextData.contextData.id}${detailUrl}`;
default:
return detailUrl;
}
}
/**
* Returns the correct from and to dates based on the selected interval
* @param intervalId the selected interval. E.g. 'none', 'hours', 'custom' ...
* @returns The calculated date context based on the selected interval.
*/
buildEventsFilter(params) {
const dateFrom = params.dateTimeContext?.dateFrom ?? params.dateRange?.[0];
const dateTo = params.dateTimeContext?.dateTo ?? params.dateRange?.[1];
return {
...(params.source && {
source: params.source,
withSourceChildren: params.withSourceChildren ?? true
}),
...(params.type && { type: params.type }),
...(dateFrom && { dateFrom: new Date(dateFrom) }),
...(dateTo && { dateTo: new Date(dateTo) })
};
}
getDateTimeContextByInterval(intervalId) {
return this.dateTimeContextPickerService.getDateTimeContextByInterval(intervalId);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsViewService, deps: [{ token: i1.EventService }, { token: i2.DateTimeContextPickerService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsViewService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsViewService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: i1.EventService }, { type: i2.DateTimeContextPickerService }] });
class EventRouterLinkPipe {
constructor() {
this.activatedRoute = inject(ActivatedRoute);
this.contextRouteService = inject(ContextRouteService);
this.eventsViewService = inject(EventsViewService);
}
transform(event, alwaysNavigateToAllEvents = false) {
if (alwaysNavigateToAllEvents) {
return this.eventsViewService.getRouterLink(null, event);
}
const contextData = this.contextRouteService.getContextData(this.activatedRoute);
return this.eventsViewService.getRouterLink(contextData, event);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventRouterLinkPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: EventRouterLinkPipe, isStandalone: true, name: "eventRouterLink" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventRouterLinkPipe, decorators: [{
type: Pipe,
args: [{
name: 'eventRouterLink',
standalone: true
}]
}] });
class EventsDateFilterComponent {
constructor() {
this.INTERVALS = INTERVALS_EXTENDED;
this.INTERVAL_TITLES = INTERVAL_TITLES_EXTENDED;
this.DATE_FORMAT = 'short';
this.defaultInterval = input('none', ...(ngDevMode ? [{ debugName: "defaultInterval" }] : []));
this.updateQueryParams = input(true, ...(ngDevMode ? [{ debugName: "updateQueryParams" }] : []));
this.date = signal(undefined, ...(ngDevMode ? [{ debugName: "date" }] : []));
this.noFilterLabel = gettext('No date filter');
this.dateFilterChange = output();
this.dropdown = viewChild(BsDropdownDirective, ...(ngDevMode ? [{ debugName: "dropdown" }] : []));
this.activatedRoute = inject(ActivatedRoute);
this.destroyRef = inject(DestroyRef);
this.eventsViewService = inject(EventsViewService);
this.formBuilder = inject(FormBuilder);
this.router = inject(Router);
}
ngOnInit() {
const context = this.getDefaultContext();
this.form = this.createForm(context);
this.date.set([
this.form.value.currentDateContextFromDate,
this.form.value.currentDateContextToDate
]);
this.activatedRoute.queryParams
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
.subscribe(params => {
if (!params.interval) {
return;
}
if (params.interval !== 'custom') {
this.updateDateTime(params.interval);
}
else {
this.form.patchValue({
currentDateContextInterval: params.interval,
temporaryUserSelectedFromDate: params.dateFrom,
temporaryUserSelectedToDate: params.dateTo
});
this.date.set([params.dateFrom, params.dateTo]);
}
});
this.subscribeToIntervalChange();
}
applyDateFilter() {
const interval = this.form.value.currentDateContextInterval;
const isNoDateFilter = interval === 'none';
const combinedFormEvent = {
interval,
selectedDates: isNoDateFilter
? undefined
: [
new Date(this.form.value.temporaryUserSelectedFromDate),
new Date(this.form.value.temporaryUserSelectedToDate)
]
};
// needed for custom interval
this.date.set([
this.form.value.temporaryUserSelectedFromDate,
this.form.value.temporaryUserSelectedToDate
]);
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: {
interval,
dateFrom: isNoDateFilter ? null : combinedFormEvent.selectedDates[0].toISOString(),
dateTo: isNoDateFilter ? null : combinedFormEvent.selectedDates[1].toISOString()
},
queryParamsHandling: 'merge'
});
this.dateFilterChange.emit(combinedFormEvent);
}
updateDateTime(interval) {
const date = this.eventsViewService.getDateTimeContextByInterval(interval);
const dropdown = this.dropdown();
if (dropdown) {
dropdown.isOpen = false;
}
this.date.set(date.map(d => d.toISOString()));
this.form.patchValue({
temporaryUserSelectedFromDate: date[0].toISOString(),
temporaryUserSelectedToDate: date[1].toISOString(),
currentDateContextInterval: interval
}, { emitEvent: false });
this.applyDateFilter();
}
getDefaultContext() {
return {
date: this.eventsViewService.getDateTimeContextByInterval(this.defaultInterval()),
interval: this.defaultInterval()
};
}
subscribeToIntervalChange() {
this.form.controls.currentDateContextInterval.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(interval => {
if (interval === 'custom') {
this.form.patchValue({
temporaryUserSelectedFromDate: this.form.controls.temporaryUserSelectedFromDate.value === new Date(0).toISOString()
? this.form.controls.currentDateContextToDate.value
: this.form.controls.temporaryUserSelectedFromDate.value,
currentDateContextInterval: interval
}, { emitEvent: false });
return;
}
this.updateDateTime(interval);
});
}
createForm(context) {
return this.formBuilder.group({
temporaryUserSelectedFromDate: context.date[0].toISOString(),
temporaryUserSelectedToDate: context.date[1].toISOString(),
currentDateContextFromDate: context.date[0].toISOString(),
currentDateContextToDate: context.date[1].toISOString(),
currentDateContextInterval: context.interval || 'custom'
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: EventsDateFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: EventsDateFilterComponent, isStandalone: true, selector: "c8y-events-date-filter", inputs: { defaultInterval: { classPropertyName: "defaultInterval", publicName: "defaultInterval", isSignal: true, isRequired: false, transformFunction: null }, updateQueryParams: { classPropertyName: "updateQueryParams", publicName: "updateQueryParams", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dateFilterChange: "dateFilterChange" }, viewQueries: [{ propertyName: "dropdown", first: true, predicate: BsDropdownDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<form\n class=\"d-flex gap-16 p-l-xs-16 p-r-xs-16 m-t-xs-8 m-b-xs-8\"\n [formGroup]=\"form\"\n>\n <div\n class=\"dropdown flex-grow\"\n #dropDirection=\"bs-dropdown\"\n dropdown\n [insideClick]=\"true\"\n >\n <button\n class=\"dropdown-toggle form-control l-h-tight d-flex a-i-center\"\n attr.aria-label=\"{{\n (form.value.currentDateContextInterval === 'none'\n ? noFilterLabel\n : (date()?.[0] | c8yDate: DATE_FORMAT) + ' \u2014 ' + (date()?.[1] | c8yDate: DATE_FORMAT)\n ) | translate\n }}\"\n tooltip=\"{{\n (form.value.currentDateContextInterval === 'none'\n ? noFilterLabel\n : (date()?.[0] | c8yDate: DATE_FORMAT) + ' \u2014 ' + (date()?.[1] | c8yDate: DATE_FORMAT)\n ) | translate\n }}\"\n placement=\"top\"\n container=\"body\"\n data-cy=\"events-date-filter--date-picker-dropdown-button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"schedule1\"\n ></i>\n <div class=\"d-col text-left fit-w\">\n <span\n class=\"text-12\"\n data-cy=\"widget-time-context--selected-interval\"\n >\n {{\n INTERVAL_TITLES[form.controls.currentDateContextInterval.value ?? 'none'] | translate\n }}\n </span>\n @if (form.controls.currentDateContextInterval.value !== 'none') {\n <span\n class=\"text-10 text-muted text-truncate\"\n data-cy=\"events-date-filter--selected-time-range\"\n >\n {{ date()?.[0] | c8yDate: DATE_FORMAT }} \u2014 {{ date()?.[1] | c8yDate: DATE_FORMAT }}\n </span>\n }\n </div>\n <span class=\"caret m-r-16 m-l-4\"></span>\n </button>\n\n <ul\n class=\"dropdown-menu dropdown-menu--date-range\"\n *dropdownMenu\n >\n <c8y-interval-picker\n class=\"d-contents\"\n formControlName=\"currentDateContextInterval\"\n [INTERVALS]=\"INTERVALS\"\n ></c8y-interval-picker>\n\n @if (form.controls.currentDateContextInterval.value === 'custom') {\n <div class=\"p-l-16 p-r-16\">\n <c8y-form-group\n class=\"m-b-8\"\n [class.has-error]=\"form.controls.temporaryUserSelectedFromDate.errors\"\n >\n <label\n [title]=\"'From`date`' | translate\"\n for=\"temporaryUserSelectedFromDate\"\n translate\n >\n From`date`\n </label>\n <c8y-date-time-picker\n [class.has-error]=\"form.controls.temporaryUserSelectedFromDate.errors\"\n id=\"temporaryUserSelectedFromDate\"\n [maxDate]=\"form.value.temporaryUserSelectedToDate ?? ''\"\n [placeholder]=\"'From`date`' | translate\"\n [formControl]=\"form.controls.temporaryUserSelectedFromDate\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"form.controls.temporaryUserSelectedFromDate.errors ?? {}\">\n <c8y-message\n name=\"dateAfterRangeMax\"\n [text]=\"'This date is after the latest allowed date.' | translate\"\n ></c8y-message>\n <c8y-message\n name=\"invalidDateTime\"\n [text]=\"'This date is invalid.' | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n\n <c8y-form-group\n class=\"m-b-8\"\n [class.has-error]=\"form.controls.temporaryUserSelectedToDate.errors\"\n >\n <label\n [title]=\"'To`date`' | translate\"\n for=\"temporaryUserSelectedToDate\"\n translate\n >\n To`date`\n </label>\n <c8y-date-time-picker\n [class.has-error]=\"form.controls.temporaryUserSelectedToDate.errors\"\n id=\"temporaryUserSelectedToDate\"\n [minDate]=\"form.value.temporaryUserSelectedFromDate ?? ''\"\n [placeholder]=\"'To`date`' | translate\"\n [formControl]=\"form.controls.temporaryUserSelectedToDate\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"form.controls.temporaryUserSelectedToDate.errors ?? {}\">\n <c8y-message\n name=\"dateBeforeRangeMin\"\n [text]=\"'This date is before the earliest allowed date.' | translate\"\n ></c8y-message>\n <c8y-message\n name=\"invalidDateTime\"\n [text]=\"'This date is invalid.' | translate\"\n ></c8y-message>\n </c8y-messages>\n </c8y-form-group>\n </div>\n\n <div class=\"p-16 d-flex gap-8 separator-top\">\n <button\n class=\"btn btn-primary btn-sm flex-grow\"\n title=\"{{ 'Apply' | translate }}\"\n type=\"button\"\n data-cy=\"events-date-filter--apply-button\"\n (click)=\"applyDateFilter(); dropDirection.isOpen = false\"\n [disabled]=\"(form.pristine && form.untouched) || form.invalid\"\n translate\n >\n Apply\n </button>\n </div>\n }\n </ul>\n </div>\n</form>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "directive", type: BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "p