Skip to content

Commit 98210d2

Browse files
DevipriyaS17punam20Copilot
authored
fix: fix refresh power button issue (#3441)
* fix: fix refresh power button issue * fix: address copilot review comments * fix: address copilot review comment * fix: fix for copilot comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: address review comments --------- Co-authored-by: punam biswal <punam.biswal@intel.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent f57367f commit 98210d2

4 files changed

Lines changed: 120 additions & 24 deletions

File tree

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,23 @@
5151
</button>
5252
<mat-divider style="height: 40px" vertical="true"></mat-divider>
5353
@if (!isLoading()()) {
54-
<mat-icon
55-
(click)="getPowerState()"
56-
style="padding: 0 18px 0 12px; cursor: pointer"
54+
<button
55+
mat-icon-button
56+
type="button"
57+
[matTooltip]="powerState() | translate"
58+
[attr.aria-label]="'deviceToolbar.power.refreshAriaLabel.value' | translate"
5759
[style.color]="
5860
powerState() === 'deviceToolbar.power.on.value'
5961
? 'green'
6062
: powerState() === 'deviceToolbar.power.sleep.value'
6163
? 'yellow'
62-
: 'red'
64+
: powerState() === 'deviceToolbar.power.off.value'
65+
? 'red'
66+
: 'gray'
6367
"
64-
[matTooltip]="powerState() | translate"
65-
>mode_standby</mat-icon
66-
>
68+
(click)="refreshPowerState()">
69+
<mat-icon>mode_standby</mat-icon>
70+
</button>
6771
}
6872
<mat-divider style="height: 40px" vertical="true"></mat-divider>
6973
<button mat-icon-button [matTooltip]="'devices.actions.powerUp.value' | translate" (click)="sendPowerAction(2)">

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,30 @@ describe('DeviceToolbarComponent', () => {
138138
expect(component.isLoading()()).toBeFalse()
139139
})
140140

141+
it('should fetch power state using cached API on initial load', () => {
142+
expect(devicesService.getPowerStateCached).toHaveBeenCalledWith('guid')
143+
expect(devicesService.getPowerState).not.toHaveBeenCalled()
144+
})
145+
146+
it('should fetch power state using non-cached API on refresh', () => {
147+
devicesService.getPowerState.calls.reset()
148+
devicesService.getPowerStateCached.calls.reset()
149+
150+
component.refreshPowerState()
151+
152+
expect(devicesService.getPowerState).toHaveBeenCalledWith('guid')
153+
expect(devicesService.getPowerStateCached).not.toHaveBeenCalled()
154+
})
155+
156+
it('should show snackbar and reset loading state when refresh power state fails', () => {
157+
devicesService.getPowerState.and.returnValue(throwError(() => new Error('Network error')))
158+
159+
component.refreshPowerState()
160+
161+
expect(snackBar.open).toHaveBeenCalled()
162+
expect(component.isLoading()()).toBeFalse()
163+
})
164+
141165
it('should navigate to device', async () => {
142166
fixture.componentRef.setInput('deviceId', '12345-pokli-456772')
143167
const routerSpy = spyOn(component.router, 'navigate')

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

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Component, OnInit, inject, signal, input, DestroyRef } from '@angular/c
77
import { catchError, finalize, switchMap } from 'rxjs/operators'
88
import { MatSnackBar } from '@angular/material/snack-bar'
99
import { Router } from '@angular/router'
10-
import { Observable, of, forkJoin } from 'rxjs'
10+
import { Observable, of, forkJoin, EMPTY, timer } from 'rxjs'
1111
import { DevicesService } from '../devices.service'
1212
import SnackbarDefaults from '../../shared/config/snackBarDefault'
1313
import { AMTFeaturesResponse, BootDetails, Device, UserConsentResponse } from '../../../models/models'
@@ -164,7 +164,7 @@ export class DeviceToolbarComponent implements OnInit {
164164
this.device = data
165165
this.devicesService.device.next(this.device)
166166
this.isPinned.set(this.device?.certHash != null && this.device?.certHash !== '')
167-
this.getPowerState()
167+
this.loadPowerState()
168168
this.loadAMTFeatures()
169169
// react to AMT feature updates emitted by service
170170
this.devicesService
@@ -217,22 +217,78 @@ export class DeviceToolbarComponent implements OnInit {
217217
this.powerOptions.set(options)
218218
}
219219

220-
getPowerState(): void {
221-
this.isLoading().set(true)
222-
// Goes through the cached variant so a simultaneous KVM deep-link fetch and
223-
// this toolbar fetch are deduped into one /power/state round-trip.
224-
this.devicesService
225-
.getPowerStateCached(this.deviceId())
226-
.pipe(takeUntilDestroyed(this.destroyRef))
220+
private setOptimisticPowerState(action: number): void {
221+
// Immediately reflect the expected outcome so the UI doesn't show the
222+
// old state while waiting for the device to transition.
223+
switch (action) {
224+
case 2: // power on
225+
this.powerState.set('deviceToolbar.power.on.value')
226+
break
227+
case 8: // power off
228+
case 12: // soft off
229+
this.powerState.set('deviceToolbar.power.off.value')
230+
break
231+
default: // reset, cycle, etc. — state is transitioning
232+
this.powerState.set('Unknown')
233+
}
234+
}
235+
236+
private setPowerStateLabel(powerstate: number): void {
237+
switch (powerstate) {
238+
case 2:
239+
this.powerState.set('deviceToolbar.power.on.value')
240+
break
241+
case 3:
242+
case 4:
243+
this.powerState.set('deviceToolbar.power.sleep.value')
244+
break
245+
default:
246+
this.powerState.set('deviceToolbar.power.off.value')
247+
}
248+
}
249+
250+
private loadPowerState(): void {
251+
// Use cached to dedupe simultaneous requests (KVM deep-link + toolbar both loading).
252+
this.fetchPowerState(this.devicesService.getPowerStateCached(this.deviceId()))
253+
}
254+
255+
refreshPowerState(): void {
256+
// Bypass the cache so manual refresh always fetches the latest state from the device.
257+
this.fetchPowerState(this.devicesService.getPowerState(this.deviceId()), false, () => {
258+
const msg: string = this.translate.instant('kvm.errorRetrievePower.value')
259+
this.snackBar.open(msg, undefined, SnackbarDefaults.defaultError)
260+
})
261+
}
262+
263+
private silentRefreshPowerState(): void {
264+
// Refresh without showing the loading spinner — used after a power action
265+
// where the optimistic state is already shown to the user.
266+
this.fetchPowerState(this.devicesService.getPowerState(this.deviceId()), true)
267+
}
268+
269+
private fetchPowerState(source: Observable<any>, silent = false, onError?: () => void): void {
270+
if (!silent) {
271+
this.isLoading().set(true)
272+
}
273+
274+
source
275+
.pipe(
276+
catchError((err) => {
277+
if (onError) {
278+
console.error(err)
279+
onError()
280+
}
281+
return EMPTY
282+
}),
283+
finalize(() => {
284+
if (!silent) {
285+
this.isLoading().set(false)
286+
}
287+
}),
288+
takeUntilDestroyed(this.destroyRef)
289+
)
227290
.subscribe((powerState) => {
228-
this.powerState.set(
229-
powerState.powerstate.toString() === '2'
230-
? 'deviceToolbar.power.on.value'
231-
: powerState.powerstate.toString() === '3' || powerState.powerstate.toString() === '4'
232-
? 'deviceToolbar.power.sleep.value'
233-
: 'deviceToolbar.power.off.value'
234-
)
235-
this.isLoading().set(false)
291+
this.setPowerStateLabel(powerState.powerstate)
236292
})
237293
}
238294

@@ -437,6 +493,14 @@ export class DeviceToolbarComponent implements OnInit {
437493
const msg: string = this.translate.instant('devices.powerActionSent.value')
438494
console.log('Power action sent successfully:', data)
439495
this.snackBar.open(msg, undefined, SnackbarDefaults.defaultSuccess)
496+
// Set expected state immediately for instant feedback.
497+
this.setOptimisticPowerState(action)
498+
// Then silently confirm actual state after device transitions — no loading spinner.
499+
timer(5000)
500+
.pipe(takeUntilDestroyed(this.destroyRef))
501+
.subscribe(() => {
502+
this.silentRefreshPowerState()
503+
})
440504
} else {
441505
console.log('Power action failed:', data)
442506
const msg: string = this.translate.instant('devices.failPowerAction.value')

src/assets/i18n/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,10 @@
12531253
"description": "Power tooltip for Off",
12541254
"value": "Power: Off"
12551255
},
1256+
"deviceToolbar.power.refreshAriaLabel": {
1257+
"description": "Aria label for the refresh power status button",
1258+
"value": "Refresh power status"
1259+
},
12561260
"deviceUserConsent.description": {
12571261
"description": "Description for user consent for devices",
12581262
"value": "A user consent code generated by Intel AMT is required to access the system."

0 commit comments

Comments
 (0)