added frontend template
This commit is contained in:
commit
2bea201bb3
41 changed files with 15481 additions and 0 deletions
20
frontend/src/app/_data/data.ts
Normal file
20
frontend/src/app/_data/data.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export type State = 'operational' | 'outage' | 'maintenance'; // ok, alerting, paused
|
||||
|
||||
export interface ApiResponse {
|
||||
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;
|
||||
}
|
16
frontend/src/app/_service/api.service.spec.ts
Normal file
16
frontend/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();
|
||||
});
|
||||
});
|
53
frontend/src/app/_service/api.service.ts
Normal file
53
frontend/src/app/_service/api.service.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {Observable, of} from "rxjs";
|
||||
import {ApiResponse} from "../_data/data";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public getServiceStates(): Observable<ApiResponse> {
|
||||
return of({
|
||||
state: "maintenance",
|
||||
groups: [{
|
||||
id: 'default',
|
||||
name: 'Some Group',
|
||||
state: "outage",
|
||||
services: [{
|
||||
id: 'nextcloud',
|
||||
name: 'Nextcloud',
|
||||
url: "https://sp-codes.de",
|
||||
state: "operational"
|
||||
}, {
|
||||
id: 'synapse',
|
||||
name: 'Synapse',
|
||||
url: "https://sp-codes.de",
|
||||
state: "outage"
|
||||
}, {
|
||||
id: 'searx',
|
||||
name: 'Searx',
|
||||
url: "https://sp-codes.de",
|
||||
state: "maintenance"
|
||||
}]
|
||||
}, {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
state: "operational",
|
||||
services: [{
|
||||
id: 'nextcloud',
|
||||
name: 'Nextcloud',
|
||||
url: "https://sp-codes.de",
|
||||
state: "operational"
|
||||
}, {
|
||||
id: 'synapse',
|
||||
name: 'Synapse',
|
||||
url: "https://sp-codes.de",
|
||||
state: "operational"
|
||||
}]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
16
frontend/src/app/app-routing.module.ts
Normal file
16
frontend/src/app/app-routing.module.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
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)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
}
|
16
frontend/src/app/app.component.html
Normal file
16
frontend/src/app/app.component.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="box">
|
||||
<header class="container pt-4">
|
||||
<h1>sp-status</h1>
|
||||
<h3>Services hosted by sp-codes</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
frontend/src/app/app.component.scss
Normal file
30
frontend/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
frontend/src/app/app.component.spec.ts
Normal file
35
frontend/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 'frontend'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('frontend');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
|
||||
});
|
||||
});
|
9
frontend/src/app/app.component.ts
Normal file
9
frontend/src/app/app.component.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
}
|
26
frontend/src/app/app.module.ts
Normal file
26
frontend/src/app/app.module.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
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";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
StatusComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
MatExpansionModule,
|
||||
MatListModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
24
frontend/src/app/status/status.component.html
Normal file
24
frontend/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
frontend/src/app/status/status.component.scss
Normal file
22
frontend/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
frontend/src/app/status/status.component.spec.ts
Normal file
25
frontend/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();
|
||||
});
|
||||
});
|
45
frontend/src/app/status/status.component.ts
Normal file
45
frontend/src/app/status/status.component.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {Component, Inject, OnDestroy, OnInit} 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 {Meta} from "@angular/platform-browser";
|
||||
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(DOCUMENT) private document: Document) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
interval(30000).pipe(
|
||||
startWith(0),
|
||||
takeUntil(this.destroyed$),
|
||||
flatMap(() => this.api.getServiceStates())
|
||||
).subscribe(response => {
|
||||
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