major refactoring
added universal added api
This commit is contained in:
parent
2bea201bb3
commit
a4542f7abd
52 changed files with 2851 additions and 313 deletions
52
.gitignore
vendored
52
.gitignore
vendored
|
@ -1,3 +1,51 @@
|
||||||
.idea/
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
*.iml
|
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
# Only exists if Bazel was run
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# profiling files
|
||||||
|
chrome-profiler-events*.json
|
||||||
|
speed-measure-plugin*.json
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# custom
|
||||||
|
|
||||||
|
config.json
|
||||||
|
cache.json
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# sp-status frontend
|
# GrafanaStatuspage
|
||||||
|
|
||||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.4.
|
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.4.
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"frontend": {
|
"grafana-statuspage": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
|
@ -17,13 +17,14 @@
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/frontend",
|
"outputPath": "dist/grafana-statuspage/browser",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
|
"src/favicon.png",
|
||||||
"src/favicon-operational.ico",
|
"src/favicon-operational.ico",
|
||||||
"src/favicon-outage.ico",
|
"src/favicon-outage.ico",
|
||||||
"src/favicon-maintenance.ico",
|
"src/favicon-maintenance.ico",
|
||||||
|
@ -68,18 +69,18 @@
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "frontend:build"
|
"browserTarget": "grafana-statuspage:build"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "frontend:build:production"
|
"browserTarget": "grafana-statuspage:build:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "frontend:build"
|
"browserTarget": "grafana-statuspage:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
|
@ -90,7 +91,10 @@
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.png",
|
||||||
|
"src/favicon-operational.ico",
|
||||||
|
"src/favicon-outage.ico",
|
||||||
|
"src/favicon-maintenance.ico",
|
||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
@ -116,15 +120,62 @@
|
||||||
"builder": "@angular-devkit/build-angular:protractor",
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
"options": {
|
"options": {
|
||||||
"protractorConfig": "e2e/protractor.conf.js",
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
"devServerTarget": "frontend:serve"
|
"devServerTarget": "grafana-statuspage:serve"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"devServerTarget": "frontend:serve:production"
|
"devServerTarget": "grafana-statuspage:serve:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"builder": "@angular-devkit/build-angular:server",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/grafana-statuspage/server",
|
||||||
|
"main": "server.ts",
|
||||||
|
"tsConfig": "tsconfig.server.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"outputHashing": "media",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sourceMap": false,
|
||||||
|
"optimization": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-ssr": {
|
||||||
|
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "grafana-statuspage:build",
|
||||||
|
"serverTarget": "grafana-statuspage:server"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "grafana-statuspage:build:production",
|
||||||
|
"serverTarget": "grafana-statuspage:server:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prerender": {
|
||||||
|
"builder": "@nguniversal/builders:prerender",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "grafana-statuspage:build:production",
|
||||||
|
"serverTarget": "grafana-statuspage:server:production",
|
||||||
|
"routes": [
|
||||||
|
"/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
"defaultProject": "frontend"
|
"defaultProject": "grafana-statuspage"
|
||||||
}
|
}
|
18
config.json
Normal file
18
config.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"authToken": "test",
|
||||||
|
"title": "sp-status",
|
||||||
|
"description": "Services hosted by sp-codes",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "test",
|
||||||
|
"name": "Test",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"id": "test",
|
||||||
|
"name": "Test",
|
||||||
|
"url": "http://test.de"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ describe('workspace-project App', () => {
|
||||||
|
|
||||||
it('should display welcome message', () => {
|
it('should display welcome message', () => {
|
||||||
page.navigateTo();
|
page.navigateTo();
|
||||||
expect(page.getTitleText()).toEqual('frontend app is running!');
|
expect(page.getTitleText()).toEqual('grafana-statuspage app is running!');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
46
frontend/.gitignore
vendored
46
frontend/.gitignore
vendored
|
@ -1,46 +0,0 @@
|
||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
/out-tsc
|
|
||||||
# Only exists if Bazel was run
|
|
||||||
/bazel-out
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# profiling files
|
|
||||||
chrome-profiler-events*.json
|
|
||||||
speed-measure-plugin*.json
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.history/*
|
|
||||||
|
|
||||||
# misc
|
|
||||||
/.sass-cache
|
|
||||||
/connect.lock
|
|
||||||
/coverage
|
|
||||||
/libpeerconnection.log
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# System Files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
|
@ -1,53 +0,0 @@
|
||||||
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"
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.scss']
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
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 { }
|
|
|
@ -1,3 +0,0 @@
|
||||||
export const environment = {
|
|
||||||
production: true
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>sp-status</title>
|
|
||||||
<base href="/">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link id="favicon" rel="icon" type="image/x-icon" href="favicon-operational.ico">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body class="mat-typography">
|
|
||||||
<app-root></app-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -16,7 +16,7 @@ module.exports = function (config) {
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
},
|
},
|
||||||
coverageIstanbulReporter: {
|
coverageIstanbulReporter: {
|
||||||
dir: require('path').join(__dirname, './coverage/frontend'),
|
dir: require('path').join(__dirname, './coverage/grafana-statuspage'),
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
fixWebpackSourcePaths: true
|
fixWebpackSourcePaths: true
|
||||||
},
|
},
|
2476
frontend/package-lock.json → package-lock.json
generated
2476
frontend/package-lock.json → package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,13 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "sp-status",
|
"name": "grafana-statuspage",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"e2e": "ng e2e"
|
"e2e": "ng e2e",
|
||||||
|
"dev:ssr": "ng run grafana-statuspage:serve-ssr",
|
||||||
|
"serve:ssr": "node dist/grafana-statuspage/server/main.js",
|
||||||
|
"build:ssr": "ng build --prod && ng run grafana-statuspage:server:production",
|
||||||
|
"prerender": "ng run grafana-statuspage:prerender"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -20,9 +24,12 @@
|
||||||
"@angular/material": "^9.2.2",
|
"@angular/material": "^9.2.2",
|
||||||
"@angular/platform-browser": "~9.1.4",
|
"@angular/platform-browser": "~9.1.4",
|
||||||
"@angular/platform-browser-dynamic": "~9.1.4",
|
"@angular/platform-browser-dynamic": "~9.1.4",
|
||||||
|
"@angular/platform-server": "~9.1.4",
|
||||||
"@angular/router": "~9.1.4",
|
"@angular/router": "~9.1.4",
|
||||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||||
|
"@nguniversal/express-engine": "^9.1.0",
|
||||||
"bootstrap": "^4.4.1",
|
"bootstrap": "^4.4.1",
|
||||||
|
"express": "^4.15.2",
|
||||||
"rxjs": "~6.5.4",
|
"rxjs": "~6.5.4",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
|
@ -32,6 +39,8 @@
|
||||||
"@angular/cli": "~9.1.4",
|
"@angular/cli": "~9.1.4",
|
||||||
"@angular/compiler-cli": "~9.1.4",
|
"@angular/compiler-cli": "~9.1.4",
|
||||||
"@angular/language-service": "~9.1.4",
|
"@angular/language-service": "~9.1.4",
|
||||||
|
"@nguniversal/builders": "^9.1.0",
|
||||||
|
"@types/express": "^4.17.0",
|
||||||
"@types/node": "^12.11.1",
|
"@types/node": "^12.11.1",
|
||||||
"@types/jasmine": "~3.5.0",
|
"@types/jasmine": "~3.5.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
61
server.ts
Normal file
61
server.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import 'zone.js/dist/zone-node';
|
||||||
|
|
||||||
|
import {ngExpressEngine} from '@nguniversal/express-engine';
|
||||||
|
import * as express from 'express';
|
||||||
|
import {join} from 'path';
|
||||||
|
|
||||||
|
import {AppServerModule} from './src/main.server';
|
||||||
|
import {APP_BASE_HREF} from '@angular/common';
|
||||||
|
import {existsSync} from 'fs';
|
||||||
|
import {api} from './src/main.status';
|
||||||
|
|
||||||
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
|
export function app() {
|
||||||
|
const server = express();
|
||||||
|
const distFolder = join(process.cwd(), 'dist/grafana-statuspage/browser');
|
||||||
|
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
||||||
|
|
||||||
|
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||||
|
server.engine('html', ngExpressEngine({
|
||||||
|
bootstrap: AppServerModule,
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.set('view engine', 'html');
|
||||||
|
server.set('views', distFolder);
|
||||||
|
|
||||||
|
server.use('/api', api);
|
||||||
|
|
||||||
|
// Serve static files from /browser
|
||||||
|
server.get('*.*', express.static(distFolder, {
|
||||||
|
maxAge: '1y'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// All regular routes use the Universal engine
|
||||||
|
server.get('*', (req, res) => {
|
||||||
|
res.render(indexHtml, {req, providers: [{provide: APP_BASE_HREF, useValue: req.baseUrl}]});
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const port = process.env.PORT || 4000;
|
||||||
|
|
||||||
|
// Start up the Node server
|
||||||
|
const server = app();
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`Node Express server listening on http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webpack will replace 'require' with '__webpack_require__'
|
||||||
|
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||||
|
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||||
|
declare const __non_webpack_require__: NodeRequire;
|
||||||
|
const mainModule = __non_webpack_require__.main;
|
||||||
|
const moduleFilename = mainModule && mainModule.filename || '';
|
||||||
|
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './src/main.server';
|
|
@ -1,6 +1,6 @@
|
||||||
export type State = 'operational' | 'outage' | 'maintenance'; // ok, alerting, paused
|
export type State = 'operational' | 'outage' | 'maintenance';
|
||||||
|
|
||||||
export interface ApiResponse {
|
export interface CurrentStatus {
|
||||||
state: State;
|
state: State;
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
}
|
}
|
||||||
|
@ -18,3 +18,8 @@ export interface Service {
|
||||||
url: string;
|
url: string;
|
||||||
state: State;
|
state: State;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MetaInfo {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,15 @@ import {NgModule} from '@angular/core';
|
||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {StatusComponent} from "./status/status.component";
|
import {StatusComponent} from "./status/status.component";
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [{
|
const routes: Routes = [{
|
||||||
path: '',
|
path: '',
|
||||||
component: StatusComponent
|
component: StatusComponent
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes)],
|
imports: [RouterModule.forRoot(routes, {
|
||||||
|
initialNavigation: 'enabled'
|
||||||
|
})],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {
|
export class AppRoutingModule {
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<header class="container pt-4">
|
<header class="container pt-4">
|
||||||
<h1>sp-status</h1>
|
<h1 *ngIf="title && title.length">{{title}}</h1>
|
||||||
<h3>Services hosted by sp-codes</h3>
|
<h3 *ngIf="description && description.length">{{description}}</h3>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
|
@ -20,16 +20,16 @@ describe('AppComponent', () => {
|
||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should have as title 'frontend'`, () => {
|
it(`should have as title 'grafana-statuspage'`, () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
expect(app.title).toEqual('frontend');
|
expect(app.title).toEqual('grafana-statuspage');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render title', () => {
|
it('should render title', () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const compiled = fixture.nativeElement;
|
const compiled = fixture.nativeElement;
|
||||||
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
|
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 {}
|
|
@ -1,10 +1,11 @@
|
||||||
import {Component, Inject, OnDestroy, OnInit} from '@angular/core';
|
import {Component, Inject, OnDestroy, OnInit, PLATFORM_ID} from '@angular/core';
|
||||||
import {ApiService} from "../_service/api.service";
|
import {ApiService} from "../_service/api.service";
|
||||||
import {Group} from "../_data/data";
|
import {Group} from "../_data/data";
|
||||||
import {interval, Subject} from "rxjs";
|
import {interval, Subject} from "rxjs";
|
||||||
import {flatMap, startWith, takeUntil} from "rxjs/operators";
|
import {flatMap, startWith, takeUntil} from "rxjs/operators";
|
||||||
import {Meta} from "@angular/platform-browser";
|
import {DOCUMENT, isPlatformBrowser} from "@angular/common";
|
||||||
import {DOCUMENT} from "@angular/common";
|
|
||||||
|
// import {DOCUMENT} from "@angular/common";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-status',
|
selector: 'app-status',
|
||||||
|
@ -22,17 +23,23 @@ export class StatusComponent implements OnInit, OnDestroy {
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
lastUpdated: Date;
|
lastUpdated: Date;
|
||||||
|
|
||||||
constructor(private api: ApiService, @Inject(DOCUMENT) private document: Document) {
|
constructor(private api: ApiService, @Inject(PLATFORM_ID) private platformId: Object,
|
||||||
|
@Inject(DOCUMENT) private document: Document) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
interval(30000).pipe(
|
this.update();
|
||||||
startWith(0),
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
takeUntil(this.destroyed$),
|
interval(30000).pipe(takeUntil(this.destroyed$)).subscribe(() => this.update());
|
||||||
flatMap(() => this.api.getServiceStates())
|
}
|
||||||
).subscribe(response => {
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.api.getServiceStates().subscribe(response => {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
const favicon: HTMLLinkElement = document.getElementById('favicon') as HTMLLinkElement;
|
const favicon: HTMLLinkElement = document.getElementById('favicon') as HTMLLinkElement;
|
||||||
favicon.href = `favicon-${response.state}.ico`;
|
favicon.href = `favicon-${response.state}.ico`;
|
||||||
|
}
|
||||||
this.groups = response.groups;
|
this.groups = response.groups;
|
||||||
this.lastUpdated = new Date();
|
this.lastUpdated = new Date();
|
||||||
});
|
});
|
4
src/environments/environment.prod.ts
Normal file
4
src/environments/environment.prod.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
serverUrl: 'http://localhost:4000'
|
||||||
|
};
|
|
@ -3,7 +3,8 @@
|
||||||
// The list of file replacements can be found in `angular.json`.
|
// The list of file replacements can be found in `angular.json`.
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false
|
production: false,
|
||||||
|
serverUrl: 'http://localhost:4200'
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
13
src/index.html
Normal file
13
src/index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>grafana-statuspage</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link id="favicon" rel="icon" type="image/x-icon" href="">
|
||||||
|
</head>
|
||||||
|
<body class="mat-typography">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
src/main.server.ts
Normal file
10
src/main.server.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AppServerModule } from './app/app.server.module';
|
||||||
|
export { renderModule, renderModuleFactory } from '@angular/platform-server';
|
117
src/main.status.ts
Normal file
117
src/main.status.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import {json, Router} from 'express';
|
||||||
|
import {CurrentStatus, State} from "./app/_data/data";
|
||||||
|
import {existsSync, readFileSync, writeFileSync} from "fs";
|
||||||
|
|
||||||
|
interface Cache {
|
||||||
|
[id: string]: State
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
authToken: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
groups: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
services: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GrafanaWebhookBody {
|
||||||
|
dashboardId: number;
|
||||||
|
evalMatches: {
|
||||||
|
value: number,
|
||||||
|
metric: string,
|
||||||
|
tags: any
|
||||||
|
}[];
|
||||||
|
imageUrl: string,
|
||||||
|
message: string,
|
||||||
|
orgId: number,
|
||||||
|
panelId: number,
|
||||||
|
ruleId: number,
|
||||||
|
ruleName: string,
|
||||||
|
ruleUrl: string,
|
||||||
|
state: "ok" | "paused" | "alerting" | "pending" | "no_data";
|
||||||
|
tags: { [key: string]: string },
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = Router();
|
||||||
|
api.use(json());
|
||||||
|
|
||||||
|
const config = JSON.parse(readFileSync('config.json', {encoding: 'UTF-8'})) as Config;
|
||||||
|
const serviceStates = existsSync('cache.json') ? JSON.parse(readFileSync('cache.json', {encoding: 'UTF-8'})) : {} as Cache;
|
||||||
|
|
||||||
|
let cache: CurrentStatus;
|
||||||
|
updateCache();
|
||||||
|
|
||||||
|
api.post('/update/health', (req, res) => {
|
||||||
|
const token = req.query.token;
|
||||||
|
if (token !== config.authToken) {
|
||||||
|
return res.status(401).send('invalid token');
|
||||||
|
}
|
||||||
|
const serviceId = req.query.service as string;
|
||||||
|
const message = req.body as GrafanaWebhookBody;
|
||||||
|
|
||||||
|
switch (message.state) {
|
||||||
|
case "no_data":
|
||||||
|
case "alerting":
|
||||||
|
serviceStates[serviceId] = "outage";
|
||||||
|
break;
|
||||||
|
case "paused":
|
||||||
|
serviceStates[serviceId] = "maintenance";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
serviceStates[serviceId] = "operational"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCache();
|
||||||
|
|
||||||
|
writeFileSync('cache.json', JSON.stringify(serviceStates), {encoding: 'UTF-8'});
|
||||||
|
|
||||||
|
return res.send('OK');
|
||||||
|
});
|
||||||
|
|
||||||
|
api.get('/status', (req, res) => {
|
||||||
|
return res.json(cache);
|
||||||
|
});
|
||||||
|
|
||||||
|
api.get('/info', (req, res) => {
|
||||||
|
return res.json({
|
||||||
|
title: config.title,
|
||||||
|
description: config.description
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateCache(): void {
|
||||||
|
const groups = config.groups.map(group => {
|
||||||
|
const services = group.services.map(service => {
|
||||||
|
return {
|
||||||
|
id: service.id,
|
||||||
|
name: service.name,
|
||||||
|
url: service.url,
|
||||||
|
state: serviceStates[service.id] || "operational"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
state: calculateOverallState(services.map(s => s.state)),
|
||||||
|
services: services
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cache = {
|
||||||
|
state: calculateOverallState(groups.map(g => g.state)),
|
||||||
|
groups: groups
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOverallState(states: State[]): State {
|
||||||
|
return states.includes("outage") ? "outage" : states.includes("maintenance") ? "maintenance" : "operational"
|
||||||
|
}
|
||||||
|
|
||||||
|
export {api};
|
|
@ -8,5 +8,7 @@ if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
.catch(err => console.error(err));
|
.catch(err => console.error(err));
|
||||||
|
});
|
17
tsconfig.server.json
Normal file
17
tsconfig.server.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app-server",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.server.ts",
|
||||||
|
"server.ts"
|
||||||
|
],
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"entryModule": "./src/app/app.server.module#AppServerModule"
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue