from __future__ import absolute_import
import json
-from .helper import DashboardTestCase
+from .helper import DashboardTestCase, JList, JObj
from .test_orchestrator import test_data
test_hostnames = {inventory_node['name'] for inventory_node in test_data['inventory']}
resp_hostnames = {host['hostname'] for host in data}
self.assertEqual(len(test_hostnames.intersection(resp_hostnames)), 0)
+
+ def test_host_devices(self):
+ hosts = self._get('{}'.format(self.URL_HOST))
+ hosts = [host['hostname'] for host in hosts if host['hostname'] != '']
+ assert hosts[0]
+ data = self._get('{}/devices'.format('{}/{}'.format(self.URL_HOST, hosts[0])))
+ self.assertStatus(200)
+ self.assertSchema(data, JList(JObj({
+ 'daemons': JList(str),
+ 'devid': str,
+ 'location': JList(JObj({'host': str, 'dev': str}))
+ })))
self.wait_until_equal(get_destroy_status, False, 10)
self.assertStatus(200)
+ def test_osd_devices(self):
+ data = self._get('/api/osd/0/devices')
+ self.assertStatus(200)
+ self.assertSchema(data, JList(JObj({
+ 'daemons': JList(str),
+ 'devid': str,
+ 'location': JList(JObj({'host': str, 'dev': str}))
+ })))
+
class OsdFlagsTest(DashboardTestCase):
def __init__(self, *args, **kwargs):
from ..security import Scope
from ..services.orchestrator import OrchClient
from ..services.exception import handle_orchestrator_error
+from ..services.ceph_service import CephService
def host_task(name, metadata, wait_for=10.0):
msg='Remove a non-existent host {} from orchestrator'.format(
hostname),
component='orchestrator')
+
+ @RESTController.Resource('GET')
+ def devices(self, hostname):
+ # (str) -> dict
+ return CephService.send_command('mon', 'device ls-by-host', host=hostname)
'is_safe_to_destroy': False,
}
+ @RESTController.Resource('GET')
+ def devices(self, svc_id):
+ # (str) -> dict
+ return CephService.send_command('mon', 'device ls-by-daemon', who='osd.{}'.format(svc_id))
+
@ApiController('/osd/flags', Scope.OSD)
class OsdFlagsController(RESTController):
-import { by, element } from 'protractor';
+import { $$, by, element } from 'protractor';
import { OSDsPageHelper } from './osds.po';
describe('OSDs page', () => {
let osds: OSDsPageHelper;
- beforeAll(() => {
+ beforeAll(async () => {
osds = new OSDsPageHelper();
+ await osds.navigateTo();
});
afterEach(async () => {
});
describe('breadcrumb and tab tests', () => {
- beforeAll(async () => {
- await osds.navigateTo();
- });
-
it('should open and show breadcrumb', async () => {
await osds.waitTextToBePresent(osds.getBreadcrumb(), 'OSDs');
});
describe('check existence of fields on OSD page', () => {
it('should check that number of rows and count in footer match', async () => {
- await osds.navigateTo();
await expect(osds.getTableTotalCount()).toEqual(osds.getTableRows().count());
});
it('should verify that selected footer increases when an entry is clicked', async () => {
- await osds.navigateTo();
- await osds.getFirstCell().click(); // clicks first osd
+ await osds.getFirstCell().click();
await expect(osds.getTableSelectedCount()).toEqual(1);
});
it('should verify that buttons exist', async () => {
- await osds.navigateTo();
await expect(element(by.cssContainingText('button', 'Scrub')).isPresent()).toBe(true);
await expect(
element(by.cssContainingText('button', 'Cluster-wide configuration')).isPresent()
).toBe(true);
});
- it('should check the number of tabs when selecting an osd is correct', async () => {
- await osds.navigateTo();
- await osds.getFirstCell().click(); // clicks first osd
- await expect(osds.getTabsCount()).toEqual(8); // includes tabs at the top of the page
- });
-
it('should show the correct text for the tab labels', async () => {
- await expect(osds.getTabText(2)).toEqual('Attributes (OSD map)');
- await expect(osds.getTabText(3)).toEqual('Metadata');
- await expect(osds.getTabText(4)).toEqual('Device health');
- await expect(osds.getTabText(5)).toEqual('Performance counter');
- await expect(osds.getTabText(6)).toEqual('Histogram');
- await expect(osds.getTabText(7)).toEqual('Performance Details');
+ await osds.getFirstCell().click();
+ const tabHeadings = $$('#tabset-osd-details > div > tab').map((e) =>
+ e.getAttribute('heading')
+ );
+ await expect(tabHeadings).toEqual([
+ 'Devices',
+ 'Attributes (OSD map)',
+ 'Metadata',
+ 'Device health',
+ 'Performance counter',
+ 'Histogram',
+ 'Performance Details'
+ ]);
});
});
});
import { SharedModule } from '../../shared/shared.module';
import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
+import { CephSharedModule } from '../shared/ceph-shared.module';
import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
import { ConfigurationComponent } from './configuration/configuration.component';
TypeaheadModule.forRoot(),
TimepickerModule.forRoot(),
BsDatepickerModule.forRoot(),
- NgBootstrapFormValidationModule
+ NgBootstrapFormValidationModule,
+ CephSharedModule
],
declarations: [
HostsComponent,
<tabset *ngIf="selection.hasSingleSelection">
+ <tab i18n-heading
+ heading="Devices">
+ <cd-device-list [hostname]="selection.first()['hostname']"></cd-device-list>
+ </tab>
<tab i18n-heading
heading="Inventory"
*ngIf="permissions.hosts.read">
- <cd-inventory
- [hostname]="selection.first()['hostname']">
+ <cd-inventory [hostname]="selection.first()['hostname']">
</cd-inventory>
</tab>
<tab i18n-heading
heading="Services"
*ngIf="permissions.hosts.read">
- <cd-services
- [hostname]="selection.first()['hostname']">
+ <cd-services [hostname]="selection.first()['hostname']">
</cd-services>
</tab>
<tab i18n-heading
import { RouterTestingModule } from '@angular/router/testing';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permissions } from '../../../../shared/models/permissions';
-import { SharedModule } from '../../../../shared/shared.module';
-import { InventoryComponent } from '../../inventory/inventory.component';
-import { ServicesComponent } from '../../services/services.component';
+import { CephModule } from '../../../ceph.module';
import { HostDetailsComponent } from './host-details.component';
describe('HostDetailsComponent', () => {
TabsModule.forRoot(),
BsDropdownModule.forRoot(),
RouterTestingModule,
- SharedModule
+ CephModule,
+ CoreModule
],
- declarations: [HostDetailsComponent, InventoryComponent, ServicesComponent],
+ declarations: [],
providers: [i18nProviders]
});
it('should show tabs', () => {
fixture.detectChanges();
- const tabs = component.tabsetChild.tabs;
- expect(tabs.length).toBe(3);
- expect(tabs[0].heading).toBe('Inventory');
- expect(tabs[1].heading).toBe('Services');
- expect(tabs[2].heading).toBe('Performance Details');
+ const tabs = component.tabsetChild.tabs.map((tab) => tab.heading);
+ expect(tabs).toEqual(['Devices', 'Inventory', 'Services', 'Performance Details']);
});
});
});
import { ToastrModule } from 'ngx-toastr';
import { of } from 'rxjs';
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../core/core.module';
import { HostService } from '../../../shared/api/host.service';
import { Permissions } from '../../../shared/models/permissions';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { SharedModule } from '../../../shared/shared.module';
-import { InventoryComponent } from '../inventory/inventory.component';
-import { ServicesComponent } from '../services/services.component';
-import { HostDetailsComponent } from './host-details/host-details.component';
+import { CephModule } from '../../ceph.module';
import { HostsComponent } from './hosts.component';
describe('HostsComponent', () => {
TabsModule.forRoot(),
BsDropdownModule.forRoot(),
RouterTestingModule,
- ToastrModule.forRoot()
+ ToastrModule.forRoot(),
+ CephModule,
+ CoreModule
],
providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }, i18nProviders],
- declarations: [HostsComponent, HostDetailsComponent, InventoryComponent, ServicesComponent]
+ declarations: []
});
beforeEach(() => {
-<tabset *ngIf="selection.hasSingleSelection">
+<tabset *ngIf="selection.hasSingleSelection"
+ id="tabset-osd-details">
+ <tab heading="Devices"
+ i18n-heading>
+ <cd-device-list *ngIf="osd.loaded && osd.id !== null"
+ [osdId]="osd.id"></cd-device-list>
+ </tab>
<tab heading="Attributes (OSD map)"
i18n-heading>
[data]="osd.details.osd_metadata">
</cd-table-key-value>
<ng-template #noMetaData>
- <cd-alert-panel type="warning" i18n>Metadata not available</cd-alert-panel>
+ <cd-alert-panel type="warning"
+ i18n>Metadata not available</cd-alert-panel>
</ng-template>
</tab>
import { TabsModule } from 'ngx-bootstrap/tabs';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
import { OsdService } from '../../../../shared/api/osd.service';
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
-import { SharedModule } from '../../../../shared/shared.module';
+import { CephModule } from '../../../ceph.module';
import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import { OsdPerformanceHistogramComponent } from '../osd-performance-histogram/osd-performance-histogram.component';
-import { OsdSmartListComponent } from '../osd-smart-list/osd-smart-list.component';
import { OsdDetailsComponent } from './osd-details.component';
describe('OsdDetailsComponent', () => {
HttpClientTestingModule,
TabsModule.forRoot(),
PerformanceCounterModule,
- SharedModule
+ CephModule,
+ CoreModule
],
- declarations: [OsdDetailsComponent, OsdPerformanceHistogramComponent, OsdSmartListComponent],
+ declarations: [],
providers: i18nProviders
});
i18nProviders,
PermissionHelper
} from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
import { OsdService } from '../../../../shared/api/osd.service';
import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permissions } from '../../../../shared/models/permissions';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
-import { SharedModule } from '../../../../shared/shared.module';
+import { CephModule } from '../../../ceph.module';
import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import { OsdDetailsComponent } from '../osd-details/osd-details.component';
-import { OsdPerformanceHistogramComponent } from '../osd-performance-histogram/osd-performance-histogram.component';
import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
-import { OsdSmartListComponent } from '../osd-smart-list/osd-smart-list.component';
import { OsdListComponent } from './osd-list.component';
describe('OsdListComponent', () => {
HttpClientTestingModule,
PerformanceCounterModule,
TabsModule.forRoot(),
- SharedModule,
+ CephModule,
ReactiveFormsModule,
- RouterTestingModule
- ],
- declarations: [
- OsdListComponent,
- OsdDetailsComponent,
- OsdPerformanceHistogramComponent,
- OsdSmartListComponent
+ RouterTestingModule,
+ CoreModule
],
+ declarations: [],
providers: [
{ provide: AuthStorageService, useValue: fakeAuthStorageService },
TableActionsComponent,
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
+import { DataTableModule } from '../../shared/datatable/datatable.module';
+import { SharedModule } from '../../shared/shared.module';
+import { DeviceListComponent } from './device-list/device-list.component';
@NgModule({
- imports: [CommonModule]
+ imports: [CommonModule, DataTableModule, SharedModule],
+ exports: [DeviceListComponent],
+ declarations: [DeviceListComponent]
})
export class CephSharedModule {}
--- /dev/null
+<cd-table *ngIf="hostname || osdId !== null"
+ [data]="devices"
+ [columns]="columns"></cd-table>
+
+<cd-alert-panel type="warning"
+ *ngIf="hostname === '' && osdId === null"
+ i18n>Neither hostname nor OSD ID given</cd-alert-panel>
+
+<ng-template #deviceLocation
+ let-value="value">
+ <span *ngFor="let location of value">{{location.dev}}</span>
+</ng-template>
+
+<ng-template #lifeExpectancy
+ let-value="value">
+ <span *ngIf="value.min && !value.max">> {{value.min | i18nPlural: translationMapping}}</span>
+ <span *ngIf="value.max && !value.min">< {{value.max | i18nPlural: translationMapping}}</span>
+ <span *ngIf="value.max && value.min">{{value.min}} to {{value.max | i18nPlural: translationMapping}}</span>
+</ng-template>
+
+<ng-template #lifeExpectancyTimestamp
+ let-value="value">
+ {{value}}
+</ng-template>
+
+<ng-template #state
+ let-value="value">
+ <ng-container *ngIf="value === 'good'">
+ <span class="badge badge-success"
+ i18n>Good</span>
+ </ng-container>
+ <ng-container *ngIf="value === 'warning'">
+ <span class="badge badge-warning"
+ i18n>Warning</span>
+ </ng-container>
+ <ng-container *ngIf="value === 'bad'">
+ <span class="badge badge-danger"
+ i18n>Bad</span>
+ </ng-container>
+ <ng-container *ngIf="value === 'stale'">
+ <span class="badge badge-info"
+ i18n>Stale</span>
+ </ng-container>
+ <ng-container *ngIf="value === 'unknown'">
+ <span class="badge badge-dark"
+ i18n>Unknown</span>
+ </ng-container>
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { i18nProviders } from '../../../../testing/unit-test-helper';
+import { SharedModule } from '../../../shared/shared.module';
+
+import { DeviceListComponent } from './device-list.component';
+
+describe('DeviceListComponent', () => {
+ let component: DeviceListComponent;
+ let fixture: ComponentFixture<DeviceListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [DeviceListComponent],
+ imports: [SharedModule, HttpClientTestingModule],
+ providers: [i18nProviders]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeviceListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { DatePipe } from '@angular/common';
+import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import { HostService } from '../../../shared/api/host.service';
+import { OsdService } from '../../../shared/api/osd.service';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdDevice } from '../../../shared/models/devices';
+
+@Component({
+ selector: 'cd-device-list',
+ templateUrl: './device-list.component.html',
+ styleUrls: ['./device-list.component.scss']
+})
+export class DeviceListComponent implements OnInit {
+ @Input()
+ hostname = '';
+ @Input()
+ osdId = null;
+
+ @ViewChild('deviceLocation', { static: true })
+ locationTemplate: TemplateRef<any>;
+ @ViewChild('lifeExpectancy', { static: true })
+ lifeExpectancyTemplate: TemplateRef<any>;
+ @ViewChild('lifeExpectancyTimestamp', { static: true })
+ lifeExpectancyTimestampTemplate: TemplateRef<any>;
+ @ViewChild('state', { static: true })
+ stateTemplate: TemplateRef<any>;
+
+ devices: CdDevice[] = null;
+ columns: CdTableColumn[] = [];
+ translationMapping = {
+ '=1': '# week',
+ other: '# weeks'
+ };
+
+ constructor(
+ private hostService: HostService,
+ private i18n: I18n,
+ private datePipe: DatePipe,
+ private osdService: OsdService
+ ) {}
+
+ ngOnInit() {
+ const updateDevicesFn = (devices) => (this.devices = devices);
+ if (this.hostname) {
+ this.hostService.getDevices(this.hostname).subscribe(updateDevicesFn);
+ } else if (this.osdId !== null) {
+ this.osdService.getDevices(this.osdId).subscribe(updateDevicesFn);
+ }
+ this.columns = [
+ { prop: 'devid', name: this.i18n('Device ID'), minWidth: 200 },
+ {
+ prop: 'state',
+ name: this.i18n('State of Health'),
+ cellTemplate: this.stateTemplate
+ },
+ {
+ prop: 'life_expectancy_weeks',
+ name: this.i18n('Life Expectancy'),
+ cellTemplate: this.lifeExpectancyTemplate
+ },
+ {
+ prop: 'life_expectancy_stamp',
+ name: this.i18n('Prediction Creation Date'),
+ cellTemplate: this.lifeExpectancyTimestampTemplate,
+ pipe: this.datePipe,
+ isHidden: true
+ },
+ { prop: 'location', name: this.i18n('Device Name'), cellTemplate: this.locationTemplate },
+ { prop: 'readableDaemons', name: this.i18n('Daemons') }
+ ];
+ }
+}
tick();
expect(result).toEqual(['foo', 'bar']);
}));
+
+ it('should make a GET request on the devices endpoint when requesting devices', () => {
+ const hostname = 'hostname';
+ service.getDevices(hostname).subscribe();
+ const req = httpTesting.expectOne(`api/host/${hostname}/devices`);
+ expect(req.request.method).toBe('GET');
+ });
});
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { CdDevice } from '../models/devices';
+import { DeviceService } from '../services/device.service';
import { ApiModule } from './api.module';
@Injectable({
export class HostService {
baseURL = 'api/host';
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient, private deviceService: DeviceService) {}
list() {
return this.http.get(this.baseURL);
remove(hostname) {
return this.http.delete(`${this.baseURL}/${hostname}`, { observe: 'response' });
}
+
+ getDevices(hostname: string): Observable<CdDevice[]> {
+ return this.http
+ .get<CdDevice[]>(`${this.baseURL}/${hostname}/devices`)
+ .pipe(map((devices) => devices.map((device) => this.deviceService.prepareDevice(device))));
+ }
}
const req = httpTesting.expectOne('api/osd/[0,1]/safe_to_destroy');
expect(req.request.method).toBe('GET');
});
+
+ it('should call the devices endpoint to retrieve smart data', () => {
+ service.getDevices(1).subscribe();
+ const req = httpTesting.expectOne('api/osd/1/devices');
+ expect(req.request.method).toBe('GET');
+ });
});
import { Injectable } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
+import { map } from 'rxjs/operators';
+import { CdDevice } from '../models/devices';
+import { DeviceService } from '../services/device.service';
import { ApiModule } from './api.module';
export interface SmartAttribute {
]
};
- constructor(private http: HttpClient, private i18n: I18n) {}
+ constructor(private http: HttpClient, private i18n: I18n, private deviceService: DeviceService) {}
getList() {
return this.http.get(`${this.path}`);
}
return this.http.get<SafeToDestroyResponse>(`${this.path}/${ids}/safe_to_destroy`);
}
+
+ getDevices(osdId: number) {
+ return this.http
+ .get<CdDevice[]>(`${this.path}/${osdId}/devices`)
+ .pipe(map((devices) => devices.map((device) => this.deviceService.prepareDevice(device))));
+ }
}
--- /dev/null
+/**
+ * Fields returned by the back-end.
+ */
+export interface CephDevice {
+ devid: string;
+ location: { host: string; dev: string }[];
+ daemons: string[];
+ life_expectancy_min?: string;
+ life_expectancy_max?: string;
+ life_expectancy_stamp?: string;
+}
+
+/**
+ * Fields added by the front-end. Fields may be empty if no expectancy is provided for the
+ * CephDevice interface.
+ */
+export interface CdDevice extends CephDevice {
+ life_expectancy_weeks?: {
+ max: number;
+ min: number;
+ };
+ state?: 'good' | 'warning' | 'bad' | 'stale' | 'unknown';
+ readableDaemons?: string; // Human readable daemons (which can wrap lines inside the table cell)
+}
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import * as moment from 'moment';
+
+import { CdDevice } from '../models/devices';
+import { DeviceService } from './device.service';
+
+describe('DeviceService', () => {
+ let service: DeviceService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.get(DeviceService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('should test getDevices pipe', () => {
+ let now = null;
+
+ const newDevice = (data: object): CdDevice => {
+ const device: CdDevice = {
+ devid: '',
+ location: [{ host: '', dev: '' }],
+ daemons: []
+ };
+ Object.assign(device, data);
+ return device;
+ };
+
+ beforeEach(() => {
+ // Mock 'moment.now()' to simplify testing by enabling testing with fixed dates.
+ now = spyOn(moment, 'now').and.returnValue(
+ moment('2019-10-01T00:00:00.00000+0100').valueOf()
+ );
+ });
+
+ afterEach(() => {
+ expect(now).toHaveBeenCalled();
+ });
+
+ it('should return status "good" for life expectancy > 6 weeks', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '2019-11-14T01:00:00.000000+0100',
+ life_expectancy_max: '0.000000',
+ life_expectancy_stamp: '2019-10-01T02:08:48.627312+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: null, min: 6 });
+ expect(preparedDevice.state).toBe('good');
+ });
+
+ it('should return status "warning" for life expectancy <= 4 weeks', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '2019-10-14T01:00:00.000000+0100',
+ life_expectancy_max: '2019-11-14T01:00:00.000000+0100',
+ life_expectancy_stamp: '2019-10-01T00:00:00.00000+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: 6, min: 2 });
+ expect(preparedDevice.state).toBe('warning');
+ });
+
+ it('should return status "bad" for life expectancy <= 2 weeks', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '0.000000',
+ life_expectancy_max: '2019-10-12T01:00:00.000000+0100',
+ life_expectancy_stamp: '2019-10-01T00:00:00.00000+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: 2, min: null });
+ expect(preparedDevice.state).toBe('bad');
+ });
+
+ it('should return status "stale" for time stamp that is older than a week', () => {
+ const preparedDevice = service.calculateAdditionalData(
+ newDevice({
+ life_expectancy_min: '0.000000',
+ life_expectancy_max: '0.000000',
+ life_expectancy_stamp: '2019-09-21T00:00:00.00000+0100'
+ })
+ );
+ expect(preparedDevice.life_expectancy_weeks).toEqual({ max: null, min: null });
+ expect(preparedDevice.state).toBe('stale');
+ });
+ });
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+
+import * as moment from 'moment';
+
+import { CdDevice } from '../models/devices';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DeviceService {
+ constructor() {}
+
+ /**
+ * Calculates additional data and appends them as new attributes to the given device.
+ */
+ calculateAdditionalData(device: CdDevice): CdDevice {
+ if (!device.life_expectancy_min || !device.life_expectancy_max) {
+ device.state = 'unknown';
+ return device;
+ }
+ const hasDate = (float: string): boolean => !!Number.parseFloat(float);
+ const weeks = (isoDate1: string, isoDate2: string): number =>
+ !isoDate1 || !isoDate2 || !hasDate(isoDate1) || !hasDate(isoDate2)
+ ? null
+ : moment.duration(moment(isoDate1).diff(moment(isoDate2))).asWeeks();
+
+ const ageOfStamp = moment
+ .duration(moment(moment.now()).diff(moment(device.life_expectancy_stamp)))
+ .asWeeks();
+ const max = weeks(device.life_expectancy_max, device.life_expectancy_stamp);
+ const min = weeks(device.life_expectancy_min, device.life_expectancy_stamp);
+
+ if (ageOfStamp > 1) {
+ device.state = 'stale';
+ } else if (max !== null && max <= 2) {
+ device.state = 'bad';
+ } else if (min !== null && min <= 4) {
+ device.state = 'warning';
+ } else {
+ device.state = 'good';
+ }
+
+ device.life_expectancy_weeks = {
+ max: max !== null ? Math.round(max) : null,
+ min: min !== null ? Math.round(min) : null
+ };
+
+ return device;
+ }
+
+ readable(device: CdDevice): CdDevice {
+ device.readableDaemons = device.daemons.join(' ');
+ return device;
+ }
+
+ prepareDevice(device: CdDevice): CdDevice {
+ return this.readable(this.calculateAdditionalData(device));
+ }
+}