major refactoring
added universal added api
This commit is contained in:
parent
2bea201bb3
commit
a4542f7abd
52 changed files with 2851 additions and 313 deletions
25
src/app/_data/data.ts
Normal file
25
src/app/_data/data.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
export type State = 'operational' | 'outage' | 'maintenance';
|
||||
|
||||
export interface CurrentStatus {
|
||||
state: State;
|
||||
groups: Group[];
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
state: State;
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
state: State;
|
||||
}
|
||||
|
||||
export interface MetaInfo {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
16
src/app/_service/api.service.spec.ts
Normal file
16
src/app/_service/api.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
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();
|
||||
});
|
||||
});
|
25
src/app/_service/api.service.ts
Normal file
25
src/app/_service/api.service.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
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";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
private readonly api;
|
||||
|
||||
constructor(private http: HttpClient, @Inject(PLATFORM_ID) platformId: Object) {
|
||||
this.api = isPlatformBrowser(platformId) ? '/api' : environment.serverUrl + '/api';
|
||||
}
|
||||
|
||||
public getServiceStates(): Observable<CurrentStatus> {
|
||||
return this.http.get<CurrentStatus>(this.api+ '/status');
|
||||
}
|
||||
|
||||
public getMetaInfo(): Observable<MetaInfo> {
|
||||
return this.http.get<MetaInfo>(this.api+ '/info');
|
||||
}
|
||||
}
|
17
src/app/app-routing.module.ts
Normal file
17
src/app/app-routing.module.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {StatusComponent} from "./status/status.component";
|
||||
|
||||
const routes: Routes = [{
|
||||
path: '',
|
||||
component: StatusComponent
|
||||
}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabled'
|
||||
})],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
}
|
16
src/app/app.component.html
Normal file
16
src/app/app.component.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="box">
|
||||
<header class="container pt-4">
|
||||
<h1 *ngIf="title && title.length">{{title}}</h1>
|
||||
<h3 *ngIf="description && description.length">{{description}}</h3>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
Made with <span class="fas fa-heart"></span> by <a href="https://sp-codes.de">sp-codes</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
30
src/app/app.component.scss
Normal file
30
src/app/app.component.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 30px 0;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #cccccc;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
35
src/app/app.component.spec.ts
Normal file
35
src/app/app.component.spec.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
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 'grafana-statuspage'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('grafana-statuspage');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('grafana-statuspage app is running!');
|
||||
});
|
||||
});
|
26
src/app/app.component.ts
Normal file
26
src/app/app.component.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
constructor(private api: ApiService, private htmlTitle: Title) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.api.getMetaInfo().subscribe(info => {
|
||||
this.title = info.title;
|
||||
this.description = info.description;
|
||||
this.htmlTitle.setTitle(this.title);
|
||||
})
|
||||
}
|
||||
}
|
29
src/app/app.module.ts
Normal file
29
src/app/app.module.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {NgModule} 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";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
StatusComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({appId: 'serverApp'}),
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
MatExpansionModule,
|
||||
MatListModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
14
src/app/app.server.module.ts
Normal file
14
src/app/app.server.module.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AppModule,
|
||||
ServerModule,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
24
src/app/status/status.component.html
Normal file
24
src/app/status/status.component.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<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> {{group.name}}</mat-panel-title>
|
||||
<!-- <mat-panel-description>-->
|
||||
<!-- <span class="text-capitalize">{{getGroupState(group.services)}}</span>-->
|
||||
<!-- </mat-panel-description>-->
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<mat-list>
|
||||
<a *ngFor="let service of group.services; last as last" mat-list-item [href]="service.url" target="_blank">
|
||||
<div matLine class="d-flex">
|
||||
<i [class]="stateClasses[service.state]"></i>
|
||||
<span>{{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>
|
||||
|
||||
<div class="text-right mt-3"><small>Last updated {{lastUpdated | date:'HH:mm:ss'}}</small></div>
|
22
src/app/status/status.component.scss
Normal file
22
src/app/status/status.component.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
.operational {
|
||||
color: #7ed321;
|
||||
}
|
||||
|
||||
.outage {
|
||||
color: #ff6f6f;
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
color: #f7ca18;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
mat-panel-title {
|
||||
.fa, .fas, .far, .fal, .fad, .fab {
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
25
src/app/status/status.component.spec.ts
Normal file
25
src/app/status/status.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StatusComponent } from './status.component';
|
||||
|
||||
describe('StatusComponent', () => {
|
||||
let component: StatusComponent;
|
||||
let fixture: ComponentFixture<StatusComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StatusComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
52
src/app/status/status.component.ts
Normal file
52
src/app/status/status.component.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
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";
|
||||
|
||||
@Component({
|
||||
selector: 'app-status',
|
||||
templateUrl: './status.component.html',
|
||||
styleUrls: ['./status.component.scss']
|
||||
})
|
||||
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'
|
||||
};
|
||||
|
||||
destroyed$ = new Subject();
|
||||
groups: Group[];
|
||||
lastUpdated: Date;
|
||||
|
||||
constructor(private api: ApiService, @Inject(PLATFORM_ID) private platformId: Object,
|
||||
@Inject(DOCUMENT) private document: Document) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.update();
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
interval(30000).pipe(takeUntil(this.destroyed$)).subscribe(() => this.update());
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.api.getServiceStates().subscribe(response => {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
const favicon: HTMLLinkElement = document.getElementById('favicon') as HTMLLinkElement;
|
||||
favicon.href = `favicon-${response.state}.ico`;
|
||||
}
|
||||
this.groups = response.groups;
|
||||
this.lastUpdated = new Date();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
Reference in a new issue