From 9be207fac62a20fd825375805496e6c56f28ce35 Mon Sep 17 00:00:00 2001 From: ShradhaGupta31 Date: Tue, 21 Jul 2026 20:04:28 +0530 Subject: [PATCH 1/4] 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 --- src/app/devices/devices.component.html | 5 +++++ src/assets/i18n/en.json | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html index 452e92ec1..8eb52315d 100644 --- a/src/app/devices/devices.component.html +++ b/src/app/devices/devices.component.html @@ -53,6 +53,11 @@

}

} @else { + + + + +
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 581cd2cdb..29162cafc 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1427,10 +1427,6 @@ "description": "Power tooltip for Off", "value": "Power: Off" }, - "deviceToolbar.power.refreshAriaLabel": { - "description": "Aria label for the refresh power status button", - "value": "Refresh power status" - }, "deviceUserConsent.description": { "description": "Description for user consent for devices", "value": "A user consent code generated by Intel AMT is required to access the system." From da43ab7f480ad2ac8dfb5f016a40e6b36ebe1753 Mon Sep 17 00:00:00 2001 From: ShradhaGupta31 Date: Mon, 31 Aug 2026 11:33:16 +0530 Subject: [PATCH 2/4] refactor: Address review comments - updated logic to include server side counts Signed-off-by: ShradhaGupta31 --- src/app/devices/devices.component.html | 5 ----- src/assets/i18n/en.json | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html index 8eb52315d..452e92ec1 100644 --- a/src/app/devices/devices.component.html +++ b/src/app/devices/devices.component.html @@ -53,11 +53,6 @@

}

} @else { - - - - -
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 29162cafc..581cd2cdb 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1427,6 +1427,10 @@ "description": "Power tooltip for Off", "value": "Power: Off" }, + "deviceToolbar.power.refreshAriaLabel": { + "description": "Aria label for the refresh power status button", + "value": "Refresh power status" + }, "deviceUserConsent.description": { "description": "Description for user consent for devices", "value": "A user consent code generated by Intel AMT is required to access the system." From 429f0cddf5a9deab138c0f218e2268d71305613e Mon Sep 17 00:00:00 2001 From: ShradhaGupta31 Date: Mon, 31 Aug 2026 13:44:29 +0530 Subject: [PATCH 3/4] feat(devices): add device type column Signed-off-by: ShradhaGupta31 --- src/app/devices/devices.component.html | 13 ++++++++++++- src/app/devices/devices.component.scss | 19 +++++++++++++++++++ src/app/devices/devices.component.spec.ts | 17 +++++++++++++++++ src/app/devices/devices.component.ts | 7 +++++++ src/assets/i18n/ar.json | 4 ++++ src/assets/i18n/de.json | 4 ++++ src/assets/i18n/en.json | 4 ++++ src/assets/i18n/es.json | 4 ++++ src/assets/i18n/fi.json | 4 ++++ src/assets/i18n/fr.json | 4 ++++ src/assets/i18n/he.json | 4 ++++ src/assets/i18n/it.json | 4 ++++ src/assets/i18n/ja.json | 4 ++++ src/assets/i18n/nl.json | 4 ++++ src/assets/i18n/ru.json | 4 ++++ src/assets/i18n/sv.json | 4 ++++ 16 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html index 452e92ec1..d0350ed4b 100644 --- a/src/app/devices/devices.component.html +++ b/src/app/devices/devices.component.html @@ -132,7 +132,18 @@

{{ 'devices.table.productType.value' | translate }} - {{ getProductType(element) }} + {{ getProductType(element) }} + + + + + {{ + 'devices.table.type.value' | translate + }} + + + {{ 'devices.tabs.' + getDeviceType(element) + '.value' | translate }} + diff --git a/src/app/devices/devices.component.scss b/src/app/devices/devices.component.scss index f18d32f9b..556f4ea11 100644 --- a/src/app/devices/devices.component.scss +++ b/src/app/devices/devices.component.scss @@ -7,6 +7,25 @@ flex: 0 0 75px; } +.product-type-chip, +.device-type-chip { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 12px; + border-radius: 16px; + background: #e5e5e5; +} + +.device-type-chip { + background: #b0bec5; +} + +.device-type-chip.activated { + background: #03a9f4; + color: #fff; +} + mat-icon.addTag { visibility: hidden; } diff --git a/src/app/devices/devices.component.spec.ts b/src/app/devices/devices.component.spec.ts index f1bf5a6cc..d5f783f91 100644 --- a/src/app/devices/devices.component.spec.ts +++ b/src/app/devices/devices.component.spec.ts @@ -285,6 +285,23 @@ describe('DevicesComponent', () => { }) }) + describe('getDeviceType', () => { + it('should return activated for a real AMT control mode', () => { + const device = { ...device01, deviceInfo: { currentMode: 'client control mode' } } as Device + expect(component.getDeviceType(device)).toBe('activated') + }) + + it('should return discovered when the device is not activated', () => { + const device = { ...device01, deviceInfo: { currentMode: 'not activated' } } as Device + expect(component.getDeviceType(device)).toBe('discovered') + }) + + it('should return discovered when currentMode is missing', () => { + const device = { ...device01, deviceInfo: undefined } as Device + expect(component.getDeviceType(device)).toBe('discovered') + }) + }) + describe('onTabChange / server-side counts', () => { beforeEach(() => { getDevicesSpy.mockClear() diff --git a/src/app/devices/devices.component.ts b/src/app/devices/devices.component.ts index 4d1155066..b0ad1292b 100644 --- a/src/app/devices/devices.component.ts +++ b/src/app/devices/devices.component.ts @@ -199,6 +199,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { 'guid', 'status', 'productType', + 'deviceType', 'tags', 'actions', 'notification' @@ -221,6 +222,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { 'select', 'hostname', 'productType', + 'deviceType', 'tags', 'actions', 'notification' @@ -435,6 +437,11 @@ export class DevicesComponent implements OnInit, AfterViewInit { return '' } + getDeviceType(device: Device): DeviceFilterStatus { + const currentMode = device.deviceInfo?.currentMode?.trim().toLowerCase() + return currentMode && currentMode !== 'not activated' ? 'activated' : 'discovered' + } + translateConnectionStatus(status?: boolean): string { switch (status) { case false: diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json index 1368112d0..46a8550e2 100644 --- a/src/assets/i18n/ar.json +++ b/src/assets/i18n/ar.json @@ -1077,6 +1077,10 @@ "description": "Table column header for product type", "value": "نوع المنتج" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "النوع" + }, "devices.table.tags": { "description": "عنوان عمود الجدول للعلامات", "value": "العلامات" diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 0b83319d6..31ef0a06f 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1073,6 +1073,10 @@ "description": "Table column header for product type", "value": "Produkttyp" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Typ" + }, "devices.table.tags": { "description": "Tabellen-Spaltenüberschrift für Tags", "value": "Tags" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 581cd2cdb..1cdb5d61f 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1395,6 +1395,10 @@ "description": "Table column header for product type", "value": "Product Type" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Type" + }, "devices.table.tags": { "description": "Table column header for tags", "value": "Tags" diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index dd10f6ef8..92fbb6f23 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -1073,6 +1073,10 @@ "description": "Table column header for product type", "value": "Tipo de producto" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Tipo" + }, "devices.table.tags": { "description": "Encabezado de columna de tabla para etiquetas", "value": "Etiquetas" diff --git a/src/assets/i18n/fi.json b/src/assets/i18n/fi.json index 36acaf083..6f27406ca 100644 --- a/src/assets/i18n/fi.json +++ b/src/assets/i18n/fi.json @@ -1073,6 +1073,10 @@ "description": "Table column header for product type", "value": "Tuotetyyppi" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Tyyppi" + }, "devices.table.tags": { "description": "Taulukon sarakkeen otsikko tunnisteille", "value": "Tunnisteet" diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index f1c3799b5..076ad2917 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -1077,6 +1077,10 @@ "description": "Table column header for product type", "value": "Type de produit" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Type" + }, "devices.table.tags": { "description": "En-tête de colonne du tableau pour les balises", "value": "Balises" diff --git a/src/assets/i18n/he.json b/src/assets/i18n/he.json index 950d01b23..8653aeff0 100644 --- a/src/assets/i18n/he.json +++ b/src/assets/i18n/he.json @@ -1069,6 +1069,10 @@ "description": "Table column header for product type", "value": "סוג מוצר" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "סוג" + }, "devices.table.tags": { "description": "כותרת עמודות טבלה לתגיות", "value": "תגיות" diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 89af0797a..4627f397f 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -1069,6 +1069,10 @@ "description": "Table column header for product type", "value": "Tipo di prodotto" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Tipo" + }, "devices.table.tags": { "description": "Intestazione della colonna della tabella per i tag", "value": "Tag" diff --git a/src/assets/i18n/ja.json b/src/assets/i18n/ja.json index a57473d7d..48f69a573 100644 --- a/src/assets/i18n/ja.json +++ b/src/assets/i18n/ja.json @@ -1069,6 +1069,10 @@ "description": "Table column header for product type", "value": "製品タイプ" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "タイプ" + }, "devices.table.tags": { "description": "タグ用テーブル列ヘッダー", "value": "タグ" diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index 0a48a08a4..54dd0de45 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -1077,6 +1077,10 @@ "description": "Table column header for product type", "value": "Producttype" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Type" + }, "devices.table.tags": { "description": "Tabelkolomkop voor tags", "value": "Tags" diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index d25e66b8f..b480b5e22 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -1077,6 +1077,10 @@ "description": "Table column header for product type", "value": "Тип продукта" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Тип" + }, "devices.table.tags": { "description": "Заголовок столбца таблицы для тегов", "value": "Теги" diff --git a/src/assets/i18n/sv.json b/src/assets/i18n/sv.json index 68d55108e..e379dd734 100644 --- a/src/assets/i18n/sv.json +++ b/src/assets/i18n/sv.json @@ -981,6 +981,10 @@ "description": "Table column header for product type", "value": "Produkttyp" }, + "devices.table.type": { + "description": "Table column header for device type", + "value": "Typ" + }, "devices.table.tags": { "description": "Tabellkolumnrubrik för taggar", "value": "Taggar" From 504a39050a5ffc3a7905b2aff6135b32a877cdf0 Mon Sep 17 00:00:00 2001 From: ShradhaGupta31 Date: Mon, 7 Sep 2026 21:34:08 +0530 Subject: [PATCH 4/4] refactor: fix review comments & updated logic for UI tab render - fixed review comments Signed-off-by: ShradhaGupta31 --- cypress/e2e/integration/device/paging.spec.ts | 12 ---------- src/app/devices/devices.component.html | 23 ++++++++++--------- src/app/devices/devices.component.scss | 2 +- src/app/devices/devices.component.spec.ts | 10 ++++++++ src/app/devices/devices.component.ts | 13 ++++++++--- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/cypress/e2e/integration/device/paging.spec.ts b/cypress/e2e/integration/device/paging.spec.ts index b6c612cbf..5210e0698 100644 --- a/cypress/e2e/integration/device/paging.spec.ts +++ b/cypress/e2e/integration/device/paging.spec.ts @@ -20,17 +20,6 @@ describe('Test Device Page', () => { body: devices.getAll.forPaging.response }).as('get-devices') - cy.myIntercept('GET', 'api/v1/devices/stats', { - statusCode: httpCodes.SUCCESS, - body: { - totalCount: deviceFixtures.totalCount, - connectedCount: 0, - disconnectedCount: 0, - activatedCount: 0, - discoveredCount: 0 - } - }).as('get-device-stats') - cy.myIntercept('GET', /tags$/, { statusCode: httpCodes.SUCCESS, body: tags.getAll.success.response @@ -42,7 +31,6 @@ describe('Test Device Page', () => { }).as('get-powerstate') cy.goToPage('Devices') - cy.wait('@get-device-stats') }) it('pagination for next page', () => { diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html index d0350ed4b..72b20d7e1 100644 --- a/src/app/devices/devices.component.html +++ b/src/app/devices/devices.component.html @@ -37,13 +37,15 @@ - - - - - - + @if (!isCloudMode) { + + + + + + + } @if (isNoData()) {

@if (!filteredTags().length) { @@ -101,7 +103,7 @@

{{ element.hostname }} - @if (element.friendlyName !== null) { + @if (element.friendlyName) {  ({{ element.friendlyName }}) } @@ -137,9 +139,8 @@

- {{ - 'devices.table.type.value' | translate - }} + + {{ 'devices.table.type.value' | translate }} {{ 'devices.tabs.' + getDeviceType(element) + '.value' | translate }} diff --git a/src/app/devices/devices.component.scss b/src/app/devices/devices.component.scss index 556f4ea11..2aa7330a0 100644 --- a/src/app/devices/devices.component.scss +++ b/src/app/devices/devices.component.scss @@ -22,7 +22,7 @@ } .device-type-chip.activated { - background: #03a9f4; + background: #0277bd; color: #fff; } diff --git a/src/app/devices/devices.component.spec.ts b/src/app/devices/devices.component.spec.ts index d5f783f91..3873bff10 100644 --- a/src/app/devices/devices.component.spec.ts +++ b/src/app/devices/devices.component.spec.ts @@ -300,10 +300,20 @@ describe('DevicesComponent', () => { const device = { ...device01, deviceInfo: undefined } as Device expect(component.getDeviceType(device)).toBe('discovered') }) + + it('should keep the computed device type logic stable for rendering', () => { + const activatedDevice = { ...device01, deviceInfo: { currentMode: 'client control mode' } } as Device + const discoveredDevice = { ...device02, deviceInfo: { currentMode: 'not activated' } } as Device + + expect(component.getDeviceType(activatedDevice)).toBe('activated') + expect(component.getDeviceType(discoveredDevice)).toBe('discovered') + }) }) describe('onTabChange / server-side counts', () => { beforeEach(() => { + component.isCloudMode = false + component.getDevices() getDevicesSpy.mockClear() }) diff --git a/src/app/devices/devices.component.ts b/src/app/devices/devices.component.ts index b0ad1292b..89ba4ed5a 100644 --- a/src/app/devices/devices.component.ts +++ b/src/app/devices/devices.component.ts @@ -285,10 +285,12 @@ export class DevicesComponent implements OnInit, AfterViewInit { getDevices(): void { this.isLoading.set(true) + let responseTotalCount: number | undefined - // Counts (all/activated/discovered) are computed server-side and shared with - // headless/API consumers, so refresh them alongside the current page. - this.loadStats() + if (!this.isCloudMode) { + // Console exposes server-side counts for the activated/discovered tabs. + this.loadStats() + } // Store previous selection before making the request const prevSelected = this.selectedDevices.selected.map((d) => d.guid) @@ -297,6 +299,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { .getDevices({ ...this.pageEvent, tags: this.filteredTags(), status: this.currentTabStatus() }) .pipe( switchMap((res) => { + responseTotalCount = res.totalCount if (!environment.cloud) { return of(res.data) // Return as-is for non-cloud } @@ -337,6 +340,10 @@ export class DevicesComponent implements OnInit, AfterViewInit { ) .subscribe((devices) => { this.devices.data = devices + if (this.isCloudMode) { + this.serverTotalCount = responseTotalCount ?? devices.length + this.totalCount.set(this.serverTotalCount) + } // Restore selection state on data retrieval this.selectedDevices.clear()