<a ngbNavLink
i18n>Cluster Logs</a>
<ng-template ngbNavContent>
- <div class="card bg-light mb-3"
- *ngIf="clog">
- <div class="btn-group"
- role="group"
- *ngIf="clog.length && showClusterLogs">
+ @if (clog) {
+ <div class="log-viewer"
+ [class.log-viewer--scrollable]="scrollable">
+ @if (clog.length && showClusterLogs && showDownloadCopyButton) {
+ <div class="log-actions">
<cd-download-button [objectItem]="clog"
[textItem]="clogText"
- fileName="cluster_log"
- *ngIf="showDownloadCopyButton">
+ fileName="cluster_log">
</cd-download-button>
- <cd-copy-2-clipboard-button
- [source]="clogText"
- [byId]="false"
- *ngIf="showDownloadCopyButton">
+ <cd-copy-2-clipboard-button [source]="clogText"
+ [byId]="false">
</cd-copy-2-clipboard-button>
</div>
- <div class="card-body"
- [ngClass]="{'overflow-auto': scrollable}">
- <p *ngFor="let line of clog">
- <span class="timestamp">{{ line.stamp | cdDate }}</span>
- <span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
- <span class="message"
- [innerHTML]="line.message | searchHighlight: search"></span>
- </p>
-
- <ng-container *ngIf="clog.length !== 0 else noEntriesTpl"></ng-container>
+ }
+
+ <div class="log-entries">
+ @for (line of clog; track trackByLogEntry($index, line)) {
+ <div class="log-entry cds--type-code-01">
+ <span class="log-entry__timestamp ">{{ line.stamp | cdDate }}</span>
+ <span class="log-entry__priority"
+ [ngClass]="'log-entry__priority--' + (line.priority | logPriority)">
+ {{ line.priority }}
+ </span>
+ <span class="log-entry__message"
+ [innerHTML]="line.message | searchHighlight: search">
+ </span>
+ </div>
+ }
+
+ @if (clog.length === 0) {
+ <div class="log-viewer__empty">
+ <ng-container *ngTemplateOutlet="noEntriesTpl"></ng-container>
+ </div>
+ }
</div>
</div>
+ }
</ng-template>
</ng-container>
<ng-container ngbNavItem="audit-logs">
<a ngbNavLink
i18n>Audit Logs</a>
<ng-template ngbNavContent>
- <div class="card bg-light mb-3"
- *ngIf="audit_log && showAuditLogs">
- <div class="btn-group"
- role="group"
- *ngIf="audit_log.length">
+ @if (audit_log && showAuditLogs) {
+ <div class="log-viewer">
+ @if (audit_log.length && showDownloadCopyButton) {
+ <div class="log-actions">
<cd-download-button [objectItem]="audit_log"
[textItem]="auditLogText"
- fileName="audit_log"
- *ngIf="showDownloadCopyButton">
+ fileName="audit_log">
</cd-download-button>
- <cd-copy-2-clipboard-button
- [source]="auditLogText"
- [byId]="false"
- *ngIf="showDownloadCopyButton">
+ <cd-copy-2-clipboard-button [source]="auditLogText"
+ [byId]="false">
</cd-copy-2-clipboard-button>
</div>
- <div class="card-body">
- <p *ngFor="let line of audit_log">
- <span class="timestamp">{{ line.stamp | cdDate }}</span>
- <span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
- <span class="message"
- [innerHTML]="line.message | searchHighlight: search"></span>
- </p>
-
- <ng-container *ngIf="audit_log.length !== 0 else noEntriesTpl"></ng-container>
+ }
+
+ <div class="log-entries">
+ @for (line of audit_log; track trackByLogEntry($index, line)) {
+ <div class="log-entry cds--type-code-01">
+ <span class="log-entry__timestamp">{{ line.stamp | cdDate }}</span>
+ <span class="log-entry__priority"
+ [ngClass]="'log-entry__priority--' + (line.priority | logPriority)">
+ {{ line.priority }}
+ </span>
+ <span class="log-entry__message"
+ [innerHTML]="line.message | searchHighlight: search">
+ </span>
+ </div>
+ }
+
+ @if (audit_log.length === 0) {
+ <div class="log-viewer__empty">
+ <ng-container *ngTemplateOutlet="noEntriesTpl"></ng-container>
+ </div>
+ }
</div>
</div>
+ }
</ng-template>
</ng-container>
<ng-container ngbNavItem="daemon-logs">
-@use './src/styles/vendor/variables' as vv;
+// Log viewer container
+.log-viewer {
+ position: relative;
+ background: var(--cds-layer-01);
+ border-radius: var(--cds-spacing-02);
+ padding: var(--cds-spacing-05);
-p {
- font-family: monospace;
-}
+ &--scrollable {
+ max-height: 50vh;
+ overflow-y: auto;
-.card {
- .btn-group {
- margin-top: -57px;
- position: absolute;
- right: 0;
- }
+ // Custom scrollbar styling for better Carbon integration
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
- div p {
- display: flex;
+ &::-webkit-scrollbar-track {
+ background: var(--cds-layer-01);
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--cds-border-subtle-01);
+ border-radius: var(--cds-spacing-02);
- &:last-child {
- margin-bottom: 0;
+ &:hover {
+ background: var(--cds-border-strong-01);
+ }
}
}
- .timestamp {
- flex-shrink: 0;
- font-weight: bold;
+ &__empty {
+ font-size: var(--cds-body-compact-01-font-size);
+ font-weight: var(--cds-body-compact-01-font-weight);
+ line-height: var(--cds-body-compact-01-line-height);
+ letter-spacing: var(--cds-body-compact-01-letter-spacing);
+ color: var(--cds-text-secondary);
+ padding: var(--cds-spacing-07) 0;
+ text-align: center;
}
+}
- .priority {
- margin-left: 0.5rem;
- }
+// Action buttons (download/copy)
+.log-actions {
+ position: absolute;
+ top: var(--cds-spacing-03);
+ right: var(--cds-spacing-03);
+ display: flex;
+ gap: var(--cds-spacing-02);
+ z-index: 1;
+}
- .message {
- margin-left: 1rem;
- }
+// Log entries container
+.log-entries {
+ display: flex;
+ flex-direction: column;
+ gap: var(--cds-spacing-02);
+}
- .err {
- color: vv.$danger;
- }
+// Individual log entry
+.log-entry {
+ display: flex;
+ gap: var(--cds-spacing-04);
+ padding: var(--cds-spacing-02) 0;
- .warn {
- color: vv.$warning;
+ &:hover {
+ background: var(--cds-layer-hover-01);
+ margin: 0 calc(-1 * var(--cds-spacing-03));
+ padding: var(--cds-spacing-02) var(--cds-spacing-03);
+ border-radius: var(--cds-spacing-01);
}
- .info {
- color: vv.$info;
+ &__timestamp {
+ flex-shrink: 0;
+ font-weight: 600;
+ color: var(--cds-text-primary);
+ min-width: 140px;
}
- .debug {
- color: vv.$gray-700;
+ &__priority {
+ flex-shrink: 0;
+ font-weight: 500;
+ min-width: 48px;
+
+ &--err {
+ color: var(--cds-support-error);
+ }
+
+ &--warn {
+ color: var(--cds-support-warning);
+ }
+
+ &--info {
+ color: var(--cds-support-info);
+ }
+
+ &--debug {
+ color: var(--cds-text-secondary);
+ }
}
-}
-::ng-deep cd-logs ngb-timepicker input.ngb-tp-input {
- width: 3.5rem !important;
+ &__message {
+ flex: 1;
+ overflow-wrap: break-word;
+ color: var(--cds-text-primary);
+ }
}
-.card-body.overflow-auto {
- height: 50vh;
+.log-entry__message mark {
+ background-color: var(--cds-background-selected);
+ color: var(--cds-text-on-color);
+ padding: 0 var(--cds-spacing-01);
+ border-radius: var(--cds-spacing-01);
}
import { DatePipe } from '@angular/common';
-import { Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
+import { Component, Input, NgZone, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
selector: 'cd-logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
+ encapsulation: ViewEncapsulation.None,
standalone: false
})
export class LogsComponent implements OnInit, OnDestroy {
}
return logText;
}
+
+ trackByLogEntry(index: number, entry: any): string {
+ return `${entry.stamp}-${index}`;
+ }
}