added last downtime log (closes #55)
added uptime statistics (closes #56) added german translations (closes #57)
This commit is contained in:
parent
acb39f6b2a
commit
e9599373ec
27 changed files with 819 additions and 265 deletions
|
@ -18,9 +18,31 @@ export interface Service {
|
|||
name: string;
|
||||
url?: string;
|
||||
state: State;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface MetaInfo {
|
||||
title: string;
|
||||
description: string;
|
||||
translations?: {
|
||||
[lang: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface UptimeStatus {
|
||||
hours24: number;
|
||||
days7: number;
|
||||
days30: number;
|
||||
days90: number;
|
||||
days: {
|
||||
date: Date;
|
||||
uptime: number;
|
||||
}[];
|
||||
events: {
|
||||
state: State;
|
||||
date: Date;
|
||||
}[];
|
||||
}
|
||||
|
|
8
src/app/_pipe/dayjs.pipe.spec.ts
Normal file
8
src/app/_pipe/dayjs.pipe.spec.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { DayjsPipe } from './dayjs.pipe';
|
||||
|
||||
describe('DayjsPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new DayjsPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
36
src/app/_pipe/dayjs.pipe.ts
Normal file
36
src/app/_pipe/dayjs.pipe.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import * as dayjs from 'dayjs';
|
||||
import * as utc from 'dayjs/plugin/utc';
|
||||
import * as relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import * as localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import 'dayjs/locale/de';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
@Pipe({
|
||||
name: 'dayjs',
|
||||
pure: false
|
||||
})
|
||||
export class DayjsPipe implements PipeTransform {
|
||||
constructor(private translate: TranslateService) {
|
||||
}
|
||||
|
||||
transform(value: string | Date, method: string, ...args: any[]): string {
|
||||
const date = dayjs.utc(value);
|
||||
switch (method) {
|
||||
case 'to':
|
||||
const to = args[0] ? dayjs.utc(args[0]) : dayjs.utc();
|
||||
const suffix = args.length > 1 && args[1] === true;
|
||||
return date.locale(this.translate.currentLang).to(to, !suffix);
|
||||
case 'from':
|
||||
const from = args[0] ? dayjs.utc(args[0]) : dayjs.utc();
|
||||
return date.locale(this.translate.currentLang).from(from);
|
||||
case 'format':
|
||||
return date.local().locale(this.translate.currentLang).format(args[0]);
|
||||
}
|
||||
throw new Error('please pass a method to use!');
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
describe('ApiService', () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ApiService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,9 +1,9 @@
|
|||
import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
|
||||
import {Observable} from "rxjs";
|
||||
import {CurrentStatus, MetaInfo} from "../_data/data";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {isPlatformBrowser} from "@angular/common";
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {CurrentStatus, MetaInfo, UptimeStatus} from '../_data/data';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -16,10 +16,14 @@ export class ApiService {
|
|||
}
|
||||
|
||||
public getServiceStates(): Observable<CurrentStatus> {
|
||||
return this.http.get<CurrentStatus>(this.api+ '/status');
|
||||
return this.http.get<CurrentStatus>(this.api + '/status');
|
||||
}
|
||||
|
||||
public getServiceUptime(id: string): Observable<UptimeStatus> {
|
||||
return this.http.get<UptimeStatus>(this.api + '/uptime', {params: {service: id}});
|
||||
}
|
||||
|
||||
public getMetaInfo(): Observable<MetaInfo> {
|
||||
return this.http.get<MetaInfo>(this.api+ '/info');
|
||||
return this.http.get<MetaInfo>(this.api + '/info');
|
||||
}
|
||||
}
|
||||
|
|
30
src/app/_service/storage.service.ts
Normal file
30
src/app/_service/storage.service.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService {
|
||||
constructor(@Inject(PLATFORM_ID) private platformId: Object) {
|
||||
}
|
||||
|
||||
getValue(key: string): any {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(key));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
setValue(key: string, value: any): void {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,22 @@
|
|||
<div class="box">
|
||||
<header class="container pt-4">
|
||||
<h1 *ngIf="title && title.length">{{title}}</h1>
|
||||
<h3 *ngIf="description && description.length">{{description}}</h3>
|
||||
<div class="d-flex">
|
||||
<div *ngIf="title && title.length">
|
||||
<h1>{{translations[getLanguage()]?.title || title}}</h1>
|
||||
</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div class="language-selection">
|
||||
<a href="#" class="mr-2" (click)="setLanguage('en'); $event.preventDefault();">
|
||||
<span class="flag-icon flag-icon-us"></span>
|
||||
</a>
|
||||
<a href="#" (click)="setLanguage('de'); $event.preventDefault();">
|
||||
<span class="flag-icon flag-icon-de"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="description && description.length">
|
||||
<h3>{{translations[getLanguage()]?.description || description}}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
|
|
|
@ -19,12 +19,6 @@ footer {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #cccccc;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.language-selection {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'universal-statuspage'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('universal-statuspage');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('universal-statuspage app is running!');
|
||||
});
|
||||
});
|
|
@ -1,8 +1,9 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {ApiService} from "./_service/api.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {MetaInfo} from "./_data/data";
|
||||
import {Title} from "@angular/platform-browser";
|
||||
import {Component, Inject, Injector, OnInit, PLATFORM_ID} from '@angular/core';
|
||||
import {ApiService} from './_service/api.service';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {StorageService} from './_service/storage.service';
|
||||
import {isPlatformServer} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -12,15 +13,42 @@ import {Title} from "@angular/platform-browser";
|
|||
export class AppComponent implements OnInit {
|
||||
title: string;
|
||||
description: string;
|
||||
translations: { [lang: string]: { title: string; description: string } };
|
||||
|
||||
constructor(private api: ApiService, private htmlTitle: Title) {
|
||||
private supportedLanguages = ['en', 'de'];
|
||||
|
||||
constructor(private translate: TranslateService, private api: ApiService,
|
||||
private storage: StorageService, private htmlTitle: Title,
|
||||
private injector: Injector, @Inject(PLATFORM_ID) private platformId: Object) {
|
||||
this.translate.setDefaultLang('en');
|
||||
if (isPlatformServer(platformId)) {
|
||||
const request = this.injector.get<any>(<any> 'REQUEST');
|
||||
const requestLanguage = request.acceptsLanguages(this.supportedLanguages) || 'en';
|
||||
this.translate.use(requestLanguage);
|
||||
return;
|
||||
}
|
||||
let language = this.storage.getValue('language') || this.translate.getBrowserLang();
|
||||
if (language ! in this.supportedLanguages) {
|
||||
language = 'en';
|
||||
}
|
||||
translate.use(language);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.api.getMetaInfo().subscribe(info => {
|
||||
this.title = info.title;
|
||||
this.description = info.description;
|
||||
this.translations = info.translations;
|
||||
this.htmlTitle.setTitle(this.title);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
getLanguage(): string {
|
||||
return this.translate.currentLang;
|
||||
}
|
||||
|
||||
setLanguage(language: string): void {
|
||||
this.translate.use(language);
|
||||
this.storage.setValue('language', language);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,56 @@
|
|||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {NgModule, PLATFORM_ID} from '@angular/core';
|
||||
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {StatusComponent} from './status/status.component';
|
||||
import {MatExpansionModule} from "@angular/material/expansion";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {HttpClientModule} from "@angular/common/http";
|
||||
import {MatExpansionModule} from '@angular/material/expansion';
|
||||
import {MatListModule} from '@angular/material/list';
|
||||
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||
import {MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {UptimeComponent} from './uptime/uptime.component';
|
||||
import {DayjsPipe} from './_pipe/dayjs.pipe';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
|
||||
import {isPlatformServer} from '@angular/common';
|
||||
import {from, Observable} from 'rxjs';
|
||||
|
||||
export class TranslateUniversalLoader extends TranslateLoader {
|
||||
getTranslation(lang: string): Observable<any> {
|
||||
return from(import(`../assets/i18n/${lang}.json`));
|
||||
}
|
||||
}
|
||||
|
||||
export function UniversalLoaderFactory(http: HttpClient, plattformId: Object) {
|
||||
if (isPlatformServer(plattformId)) {
|
||||
return new TranslateUniversalLoader();
|
||||
}
|
||||
return new TranslateHttpLoader(http);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
StatusComponent
|
||||
StatusComponent,
|
||||
UptimeComponent,
|
||||
DayjsPipe
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({appId: 'serverApp'}),
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: UniversalLoaderFactory,
|
||||
deps: [HttpClient, PLATFORM_ID]
|
||||
}
|
||||
}),
|
||||
MatExpansionModule,
|
||||
MatListModule
|
||||
MatListModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
|
|
@ -1,25 +1,38 @@
|
|||
<mat-accordion [multi]="true">
|
||||
<mat-expansion-panel *ngFor="let group of groups" [expanded]="true">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<i [class]="stateClasses[group.state]"></i>
|
||||
<a *ngIf="group.url" class="name" [href]="group.url" target="_blank" (click)="$event.stopPropagation()">{{group.name}}</a>
|
||||
<span *ngIf="!group.url">{{group.name}}</span>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="py-3" *ngFor="let group of groups">
|
||||
<h2>
|
||||
<i [class]="stateClasses[group.state]"></i>
|
||||
<a *ngIf="group.url" [href]="group.url" target="_blank"
|
||||
(click)="$event.stopPropagation()">{{group.name}}</a>
|
||||
<span *ngIf="!group.url">{{group.name}}</span>
|
||||
</h2>
|
||||
<mat-accordion [multi]="true">
|
||||
<mat-expansion-panel *ngFor="let service of group.services" [hideToggle]="true"
|
||||
[(expanded)]="expandedCache[group.id + '-' + service.id]"
|
||||
(afterExpand)="saveExpandedCache()" (afterCollapse)="saveExpandedCache()">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<div matLine class="w-100 d-flex">
|
||||
<i [class]="stateClasses[service.state]"></i>
|
||||
<a *ngIf="service.url" class="text-truncate" [href]="service.url" target="_blank"
|
||||
(click)="$event.stopPropagation()">{{service.name}}</a>
|
||||
<span *ngIf="!service.url" class="text-truncate">{{service.name}}</span>
|
||||
<span class="ml-2 font-weight-normal d-none d-sm-block" [class.operational]="service.uptime >= 99"
|
||||
[class.maintenance]="service.uptime < 99 && service.uptime >= 95"
|
||||
[class.outage]="service.uptime < 95">{{service.uptime?.toFixed(2)}}%</span>
|
||||
<span class="flex-grow-1"></span>
|
||||
<span class="{{service.state}}">{{'state.' + service.state | translate}}</span>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<mat-list>
|
||||
<a *ngFor="let service of group.services; last as last" mat-list-item [href]="service.url || group.url || '#'" target="_blank">
|
||||
<div matLine class="d-flex">
|
||||
<i [class]="stateClasses[service.state]"></i>
|
||||
<span class="text-truncate">{{service.name}}</span>
|
||||
<span class="flex-grow-1"></span>
|
||||
<span class="text-capitalize {{service.state}}">{{service.state}}</span>
|
||||
</div>
|
||||
<mat-divider [inset]="true" *ngIf="!last"></mat-divider>
|
||||
</a>
|
||||
</mat-list>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<app-uptime [id]="service.id"></app-uptime>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
||||
<div class="text-right mt-3"><small>Last updated {{lastUpdated | date:'HH:mm:ss'}}</small></div>
|
||||
<div class="text-right pb-3"><small matTooltip="{{lastUpdated | dayjs:'format':'LTS'}}" matTooltipPosition="above"
|
||||
[matTooltipShowDelay]="0"
|
||||
[matTooltipHideDelay]="0">{{'last-updated' | translate:{'time': lastUpdated | dayjs:'from'} }}</small>
|
||||
</div>
|
||||
|
|
|
@ -1,23 +1,5 @@
|
|||
.operational {
|
||||
color: #7ed321;
|
||||
}
|
||||
|
||||
.outage {
|
||||
color: #ff6f6f;
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
color: #f7ca18;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover.name, &:hover .name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
mat-panel-title {
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { StatusComponent } from './status.component';
|
||||
|
||||
describe('StatusComponent', () => {
|
||||
let component: StatusComponent;
|
||||
let fixture: ComponentFixture<StatusComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StatusComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,11 +1,10 @@
|
|||
import {Component, Inject, OnDestroy, OnInit, PLATFORM_ID} from '@angular/core';
|
||||
import {ApiService} from "../_service/api.service";
|
||||
import {Group} from "../_data/data";
|
||||
import {interval, Subject} from "rxjs";
|
||||
import {flatMap, startWith, takeUntil} from "rxjs/operators";
|
||||
import {DOCUMENT, isPlatformBrowser} from "@angular/common";
|
||||
|
||||
// import {DOCUMENT} from "@angular/common";
|
||||
import {ApiService} from '../_service/api.service';
|
||||
import {Group} from '../_data/data';
|
||||
import {interval, Subject} from 'rxjs';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {StorageService} from '../_service/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-status',
|
||||
|
@ -14,17 +13,24 @@ import {DOCUMENT, isPlatformBrowser} from "@angular/common";
|
|||
})
|
||||
export class StatusComponent implements OnInit, OnDestroy {
|
||||
readonly stateClasses = {
|
||||
"operational": 'fas fa-fw fa-heart operational mr-2',
|
||||
"outage": 'fas fa-fw fa-heart-broken outage mr-2',
|
||||
"maintenance": 'fas fa-fw fa-heartbeat maintenance mr-2'
|
||||
'operational': 'fas fa-fw fa-heart operational mr-2',
|
||||
'outage': 'fas fa-fw fa-heart-broken outage mr-2',
|
||||
'maintenance': 'fas fa-fw fa-heartbeat maintenance mr-2'
|
||||
};
|
||||
|
||||
destroyed$ = new Subject();
|
||||
groups: Group[];
|
||||
lastUpdated: Date;
|
||||
expandedCache: { [id: string]: boolean };
|
||||
|
||||
constructor(private api: ApiService, @Inject(PLATFORM_ID) private platformId: Object,
|
||||
constructor(private api: ApiService, private storage: StorageService,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
@Inject(DOCUMENT) private document: Document) {
|
||||
let cache = this.storage.getValue('expanded');
|
||||
if (typeof cache !== 'object') {
|
||||
cache = null;
|
||||
}
|
||||
this.expandedCache = cache || {};
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -49,4 +55,8 @@ export class StatusComponent implements OnInit, OnDestroy {
|
|||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
saveExpandedCache() {
|
||||
this.storage.setValue('expanded', this.expandedCache);
|
||||
}
|
||||
}
|
||||
|
|
70
src/app/uptime/uptime.component.html
Normal file
70
src/app/uptime/uptime.component.html
Normal file
|
@ -0,0 +1,70 @@
|
|||
<div *ngIf="uptime$ | async as uptime; else loadingOrError">
|
||||
<h2 class="m-0">{{'uptime.title' | translate}}</h2>
|
||||
|
||||
<div class="row m-0">
|
||||
<div class="col-6 col-md-3 p-0">
|
||||
<div class="my-4 px-4 border-right">
|
||||
<h1 class="m-0">{{uptime.hours24?.toFixed(2)}}%</h1>
|
||||
{{'uptime.last24hours' | translate}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 p-0">
|
||||
<div class="my-4 px-4 border-md-right">
|
||||
<h1 class="m-0">{{uptime.days7?.toFixed(2)}}%</h1>
|
||||
{{'uptime.last7days' | translate}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 p-0">
|
||||
<div class="my-4 px-4 border-right">
|
||||
<h1 class="m-0">{{uptime.days30?.toFixed(2)}}%</h1>
|
||||
{{'uptime.last30days' | translate}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 p-0">
|
||||
<div class="my-4 px-4">
|
||||
<h1 class="m-0">{{uptime.days90?.toFixed(2)}}%</h1>
|
||||
{{'uptime.last90days' | translate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex mb-4" style="height: 2rem">
|
||||
<ng-container *ngFor="let day of uptime.days; index as index">
|
||||
<div class="flex-grow-1" style="margin: 1px"
|
||||
[class.d-none]="index < 60" [class.d-lg-block]="index < 30" [class.d-sm-block]="index >= 30 && index < 60"
|
||||
[class.bg-operational]="day.uptime >= 99" [class.bg-maintenance]="day.uptime < 99 && day.uptime >= 95"
|
||||
[class.bg-outage]="day.uptime < 95"
|
||||
matTooltip="{{day.date | dayjs:'format':'l'}} {{day.uptime.toFixed(2)}}%" matTooltipPosition="above"
|
||||
[matTooltipShowDelay]="0" [matTooltipHideDelay]="0" matTooltipClass="multiline-tooltip"></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="uptime.events.length">
|
||||
<h2 class="m-0">{{'recent-events.title' | translate}}</h2>
|
||||
<mat-list>
|
||||
<ng-container *ngFor="let event of uptime.events; index as index; last as last">
|
||||
<mat-list-item *ngIf="index < 4 || expanded">
|
||||
<i mat-list-icon [class]="stateClasses[event.state]"></i>
|
||||
<p matLine>
|
||||
<span *ngIf="event.state === 'operational'"
|
||||
class="text-truncate">{{'recent-events.operational' | translate}}</span>
|
||||
<span *ngIf="event.state === 'maintenance'"
|
||||
class="text-truncate">{{'recent-events.maintenance' | translate: {'time': event.date | dayjs:'to':uptime.events[index - 1]?.date} }}</span>
|
||||
<span *ngIf="event.state === 'outage'"
|
||||
class="text-truncate">{{'recent-events.outage' | translate:{'time': event.date | dayjs:'to':uptime.events[index - 1]?.date} }}</span>
|
||||
</p>
|
||||
<div matLine><small matTooltip="{{event.date | dayjs:'format':'LLL'}}" matTooltipPosition="above"
|
||||
[matTooltipShowDelay]="0" [matTooltipHideDelay]="0">{{event.date | dayjs:'from'}}</small>
|
||||
</div>
|
||||
<mat-divider [inset]="true"
|
||||
*ngIf="!(last || uptime.events.length > 4 && !expanded && index >= 3)"></mat-divider>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
</mat-list>
|
||||
<a href="#" class="mt-3" *ngIf="uptime.events.length > 4"
|
||||
(click)="toggleExpanded(); $event.preventDefault()">{{(expanded ? 'show-less' : 'show-all') | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #loadingOrError>
|
||||
<div *ngIf="error">{{error}}</div>
|
||||
<div *ngIf="!error">{{'loading' | translate}}</div>
|
||||
</ng-template>
|
0
src/app/uptime/uptime.component.scss
Normal file
0
src/app/uptime/uptime.component.scss
Normal file
52
src/app/uptime/uptime.component.ts
Normal file
52
src/app/uptime/uptime.component.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {UptimeStatus} from '../_data/data';
|
||||
import {ApiService} from '../_service/api.service';
|
||||
import {catchError} from 'rxjs/operators';
|
||||
import {StorageService} from '../_service/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-uptime',
|
||||
templateUrl: './uptime.component.html',
|
||||
styleUrls: ['./uptime.component.scss']
|
||||
})
|
||||
export class UptimeComponent implements OnInit {
|
||||
@Input() id: string;
|
||||
readonly stateClasses = {
|
||||
'operational': 'fas fa-fw fa-heart operational mr-2',
|
||||
'outage': 'fas fa-fw fa-heart-broken outage mr-2',
|
||||
'maintenance': 'fas fa-fw fa-heartbeat maintenance mr-2'
|
||||
};
|
||||
uptime$: Observable<UptimeStatus>;
|
||||
error: string;
|
||||
expanded: boolean;
|
||||
|
||||
constructor(private api: ApiService, private storage: StorageService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.id) {
|
||||
throw new Error('please pass a service id!');
|
||||
}
|
||||
let value = this.storage.getValue('show-events-' + this.id);
|
||||
console.log(value, typeof value);
|
||||
if (typeof value !== 'boolean') {
|
||||
value = false;
|
||||
}
|
||||
this.expanded = value;
|
||||
this.uptime$ = this.api.getServiceUptime(this.id)
|
||||
.pipe(catchError(err => {
|
||||
if (err.status === 404) {
|
||||
this.error = 'No uptime information available.';
|
||||
} else {
|
||||
this.error = 'An unexpected error occurred: ' + err.error;
|
||||
}
|
||||
return of(null);
|
||||
}));
|
||||
}
|
||||
|
||||
toggleExpanded(): void {
|
||||
this.expanded = !this.expanded;
|
||||
this.storage.setValue('show-events-' + this.id, this.expanded);
|
||||
}
|
||||
}
|
Reference in a new issue