Skip to content

Commit 3dba353

Browse files
feat(devices): add tabs, product type column, and discovered field
- Add All/Activated/Discovered tab group to devices list - Add Product Type column derived from fwSku bitmask (ISM/vPro) - Add discovered field to DeviceInfo model - Show paginator on all tabs using server total count - Add i18n keys for tab labels and Product Type header in all 12 locales - Add unit tests for getProductType and tab filter logic - Fix cdk-overlay-backdrop leak in device.spec.ts cypress test Resolves: #3417
1 parent 7f29ae4 commit 3dba353

17 files changed

Lines changed: 380 additions & 8 deletions

File tree

cypress/e2e/integration/device/device.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('Test Device Page', () => {
4141
if (Cypress.env('ISOLATE').charAt(0).toLowerCase() !== 'n') {
4242
cy.myIntercept('GET', '**/devices?tags=Windows&$top=25&$skip=0&$count=true', {
4343
statusCode: httpCodes.SUCCESS,
44-
body: devices.getAll.windows.response.data
44+
body: devices.getAll.windows.response
4545
}).as('get-windows')
4646

4747
cy.goToPage('Devices')

src/app/devices/devices.component.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ <h3 class="flex justify-center">
4646
}
4747
</h3>
4848
} @else {
49+
<mat-tab-group (selectedTabChange)="onTabChange($event.index)">
50+
<mat-tab [label]="allTabLabel"></mat-tab>
51+
<mat-tab [label]="activatedTabLabel"></mat-tab>
52+
<mat-tab [label]="discoveredTabLabel"></mat-tab>
53+
</mat-tab-group>
4954
<div class="flex-row flex-wrap flex-1">
5055
<div class="flex flex-66" style="width: 66%">
5156
<mat-form-field data-cy="filterSearch" style="width: 100%">
@@ -120,6 +125,15 @@ <h3 class="flex justify-center">
120125
}
121126
</mat-cell>
122127
</ng-container>
128+
<!-- productType Column -->
129+
<ng-container matColumnDef="productType">
130+
<mat-header-cell *matHeaderCellDef mat-sort-header>{{
131+
'devices.table.productType.value' | translate
132+
}}</mat-header-cell>
133+
<mat-cell *matCellDef="let element" (click)="navigateTo(element.guid)">
134+
{{ getProductType(element) }}
135+
</mat-cell>
136+
</ng-container>
123137
<!-- tags Column -->
124138
<ng-container matColumnDef="tags">
125139
<mat-header-cell *matHeaderCellDef mat-sort-header>
@@ -203,7 +217,7 @@ <h3 class="flex justify-center">
203217
<mat-paginator
204218
[pageSizeOptions]="[25, 50, 100]"
205219
[pageSize]="pageEvent.pageSize"
206-
[length]="totalCount()"
220+
[length]="allCount"
207221
(page)="pageChanged($event)"
208222
showFirstLastButtons>
209223
</mat-paginator>

src/app/devices/devices.component.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,100 @@ describe('DevicesComponent', () => {
250250
component.tagFilterChange(matSelectChange)
251251
expect(component.filteredTags()).toBe(mockValue)
252252
})
253+
254+
describe('getProductType', () => {
255+
it('should return ISM when bit 4 (0x10) is set', () => {
256+
const device = { ...device01, deviceInfo: { fwSku: '16' } } as Device // 0x10 = 16
257+
expect(component.getProductType(device)).toBe('ISM')
258+
})
259+
260+
it('should return vPro when bit 3 (0x08) is set and bit 4 is not', () => {
261+
const device = { ...device01, deviceInfo: { fwSku: '8' } } as Device // 0x08 = 8
262+
expect(component.getProductType(device)).toBe('vPro')
263+
})
264+
265+
it('should return ISM when both bit 4 and bit 3 are set (ISM takes priority)', () => {
266+
const device = { ...device01, deviceInfo: { fwSku: '24' } } as Device // 0x18 = 24
267+
expect(component.getProductType(device)).toBe('ISM')
268+
})
269+
270+
it('should return empty string when neither bit is set', () => {
271+
const device = { ...device01, deviceInfo: { fwSku: '4' } } as Device // 0x04 = 4
272+
expect(component.getProductType(device)).toBe('')
273+
})
274+
275+
it('should return empty string when fwSku is undefined', () => {
276+
const device = { ...device01, deviceInfo: undefined } as Device
277+
expect(component.getProductType(device)).toBe('')
278+
})
279+
280+
it('should return empty string when fwSku is not a number', () => {
281+
const device = { ...device01, deviceInfo: { fwSku: 'notanumber' } } as Device
282+
expect(component.getProductType(device)).toBe('')
283+
})
284+
})
285+
286+
describe('onTabChange / applyTabFilter', () => {
287+
beforeEach(() => {
288+
const baseInfo = { fwVersion: '', fwBuild: '', fwSku: '0', features: '', ipAddress: '' }
289+
component.allDevicesData = [
290+
{ ...device01, deviceInfo: { ...baseInfo, currentMode: 'acm', discovered: false } },
291+
{ ...device02, deviceInfo: { ...baseInfo, currentMode: 'not activated', discovered: true } }
292+
]
293+
})
294+
295+
it('should show all devices on tab 0', () => {
296+
component.onTabChange(0)
297+
expect(component.devices.data.length).toBe(2)
298+
})
299+
300+
it('should filter to activated devices on tab 1', () => {
301+
component.onTabChange(1)
302+
expect(component.devices.data.length).toBe(1)
303+
expect(component.devices.data[0].guid).toBe(device01.guid)
304+
})
305+
306+
it('should filter to discovered devices on tab 2', () => {
307+
component.onTabChange(2)
308+
expect(component.devices.data.length).toBe(1)
309+
expect(component.devices.data[0].guid).toBe(device02.guid)
310+
})
311+
312+
it('should set totalCount to serverTotalCount on tab 0', () => {
313+
;(component as any).serverTotalCount = 42
314+
component.onTabChange(0)
315+
expect(component.totalCount()).toBe(42)
316+
})
317+
318+
it('should set totalCount to filtered length on tab 1', () => {
319+
component.onTabChange(1)
320+
expect(component.totalCount()).toBe(1)
321+
})
322+
323+
it('should set totalCount to filtered length on tab 2', () => {
324+
component.onTabChange(2)
325+
expect(component.totalCount()).toBe(1)
326+
})
327+
})
328+
329+
describe('isNoData', () => {
330+
it('should return false when allDevicesData has entries regardless of totalCount', () => {
331+
component.allDevicesData = [device01]
332+
component.isLoading.set(false)
333+
component.totalCount.set(0) // filtered tab has 0 — should not trigger no-data
334+
expect(component.isNoData()).toBeFalse()
335+
})
336+
337+
it('should return true only when allDevicesData is empty and not loading', () => {
338+
component.allDevicesData = []
339+
component.isLoading.set(false)
340+
expect(component.isNoData()).toBeTrue()
341+
})
342+
343+
it('should return false when loading even if allDevicesData is empty', () => {
344+
component.allDevicesData = []
345+
component.isLoading.set(true)
346+
expect(component.isNoData()).toBeFalse()
347+
})
348+
})
253349
})

