UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

523 lines (515 loc) 126 kB
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