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/cypress/e2e/integration/device/paging.spec.ts b/cypress/e2e/integration/device/paging.spec.ts index 5210e0698..b6c612cbf 100644 --- a/cypress/e2e/integration/device/paging.spec.ts +++ b/cypress/e2e/integration/device/paging.spec.ts @@ -20,6 +20,17 @@ 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 @@ -31,6 +42,7 @@ describe('Test Device Page', () => { }).as('get-powerstate') cy.goToPage('Devices') + cy.wait('@get-device-stats') }) it('pagination for next page', () => { diff --git a/package-lock.json b/package-lock.json index 063a81c3b..4ed689b15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7220,9 +7220,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7240,9 +7237,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7260,9 +7254,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7280,9 +7271,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7459,9 +7447,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7476,9 +7461,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7493,9 +7475,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7510,9 +7489,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7527,9 +7503,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7544,9 +7517,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7561,9 +7531,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7578,9 +7545,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7595,9 +7559,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7612,9 +7573,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7629,9 +7587,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7646,9 +7601,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7663,9 +7615,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html index 4b3923ada..452e92ec1 100644 --- a/src/app/devices/devices.component.html +++ b/src/app/devices/devices.component.html @@ -37,6 +37,13 @@ + + + + + + @if (isNoData()) {

@if (!filteredTags().length) { @@ -120,6 +127,14 @@

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

diff --git a/src/app/devices/devices.component.spec.ts b/src/app/devices/devices.component.spec.ts index aedb57c83..f1bf5a6cc 100644 --- a/src/app/devices/devices.component.spec.ts +++ b/src/app/devices/devices.component.spec.ts @@ -66,7 +66,8 @@ describe('DevicesComponent', () => { 'sendPowerAction', 'bulkPowerAction', 'sendDeactivate', - 'sendBulkDeactivate' + 'sendBulkDeactivate', + 'getStats' ]) devicesService.PowerStates.mockReturnValue({ 2: 'On', @@ -84,6 +85,9 @@ describe('DevicesComponent', () => { }) getTagsSpy = devicesService.getTags.mockReturnValue(of([])) devicesService.getPowerState.mockReturnValue(of({ powerstate: 2 })) + devicesService.getStats.mockReturnValue( + of({ totalCount: 42, connectedCount: 10, disconnectedCount: 5, activatedCount: 7, discoveredCount: 3 }) + ) sendPowerActionSpy = devicesService.sendPowerAction.mockReturnValue(of({ Body: { ReturnValueStr: 'SUCCESS' } })) sendDeactivateSpy = devicesService.sendDeactivate.mockReturnValue(of({ status: 'SUCCESS' })) TestBed.configureTestingModule({ @@ -248,4 +252,102 @@ 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 / server-side counts', () => { + beforeEach(() => { + getDevicesSpy.mockClear() + }) + + it('should request all devices (no status filter) on tab 0', () => { + component.onTabChange(0) + expect(component.activeTab()).toBe(0) + expect(getDevicesSpy).toHaveBeenCalledWith(expect.objectContaining({ status: undefined })) + }) + + it('should request activated devices from the server on tab 1', () => { + component.onTabChange(1) + expect(component.activeTab()).toBe(1) + expect(getDevicesSpy).toHaveBeenCalledWith(expect.objectContaining({ status: 'activated' })) + }) + + it('should request discovered devices from the server on tab 2', () => { + component.onTabChange(2) + expect(component.activeTab()).toBe(2) + expect(getDevicesSpy).toHaveBeenCalledWith(expect.objectContaining({ status: 'discovered' })) + }) + + it('should reset paging to the first page when switching tabs', () => { + component.pageEvent.startsFrom = 50 + component.onTabChange(1) + expect(component.pageEvent.startsFrom).toBe(0) + }) + + it('should expose server-provided counts', () => { + expect(component.allCount).toBe(42) + expect(component.activatedCount).toBe(7) + expect(component.discoveredCount).toBe(3) + }) + + it('should set currentTabCount from the active tab', () => { + component.onTabChange(0) + expect(component.currentTabCount).toBe(42) + component.onTabChange(1) + expect(component.currentTabCount).toBe(7) + component.onTabChange(2) + expect(component.currentTabCount).toBe(3) + }) + }) + + describe('isNoData', () => { + it('should return false when the table has entries regardless of totalCount', () => { + component.devices.data = [device01] + component.isLoading.set(false) + component.totalCount.set(0) // filtered tab has 0 — should not trigger no-data + expect(component.isNoData()).toBe(false) + }) + + it('should return true only when the table is empty and not loading', () => { + component.devices.data = [] + component.isLoading.set(false) + expect(component.isNoData()).toBe(true) + }) + + it('should return false when loading even if the table is empty', () => { + component.devices.data = [] + component.isLoading.set(true) + expect(component.isNoData()).toBe(false) + }) + }) }) diff --git a/src/app/devices/devices.component.ts b/src/app/devices/devices.component.ts index 27d9df77f..4d1155066 100644 --- a/src/app/devices/devices.component.ts +++ b/src/app/devices/devices.component.ts @@ -12,7 +12,7 @@ import { MatSnackBar } from '@angular/material/snack-bar' import { Router, RouterModule } from '@angular/router' import { catchError, concatMap, delay, finalize, map, switchMap } from 'rxjs/operators' import { forkJoin, from, Observable, of, throwError } from 'rxjs' -import { Device, PageEventOptions } from '../../models/models' +import { Device, DeviceFilterStatus, PageEventOptions } from '../../models/models' import { AddDeviceComponent } from '../shared/add-device/add-device.component' import SnackbarDefaults from '../shared/config/snackBarDefault' import { DevicesService } from './devices.service' @@ -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,82 @@ export class DevicesComponent implements OnInit, AfterViewInit { public powerStates: any public isCloudMode: boolean = environment.cloud + public activeTab = signal(0) + private serverTotalCount = 0 + private serverActivatedCount = 0 + private serverDiscoveredCount = 0 + + get allCount(): number { + return this.serverTotalCount + } + + // Count for the currently selected tab, used to drive the paginator length. + get currentTabCount(): number { + switch (this.activeTab()) { + case 1: + return this.serverActivatedCount + case 2: + return this.serverDiscoveredCount + default: + 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.serverActivatedCount + } + + get discoveredCount(): number { + return this.serverDiscoveredCount + } + + onTabChange(index: number): void { + this.activeTab.set(index) + // Different tabs return different result sets, so reset paging to the first page. + this.pageEvent.startsFrom = 0 + if (this.paginator) { + this.paginator.pageIndex = 0 + } + this.getDevices() + } + + private currentTabStatus(): DeviceFilterStatus | undefined { + switch (this.activeTab()) { + case 1: + return 'activated' + case 2: + return 'discovered' + default: + return undefined + } + } + + private loadStats(): void { + this.devicesService.getStats().subscribe({ + next: (stats) => { + this.serverTotalCount = stats.totalCount + this.serverActivatedCount = stats.activatedCount + this.serverDiscoveredCount = stats.discoveredCount + this.totalCount.set(this.currentTabCount) + }, + error: (err) => { + console.error('Error loading device stats:', err) + } + }) + } + get deleteDeviceLabel(): string { return this.isCloudMode ? this.translate.instant('devices.actions.deactivateCloud.value') @@ -119,6 +198,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { 'hostname', 'guid', 'status', + 'productType', 'tags', 'actions', 'notification' @@ -140,6 +220,7 @@ export class DevicesComponent implements OnInit, AfterViewInit { this.displayedColumns = [ 'select', 'hostname', + 'productType', 'tags', 'actions', 'notification' @@ -203,15 +284,17 @@ export class DevicesComponent implements OnInit, AfterViewInit { getDevices(): void { this.isLoading.set(true) + // Counts (all/activated/discovered) are computed server-side and shared with + // headless/API consumers, so refresh them alongside the current page. + this.loadStats() + // Store previous selection before making the request const prevSelected = this.selectedDevices.selected.map((d) => d.guid) this.devicesService - .getDevices({ ...this.pageEvent, tags: this.filteredTags() }) + .getDevices({ ...this.pageEvent, tags: this.filteredTags(), status: this.currentTabStatus() }) .pipe( switchMap((res) => { - this.totalCount.set(res.totalCount) - if (!environment.cloud) { return of(res.data) // Return as-is for non-cloud } @@ -335,13 +418,23 @@ export class DevicesComponent implements OnInit, AfterViewInit { } isNoData(): boolean { - return !this.isLoading() && this.totalCount() === 0 + return !this.isLoading() && this.devices.data.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/app/devices/devices.service.spec.ts b/src/app/devices/devices.service.spec.ts index bb4600df9..3bec2f8e1 100644 --- a/src/app/devices/devices.service.spec.ts +++ b/src/app/devices/devices.service.spec.ts @@ -524,6 +524,40 @@ describe('DevicesService', () => { const req = httpMock.expectOne(`${mockEnvironment.mpsServer}/api/v1/devices?$top=10&$skip=0&$count=true`) req.flush(null, mockError) }) + + it('should append the activated filter when status is activated', () => { + const mockDevices: DataWithCount = { + data: [{ hostname: 'device1', guid: 'guid1', connectionStatus: true }] as any, + totalCount: 1 + } + + service.getDevices({ pageSize: 10, startsFrom: 0, count: 'true', status: 'activated' }).subscribe((response) => { + expect(response).toEqual(mockDevices) + }) + + const req = httpMock.expectOne( + `${mockEnvironment.mpsServer}/api/v1/devices?$top=10&$skip=0&$count=true&activated=true` + ) + expect(req.request.method).toBe('GET') + req.flush(mockDevices) + }) + + it('should append the discovered filter when status is discovered', () => { + const mockDevices: DataWithCount = { + data: [{ hostname: 'device1', guid: 'guid1', connectionStatus: true }] as any, + totalCount: 1 + } + + service.getDevices({ pageSize: 10, startsFrom: 0, count: 'true', status: 'discovered' }).subscribe((response) => { + expect(response).toEqual(mockDevices) + }) + + const req = httpMock.expectOne( + `${mockEnvironment.mpsServer}/api/v1/devices?$top=10&$skip=0&$count=true&discovered=true` + ) + expect(req.request.method).toBe('GET') + req.flush(mockDevices) + }) }) describe('updateDevice', () => { @@ -900,7 +934,13 @@ describe('DevicesService', () => { describe('getStats', () => { it('should fetch device statistics', () => { - const mockResponse: DeviceStats = { totalCount: 100, connectedCount: 80, disconnectedCount: 20 } + const mockResponse: DeviceStats = { + totalCount: 100, + connectedCount: 80, + disconnectedCount: 20, + activatedCount: 60, + discoveredCount: 40 + } service.getStats().subscribe((response) => { expect(response).toEqual(mockResponse) diff --git a/src/app/devices/devices.service.ts b/src/app/devices/devices.service.ts index 57a956124..890ad74ff 100644 --- a/src/app/devices/devices.service.ts +++ b/src/app/devices/devices.service.ts @@ -443,6 +443,11 @@ export class DevicesService { } else { query += `?$top=${pageEvent.pageSize}&$skip=${pageEvent.startsFrom}&$count=${pageEvent.count}` } + if (pageEvent?.status === 'activated') { + query += '&activated=true' + } else if (pageEvent?.status === 'discovered') { + query += '&discovered=true' + } return this.http.get>(query).pipe( catchError((err) => { throw err diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json index de08542c7..1368112d0 100644 --- a/src/assets/i18n/ar.json +++ b/src/assets/i18n/ar.json @@ -1073,6 +1073,10 @@ "description": "عنوان عمود الجدول لحالة الجهاز", "value": "الحالة" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "نوع المنتج" + }, "devices.table.tags": { "description": "عنوان عمود الجدول للعلامات", "value": "العلامات" @@ -1081,6 +1085,18 @@ "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 0fade9a0c..0b83319d6 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1069,6 +1069,10 @@ "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" @@ -1077,6 +1081,18 @@ "description": "Energie-Tooltip für Ausgeschaltet", "value": "Strom: Aus" }, + "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 a19e76dec..581cd2cdb 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1391,6 +1391,10 @@ "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" @@ -1399,6 +1403,18 @@ "description": "Power tooltip for Off", "value": "Power: Off" }, + "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" diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 12b610279..dd10f6ef8 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -1069,6 +1069,10 @@ "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" @@ -1077,6 +1081,18 @@ "description": "Información de energía para Apagado", "value": "Energía: Apagado" }, + "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 601fe9c74..36acaf083 100644 --- a/src/assets/i18n/fi.json +++ b/src/assets/i18n/fi.json @@ -1069,6 +1069,10 @@ "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" @@ -1077,6 +1081,18 @@ "description": "Virtatilan vihjeteksti tilalle pois päältä", "value": "Virta: Pois päältä" }, + "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 161d5ad0d..f1c3799b5 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -1073,6 +1073,10 @@ "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" @@ -1081,6 +1085,18 @@ "description": "Info-bulle d'alimentation pour l'état éteint", "value": "Alimentation : Éteint" }, + "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 9bde55f65..950d01b23 100644 --- a/src/assets/i18n/he.json +++ b/src/assets/i18n/he.json @@ -1065,6 +1065,10 @@ "description": "כותרת עמודות טבלה למצב המכשיר", "value": "סטָטוּס" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "סוג מוצר" + }, "devices.table.tags": { "description": "כותרת עמודות טבלה לתגיות", "value": "תגיות" @@ -1073,6 +1077,18 @@ "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 2fc13d98e..89af0797a 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -1065,6 +1065,10 @@ "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" @@ -1073,6 +1077,18 @@ "description": "Tooltip di alimentazione per spento", "value": "Alimentazione: Spento" }, + "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 9e1007124..a57473d7d 100644 --- a/src/assets/i18n/ja.json +++ b/src/assets/i18n/ja.json @@ -1065,6 +1065,10 @@ "description": "デバイス状態のテーブル列見出し", "value": "ステータス" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "製品タイプ" + }, "devices.table.tags": { "description": "タグ用テーブル列ヘッダー", "value": "タグ" @@ -1073,6 +1077,18 @@ "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 439175801..0a48a08a4 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -1073,6 +1073,10 @@ "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" @@ -1081,6 +1085,18 @@ "description": "Energie-tooltip voor uitgeschakeld", "value": "Stroom: Uit" }, + "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 1e303836c..d25e66b8f 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -1073,6 +1073,10 @@ "description": "Заголовок столбца таблицы для состояния устройства", "value": "Статус" }, + "devices.table.productType": { + "description": "Table column header for product type", + "value": "Тип продукта" + }, "devices.table.tags": { "description": "Заголовок столбца таблицы для тегов", "value": "Теги" @@ -1081,6 +1085,18 @@ "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 7b5999397..68d55108e 100644 --- a/src/assets/i18n/sv.json +++ b/src/assets/i18n/sv.json @@ -977,6 +977,10 @@ "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" @@ -985,6 +989,18 @@ "description": "Strömverktygstips för av", "value": "Ström: Av" }, + "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 06b395413..2d706e012 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 } @@ -38,6 +39,8 @@ export interface DeviceStats { totalCount: number connectedCount: number disconnectedCount: number + activatedCount: number + discoveredCount: number } export interface Domain { profileName: string @@ -349,8 +352,11 @@ export interface PageEventOptions { startsFrom: number count: string tags?: string[] + status?: DeviceFilterStatus } +export type DeviceFilterStatus = 'activated' | 'discovered' + export interface Header { To: string RelatesTo: string