diff --git a/cypress/e2e/integration/device/device.spec.ts b/cypress/e2e/integration/device/device.spec.ts index 1ee01f3cf..8b8a4498e 100644 --- a/cypress/e2e/integration/device/device.spec.ts +++ b/cypress/e2e/integration/device/device.spec.ts @@ -41,7 +41,7 @@ describe('Test Device Page', () => { if (Cypress.env('ISOLATE').charAt(0).toLowerCase() !== 'n') { cy.myIntercept('GET', '**/devices?tags=Windows&$top=25&$skip=0&$count=true', { statusCode: httpCodes.SUCCESS, - body: devices.getAll.windows.response.data + body: devices.getAll.windows.response }).as('get-windows') cy.goToPage('Devices') diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html index 4b3923ada..008a7e854 100644 --- a/src/app/devices/devices.component.html +++ b/src/app/devices/devices.component.html @@ -46,6 +46,11 @@

}

} @else { + + + + +
@@ -120,6 +125,15 @@

} + + + {{ + 'devices.table.productType.value' | translate + }} + + {{ getProductType(element) }} + + @@ -203,7 +217,7 @@

diff --git a/src/app/devices/devices.component.spec.ts b/src/app/devices/devices.component.spec.ts index 2e0b472ae..0b53e5144 100644 --- a/src/app/devices/devices.component.spec.ts +++ b/src/app/devices/devices.component.spec.ts @@ -250,4 +250,100 @@ describe('DevicesComponent', () => { component.tagFilterChange(matSelectChange) expect(component.filteredTags()).toBe(mockValue) }) + + describe('getProductType', () => { + it('should return ISM when bit 4 (0x10) is set', () => { + const device = { ...device01, deviceInfo: { fwSku: '16' } } as Device // 0x10 = 16 + expect(component.getProductType(device)).toBe('ISM') + }) + + it('should return vPro when bit 3 (0x08) is set and bit 4 is not', () => { + const device = { ...device01, deviceInfo: { fwSku: '8' } } as Device // 0x08 = 8 + expect(component.getProductType(device)).toBe('vPro') + }) + + it('should return ISM when both bit 4 and bit 3 are set (ISM takes priority)', () => { + const device = { ...device01, deviceInfo: { fwSku: '24' } } as Device // 0x18 = 24 + expect(component.getProductType(device)).toBe('ISM') + }) + + it('should return empty string when neither bit is set', () => { + const device = { ...device01, deviceInfo: { fwSku: '4' } } as Device // 0x04 = 4 + expect(component.getProductType(device)).toBe('') + }) + + it('should return empty string when fwSku is undefined', () => { + const device = { ...device01, deviceInfo: undefined } as Device + expect(component.getProductType(device)).toBe('') + }) + + it('should return empty string when fwSku is not a number', () => { + const device = { ...device01, deviceInfo: { fwSku: 'notanumber' } } as Device + expect(component.getProductType(device)).toBe('') + }) + }) + + describe('onTabChange / applyTabFilter', () => { + beforeEach(() => { + const baseInfo = { fwVersion: '', fwBuild: '', fwSku: '0', features: '', ipAddress: '' } + component.allDevicesData = [ + { ...device01, deviceInfo: { ...baseInfo, currentMode: 'acm', discovered: false } }, + { ...device02, deviceInfo: { ...baseInfo, currentMode: 'not activated', discovered: true } } + ] + }) + + it('should show all devices on tab 0', () => { + component.onTabChange(0) + expect(component.devices.data.length).toBe(2) + }) + + it('should filter to activated devices on tab 1', () => { + component.onTabChange(1) + expect(component.devices.data.length).toBe(1) + expect(component.devices.data[0].guid).toBe(device01.guid) + }) + + it('should filter to discovered devices on tab 2', () => { + component.onTabChange(2) + expect(component.devices.data.length).toBe(1) + expect(component.devices.data[0].guid).toBe(device02.guid) + }) + + it('should set totalCount to serverTotalCount on tab 0', () => { + ;(component as any).serverTotalCount = 42 + component.onTabChange(0) + expect(component.totalCount()).toBe(42) + }) + + it('should set totalCount to filtered length on tab 1', () => { + component.onTabChange(1) + expect(component.totalCount()).toBe(1) + }) + + it('should set totalCount to filtered length on tab 2', () => { + component.onTabChange(2) + expect(component.totalCount()).toBe(1) + }) + }) + + describe('isNoData', () => { + it('should return false when allDevicesData has entries regardless of totalCount', () => { + component.allDevicesData = [device01] + component.isLoading.set(false) + component.totalCount.set(0) // filtered tab has 0 — should not trigger no-data + expect(component.isNoData()).toBeFalse() + }) + + it('should return true only when allDevicesData is empty and not loading', () => { + component.allDevicesData = [] + component.isLoading.set(false) + expect(component.isNoData()).toBeTrue() + }) + + it('should return false when loading even if allDevicesData is empty', () => { + component.allDevicesData = [] + component.isLoading.set(true) + expect(component.isNoData()).toBeFalse() + }) + }) }) diff --git a/src/app/devices/devices.component.ts b/src/app/devices/devices.component.ts index 27d9df77f..5907ba1df 100644 --- a/src/app/devices/devices.component.ts +++ b/src/app/devices/devices.component.ts @@ -47,6 +47,7 @@ import { MatButton, MatIconButton } from '@angular/material/button' import { MatToolbar } from '@angular/material/toolbar' import { MatSort } from '@angular/material/sort' import { MatInput } from '@angular/material/input' +import { MatTabGroup, MatTab } from '@angular/material/tabs' import { TranslatePipe, TranslateService } from '@ngx-translate/core' @Component({ @@ -86,6 +87,8 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core' MatPaginator, MatHint, RouterModule, + MatTabGroup, + MatTab, TranslatePipe ] }) @@ -108,6 +111,62 @@ export class DevicesComponent implements OnInit, AfterViewInit { public powerStates: any public isCloudMode: boolean = environment.cloud + public activeTab = signal(0) + public allDevicesData: Device[] = [] + private serverTotalCount = 0 + + get allCount(): number { + return this.serverTotalCount + } + + get allTabLabel(): string { + return `${this.translate.instant('devices.tabs.all.value')} (${this.allCount})` + } + + get activatedTabLabel(): string { + return `${this.translate.instant('devices.tabs.activated.value')} (${this.activatedCount})` + } + + get discoveredTabLabel(): string { + return `${this.translate.instant('devices.tabs.discovered.value')} (${this.discoveredCount})` + } + + get activatedCount(): number { + return this.allDevicesData.filter( + (d) => d.deviceInfo?.currentMode != null && d.deviceInfo.currentMode !== 'not activated' + ).length + } + + get discoveredCount(): number { + return this.allDevicesData.filter((d) => d.deviceInfo?.discovered === true).length + } + + onTabChange(index: number): void { + this.activeTab.set(index) + this.applyTabFilter() + } + + private applyTabFilter(): void { + let filtered: Device[] + switch (this.activeTab()) { + case 1: + filtered = this.allDevicesData.filter( + (d) => d.deviceInfo?.currentMode != null && d.deviceInfo.currentMode !== 'not activated' + ) + this.totalCount.set(filtered.length) + break + case 2: + filtered = this.allDevicesData.filter((d) => d.deviceInfo?.discovered === true) + this.totalCount.set(filtered.length) + break + default: + filtered = this.allDevicesData + this.totalCount.set(this.serverTotalCount) + break + } + this.devices.data = filtered + } + get deleteDeviceLabel(): string { return this.isCloudMode ? this.translate.instant('devices.actions.deactivateCloud.value') @@ -119,6 +178,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { 'hostname', 'guid', 'status', + 'productType', 'tags', 'actions', 'notification' @@ -140,6 +200,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { this.displayedColumns = [ 'select', 'hostname', + 'productType', 'tags', 'actions', 'notification' @@ -211,6 +272,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { .pipe( switchMap((res) => { this.totalCount.set(res.totalCount) + this.serverTotalCount = res.totalCount if (!environment.cloud) { return of(res.data) // Return as-is for non-cloud @@ -251,7 +313,8 @@ export class DevicesComponent implements OnInit, AfterViewInit { }) ) .subscribe((devices) => { - this.devices.data = devices + this.allDevicesData = devices + this.applyTabFilter() // Restore selection state on data retrieval this.selectedDevices.clear() @@ -335,13 +398,23 @@ export class DevicesComponent implements OnInit, AfterViewInit { } isNoData(): boolean { - return !this.isLoading() && this.totalCount() === 0 + return !this.isLoading() && this.allDevicesData.length === 0 } async navigateTo(path: string): Promise { await this.router.navigate([`/devices/${path}`]) } + getProductType(device: Device): string { + const skuNum = parseInt(device.deviceInfo?.fwSku ?? '', 10) + if (isNaN(skuNum)) return '' + const isISM = (skuNum & 0x10) > 0 + const isVPro = (skuNum & 0x08) > 0 + if (isISM) return 'ISM' + if (isVPro) return 'vPro' + return '' + } + translateConnectionStatus(status?: boolean): string { switch (status) { case false: diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json index 57c85e7f4..e91b1eff5 100644 --- a/src/assets/i18n/ar.json +++ b/src/assets/i18n/ar.json @@ -1065,10 +1065,26 @@ "description": "عنوان عمود الجدول لحالة الجهاز", "value": "الحالة" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "نوع المنتج" + }, "devices.table.tags": { "description": "عنوان عمود الجدول للعلامات", "value": "العلامات" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "الكل" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "مُفعَّل" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "مكتشَف" + }, "deviceToolbar.power.on": { "description": "تلميح الطاقة للحالة تشغيل", "value": "الطاقة: تشغيل" diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index bc84346e9..b1aeecf32 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1065,10 +1065,26 @@ "description": "Tabellen-Spaltenüberschrift für Gerätestatus", "value": "Status" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Produkttyp" + }, "devices.table.tags": { "description": "Tabellen-Spaltenüberschrift für Tags", "value": "Tags" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Alle" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Aktiviert" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Entdeckt" + }, "deviceToolbar.power.on": { "description": "Energie-Tooltip für Eingeschaltet", "value": "Strom: Ein" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5830a8654..d0d4819bd 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1237,10 +1237,26 @@ "description": "Table column header for device status", "value": "Status" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Product Type" + }, "devices.table.tags": { "description": "Table column header for tags", "value": "Tags" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "All" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Activated" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Discovered" + }, "deviceToolbar.power.on": { "description": "Power tooltip for On", "value": "Power: On" @@ -1253,10 +1269,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." diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 4a5583b57..3d7c5afcc 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -1065,10 +1065,26 @@ "description": "Encabezado de columna de la tabla para el estado del dispositivo", "value": "Estado" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Tipo de producto" + }, "devices.table.tags": { "description": "Encabezado de columna de tabla para etiquetas", "value": "Etiquetas" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Todos" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Activado" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Descubierto" + }, "deviceToolbar.power.on": { "description": "Información de energía para Encendido", "value": "Energía: Encendido" diff --git a/src/assets/i18n/fi.json b/src/assets/i18n/fi.json index 2aeabbdbc..a9a70a9db 100644 --- a/src/assets/i18n/fi.json +++ b/src/assets/i18n/fi.json @@ -1065,10 +1065,26 @@ "description": "Laitteen tilan taulukon sarakkeen otsikko", "value": "Tila" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Tuotetyyppi" + }, "devices.table.tags": { "description": "Taulukon sarakkeen otsikko tunnisteille", "value": "Tunnisteet" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Kaikki" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Aktivoitu" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Löydetty" + }, "deviceToolbar.power.on": { "description": "Virtatilan vihjeteksti tilalle päällä", "value": "Virta: Päällä" diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index fbe4a1230..3652c56d3 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -1065,10 +1065,26 @@ "description": "En-tête de colonne du tableau pour l'état de l'appareil", "value": "Statut" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Type de produit" + }, "devices.table.tags": { "description": "En-tête de colonne du tableau pour les balises", "value": "Balises" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Tous" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Activé" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Découvert" + }, "deviceToolbar.power.on": { "description": "Info-bulle d'alimentation pour l'état allumé", "value": "Alimentation : Allumé" diff --git a/src/assets/i18n/he.json b/src/assets/i18n/he.json index 6315ca2cb..d38a34010 100644 --- a/src/assets/i18n/he.json +++ b/src/assets/i18n/he.json @@ -1065,10 +1065,26 @@ "description": "כותרת עמודות טבלה למצב המכשיר", "value": "סטָטוּס" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "סוג מוצר" + }, "devices.table.tags": { "description": "כותרת עמודות טבלה לתגיות", "value": "תגיות" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "הכל" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "מופעל" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "התגלה" + }, "deviceToolbar.power.on": { "description": "רמז מצב צריכת חשמל עבור פועל", "value": "חשמל: פועל" diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 398e4c77d..de20e95b7 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -1065,10 +1065,26 @@ "description": "Intestazione della colonna della tabella per lo stato del dispositivo", "value": "Stato" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Tipo di prodotto" + }, "devices.table.tags": { "description": "Intestazione della colonna della tabella per i tag", "value": "Tag" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Tutti" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Attivato" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Scoperto" + }, "deviceToolbar.power.on": { "description": "Tooltip di alimentazione per acceso", "value": "Alimentazione: Acceso" diff --git a/src/assets/i18n/ja.json b/src/assets/i18n/ja.json index c85d85cd0..63fe8f091 100644 --- a/src/assets/i18n/ja.json +++ b/src/assets/i18n/ja.json @@ -1065,10 +1065,26 @@ "description": "デバイス状態のテーブル列見出し", "value": "ステータス" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "製品タイプ" + }, "devices.table.tags": { "description": "タグ用テーブル列ヘッダー", "value": "タグ" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "すべて" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "アクティベート済み" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "検出済み" + }, "deviceToolbar.power.on": { "description": "電源オンのツールチップ", "value": "電源: オン" diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index f5a255335..60ad0e8c7 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -1065,10 +1065,26 @@ "description": "Tabelkolomkop voor apparaatstatus", "value": "Status" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Producttype" + }, "devices.table.tags": { "description": "Tabelkolomkop voor tags", "value": "Tags" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Alle" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Geactiveerd" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Ontdekt" + }, "deviceToolbar.power.on": { "description": "Energie-tooltip voor ingeschakeld", "value": "Stroom: Aan" diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 15c59a596..2f87bc9f5 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -1065,10 +1065,26 @@ "description": "Заголовок столбца таблицы для состояния устройства", "value": "Статус" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Тип продукта" + }, "devices.table.tags": { "description": "Заголовок столбца таблицы для тегов", "value": "Теги" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Все" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Активирован" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Обнаружен" + }, "deviceToolbar.power.on": { "description": "Подсказка питания для состояния включено", "value": "Питание: Включено" diff --git a/src/assets/i18n/sv.json b/src/assets/i18n/sv.json index b76e46eee..ef06304c5 100644 --- a/src/assets/i18n/sv.json +++ b/src/assets/i18n/sv.json @@ -1065,10 +1065,26 @@ "description": "Tabellkolumnrubrik för enhetsstatus", "value": "Status" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Produkttyp" + }, "devices.table.tags": { "description": "Tabellkolumnrubrik för taggar", "value": "Taggar" }, + "devices.tabs.all": { + "description": "Tab label for all devices", + "value": "Alla" + }, + "devices.tabs.activated": { + "description": "Tab label for activated devices", + "value": "Aktiverad" + }, + "devices.tabs.discovered": { + "description": "Tab label for discovered devices", + "value": "Upptäckt" + }, "deviceToolbar.power.on": { "description": "Strömverktygstips för på", "value": "Ström: På" diff --git a/src/models/models.ts b/src/models/models.ts index ef1e1db38..e0200c13b 100644 --- a/src/models/models.ts +++ b/src/models/models.ts @@ -31,6 +31,7 @@ export interface DeviceInfo { currentMode: string features: string ipAddress: string + discovered?: boolean firstDiscovered?: Date lastSynced?: Date }