Skip to content

Commit

Permalink
add last backup time display
Browse files Browse the repository at this point in the history
  • Loading branch information
mucsi96 committed Jul 27, 2023
1 parent 4fd0c5a commit 30c8ff8
Show file tree
Hide file tree
Showing 26 changed files with 372 additions and 176 deletions.
6 changes: 6 additions & 0 deletions client/src/app/app.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
justify-content: center;
height: 90vh;
}

.lastBackup {
display: flex;
align-items: center;
gap: 8px;
}
7 changes: 6 additions & 1 deletion client/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<app-header title="Workout"></app-header>
<app-header title="Workout"
><app-heading *ngIf="lastNackupState.isReady" level="3" class="lastBackup"
>Last backup
<app-badge >{{ lastNackupState.value.time | relativeTime }}</app-badge></app-heading
></app-header
>
<app-main>
<div id="main">
<app-weight *ngIf="syncState.isReady"></app-weight>
Expand Down
9 changes: 8 additions & 1 deletion client/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppComponent } from './app.component';
import { CommonComponentsModule } from './common-components/common-components.module';
import { NotificationService } from './common-components/notification.service';
import { WithingsService } from './withings.service';
import { BackupService, LastBackup } from './backup.service';

@Component({
selector: 'app-weight',
Expand All @@ -19,11 +20,16 @@ async function setup() {
jasmine.createSpyObj(['sync']);
mockWithingsService.sync.and.returnValue(syncSubject.asObservable());

const lastBackupSubject = new Subject<LastBackup>();
const mockBackupService: jasmine.SpyObj<BackupService> = jasmine.createSpyObj(['getLastBackupTime'])
mockBackupService.getLastBackupTime.and.returnValue(lastBackupSubject)

await TestBed.configureTestingModule({
declarations: [AppComponent, MockWeightComponent],
providers: [
{ provide: WithingsService, useValue: mockWithingsService },
NotificationService,
{ provide: BackupService, useValue: mockBackupService },
],
imports: [CommonComponentsModule, NoopAnimationsModule],
}).compileComponents();
Expand All @@ -34,7 +40,8 @@ async function setup() {
return {
fixture,
element: fixture.nativeElement as HTMLElement,
syncSubject
syncSubject,
lastBackupSubject
};
}

Expand Down
31 changes: 27 additions & 4 deletions client/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { NotificationService } from './common-components/notification.service';
import { HttpRequestState } from './types';
import { initialHttpRequestState, subscribeToRequestState } from './utils';
import { WithingsService } from './withings.service';
import { BackupService, LastBackup } from './backup.service';
import { HttpRequestState, initialHttpRequestState, requestState } from './utils/request-state';

@Component({
selector: 'app-root',
Expand All @@ -12,14 +12,16 @@ import { WithingsService } from './withings.service';
export class AppComponent implements OnInit {
constructor(
private withingsService: WithingsService,
private notificationService: NotificationService
private notificationService: NotificationService,
private backupService: BackupService
) {}

syncState: HttpRequestState<void> = initialHttpRequestState;
lastNackupState: HttpRequestState<LastBackup> = initialHttpRequestState;

ngOnInit(): void {
// setInterval(() => this.notificationService.showNotification('hello'), 1000);
subscribeToRequestState(this.withingsService.sync(), (newState) => {
requestState(this.withingsService.sync(), (newState) => {
this.syncState = newState;

if (newState.hasFailed) {
Expand All @@ -29,5 +31,26 @@ export class AppComponent implements OnInit {
);
}
});

requestState(
this.backupService.getLastBackupTime(),
(newState) => {
this.lastNackupState = newState;

if (newState.isReady && newState.value.errorMessage) {
this.notificationService.showNotification(
newState.value.errorMessage,
'error'
);
}

if (newState.hasFailed) {
this.notificationService.showNotification(
'Unable to fetch last backup time',
'error'
);
}
}
);
}
}
2 changes: 2 additions & 0 deletions client/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { AppComponent } from './app.component';
import { WeightComponent } from './weight/weight.component';
import { CommonComponentsModule } from './common-components/common-components.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RelativeTimePipe } from './utils/relative-time.pipe';

@NgModule({
declarations: [
AppComponent,
WeightComponent,
RelativeTimePipe,
],
imports: [
BrowserModule,
Expand Down
36 changes: 36 additions & 0 deletions client/src/app/backup.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BackupService } from './backup.service';

function setup() {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule]
});
const service = TestBed.inject(BackupService);
const httpTestingController = TestBed.inject(HttpTestingController)
return {service, httpTestingController}
}

describe('BackupService', () => {
describe('getLastBackupTime', () => {
it('should return last backup time', () => {
const mockTime = new Date();
const { service, httpTestingController } = setup();
service.getLastBackupTime().subscribe(lastBackup => {
expect(lastBackup).toEqual({ time: mockTime });
})
httpTestingController.expectOne('/db/last-backup-time').flush(mockTime.toISOString())
httpTestingController.verify();
});

it('should return error message if last backup was more that 1 day ago', () => {
const mockTime = new Date(Date.now() - 25 * 60 * 60 * 1000 );
const { service, httpTestingController } = setup();
service.getLastBackupTime().subscribe(lastBackup => {
expect(lastBackup).toEqual({ time: mockTime, errorMessage: 'No backup since 1 day' });
})
httpTestingController.expectOne('/db/last-backup-time').flush(mockTime.toISOString())
httpTestingController.verify();
});
});
});
24 changes: 24 additions & 0 deletions client/src/app/backup.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, map } from 'rxjs';

export type LastBackup = {
time: Date;
errorMessage?: string;
};

@Injectable({
providedIn: 'root',
})
export class BackupService {
constructor(private http: HttpClient) {}

getLastBackupTime(): Observable<LastBackup> {
return this.http.get<Date>('/db/last-backup-time').pipe(
map((date) => ({
time: new Date(date),
...(new Date(date).getTime() + 24 * 60 * 60 * 1000 < Date.now() && { errorMessage: 'No backup since 1 day' })
}))
);
}
}
4 changes: 1 addition & 3 deletions client/src/app/common-components/badge/badge.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
color: rgb(30, 66, 159);
display: inline-block;
padding: 0.125em 0.4em;
font-size: 90%;
font-weight: 700;
line-height: 1;
white-space: nowrap;
border-radius: 0.6em;
margin: 0 0.6em;
transform: scale(0.8) translateY(0.1em);
transform-origin: center left;
}
4 changes: 2 additions & 2 deletions client/src/app/common-components/header/header.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core';
import { Attribute, Component, Input } from '@angular/core';

@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css'],
})
export class HeaderComponent {
@Input() title = '';
constructor(@Attribute('title') public title = '') {}
}
15 changes: 10 additions & 5 deletions client/src/app/common-components/heading/heading.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Component, HostBinding, Input } from '@angular/core';
import { Attribute, Component, HostBinding, Input } from '@angular/core';

@Component({
selector: 'app-heading',
templateUrl: './heading.component.html',
styleUrls: ['./heading.component.css'],
host: {
'[class]': "'level' + level",
},
})
export class HeadingComponent {
@Input() level = 1;
constructor(
@Attribute('level') public level: string,
@Attribute('class') public className: string
) {}

@HostBinding('class')
get class(): string {
return [`level${this.level ?? 1}`, this.className].join(' ');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import {
sequence,
style,
transition,
trigger
trigger,
} from '@angular/animations';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import {
Attribute,
Component,
EventEmitter,
HostBinding,
HostListener,
Input,
Output,
} from '@angular/core';
import { NotificationType } from '../notification.service';

@Component({
Expand Down Expand Up @@ -37,16 +45,18 @@ import { NotificationType } from '../notification.service';
]),
]),
],
host: {
'[class]': 'type',
'[@toast]': 'toastState',
'(@toast.done)': 'animationEnd($event)',
},
})
export class NotificationComponent {
@Input() type: NotificationType = 'success';
@Output() settled = new EventEmitter();
@HostBinding('@toast') toastState = null;

@HostBinding('class')
get class() {
return this.type;
}

@HostListener('@toast.done')
animationEnd(_event: AnimationEvent) {
this.settled.emit();
}
Expand Down
13 changes: 0 additions & 13 deletions client/src/app/types.ts

This file was deleted.

57 changes: 0 additions & 57 deletions client/src/app/utils.spec.ts

This file was deleted.

30 changes: 30 additions & 0 deletions client/src/app/utils/relative-time.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RelativeTimePipe } from './relative-time.pipe';

