Skip to content

Commit ba2fd3c

Browse files
feat(devices): add live IDER status strip and ISM info card
Add a reusable app-ider-status component that surfaces live IDE-R connection state and data-transferred, driven by the amt-ider iderData output. Lay the IDER tab out as two cards (info + status/attach), show a note that KVM is unavailable on Intel Standard Manageability systems, move the IDER tab next to SOL, and add the supporting i18n strings across all locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d7d740c commit ba2fd3c

22 files changed

Lines changed: 678 additions & 180 deletions

src/app/devices/device-detail/device-detail.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ <h3 matListItemTitle>{{ item.name | translate }}</h3>
4444
} @else if (!isISMSystem()) {
4545
<app-kvm [deviceId]="deviceId"></app-kvm>
4646
} @else {
47-
<app-ider [deviceId]="deviceId"></app-ider>
47+
<app-ider [deviceId]="deviceId" [isISM]="isISMSystem()"></app-ider>
4848
}
4949
}
5050
@case ('ider') {
5151
@if (isLoading()) {
5252
<div aria-busy="true"></div>
5353
} @else if (isISMSystem()) {
54-
<app-ider [deviceId]="deviceId"></app-ider>
54+
<app-ider [deviceId]="deviceId" [isISM]="isISMSystem()"></app-ider>
5555
} @else {
5656
<app-kvm [deviceId]="deviceId"></app-kvm>
5757
}

src/app/devices/device-detail/device-detail.component.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('DeviceDetailComponent', () => {
5959
})
6060
class TestIderComponent {
6161
public readonly deviceId = input('')
62+
public readonly isISM = input(false)
6263
}
6364

6465
beforeEach(() => {

src/app/devices/device-detail/device-detail.component.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ export class DeviceDetailComponent implements OnInit, OnDestroy {
105105
component: 'sol',
106106
icon: 'keyboard'
107107
},
108+
{
109+
name: 'deviceDetail.ider.value',
110+
description: 'deviceDetail.iderDescription.value',
111+
component: 'ider',
112+
icon: 'storage'
113+
},
108114
{
109115
name: 'deviceDetail.hardwareInfo.value',
110116
description: 'deviceDetail.hardwareInfoDescription.value',
@@ -134,12 +140,6 @@ export class DeviceDetailComponent implements OnInit, OnDestroy {
134140
description: 'deviceDetail.certificatesDescription.value',
135141
component: 'certificates',
136142
icon: 'verified'
137-
},
138-
{
139-
name: 'deviceDetail.ider.value',
140-
description: 'deviceDetail.iderDescription.value',
141-
component: 'ider',
142-
icon: 'storage'
143143
}
144144
]
145145
const filtered = base
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<div class="flex items-center gap-8">
2+
@if (connecting() || transferring()) {
3+
<mat-spinner diameter="20" />
4+
} @else if (active()) {
5+
<mat-icon color="primary">check_circle</mat-icon>
6+
} @else {
7+
<mat-icon>storage</mat-icon>
8+
}
9+
<div class="flex-col">
10+
<span>
11+
@if (transferring()) {
12+
{{ 'ider.activity.transferring.value' | translate }}
13+
} @else if (active()) {
14+
{{ 'warning.iderActive.value' | translate }}
15+
} @else if (connecting() && statusLabel()) {
16+
{{ statusLabel() | translate }}
17+
} @else {
18+
{{ 'ider.status.notConnected.value' | translate }}
19+
}
20+
</span>
21+
@if (active()) {
22+
<span class="mat-caption">
23+
{{ 'ider.activity.transferred.value' | translate }}: {{ formatBytes(bytes()) }}
24+
</span>
25+
} @else if (!connecting() && idleHint()) {
26+
<span class="mat-caption">{{ idleHint() | translate }}</span>
27+
}
28+
</div>
29+
</div>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*********************************************************************
2+
* Copyright (c) Intel Corporation 2022
3+
* SPDX-License-Identifier: Apache-2.0
4+
**********************************************************************/
5+
6+
import { ComponentFixture, TestBed } from '@angular/core/testing'
7+
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
8+
import { provideTranslateService } from '@ngx-translate/core'
9+
import { IderStatusComponent } from './ider-status.component'
10+
11+
describe('IderStatusComponent', () => {
12+
let component: IderStatusComponent
13+
let fixture: ComponentFixture<IderStatusComponent>
14+
15+
beforeEach(async () => {
16+
await TestBed.configureTestingModule({
17+
imports: [NoopAnimationsModule, IderStatusComponent],
18+
providers: [provideTranslateService()]
19+
}).compileComponents()
20+
21+
fixture = TestBed.createComponent(IderStatusComponent)
22+
component = fixture.componentInstance
23+
})
24+
25+
it('should create', () => {
26+
expect(component).toBeTruthy()
27+
})
28+
29+
it('formatBytes formats byte counts into human-readable units', () => {
30+
expect(component.formatBytes(0)).toBe('0 B')
31+
expect(component.formatBytes(512)).toBe('512 B')
32+
expect(component.formatBytes(2048)).toBe('2.0 KB')
33+
expect(component.formatBytes(5 * 1024 * 1024)).toBe('5.0 MB')
34+
})
35+
36+
it('formatBytes returns 0 B for invalid input', () => {
37+
expect(component.formatBytes(-1)).toBe('0 B')
38+
expect(component.formatBytes(NaN)).toBe('0 B')
39+
})
40+
41+
it('shows the not-connected state by default', () => {
42+
fixture.detectChanges()
43+
const el = fixture.nativeElement
44+
expect(el.querySelector('mat-icon').textContent.trim()).toBe('storage')
45+
expect(el.querySelector('mat-spinner')).toBeNull()
46+
})
47+
48+
it('shows the transferred total when active', () => {
49+
fixture.componentRef.setInput('active', true)
50+
fixture.componentRef.setInput('bytes', 2048)
51+
fixture.detectChanges()
52+
const el = fixture.nativeElement
53+
expect(el.querySelector('mat-icon').textContent.trim()).toBe('check_circle')
54+
expect(el.textContent).toContain('2.0 KB')
55+
})
56+
57+
it('shows a spinner while transferring', () => {
58+
fixture.componentRef.setInput('active', true)
59+
fixture.componentRef.setInput('transferring', true)
60+
fixture.detectChanges()
61+
const el = fixture.nativeElement
62+
expect(el.querySelector('mat-spinner')).toBeTruthy()
63+
expect(el.querySelector('mat-icon')).toBeNull()
64+
})
65+
66+
it('shows the idle hint when provided and idle', () => {
67+
fixture.componentRef.setInput('idleHint', 'ider.status.attachHelp.value')
68+
fixture.detectChanges()
69+
expect(fixture.nativeElement.textContent).toContain('ider.status.attachHelp.value')
70+
})
71+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*********************************************************************
2+
* Copyright (c) Intel Corporation 2022
3+
* SPDX-License-Identifier: Apache-2.0
4+
**********************************************************************/
5+
6+
import { Component, input } from '@angular/core'
7+
import { MatIcon } from '@angular/material/icon'
8+
import { MatProgressSpinner } from '@angular/material/progress-spinner'
9+
import { TranslatePipe } from '@ngx-translate/core'
10+
11+
/**
12+
* Compact, reusable status strip for an IDE-Redirection (IDER) session.
13+
* Renders the live connection/transfer state so it can sit inline at the same
14+
* level as the "Attach Disk Image" action, in both the dedicated IDER tab
15+
* (ISM systems) and the KVM tab (vPro systems, which also run IDER).
16+
*/
17+
@Component({
18+
selector: 'app-ider-status',
19+
templateUrl: './ider-status.component.html',
20+
imports: [MatIcon, MatProgressSpinner, TranslatePipe]
21+
})
22+
export class IderStatusComponent {
23+
// Session is established and running.
24+
public readonly active = input(false)
25+
// Disk sectors are actively moving right now.
26+
public readonly transferring = input(false)
27+
// Connection is being set up (power/consent/token checks).
28+
public readonly connecting = input(false)
29+
// Translate key for the granular connecting status (e.g. checking power state).
30+
public readonly statusLabel = input('')
31+
// Cumulative bytes transferred during the session.
32+
public readonly bytes = input(0)
33+
// Optional translate key shown as a subtitle while idle (e.g. attach hint).
34+
public readonly idleHint = input('')
35+
36+
// Human-readable byte count for the live transfer indicator.
37+
formatBytes(bytes: number): string {
38+
if (!bytes || bytes < 0) {
39+
return '0 B'
40+
}
41+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
42+
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
43+
const value = bytes / Math.pow(1024, exponent)
44+
return `${value.toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`
45+
}
46+
}
Lines changed: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,51 @@
1-
<mat-toolbar class="flex flex-row ider-toolbar">
2-
<div class="ider-status" [class.active]="isIDERActive()" [class.loading]="isLoading()">
3-
<mat-icon>
4-
@if (isLoading()) {
5-
sync
6-
} @else if (isIDERActive()) {
7-
check_circle
8-
} @else {
9-
radio_button_unchecked
1+
<div class="flex flex-row flex-wrap gap-16" style="margin-top: 16px">
2+
<mat-card class="flex flex-50 gap-12">
3+
<mat-card-header>
4+
<mat-card-title>{{ 'ider.overview.title.value' | translate }}</mat-card-title>
5+
<mat-card-subtitle>{{ 'ider.overview.lead.value' | translate }}</mat-card-subtitle>
6+
</mat-card-header>
7+
<mat-card-content>
8+
@if (isISM()) {
9+
<div class="flex items-start gap-8">
10+
<mat-icon color="primary" style="flex: none">info</mat-icon>
11+
<span style="font-size: 15px; font-weight: 500">{{ 'ider.ism.info.value' | translate }}</span>
12+
</div>
1013
}
11-
</mat-icon>
12-
<div class="ider-status-copy">
13-
<div class="ider-status-title">
14+
</mat-card-content>
15+
</mat-card>
16+
17+
<mat-card class="flex flex-50 gap-12">
18+
<mat-card-header>
19+
<mat-card-title>{{ 'ider.status.title.value' | translate }}</mat-card-title>
20+
</mat-card-header>
21+
<mat-card-content class="flex items-center space-between flex-wrap gap-12">
22+
<app-ider-status
23+
[active]="isIDERActive()"
24+
[transferring]="isTransferring()"
25+
[connecting]="isLoading()"
26+
[statusLabel]="loadingStatus()"
27+
[bytes]="bytesTransferred()"
28+
[idleHint]="'ider.status.attachHelp.value'" />
29+
30+
<div class="flex items-center gap-8">
31+
<button
32+
mat-flat-button
33+
color="primary"
34+
[disabled]="isLoading() || isIDERActive()"
35+
(click)="onAttachDiskImage(fileInput)">
36+
<mat-icon>upload_file</mat-icon>
37+
{{ 'ider.attachDisk.label.value' | translate }}
38+
</button>
39+
<input hidden (change)="onFileSelected($event)" #fileInput type="file" id="file" accept=".iso" />
1440
@if (isIDERActive()) {
15-
{{ 'warning.iderActive.value' | translate }}
16-
} @else if (isLoading() && loadingStatus()) {
17-
{{ loadingStatus() | translate }}
18-
} @else {
19-
{{ 'ider.status.notConnected.value' | translate }}
41+
<button mat-flat-button color="warn" (click)="onCancelIDER()">
42+
<mat-icon>stop</mat-icon>{{ 'ider.stopIder.label.value' | translate }}
43+
</button>
2044
}
2145
</div>
22-
@if (!isIDERActive() && !isLoading()) {
23-
<div class="ider-status-subtitle">{{ 'ider.status.attachHelp.value' | translate }}</div>
24-
}
25-
</div>
26-
</div>
27-
28-
<div class="ider-actions">
29-
<button
30-
mat-flat-button
31-
color="primary"
32-
[disabled]="isLoading() || isIDERActive()"
33-
(click)="onAttachDiskImage(fileInput)">
34-
<mat-icon [class.loading-icon]="isLoading()">
35-
@if (isLoading()) {
36-
sync
37-
} @else {
38-
upload_file
39-
}
40-
</mat-icon>
41-
{{ 'ider.attachDisk.label.value' | translate }}
42-
</button>
43-
<input hidden (change)="onFileSelected($event)" #fileInput type="file" id="file" accept=".iso" />
44-
@if (isIDERActive()) {
45-
<button mat-flat-button color="warn" (click)="onCancelIDER()" class="stop-ider-button">
46-
<mat-icon>stop</mat-icon>{{ 'ider.stopIder.label.value' | translate }}
47-
</button>
48-
}
49-
</div>
50-
</mat-toolbar>
51-
52-
@if (isLoading()) {
53-
<div class="loading-container">
54-
<div class="loading-status">{{ loadingStatus() | translate }}</div>
55-
</div>
56-
}
57-
58-
@if (!diskImage && !isLoading()) {
59-
<div class="ider-empty-state">
60-
<mat-icon>dns</mat-icon>
61-
<p>{{ 'ider.emptyState.selectIso.value' | translate }}</p>
62-
</div>
63-
}
46+
</mat-card-content>
47+
</mat-card>
48+
</div>
6449

6550
@if (diskImage && deviceIDERConnection()) {
6651
<amt-ider
@@ -71,6 +56,7 @@
7156
[deviceConnection]="deviceIDERConnection()"
7257
[cdrom]="diskImage"
7358
[floppy]="null"
74-
(deviceStatus)="deviceIDERStatus($event)">
59+
(deviceStatus)="deviceIDERStatus($event)"
60+
(iderData)="onIderData($event)">
7561
</amt-ider>
7662
}

0 commit comments

Comments
 (0)