import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
import { CreateClusterComponent } from './ceph/cluster/create-cluster/create-cluster.component';
import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component';
+import { HostDetailsComponent } from './ceph/cluster/hosts/host-details/host-details.component';
import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component';
+import { HostDetailsBreadcrumbResolver } from './ceph/cluster/hosts/host-details/host-details-breadcrumb.resolver';
+import { HostDetailsSectionComponent } from './ceph/cluster/hosts/host-details/host-details-section.component';
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
import { InventoryComponent } from './ceph/cluster/inventory/inventory.component';
import { LogsComponent } from './ceph/cluster/logs/logs.component';
}
]
},
+ {
+ path: 'hosts/:hostname',
+ component: HostDetailsComponent,
+ data: { breadcrumbs: HostDetailsBreadcrumbResolver },
+ children: [
+ { path: '', redirectTo: 'devices', pathMatch: 'full' },
+ {
+ path: 'devices',
+ component: HostDetailsSectionComponent,
+ data: { breadcrumbs: 'Devices', section: 'devices' }
+ },
+ {
+ path: 'physical-disks',
+ component: HostDetailsSectionComponent,
+ data: { breadcrumbs: 'Physical Disks', section: 'physical-disks' }
+ },
+ {
+ path: 'daemons',
+ component: HostDetailsSectionComponent,
+ data: { breadcrumbs: 'Daemons', section: 'daemons' }
+ },
+ {
+ path: 'performance-details',
+ component: HostDetailsSectionComponent,
+ data: { breadcrumbs: 'Performance Details', section: 'performance-details' }
+ },
+ {
+ path: 'device-health',
+ component: HostDetailsSectionComponent,
+ data: { breadcrumbs: 'Device health', section: 'device-health' }
+ }
+ ]
+ },
{
path: 'ceph-users',
component: CRUDTableComponent,
import { CreateClusterComponent } from './create-cluster/create-cluster.component';
import { CrushmapComponent } from './crushmap/crushmap.component';
import { HostDetailsComponent } from './hosts/host-details/host-details.component';
+import { HostDetailsSectionComponent } from './hosts/host-details/host-details-section.component';
import { HostFormComponent } from './hosts/host-form/host-form.component';
import { HostsComponent } from './hosts/hosts.component';
import { InventoryDevicesComponent } from './inventory/inventory-devices/inventory-devices.component';
OsdScrubModalComponent,
OsdFlagsModalComponent,
HostDetailsComponent,
+ HostDetailsSectionComponent,
ConfigurationDetailsComponent,
ConfigurationFormComponent,
OsdReweightModalComponent,
--- /dev/null
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot } from '@angular/router';
+
+import { BreadcrumbsResolver, IBreadcrumb } from '~/app/shared/models/breadcrumbs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HostDetailsBreadcrumbResolver extends BreadcrumbsResolver {
+ resolve(route: ActivatedRouteSnapshot): IBreadcrumb[] {
+ const hostname = route.parent?.params?.hostname || route.params?.hostname || '';
+ return [
+ { text: 'Cluster/Hosts', path: '/hosts' },
+ { text: hostname, path: this.getFullPath(route) }
+ ];
+ }
+}
--- /dev/null
+@if (hostname) {
+ @switch (section) {
+ @case ('devices') {
+ <cd-device-list [hostname]="hostname"></cd-device-list>
+ }
+ @case ('physical-disks') {
+ <cd-inventory [hostname]="hostname"></cd-inventory>
+ }
+ @case ('daemons') {
+ <cd-service-daemon-list [hostname]="hostname"
+ flag="hostDetails"
+ [hiddenColumns]="['hostname']">
+ </cd-service-daemon-list>
+ }
+ @case ('performance-details') {
+ <cd-grafana i18n-title
+ title="Host details"
+ [grafanaPath]="'ceph-host-details?var-ceph_hosts=' + hostname"
+ [type]="'metrics'"
+ uid="rtOg0AiWz"
+ grafanaStyle="five">
+ </cd-grafana>
+ }
+ @case ('device-health') {
+ <cd-smart-list [hostname]="hostname"></cd-smart-list>
+ }
+ }
+} @else {
+ <cd-alert-panel type="error"
+ i18n>No hostname found.</cd-alert-panel>
+}
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+
+@Component({
+ selector: 'cd-host-details-section',
+ templateUrl: './host-details-section.component.html',
+ standalone: false
+})
+export class HostDetailsSectionComponent implements OnInit {
+ hostname = '';
+ section = '';
+
+ constructor(private route: ActivatedRoute) {}
+
+ ngOnInit(): void {
+ this.route.parent?.paramMap.subscribe((pm: ParamMap) => {
+ this.hostname = pm.get('hostname') ?? '';
+ });
+ this.section = this.route.snapshot.data['section'] ?? '';
+ }
+}
-<ng-container *ngIf="selection">
- <nav ngbNav
- #nav="ngbNav"
- class="nav-tabs"
- cdStatefulTab="host-details">
- <ng-container ngbNavItem="devices">
- <a ngbNavLink
- i18n>Devices</a>
- <ng-template ngbNavContent>
- <cd-device-list [hostname]="selection['hostname']"></cd-device-list>
- </ng-template>
- </ng-container>
- <ng-container ngbNavItem="inventory"
- *ngIf="permissions.hosts.read">
- <a ngbNavLink
- i18n>Physical Disks</a>
- <ng-template ngbNavContent>
- <cd-inventory [hostname]="selectedHostname"></cd-inventory>
- </ng-template>
- </ng-container>
- <ng-container ngbNavItem="daemons"
- *ngIf="permissions.hosts.read">
- <a ngbNavLink
- i18n>Daemons</a>
- <ng-template ngbNavContent>
- <cd-service-daemon-list [hostname]="selectedHostname"
- flag="hostDetails"
- [hiddenColumns]="['hostname']">
- </cd-service-daemon-list>
- </ng-template>
- </ng-container>
- <ng-container ngbNavItem="performance-details"
- *ngIf="permissions.grafana.read">
- <a ngbNavLink
- i18n>Performance Details</a>
- <ng-template ngbNavContent>
- <cd-grafana i18n-title
- title="Host details"
- [grafanaPath]="'ceph-host-details?var-ceph_hosts=' + selectedHostname"
- [type]="'metrics'"
- uid="rtOg0AiWz"
- grafanaStyle="five">
- </cd-grafana>
- </ng-template>
- </ng-container>
- <ng-container ngbNavItem="device-health">
- <a ngbNavLink
- i18n>Device health</a>
- <ng-template ngbNavContent>
- <cd-smart-list *ngIf="selectedHostname; else noHostname"
- [hostname]="selectedHostname"></cd-smart-list>
- </ng-template>
- </ng-container>
- </nav>
-
- <div [ngbNavOutlet]="nav"></div>
-</ng-container>
-
-<ng-template #noHostname>
- <cd-alert-panel type="error"
- i18n>No hostname found.</cd-alert-panel>
-</ng-template>
+<cd-sidebar-layout
+ [title]="hostname"
+ [items]="sidebarItems"
+></cd-sidebar-layout>
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
+import { of } from 'rxjs';
import { CephModule } from '~/app/ceph/ceph.module';
import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
import { CoreModule } from '~/app/core/core.module';
import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed, TabHelper } from '~/testing/unit-test-helper';
+import { configureTestBed } from '~/testing/unit-test-helper';
import { HostDetailsComponent } from './host-details.component';
describe('HostDetailsComponent', () => {
CoreModule,
CephSharedModule,
SharedModule
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ paramMap: of(convertToParamMap({ hostname: 'localhost' }))
+ }
+ },
+ {
+ provide: AuthStorageService,
+ useValue: {
+ getPermissions: () => new Permissions({ hosts: ['read'], grafana: ['read'] })
+ }
+ }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(HostDetailsComponent);
component = fixture.componentInstance;
- component.selection = undefined;
- component.permissions = new Permissions({
- hosts: ['read'],
- grafana: ['read']
- });
});
it('should create', () => {
expect(component).toBeTruthy();
});
- describe('Host details tabset', () => {
+ describe('Host resource layout', () => {
beforeEach(() => {
- component.selection = { hostname: 'localhost' };
fixture.detectChanges();
});
- it('should recognize a tabset child', () => {
- const tabsetChild = TabHelper.getNgbNav(fixture);
- expect(tabsetChild).toBeDefined();
+ it('should render the sidebar layout', () => {
+ const layout = fixture.nativeElement.querySelector('cd-sidebar-layout');
+ expect(layout).toBeTruthy();
});
- it('should show tabs', () => {
- expect(TabHelper.getTextContents(fixture)).toEqual([
+ it('should build the sidebar items', () => {
+ expect(component.sidebarItems.map((item) => item.label)).toEqual([
'Devices',
'Physical Disks',
'Daemons',
'Device health'
]);
});
+
+ it('should set the hostname title', () => {
+ expect(component.hostname).toBe('localhost');
+ });
});
});
-import { Component, Input } from '@angular/core';
+import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
-import { Permissions } from '~/app/shared/models/permissions';
+import { Subscription } from 'rxjs';
+
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { SidebarItem } from '~/app/shared/components/sidebar-layout/sidebar-layout.component';
@Component({
selector: 'cd-host-details',
templateUrl: './host-details.component.html',
styleUrls: ['./host-details.component.scss'],
+ encapsulation: ViewEncapsulation.None,
standalone: false
})
-export class HostDetailsComponent {
- @Input()
- permissions: Permissions;
+export class HostDetailsComponent implements OnInit, OnDestroy {
+ private sub = new Subscription();
+ public readonly basePath = '/hosts';
+ hostname = '';
+ sidebarItems: SidebarItem[] = [];
+
+ constructor(private route: ActivatedRoute, private authStorageService: AuthStorageService) {}
+
+ ngOnInit(): void {
+ const permissions = this.authStorageService.getPermissions();
+ this.sub.add(
+ this.route.paramMap.subscribe((pm: ParamMap) => {
+ this.hostname = pm.get('hostname') ?? '';
+ this.buildSidebarItems(permissions);
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.sub.unsubscribe();
+ }
+
+ private buildSidebarItems(permissions: any): void {
+ const items: SidebarItem[] = [
+ {
+ label: $localize`Devices`,
+ route: [this.basePath, this.hostname, 'devices'],
+ routerLinkActiveOptions: { exact: true }
+ }
+ ];
+
+ if (permissions.hosts?.read) {
+ items.push(
+ {
+ label: $localize`Physical Disks`,
+ route: [this.basePath, this.hostname, 'physical-disks'],
+ routerLinkActiveOptions: { exact: true }
+ },
+ {
+ label: $localize`Daemons`,
+ route: [this.basePath, this.hostname, 'daemons'],
+ routerLinkActiveOptions: { exact: true }
+ }
+ );
+ }
+
+ if (permissions.grafana?.read) {
+ items.push({
+ label: $localize`Performance Details`,
+ route: [this.basePath, this.hostname, 'performance-details'],
+ routerLinkActiveOptions: { exact: true }
+ });
+ }
- @Input()
- selection: any;
+ items.push({
+ label: $localize`Device health`,
+ route: [this.basePath, this.hostname, 'device-health'],
+ routerLinkActiveOptions: { exact: true }
+ });
- get selectedHostname(): string {
- return this.selection !== undefined ? this.selection['hostname'] : null;
+ this.sidebarItems = items;
}
}
(fetchData)="getHosts($event)"
selectionType="single"
[searchableObjects]="true"
- [hasDetails]="hasTableDetails"
[serverSide]="true"
[count]="count"
[maxLimit]="25"
- (setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)"
[toolHeader]="!hideToolHeader">
<div class="table-actions">
[tableActions]="expandClusterActions">
</cd-table-actions>
</div>
- <cd-host-details *cdTableDetail
- [permissions]="permissions"
- [selection]="expandedRow">
- </cd-host-details>
</cd-table>
</ng-template>
</ng-container>
<ng-template #hostNameTpl
let-row="data.row">
<span data-testid="hostname">
- {{ row.hostname }}
+ <a cdsLink
+ [routerLink]="[viewUrl, row.hostname | encodeUri]"
+ (click)="$event.stopPropagation()">
+ {{ row.hostname }}
+ </a>
</span><br>
<span class="text-muted fst-italic"
*ngIf="row.addr"
icons = Icons;
private tableContext: CdTableFetchDataContext = null;
count = 5;
+ viewUrl = '/hosts';
messages = {
nonOrchHost: $localize`The feature is disabled because the selected host is not managed by Orchestrator.`