describe('RelativeTimePipe', () => {
[
{ date: new Date(), result: 'now' },
{ date: new Date(Date.now() - 999), result: '1 second ago' },
{ date: new Date(Date.now() - 1000), result: '1 second ago' },
{ date: new Date(Date.now() - 59 * 1000), result: '59 seconds ago' },
{ date: new Date(Date.now() - 60 * 1000), result: '1 minute ago' },
{ date: new Date(Date.now() - 59 * 60 * 1000), result: '59 minutes ago' },
{ date: new Date(Date.now() - 60 * 60 * 1000), result: '1 hour ago' },
{ date: new Date(Date.now() - 23 * 60 * 60 * 1000), result: '23 hours ago' },
{ date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), result: 'yesterday' },
{ date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), result: '2 days ago' },
{ date: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), result: '6 days ago' },
{ date: new Date(Date.now() - 1 * 7 * 24 * 60 * 60 * 1000), result: 'last week' },
{ date: new Date(Date.now() - 4 * 7 * 24 * 60 * 60 * 1000), result: '4 weeks ago' },
{ date: new Date(Date.now() - 29 * 24 * 60 * 60 * 1000), result: '4 weeks ago' },
{ date: new Date(Date.now() - 1 * 30 * 24 * 60 * 60 * 1000), result: 'last month' },
{ date: new Date(Date.now() - 12 * 30 * 24 * 60 * 60 * 1000), result: '12 months ago' },
{ date: new Date(Date.now() - 364 * 24 * 60 * 60 * 1000), result: '12 months ago' },
{ date: new Date(Date.now() - 1 * 365 * 24 * 60 * 60 * 1000), result: 'last year' },
{ date: new Date(Date.now() - 5 * 365 * 24 * 60 * 60 * 1000), result: '5 years ago' },
].forEach(({ date, result }) =>
it(`formats ${date} as ${result}`, () => {
const pipe = new RelativeTimePipe();
expect(pipe.transform(date)).toBe(result);
})
);
});
Loading

0 comments on commit 30c8ff8

Please sign in to comment.