src/app/devices/devices.component.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { MatButton, MatIconButton } from '@angular/material/button'
4747
import { MatToolbar } from '@angular/material/toolbar'
4848
import { MatSort } from '@angular/material/sort'
4949
import { MatInput } from '@angular/material/input'
50+
import { MatTabGroup, MatTab } from '@angular/material/tabs'
5051
import { TranslatePipe, TranslateService } from '@ngx-translate/core'
5152

5253
@Component({
@@ -86,6 +87,8 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core'
8687
MatPaginator,
8788
MatHint,
8889
RouterModule,
90+
MatTabGroup,
91+
MatTab,
8992
TranslatePipe
9093
]
9194
})
@@ -108,6 +111,62 @@ export class DevicesComponent implements OnInit, AfterViewInit {
108111
public powerStates: any
109112
public isCloudMode: boolean = environment.cloud
110113

114+
public activeTab = signal(0)
115+
public allDevicesData: Device[] = []
116+
private serverTotalCount = 0
117+
118+
get allCount(): number {
119+
return this.serverTotalCount
120+
}
121+
122+
get allTabLabel(): string {
123+
return `${this.translate.instant('devices.tabs.all.value')} (${this.allCount})`
124+
}
125+
126+
get activatedTabLabel(): string {
127+
return `${this.translate.instant('devices.tabs.activated.value')} (${this.activatedCount})`
128+
}
129+
130+
get discoveredTabLabel(): string {
131+
return `${this.translate.instant('devices.tabs.discovered.value')} (${this.discoveredCount})`
132+
}
133+
134+
get activatedCount(): number {
135+
return this.allDevicesData.filter(
136+
(d) => d.deviceInfo?.currentMode != null && d.deviceInfo.currentMode !== 'not activated'
137+
).length
138+
}
139+
140+
get discoveredCount(): number {
141+
return this.allDevicesData.filter((d) => d.deviceInfo?.discovered === true).length
142+
}
143+
144+
onTabChange(index: number): void {
145+
this.activeTab.set(index)
146+
this.applyTabFilter()
147+
}
148+
149+
private applyTabFilter(): void {
150+
let filtered: Device[]
151+
switch (this.activeTab()) {
152+
case 1:
153+
filtered = this.allDevicesData.filter(
154+
(d) => d.deviceInfo?.currentMode != null && d.deviceInfo.currentMode !== 'not activated'
155+
)
156+
this.totalCount.set(filtered.length)
157+
break
158+
case 2:
159+
filtered = this.allDevicesData.filter((d) => d.deviceInfo?.discovered === true)
160+
this.totalCount.set(filtered.length)
161+
break
162+
default:
163+
filtered = this.allDevicesData
164+
this.totalCount.set(this.serverTotalCount)
165+
break
166+
}
167+
this.devices.data = filtered
168+
}
169+
111170
get deleteDeviceLabel(): string {
112171
return this.isCloudMode
113172
? this.translate.instant('devices.actions.deactivateCloud.value')
@@ -119,6 +178,7 @@ export class DevicesComponent implements OnInit, AfterViewInit {
119178
'hostname',
120179
'guid',
121180
'status',
181+
'productType',
122182
'tags',
123183
'actions',
124184
'notification'
@@ -140,6 +200,7 @@ export class DevicesComponent implements OnInit, AfterViewInit {
140200
this.displayedColumns = [
141201
'select',
142202
'hostname',
203+
'productType',
143204
'tags',
144205
'actions',
145206
'notification'
@@ -211,6 +272,7 @@ export class DevicesComponent implements OnInit, AfterViewInit {
211272
.pipe(
212273
switchMap((res) => {
213274
this.totalCount.set(res.totalCount)
275+
this.serverTotalCount = res.totalCount
214276

215277
if (!environment.cloud) {
216278
return of(res.data) // Return as-is for non-cloud
@@ -251,7 +313,8 @@ export class DevicesComponent implements OnInit, AfterViewInit {
251313
})
252314
)
253315
.subscribe((devices) => {
254-
this.devices.data = devices
316+
this.allDevicesData = devices
317+
this.applyTabFilter()
255318

256319
// Restore selection state on data retrieval
257320
this.selectedDevices.clear()
@@ -335,13 +398,23 @@ export class DevicesComponent implements OnInit, AfterViewInit {
335398
}
336399

337400
isNoData(): boolean {
338-
return !this.isLoading() && this.totalCount() === 0
401+
return !this.isLoading() && this.allDevicesData.length === 0
339402
}
340403

341404
async navigateTo(path: string): Promise<void> {
342405
await this.router.navigate([`/devices/${path}`])
343406
}
344407

408+
getProductType(device: Device): string {
409+
const skuNum = parseInt(device.deviceInfo?.fwSku ?? '', 10)
410+
if (isNaN(skuNum)) return ''
411+
const isISM = (skuNum & 0x10) > 0
412+
const isVPro = (skuNum & 0x08) > 0
413+
if (isISM) return 'ISM'
414+
if (isVPro) return 'vPro'
415+
return ''
416+
}
417+
345418
translateConnectionStatus(status?: boolean): string {
346419
switch (status) {
347420
case false:

src/assets/i18n/ar.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,10 +1065,26 @@
10651065
"description": "عنوان عمود الجدول لحالة الجهاز",
10661066
"value": "الحالة"
10671067
},
1068+
"devices.table.productType": {
1069+
"description": "Table column header for product type",
1070+
"value": "نوع المنتج"
1071+
},
10681072
"devices.table.tags": {
10691073
"description": "عنوان عمود الجدول للعلامات",
10701074
"value": "العلامات"
10711075
},
1076+
"devices.tabs.all": {
1077+
"description": "Tab label for all devices",
1078+
"value": "الكل"
1079+
},
1080+
"devices.tabs.activated": {
1081+
"description": "Tab label for activated devices",
1082+
"value": "مُفعَّل"
1083+
},
1084+
"devices.tabs.discovered": {
1085+
"description": "Tab label for discovered devices",
1086+
"value": "مكتشَف"
1087+
},
10721088
"deviceToolbar.power.on": {
10731089
"description": "تلميح الطاقة للحالة تشغيل",
10741090
"value": "الطاقة: تشغيل"

src/assets/i18n/de.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,10 +1065,26 @@
10651065
"description": "Tabellen-Spaltenüberschrift für Gerätestatus",
10661066
"value": "Status"
10671067
},
1068+
"devices.table.productType": {
1069+
"description": "Table column header for product type",
1070+
"value": "Produkttyp"
1071+
},
10681072
"devices.table.tags": {
10691073
"description": "Tabellen-Spaltenüberschrift für Tags",
10701074
"value": "Tags"
10711075
},
1076+
"devices.tabs.all": {
1077+
"description": "Tab label for all devices",
1078+
"value": "Alle"
1079+
},
1080+
"devices.tabs.activated": {
1081+
"description": "Tab label for activated devices",
1082+
"value": "Aktiviert"
1083+
},
1084+
"devices.tabs.discovered": {
1085+
"description": "Tab label for discovered devices",
1086+
"value": "Entdeckt"
1087+
},
10721088
"deviceToolbar.power.on": {
10731089
"description": "Energie-Tooltip für Eingeschaltet",
10741090
"value": "Strom: Ein"

src/assets/i18n/en.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,10 +1237,26 @@
12371237
"description": "Table column header for device status",
12381238
"value": "Status"
12391239
},
1240+
"devices.table.productType": {
1241+
"description": "Table column header for product type",
1242+
"value": "Product Type"
1243+
},
12401244
"devices.table.tags": {
12411245
"description": "Table column header for tags",
12421246
"value": "Tags"
12431247
},
1248+
"devices.tabs.all": {
1249+
"description": "Tab label for all devices",
1250+
"value": "All"
1251+
},
1252+
"devices.tabs.activated": {
1253+
"description": "Tab label for activated devices",
1254+
"value": "Activated"
1255+
},
1256+
"devices.tabs.discovered": {
1257+
"description": "Tab label for discovered devices",
1258+
"value": "Discovered"
1259+
},
12441260
"deviceToolbar.power.on": {
12451261
"description": "Power tooltip for On",
12461262
"value": "Power: On"
@@ -1253,10 +1269,6 @@
12531269
"description": "Power tooltip for Off",
12541270
"value": "Power: Off"
12551271
},
1256-
"deviceToolbar.power.refreshAriaLabel": {
1257-
"description": "Aria label for the refresh power status button",
1258-
"value": "Refresh power status"
1259-
},
12601272
"deviceUserConsent.description": {
12611273
"description": "Description for user consent for devices",
12621274
"value": "A user consent code generated by Intel AMT is required to access the system."

0 commit comments

Comments
 (0)