New web ui
This commit is contained in:
parent
e09d5cb4ec
commit
9c651ae913
105 changed files with 7314 additions and 5514 deletions
32
webui/src/app/app.component.spec.ts
Normal file
32
webui/src/app/app.component.spec.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
}));
|
||||
|
||||
it(`should have as title 'app'`, async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('app');
|
||||
}));
|
||||
|
||||
it('should render title in a h1 tag', async(() => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
|
||||
}));
|
||||
});
|
10
webui/src/app/app.component.ts
Normal file
10
webui/src/app/app.component.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<app-header></app-header>
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
})
|
||||
export class AppComponent { }
|
43
webui/src/app/app.module.ts
Normal file
43
webui/src/app/app.module.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ApiService } from './services/api.service';
|
||||
import { WindowService } from './services/window.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { ProvidersComponent } from './components/providers/providers.component';
|
||||
import { HealthComponent } from './components/health/health.component';
|
||||
import { LineChartComponent } from './charts/line-chart/line-chart.component';
|
||||
import { BarChartComponent } from './charts/bar-chart/bar-chart.component';
|
||||
import { KeysPipe } from './pipes/keys.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HeaderComponent,
|
||||
ProvidersComponent,
|
||||
HealthComponent,
|
||||
LineChartComponent,
|
||||
BarChartComponent,
|
||||
KeysPipe
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
RouterModule.forRoot([
|
||||
{ path: '', component: ProvidersComponent, pathMatch: 'full' },
|
||||
{ path: 'status', component: HealthComponent }
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
WindowService
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
7
webui/src/app/charts/bar-chart/bar-chart.component.html
Normal file
7
webui/src/app/charts/bar-chart/bar-chart.component.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class="bar-chart" [class.is-hidden]="loading"></div>
|
||||
<div class="loading-text" [class.is-hidden]="!loading">
|
||||
<span>
|
||||
<span>Loading, please wait...</span>
|
||||
<img src="./assets/images/loader.svg" class="main-loader">
|
||||
</span>
|
||||
</div>
|
25
webui/src/app/charts/bar-chart/bar-chart.component.spec.ts
Normal file
25
webui/src/app/charts/bar-chart/bar-chart.component.spec.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BarChartComponent } from './bar-chart.component';
|
||||
|
||||
describe('BarChartComponent', () => {
|
||||
let component: BarChartComponent;
|
||||
let fixture: ComponentFixture<BarChartComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ BarChartComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BarChartComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
114
webui/src/app/charts/bar-chart/bar-chart.component.ts
Normal file
114
webui/src/app/charts/bar-chart/bar-chart.component.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { WindowService } from '../../services/window.service';
|
||||
import {
|
||||
min,
|
||||
max,
|
||||
easeLinear,
|
||||
select,
|
||||
axisLeft,
|
||||
axisBottom,
|
||||
scaleBand,
|
||||
scaleLinear
|
||||
} from 'd3';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart',
|
||||
templateUrl: './bar-chart.component.html'
|
||||
})
|
||||
export class BarChartComponent implements OnInit, OnChanges {
|
||||
@Input() value: any;
|
||||
|
||||
barChartEl: HTMLElement;
|
||||
svg: any;
|
||||
x: any;
|
||||
y: any;
|
||||
g: any;
|
||||
bars: any;
|
||||
width: number;
|
||||
height: number;
|
||||
margin = { top: 40, right: 40, bottom: 40, left: 40 };
|
||||
loading: boolean;
|
||||
data: any[];
|
||||
|
||||
constructor(public elementRef: ElementRef, public windowService: WindowService) {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.barChartEl = this.elementRef.nativeElement.querySelector('.bar-chart');
|
||||
this.setup();
|
||||
setTimeout(() => this.loading = false, 4000);
|
||||
|
||||
this.windowService.resize.subscribe(w => this.draw());
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!this.value || !this.svg) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data = this.value;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
|
||||
this.svg = select(this.barChartEl).append('svg')
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.top + this.margin.bottom);
|
||||
|
||||
this.g = this.svg.append('g')
|
||||
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
|
||||
|
||||
this.x = scaleBand().padding(0.05);
|
||||
this.y = scaleLinear();
|
||||
|
||||
this.g.append('g')
|
||||
.attr('class', 'axis axis--x');
|
||||
|
||||
this.g.append('g')
|
||||
.attr('class', 'axis axis--y');
|
||||
}
|
||||
|
||||
draw(): void {
|
||||
this.x.domain(this.data.map((d: any) => d.code));
|
||||
this.y.domain([0, max(this.data, (d: any) => d.count)]);
|
||||
|
||||
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
|
||||
this.svg
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.top + this.margin.bottom);
|
||||
|
||||
this.x.rangeRound([0, this.width]);
|
||||
this.y.rangeRound([this.height, 0]);
|
||||
|
||||
this.g.select('.axis--x')
|
||||
.attr('transform', `translate(0, ${this.height})`)
|
||||
.call(axisBottom(this.x));
|
||||
|
||||
this.g.select('.axis--y')
|
||||
.call(axisLeft(this.y).tickSize(-this.width));
|
||||
|
||||
const bars = this.g.selectAll('.bar').data(this.data);
|
||||
|
||||
bars.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'bar')
|
||||
.attr('x', (d: any) => d.code)
|
||||
.attr('y', (d: any) => d.count)
|
||||
.attr('width', this.x.bandwidth())
|
||||
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
|
||||
|
||||
bars.attr('x', (d: any) => this.x(d.code))
|
||||
.attr('y', (d: any) => this.y(d.count))
|
||||
.attr('width', this.x.bandwidth())
|
||||
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
|
||||
|
||||
bars.exit().remove();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<div class="line-chart" [class.is-hidden]="loading"></div>
|
||||
<div class="loading-text" [class.is-hidden]="!loading">
|
||||
<span>
|
||||
<span>Loading, please wait...</span>
|
||||
<img src="./assets/images/loader.svg" class="main-loader">
|
||||
</span>
|
||||
</div>
|
162
webui/src/app/charts/line-chart/line-chart.component.ts
Normal file
162
webui/src/app/charts/line-chart/line-chart.component.ts
Normal file
|
@ -0,0 +1,162 @@
|
|||
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { WindowService } from '../../services/window.service';
|
||||
import {
|
||||
range,
|
||||
scaleTime,
|
||||
scaleLinear,
|
||||
min,
|
||||
max,
|
||||
curveLinear,
|
||||
line,
|
||||
easeLinear,
|
||||
select,
|
||||
axisLeft,
|
||||
axisBottom,
|
||||
timeSecond,
|
||||
timeFormat
|
||||
} from 'd3';
|
||||
|
||||
@Component({
|
||||
selector: 'app-line-chart',
|
||||
templateUrl: 'line-chart.component.html'
|
||||
})
|
||||
export class LineChartComponent implements OnChanges, OnInit {
|
||||
@Input() value: { count: number, date: string };
|
||||
|
||||
lineChartEl: HTMLElement;
|
||||
svg: any;
|
||||
g: any;
|
||||
line: any;
|
||||
path: any;
|
||||
x: any;
|
||||
y: any;
|
||||
data: number[];
|
||||
now: Date;
|
||||
duration: number;
|
||||
limit: number;
|
||||
options: any;
|
||||
xAxis: any;
|
||||
yAxis: any;
|
||||
height: number;
|
||||
width: number;
|
||||
margin = { top: 40, right: 40, bottom: 60, left: 60 };
|
||||
loading = true;
|
||||
|
||||
constructor(private elementRef: ElementRef, public windowService: WindowService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.lineChartEl = this.elementRef.nativeElement.querySelector('.line-chart');
|
||||
this.limit = 40;
|
||||
this.duration = 3000;
|
||||
this.now = new Date(Date.now() - this.duration);
|
||||
|
||||
this.options = {
|
||||
title: '',
|
||||
color: '#3A84C5'
|
||||
};
|
||||
|
||||
this.render();
|
||||
setTimeout(() => this.loading = false, 4000);
|
||||
this.windowService.resize.subscribe(w => {
|
||||
if (this.svg) {
|
||||
const el = this.lineChartEl.querySelector('svg');
|
||||
el.parentNode.removeChild(el);
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
this.width = this.lineChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.lineChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
|
||||
this.svg = select(this.lineChartEl).append('svg')
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.top + this.margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
|
||||
|
||||
if (!this.data) {
|
||||
this.data = range(this.limit).map(i => 0);
|
||||
}
|
||||
|
||||
this.x = scaleTime().range([0, this.width]);
|
||||
this.y = scaleLinear().range([this.height, 0]);
|
||||
|
||||
this.x.domain([<any>this.now - (this.limit - 2), <any>this.now - this.duration]);
|
||||
this.y.domain([0, max(this.data, (d: any) => d)]);
|
||||
|
||||
this.line = line()
|
||||
.x((d: any, i: number) => this.x(<any>this.now - (this.limit - 1 - i) * this.duration))
|
||||
.y((d: any) => this.y(d))
|
||||
.curve(curveLinear);
|
||||
|
||||
this.svg.append('defs').append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height);
|
||||
|
||||
this.xAxis = this.svg.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', `translate(0, ${this.height})`)
|
||||
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')));
|
||||
|
||||
this.yAxis = this.svg.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.call(axisLeft(this.y).tickSize(-this.width));
|
||||
|
||||
this.path = this.svg.append('g')
|
||||
.attr('clip-path', 'url(#clip)')
|
||||
.append('path')
|
||||
.data([this.data])
|
||||
.attr('class', 'line');
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (!this.value || !this.svg) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateData(this.value.count);
|
||||
}
|
||||
|
||||
updateData = (value: number) => {
|
||||
this.data.push(value * 1000000);
|
||||
this.now = new Date();
|
||||
|
||||
this.x.domain([<any>this.now - (this.limit - 2) * this.duration, <any>this.now - this.duration]);
|
||||
const minv = min(this.data, (d: any) => d) > 0 ? min(this.data, (d: any) => d) - 4 : 0;
|
||||
const maxv = max(this.data, (d: any) => d) + 4;
|
||||
this.y.domain([minv, maxv]);
|
||||
|
||||
this.xAxis
|
||||
.transition()
|
||||
.duration(this.duration)
|
||||
.ease(easeLinear)
|
||||
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')))
|
||||
.selectAll('text')
|
||||
.style('text-anchor', 'end')
|
||||
.attr('dx', '-.8em')
|
||||
.attr('dy', '.15em')
|
||||
.attr('transform', 'rotate(-65)');
|
||||
|
||||
this.yAxis
|
||||
.transition()
|
||||
.duration(500)
|
||||
.ease(easeLinear)
|
||||
.call(axisLeft(this.y).tickSize(-this.width));
|
||||
|
||||
this.path
|
||||
.transition()
|
||||
.duration(0)
|
||||
.attr('d', this.line(this.data))
|
||||
.attr('transform', null)
|
||||
.transition()
|
||||
.duration(this.duration)
|
||||
.ease(easeLinear)
|
||||
.attr('transform', `translate(${this.x(<any>this.now - (this.limit - 1) * this.duration)})`);
|
||||
|
||||
this.data.shift();
|
||||
}
|
||||
}
|
29
webui/src/app/components/header/header.component.html
Normal file
29
webui/src/app/components/header/header.component.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" routerLink="/">
|
||||
<img src="./assets/images/traefik.logo.svg" alt="Traefik" class="navbar-logo">
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-menu">
|
||||
<a class="navbar-item" routerLink="/" routerLinkActive="is-active" [routerLinkActiveOptions]="{ exact: true }">
|
||||
Providers
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="/status" routerLinkActive="is-active">
|
||||
Health
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end is-hidden-mobile">
|
||||
<a class="navbar-item" [href]="releaseLink" target="_blank">
|
||||
{{ version }} / {{ codename }}
|
||||
</a>
|
||||
<a class="navbar-item" href="https://docs.traefik.io" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
23
webui/src/app/components/header/header.component.ts
Normal file
23
webui/src/app/components/header/header.component.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: 'header.component.html'
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
version: string;
|
||||
codename: string;
|
||||
releaseLink: string;
|
||||
|
||||
constructor(private apiService: ApiService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.apiService.fetchVersion()
|
||||
.subscribe(data => {
|
||||
this.version = data.Version;
|
||||
this.codename = data.Codename;
|
||||
this.releaseLink = 'https://github.com/containous/traefik/tree/' + data.Version;
|
||||
});
|
||||
}
|
||||
}
|
104
webui/src/app/components/health/health.component.html
Normal file
104
webui/src/app/components/health/health.component.html
Normal file
|
@ -0,0 +1,104 @@
|
|||
<div class="container">
|
||||
<div class="content">
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="content-item">
|
||||
<div class="content-item-data">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="item-data border-right">
|
||||
<span class="data-grey">Total Response Time</span>
|
||||
<span class="data-blue">{{ totalResponseTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="item-data border-right">
|
||||
<span class="data-grey">Total Code Count</span>
|
||||
<span class="data-blue">{{ totalCodeCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="item-data">
|
||||
<span class="data-grey">Uptime Since <br/>{{ uptimeSince }}</span>
|
||||
<span class="data-blue">{{ uptime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-item">
|
||||
<div class="content-item-data">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="item-data border-right">
|
||||
<span class="data-grey">Average Response Time</span>
|
||||
<span class="data-blue">{{ averageResponseTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="item-data border-right">
|
||||
<span class="data-grey">Code Count</span>
|
||||
<span class="data-blue">{{ codeCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="item-data">
|
||||
<span class="data-grey">PID</span>
|
||||
<span class="data-blue">{{ pid }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="content-item">
|
||||
<h2>Average Response Time (µs)</h2>
|
||||
<app-line-chart [value]="chartValue"></app-line-chart>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="content-item">
|
||||
<h2>Total Status Code Count</h2>
|
||||
<app-bar-chart [value]="statusCodeValue"></app-bar-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content" *ngIf="recentErrors">
|
||||
<div class="content-item">
|
||||
<h2>Recent HTTP Errors</h2>
|
||||
<table class="table is-fullwidth">
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Request</td>
|
||||
<td>Time</td>
|
||||
</tr>
|
||||
<tr *ngFor="let entry of recentErrors">
|
||||
<td>
|
||||
<span class="tag is-info">{{ entry.status_code }}</span> <span>{{ entry.status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag">{{ entry.method }}</span> <a>{{ entry.host }}{{ entry.path }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ entry.time }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="!recentErrors?.length">
|
||||
<td colspan="3">
|
||||
<p class="text-muted text-center">No entries</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
57
webui/src/app/components/health/health.component.ts
Normal file
57
webui/src/app/components/health/health.component.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import 'rxjs/add/observable/timer';
|
||||
import 'rxjs/add/operator/timeInterval';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
selector: 'app-health',
|
||||
templateUrl: 'health.component.html'
|
||||
})
|
||||
export class HealthComponent implements OnInit, OnDestroy {
|
||||
sub: Subscription;
|
||||
recentErrors: any;
|
||||
pid: number;
|
||||
uptime: string;
|
||||
uptimeSince: string;
|
||||
averageResponseTime: string;
|
||||
totalResponseTime: string;
|
||||
codeCount: number;
|
||||
totalCodeCount: number;
|
||||
chartValue: any;
|
||||
statusCodeValue: any;
|
||||
|
||||
constructor(private apiService: ApiService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.sub = Observable.timer(0, 3000)
|
||||
.timeInterval()
|
||||
.mergeMap(() => this.apiService.fetchHealthStatus())
|
||||
.subscribe(data => {
|
||||
if (data) {
|
||||
this.recentErrors = data.recent_errors;
|
||||
this.chartValue = { count: data.average_response_time_sec, date: data.time };
|
||||
this.statusCodeValue = Object.keys(data.total_status_code_count)
|
||||
.map(key => ({ code: key, count: data.total_status_code_count[key] }));
|
||||
|
||||
this.pid = data.pid;
|
||||
this.uptime = distanceInWordsStrict(subSeconds(new Date(), data.uptime_sec), new Date());
|
||||
this.uptimeSince = format(subSeconds(new Date(), data.uptime_sec), 'MM/DD/YYYY HH:mm:ss');
|
||||
this.totalResponseTime = data.total_response_time;
|
||||
this.averageResponseTime = data.average_response_time;
|
||||
this.codeCount = data.count;
|
||||
this.totalCodeCount = data.total_count;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
566
webui/src/app/components/providers/providers.component.html
Normal file
566
webui/src/app/components/providers/providers.component.html
Normal file
|
@ -0,0 +1,566 @@
|
|||
<div class="container">
|
||||
<div class="content">
|
||||
|
||||
<div class="columns is-multiline" *ngIf="keys?.length">
|
||||
<div class="column is-12">
|
||||
|
||||
<div class="search-container">
|
||||
<span class="icon"><i class="fas fa-search"></i></span>
|
||||
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword" (ngModelChange)="filter()">
|
||||
</div>
|
||||
|
||||
<div class="tabs" *ngIf="keys?.length">
|
||||
<ul>
|
||||
<li *ngFor="let provider of keys" [class.is-active]="tab === provider" (click)="tab = provider">
|
||||
<a>{{ provider }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div *ngIf="keys?.length">
|
||||
<div class="columns">
|
||||
<!-- Frontends -->
|
||||
<div class="column is-6">
|
||||
<h2 class="subtitle"><span class="tag is-info">{{ providers[tab]?.frontends.length }}</span> Frontends</h2>
|
||||
<div class="message" *ngFor="let p of providers[tab]?.frontends; let i = index;">
|
||||
<div class="message-header">
|
||||
<h2>
|
||||
<div>
|
||||
<i class="icon fas fa-globe"></i>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-info">{{ p.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="p.backend">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<a class="tags has-addons" [href]="'#' + p.backend">
|
||||
<span class="tag is-light">Backend</span>
|
||||
<span class="tag is-primary">{{ p.backend }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
<div class="tabs is-fullwidth is-small is-boxed">
|
||||
<ul>
|
||||
<li [class.is-active]="p.section !== 'details'" (click)="p.section = 'main'"><a>Main</a></li>
|
||||
<li [class.is-active]="p.section === 'details'" (click)="p.section = 'details'"><a>Details</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div *ngIf="p.section !== 'details'">
|
||||
|
||||
<div *ngIf="p.routes && p.routes.length">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Route Rule</td>
|
||||
</tr>
|
||||
<tr *ngFor="let route of p.routes; let ri = index;">
|
||||
<td><span class="has-text-grey" title="{{ route.title }}">{{ route.rule }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.entryPoints && p.entryPoints.length">
|
||||
<hr>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Entry Points</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags">
|
||||
<span class="tag is-info" *ngFor="let ep of p.entryPoints; let ri = index;">{{ ep }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div *ngIf="p.section === 'details'">
|
||||
|
||||
<div>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Misc.</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Priority</span>
|
||||
<span class="tag is-info">{{ p.priority }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Host Header</span>
|
||||
<span class="tag is-info">{{ p.passHostHeader }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" *ngIf="p.passTLSCert">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">TLS Cert</span>
|
||||
<span class="tag is-info">{{ p.passTLSCert }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.basicAuth && p.basicAuth.length">
|
||||
<hr/>
|
||||
<h2>Basic Authentication</h2>
|
||||
<div class="tags padding-5-10">
|
||||
<span class="tag is-info" *ngFor="let auth of p.basicAuth; let ri = index;">{{ auth }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.errors">
|
||||
<hr/>
|
||||
<h2>Error Pages</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Query</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
<tr *ngFor="let key of p.errors | keys">
|
||||
<td><span class="has-text-grey-light">{{ p.errors[key].backend }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.errors[key].query }}</span></td>
|
||||
<td>
|
||||
<span class="tag is-light" *ngFor="let state of p.errors[key].status">{{ state }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.whiteList">
|
||||
<hr/>
|
||||
<div class="columns is-gapless is-multiline is-mobile">
|
||||
<div class="column is-half">
|
||||
<h2>Whitelist</h2>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">useXForwardedFor</span>
|
||||
<span class="tag is-info">{{ p.whiteList.useXForwardedFor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags">
|
||||
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange; let ri = index;">{{ wlRange }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.headers">
|
||||
<hr/>
|
||||
<h2>Headers</h2>
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.customRequestHeaders">
|
||||
<h2>Custom Request Headers</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngFor="let key of p.headers.customRequestHeaders | keys">
|
||||
<td><span class="has-text-grey-light">{{ key }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customRequestHeaders[key] }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.customResponseHeaders">
|
||||
<h2>Custom Response Headers</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngFor="let key of p.headers.customResponseHeaders | keys">
|
||||
<td><span class="has-text-grey-light">{{ key }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customResponseHeaders[key] }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<h2>Secure</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngIf="p.headers.browserXssFilter">
|
||||
<td><span class="has-text-grey">Browser XSS Filter</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.contentSecurityPolicy">
|
||||
<td><span class="has-text-grey">Content Security Policy</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.contentSecurityPolicy }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.contentTypeNoSniff">
|
||||
<td><span class="has-text-grey">Content Type (No sniff)</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.contentTypeNoSniff }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.customFrameOptionsValue">
|
||||
<td><span class="has-text-grey">Custom Frame Options Value</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customFrameOptionsValue }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.forceSTSHeader">
|
||||
<td><span class="has-text-grey">Force STS Header</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.forceSTSHeader }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.frameDeny">
|
||||
<td><span class="has-text-grey">Frame Deny</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.frameDeny }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.isDevelopment">
|
||||
<td><span class="has-text-grey">Is Development</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.isDevelopment }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.publicKey">
|
||||
<td><span class="has-text-grey">Public Key</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.publicKey }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.referrerPolicy">
|
||||
<td><span class="has-text-grey">Referrer Policy</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.referrerPolicy }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslHost">
|
||||
<td><span class="has-text-grey">SSL Host</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslHost }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslRedirect">
|
||||
<td><span class="has-text-grey">SSL Redirect</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslRedirect }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslTemporaryRedirect">
|
||||
<td><span class="has-text-grey">SSL Temporary Redirect</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslTemporaryRedirect }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsIncludeSubdomains">
|
||||
<td><span class="has-text-grey">STS Include Subdomains</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsIncludeSubdomains }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsPreload">
|
||||
<td><span class="has-text-grey">STS Preload</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsPreload }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsSeconds">
|
||||
<td><span class="has-text-grey">STS Seconds</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsSeconds }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.allowedHosts">
|
||||
<h2>Allowed Hosts</h2>
|
||||
<div class="tags-list">
|
||||
<span class="tag is-light" *ngFor="let host of p.headers.allowedHosts">{{ host }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.sslProxyHeaders">
|
||||
<h2>SSL Proxy Headers</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngFor="let key of p.headers.sslProxyHeaders | keys">
|
||||
<td><span class="has-text-grey-light">{{ key }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslProxyHeaders[key] }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.hostsProxyHeaders">
|
||||
<h2>Hosts Proxy Headers</h2>
|
||||
<div class="tags-list">
|
||||
<span class="tag is-light" *ngFor="let h of p.headers.hostsProxyHeaders">{{ h }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backends -->
|
||||
<div class="column is-6">
|
||||
<h2 class="subtitle"><span class="tag is-primary">{{ providers[tab]?.backends.length }}</span> Backends</h2>
|
||||
<div class="message" *ngFor="let p of providers[tab]?.backends; let i = index;">
|
||||
<div class="message-header">
|
||||
<h2 [id]="p.id">
|
||||
<div>
|
||||
<i class="icon fas fa-server"></i>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-primary">{{ p.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
|
||||
<div class="tabs is-fullwidth is-small is-boxed">
|
||||
<ul>
|
||||
<li [class.is-active]="p.section !== 'details'" (click)="p.section = 'main'"><a>Main</a></li>
|
||||
<li [class.is-active]="p.section === 'details'" (click)="p.section = 'details'"><a>Details</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div *ngIf="p.section !== 'details'">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Server</td>
|
||||
<td>Weight</td>
|
||||
</tr>
|
||||
<tr *ngFor="let server of p.servers; let ri = index;">
|
||||
<td><a href="{{ server.url }}" title="{{ server.title }}">{{ server.url }}</a></td>
|
||||
<td><span class="has-text-grey">{{ server.weight }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div *ngIf="p.section === 'details'">
|
||||
|
||||
<div *ngIf="p.loadBalancer">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Load Balancer</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Method</span>
|
||||
<span class="tag is-info">{{ p.loadBalancer.method }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="tags has-addons" *ngIf="p.loadBalancer.stickiness || p.loadBalancer.sticky">
|
||||
<span class="tag is-light">Stickiness</span>
|
||||
<span class="tag is-info">true</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" *ngIf="p.loadBalancer.stickiness">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Cookie Name</span>
|
||||
<span class="tag is-info">{{ p.loadBalancer.stickiness.cookieName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.maxConn">
|
||||
<hr/>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Max Connections</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Amount</span>
|
||||
<span class="tag is-info">{{ p.maxConn.amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Extractor Function</span>
|
||||
<span class="tag is-info">{{ p.maxConn.extractorFunc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.circuitBreaker">
|
||||
<hr/>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Circuit Breaker</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Expression</span>
|
||||
<span class="tag is-info">{{ p.circuitBreaker.expression }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.healthCheck">
|
||||
<hr/>
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Health Check</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Path</span>
|
||||
<span class="tag is-info">{{ p.healthCheck.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" *ngIf="p.healthCheck.port">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Port</span>
|
||||
<span class="tag is-info">{{ p.healthCheck.port }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" *ngIf="p.healthCheck.interval">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Interval</span>
|
||||
<span class="tag is-info">{{ p.healthCheck.interval }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" *ngIf="p.healthCheck.hostname">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Hostname</span>
|
||||
<span class="tag is-info">{{ p.healthCheck.hostname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.buffering">
|
||||
<hr>
|
||||
<div class="columns list-title">
|
||||
<div class="column is-12">
|
||||
<h2>Buffering</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<span>Request Body Bytes</span>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Max</span>
|
||||
<span class="tag is-info">{{ p.buffering.maxRequestBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Men</span>
|
||||
<span class="tag is-info">{{ p.buffering.memRequestBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<span>Response Body Bytes</span>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Max</span>
|
||||
<span class="tag is-info">{{ p.buffering.maxResponseBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Men</span>
|
||||
<span class="tag is-info">{{ p.buffering.memResponseBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<span>Retry Expression</span>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<span class="tag is-info">{{ p.buffering.retryExpression }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns" *ngIf="!keys || !keys.length">
|
||||
<div class="column is-12">
|
||||
<div class="notification">
|
||||
No providers found.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
59
webui/src/app/components/providers/providers.component.ts
Normal file
59
webui/src/app/components/providers/providers.component.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import * as _ from "lodash";
|
||||
|
||||
@Component({
|
||||
selector: 'app-providers',
|
||||
templateUrl: 'providers.component.html'
|
||||
})
|
||||
export class ProvidersComponent implements OnInit, OnDestroy {
|
||||
sub: Subscription;
|
||||
keys: string[];
|
||||
data: any;
|
||||
previousData: any;
|
||||
providers: any;
|
||||
tab: string;
|
||||
keyword: string;
|
||||
|
||||
constructor(private apiService: ApiService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.keyword = '';
|
||||
this.sub = Observable.timer(0, 2000)
|
||||
.timeInterval()
|
||||
.mergeMap(() => this.apiService.fetchProviders())
|
||||
.subscribe(data => {
|
||||
if (!_.isEqual(this.previousData, data)) {
|
||||
this.previousData = _.cloneDeep(data);
|
||||
this.data = data;
|
||||
this.providers = data;
|
||||
this.keys = Object.keys(this.providers);
|
||||
this.tab = this.keys[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filter(): void {
|
||||
const keyword = this.keyword.toLowerCase();
|
||||
this.providers = Object.keys(this.data)
|
||||
.filter(value => value !== 'acme' && value !== 'ACME')
|
||||
.reduce((acc, curr) => {
|
||||
return Object.assign(acc, {
|
||||
[curr]: {
|
||||
backends: this.data[curr].backends.filter(d => d.id.toLowerCase().includes(keyword)),
|
||||
frontends: this.data[curr].frontends.filter(d => {
|
||||
return d.id.toLowerCase().includes(keyword) || d.backend.toLowerCase().includes(keyword);
|
||||
})
|
||||
}
|
||||
});
|
||||
}, {});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
|
||||
var traefikCoreHealth = 'traefik.core.health';
|
||||
module.exports = traefikCoreHealth;
|
||||
|
||||
angular
|
||||
.module(traefikCoreHealth, ['ngResource'])
|
||||
.factory('Health', Health);
|
||||
|
||||
/** @ngInject */
|
||||
function Health($resource) {
|
||||
return $resource('../health');
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
|
||||
var traefikCoreProvider = 'traefik.core.provider';
|
||||
module.exports = traefikCoreProvider;
|
||||
|
||||
angular
|
||||
.module(traefikCoreProvider, ['ngResource'])
|
||||
.factory('Providers', Providers);
|
||||
|
||||
/** @ngInject */
|
||||
function Providers($resource, $q) {
|
||||
const resourceProvider = $resource('../api/providers');
|
||||
return {
|
||||
get: function () {
|
||||
return $q((resolve, reject) => {
|
||||
resourceProvider.get()
|
||||
.$promise
|
||||
.then((rawProviders) => {
|
||||
delete rawProviders.acme;
|
||||
delete rawProviders.ACME;
|
||||
|
||||
for (let providerName in rawProviders) {
|
||||
if (rawProviders.hasOwnProperty(providerName)) {
|
||||
if (!providerName.startsWith('$')) {
|
||||
// BackEnds mapping
|
||||
let bckends = rawProviders[providerName].backends || {};
|
||||
rawProviders[providerName].backends = Object.keys(bckends)
|
||||
.map(key => {
|
||||
const goodBackend = bckends[key];
|
||||
goodBackend.backendId = key;
|
||||
return goodBackend;
|
||||
});
|
||||
|
||||
// FrontEnds mapping
|
||||
let frtends = rawProviders[providerName].frontends || {};
|
||||
rawProviders[providerName].frontends = Object.keys(frtends)
|
||||
.map(key => {
|
||||
const goodFrontend = frtends[key];
|
||||
goodFrontend.frontendId = key;
|
||||
return goodFrontend;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(rawProviders);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
|
||||
var traefikCoreVersion = 'traefik.core.version';
|
||||
module.exports = traefikCoreVersion;
|
||||
|
||||
angular
|
||||
.module(traefikCoreVersion, ['ngResource'])
|
||||
.factory('Version', Version);
|
||||
|
||||
/** @ngInject */
|
||||
function Version($resource) {
|
||||
return $resource('../api/version');
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* If you want to override some bootstrap variables, you have to change values here.
|
||||
* The list of variables are listed here bower_components/bootstrap-sass/assets/stylesheets/bootstrap/_variables.scss
|
||||
*/
|
||||
$navbar-inverse-link-color: #5AADBB;
|
||||
|
||||
/**
|
||||
* Do not remove the comments below. It's the markers used by wiredep to inject
|
||||
* sass dependencies when defined in the bower.json of your dependencies
|
||||
*/
|
||||
// bower:scss
|
||||
// endbower
|
||||
|
||||
.browsehappy {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
height: 200px;
|
||||
|
||||
img.pull-right {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not remove the comments below. It's the markers used by gulp-inject to inject
|
||||
* all your sass files automatically
|
||||
*/
|
||||
// injector
|
||||
// endinjector
|
8
webui/src/app/pipes/keys.pipe.ts
Normal file
8
webui/src/app/pipes/keys.pipe.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { PipeTransform, Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'keys' })
|
||||
export class KeysPipe implements PipeTransform {
|
||||
transform(value, args: string[]): any {
|
||||
return Object.keys(value);
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
'use strict';
|
||||
var d3 = require('d3'),
|
||||
moment = require('moment'),
|
||||
HttpStatus = require('http-status-codes');
|
||||
|
||||
/** @ngInject */
|
||||
function HealthController($scope, $interval, $log, Health) {
|
||||
|
||||
var vm = this;
|
||||
|
||||
vm.graph = {
|
||||
averageResponseTime: {},
|
||||
totalStatusCodeCount: {}
|
||||
};
|
||||
|
||||
vm.graph.totalStatusCodeCount.options = {
|
||||
"chart": {
|
||||
type: 'discreteBarChart',
|
||||
tooltip: {
|
||||
contentGenerator: function (e) {
|
||||
var d = e.data;
|
||||
return d.label + " " + d.text;
|
||||
}
|
||||
},
|
||||
height: 200,
|
||||
margin: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 40,
|
||||
left: 55
|
||||
},
|
||||
x: function (d) {
|
||||
return d.label;
|
||||
},
|
||||
y: function (d) {
|
||||
return d.value;
|
||||
},
|
||||
showValues: true,
|
||||
valueFormat: function (d) {
|
||||
return d3.format('d')(d);
|
||||
},
|
||||
yAxis: {
|
||||
axisLabelDistance: 30,
|
||||
tickFormat: d3.format('d')
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"enable": true,
|
||||
"text": "Total Status Code Count",
|
||||
"css": {
|
||||
"textAlign": "center"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vm.graph.totalStatusCodeCount.data = [
|
||||
{
|
||||
key: "Total Status Code Count",
|
||||
values: [
|
||||
{
|
||||
"label": "200",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Update Total Status Code Count graph
|
||||
*
|
||||
* @param {Object} totalStatusCodeCount Object from API
|
||||
*/
|
||||
function updateTotalStatusCodeCount(totalStatusCodeCount) {
|
||||
|
||||
// extract values
|
||||
vm.graph.totalStatusCodeCount.data[0].values = [];
|
||||
for (var code in totalStatusCodeCount) {
|
||||
if (totalStatusCodeCount.hasOwnProperty(code)) {
|
||||
var statusCodeText = "";
|
||||
try {
|
||||
statusCodeText = HttpStatus.getStatusText(code);
|
||||
} catch (e) {
|
||||
// HttpStatus.getStatusText throws error on unknown codes
|
||||
statusCodeText = "Unknown status code";
|
||||
}
|
||||
vm.graph.totalStatusCodeCount.data[0].values.push({
|
||||
label: code,
|
||||
value: totalStatusCodeCount[code],
|
||||
text: statusCodeText
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update Total Status Code Count graph render
|
||||
if (vm.graph.totalStatusCodeCount.api) {
|
||||
vm.graph.totalStatusCodeCount.api.update();
|
||||
} else {
|
||||
$log.error('fail');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vm.graph.averageResponseTime.options = {
|
||||
chart: {
|
||||
type: 'lineChart',
|
||||
height: 200,
|
||||
margin: {
|
||||
top: 20,
|
||||
right: 40,
|
||||
bottom: 40,
|
||||
left: 55
|
||||
},
|
||||
x: function (d) {
|
||||
return d.x;
|
||||
},
|
||||
y: function (d) {
|
||||
return d.y;
|
||||
},
|
||||
useInteractiveGuideline: true,
|
||||
xAxis: {
|
||||
tickFormat: function (d) {
|
||||
return d3.time.format('%X')(new Date(d));
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
tickFormat: function (d) {
|
||||
return d3.format(',.1f')(d);
|
||||
}
|
||||
},
|
||||
forceY: [0., 1.], // This prevents the chart from showing -1 on Oy when all the input data points
|
||||
// have y = 0. It won't disable the automatic adjustment of the max value.
|
||||
duration: 0 // Bug: Markers will not be drawn if you set this to some other value...
|
||||
},
|
||||
"title": {
|
||||
"enable": true,
|
||||
"text": "Average response time",
|
||||
"css": {
|
||||
"textAlign": "center"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var initialPoint = {
|
||||
x: Date.now() - 3000,
|
||||
y: 0
|
||||
};
|
||||
vm.graph.averageResponseTime.data = [
|
||||
{
|
||||
values: [initialPoint],
|
||||
key: 'Average response time (ms)',
|
||||
type: 'line',
|
||||
color: '#2ca02c'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Update average response time graph
|
||||
*
|
||||
* @param {Number} x Coordinate X
|
||||
* @param {Number} y Coordinate Y
|
||||
*/
|
||||
function updateAverageResponseTimeGraph(x, y) {
|
||||
|
||||
// x multiply 1000 by because unix time is in seconds and JS Date are in milliseconds
|
||||
var data = {
|
||||
x: x * 1000,
|
||||
y: y * 1000
|
||||
};
|
||||
vm.graph.averageResponseTime.data[0].values.push(data);
|
||||
// limit graph entries
|
||||
if (vm.graph.averageResponseTime.data[0].values.length > 100) {
|
||||
vm.graph.averageResponseTime.data[0].values.shift();
|
||||
}
|
||||
|
||||
// Update Average Response Time graph render
|
||||
if (vm.graph.averageResponseTime.api) {
|
||||
vm.graph.averageResponseTime.api.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the timestamp as "x seconds ago", etc.
|
||||
*
|
||||
* @param {String} t Timestamp returned from the API
|
||||
*/
|
||||
function formatTimestamp(t) {
|
||||
return moment(t, "YYYY-MM-DDTHH:mm:ssZ").fromNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all graph's datas
|
||||
*
|
||||
* @param {Object} health Health data from server
|
||||
*/
|
||||
function loadData(health) {
|
||||
// Load datas and update Average Response Time graph render
|
||||
updateAverageResponseTimeGraph(health.unixtime, health.average_response_time_sec);
|
||||
|
||||
// Load datas and update Total Status Code Count graph render
|
||||
updateTotalStatusCodeCount(health.total_status_code_count);
|
||||
|
||||
// Format the timestamps
|
||||
if (health.recent_errors) {
|
||||
angular.forEach(health.recent_errors, function(i) {
|
||||
i.time_formatted = formatTimestamp(i.time);
|
||||
});
|
||||
}
|
||||
|
||||
// set data's view
|
||||
vm.health = health;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action when load datas failed
|
||||
*
|
||||
* @param {Object} error Error state object
|
||||
*/
|
||||
function erroData(error) {
|
||||
vm.health = {};
|
||||
$log.error(error);
|
||||
}
|
||||
|
||||
// first load
|
||||
Health.get(loadData, erroData);
|
||||
|
||||
// Auto refresh data
|
||||
var intervalId = $interval(function () {
|
||||
Health.get(loadData, erroData);
|
||||
}, 3000);
|
||||
|
||||
// Stop auto refresh when page change
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(intervalId);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports = HealthController;
|
|
@ -1,73 +0,0 @@
|
|||
<div>
|
||||
<h1 class="text-danger">
|
||||
<span class="glyphicon glyphicon-heart" aria-hidden="true"></span> Health
|
||||
</h1>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<nvd3 options="healthCtrl.graph.averageResponseTime.options" data="healthCtrl.graph.averageResponseTime.data" api="healthCtrl.graph.averageResponseTime.api"></nvd3>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span>Total response time :</span><span class="badge">{{healthCtrl.health.total_response_time}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span>PID :</span><span class="badge">{{healthCtrl.health.pid}}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Uptime :</span><span class="badge">{{healthCtrl.health.uptime}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<nvd3 options="healthCtrl.graph.totalStatusCodeCount.options" data="healthCtrl.graph.totalStatusCodeCount.data" api="healthCtrl.graph.totalStatusCodeCount.api"></nvd3>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<span>Total count :</span><span class="badge">{{healthCtrl.health.total_count}}</span>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span>Count :</span><span class="badge">{{healthCtrl.health.count}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-if="healthCtrl.health.recent_errors">
|
||||
<h3>Recent HTTP Errors</h3>
|
||||
<table class="table table-striped table-bordered">
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Request</td>
|
||||
<td>Time</td>
|
||||
</tr>
|
||||
<tr ng-repeat="entry in healthCtrl.health.recent_errors"
|
||||
ng-class="{'text-danger': entry.status_code >= 500}">
|
||||
<td>{{ entry.status_code }} — {{ entry.status }}</td>
|
||||
<td>
|
||||
<span class="badge">{{ entry.method }}</span>
|
||||
|
||||
{{ entry.host }}{{ entry.path }}
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{ entry.time }}">
|
||||
{{ entry.time_formatted }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="healthCtrl.health.recent_errors.length == 0">
|
||||
<td colspan="3">
|
||||
<p class="text-muted text-center">No entries</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,24 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
var traefikCoreHealth = require('../../core/health.resource');
|
||||
var HealthController = require('./health.controller');
|
||||
|
||||
var traefikSectionHealth = 'traefik.section.health';
|
||||
module.exports = traefikSectionHealth;
|
||||
|
||||
angular
|
||||
.module(traefikSectionHealth, [traefikCoreHealth])
|
||||
.controller('HealthController', HealthController)
|
||||
.config(config);
|
||||
|
||||
/** @ngInject */
|
||||
function config($stateProvider) {
|
||||
|
||||
$stateProvider.state('health', {
|
||||
url: '/health',
|
||||
template: require('./health.html'),
|
||||
controller: 'HealthController',
|
||||
controllerAs: 'healthCtrl'
|
||||
});
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
function backendMonitor() {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
template: require('./backend-monitor.html'),
|
||||
controller: BackendMonitorController,
|
||||
controllerAs: 'backendCtrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
backend: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function BackendMonitorController() {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
module.exports = backendMonitor;
|
|
@ -1,23 +0,0 @@
|
|||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<strong><span class="glyphicon glyphicon-tasks" aria-hidden="true"></span> {{backendCtrl.backend.backendId}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="panel-table__servers table table-striped table-hover">
|
||||
<tr>
|
||||
<td><em>Server</em></td>
|
||||
<td><em>URL</em></td>
|
||||
<td><em>Weight</em></td>
|
||||
</tr>
|
||||
<tr data-ng-repeat="(serverId, server) in backendCtrl.backend.servers">
|
||||
<td>{{serverId}}</td>
|
||||
<td><code><a data-ng-href="{{server.url}}">{{server.url}}</a></code></td>
|
||||
<td>{{server.weight}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer" data-ng-show="backendCtrl.backend.loadBalancer || backendCtrl.backend.circuitBreaker">
|
||||
<span data-ng-show="backendCtrl.backend.loadBalancer" class="label label-success">Load Balancer: {{backendCtrl.backend.loadBalancer.method}}</span>
|
||||
<span data-ng-show="backendCtrl.backend.circuitBreaker" class="label label-success">Circuit Breaker: {{backendCtrl.backend.circuitBreaker.expression}}</span>
|
||||
</div>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
var backendMonitor = require('./backend-monitor.directive');
|
||||
|
||||
var traefikBackendMonitor = 'traefik.section.providers.backend-monitor';
|
||||
module.exports = traefikBackendMonitor;
|
||||
|
||||
angular
|
||||
.module(traefikBackendMonitor, [])
|
||||
.directive('backendMonitor', backendMonitor);
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
function frontendMonitor() {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
template: require('./frontend-monitor.html'),
|
||||
controller: FrontendMonitorController,
|
||||
controllerAs: 'frontendCtrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
frontend: '='
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function FrontendMonitorController() {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
module.exports = frontendMonitor;
|
|
@ -1,29 +0,0 @@
|
|||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<strong><span class="glyphicon glyphicon-globe" aria-hidden="true"></span> {{frontendCtrl.frontend.frontendId}}</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="panel-table__routes table table-striped table-hover">
|
||||
<tr>
|
||||
<td><em>Route</em></td>
|
||||
<td><em>Rule</em></td>
|
||||
</tr>
|
||||
<tr data-ng-repeat="(routeId, route) in frontendCtrl.frontend.routes">
|
||||
<td>{{routeId}}</td>
|
||||
<td><code>{{route.rule}}</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer">
|
||||
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints">
|
||||
<span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last"> </span>
|
||||
</span>
|
||||
<span data-ng-show="frontendCtrl.frontend.redirect" class="label label-success">Redirect to {{frontendCtrl.frontend.redirect}}</span>
|
||||
<span class="label label-warning" role="button" data-toggle="collapse" href="#{{frontendCtrl.frontend.backend}}" aria-expanded="false">Backend:{{frontendCtrl.frontend.backend}}</span>
|
||||
<span data-ng-show="frontendCtrl.frontend.passHostHeader" class="label label-warning">PassHostHeader</span>
|
||||
<span data-ng-repeat="whitelistSourceRange in frontendCtrl.frontend.whitelistSourceRange">
|
||||
<span class="label label-warning">Whitelist {{ whitelistSourceRange }}</span>
|
||||
</span>
|
||||
<span data-ng-show="frontendCtrl.frontend.priority" class="label label-warning">Priority:{{frontendCtrl.frontend.priority}}</span>
|
||||
</div>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
var frontendMonitor = require('./frontend-monitor.directive');
|
||||
|
||||
var traefikFrontendMonitor = 'traefik.section.providers.frontend-monitor';
|
||||
module.exports = traefikFrontendMonitor;
|
||||
|
||||
angular
|
||||
.module(traefikFrontendMonitor, [])
|
||||
.directive('frontendMonitor', frontendMonitor);
|
|
@ -1,33 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
/** @ngInject */
|
||||
function ProvidersController($scope, $interval, $log, Providers) {
|
||||
const vm = this;
|
||||
|
||||
function loadProviders() {
|
||||
Providers
|
||||
.get()
|
||||
.then(providers => {
|
||||
if (!_.isEqual(vm.previousProviders, providers)) {
|
||||
vm.providers = providers;
|
||||
vm.previousProviders = _.cloneDeep(providers);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
vm.providers = {};
|
||||
$log.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
loadProviders();
|
||||
|
||||
const intervalId = $interval(loadProviders, 2000);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(intervalId);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = ProvidersController;
|
|
@ -1,22 +0,0 @@
|
|||
<div>
|
||||
<div><input type="text" data-ng-model="providersCtrl.providerFilter" placeholder="Filter" class="form-control"></div>
|
||||
<br>
|
||||
<uib-tabset>
|
||||
<uib-tab data-ng-repeat="(providerId, provider) in providersCtrl.providers" heading="{{providerId}}">
|
||||
|
||||
<div class="row tabset-row__providers">
|
||||
<div class="col-md-6">
|
||||
<div data-ng-repeat="frontend in provider.frontends | filter: providersCtrl.providerFilter ">
|
||||
<frontend-monitor data-provider-id="providerId" data-frontend="frontend"></frontend-monitor>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div data-ng-repeat="backend in provider.backends | filter: providersCtrl.providerFilter">
|
||||
<backend-monitor data-provider-id="providerId" data-backend="backend"></backend-monitor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</div>
|
|
@ -1,30 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
var traefikCoreProvider = require('../../core/providers.resource');
|
||||
var ProvidersController = require('./providers.controller');
|
||||
var traefikBackendMonitor = require('./backend-monitor/backend-monitor.module');
|
||||
var traefikFrontendMonitor = require('./frontend-monitor/frontend-monitor.module');
|
||||
|
||||
var traefikSectionProviders = 'traefik.section.providers';
|
||||
module.exports = traefikSectionProviders;
|
||||
|
||||
angular
|
||||
.module(traefikSectionProviders, [
|
||||
traefikCoreProvider,
|
||||
traefikBackendMonitor,
|
||||
traefikFrontendMonitor
|
||||
])
|
||||
.config(config)
|
||||
.controller('ProvidersController', ProvidersController);
|
||||
|
||||
/** @ngInject */
|
||||
function config($stateProvider) {
|
||||
|
||||
$stateProvider.state('provider', {
|
||||
url: '/',
|
||||
template: require('./providers.html'),
|
||||
controller: 'ProvidersController',
|
||||
controllerAs: 'providersCtrl'
|
||||
});
|
||||
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
require('nvd3');
|
||||
var ndv3 = require('angular-nvd3');
|
||||
var traefikSectionHealth = require('./health/health.module');
|
||||
var traefikSectionProviders = require('./providers/providers.module');
|
||||
|
||||
var traefikSection = 'traefik.section';
|
||||
module.exports = traefikSection;
|
||||
|
||||
angular
|
||||
.module(traefikSection, [
|
||||
'ui.router',
|
||||
'ui.bootstrap',
|
||||
ndv3,
|
||||
traefikSectionProviders,
|
||||
traefikSectionHealth
|
||||
])
|
||||
.config(config);
|
||||
|
||||
/** @ngInject */
|
||||
function config($urlRouterProvider) {
|
||||
$urlRouterProvider.otherwise('/');
|
||||
}
|
88
webui/src/app/services/api.service.ts
Normal file
88
webui/src/app/services/api.service.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/observable/empty';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/retry';
|
||||
|
||||
export interface ProviderType {
|
||||
[provider: string]: {
|
||||
backends: any;
|
||||
frontends: any;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiService {
|
||||
headers: HttpHeaders;
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.headers = new HttpHeaders({
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
}
|
||||
|
||||
fetchVersion(): Observable<any> {
|
||||
return this.http.get(`/api/version`, { headers: this.headers })
|
||||
.retry(4)
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(`[version] returned code ${err.status}, body was: ${err.error}`);
|
||||
return Observable.empty<any>();
|
||||
});
|
||||
}
|
||||
|
||||
fetchHealthStatus(): Observable<any> {
|
||||
return this.http.get(`/health`, { headers: this.headers })
|
||||
.retry(2)
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(`[health] returned code ${err.status}, body was: ${err.error}`);
|
||||
return Observable.empty<any>();
|
||||
});
|
||||
}
|
||||
|
||||
fetchProviders(): Observable<any> {
|
||||
return this.http.get(`/api/providers`, { headers: this.headers })
|
||||
.retry(2)
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(`[providers] returned code ${err.status}, body was: ${err.error}`);
|
||||
return Observable.of<any>({});
|
||||
})
|
||||
.map(this.parseProviders);
|
||||
}
|
||||
|
||||
parseProviders(data: any): ProviderType {
|
||||
return Object.keys(data)
|
||||
.filter(value => value !== 'acme' && value !== 'ACME')
|
||||
.reduce((acc, curr) => {
|
||||
acc[curr] = {
|
||||
backends: Object.keys(data[curr].backends || {}).map(key => {
|
||||
data[curr].backends[key].id = key;
|
||||
data[curr].backends[key].servers = Object.keys(data[curr].backends[key].servers || {}).map(server => {
|
||||
return {
|
||||
title: server,
|
||||
url: data[curr].backends[key].servers[server].url,
|
||||
weight: data[curr].backends[key].servers[server].weight
|
||||
};
|
||||
});
|
||||
|
||||
return data[curr].backends[key];
|
||||
}),
|
||||
frontends: Object.keys(data[curr].frontends || {}).map(key => {
|
||||
data[curr].frontends[key].id = key;
|
||||
data[curr].frontends[key].routes = Object.keys(data[curr].frontends[key].routes || {}).map(route => {
|
||||
return {
|
||||
title: route,
|
||||
rule: data[curr].frontends[key].routes[route].rule
|
||||
};
|
||||
});
|
||||
|
||||
return data[curr].frontends[key];
|
||||
}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
17
webui/src/app/services/window.service.ts
Normal file
17
webui/src/app/services/window.service.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { EventManager } from '@angular/platform-browser';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
@Injectable()
|
||||
export class WindowService {
|
||||
resize: Subject<any>;
|
||||
|
||||
constructor(private eventManager: EventManager) {
|
||||
this.resize = new Subject();
|
||||
this.eventManager.addGlobalEventListener('window', 'resize', this.onResize);
|
||||
}
|
||||
|
||||
onResize = (event: UIEvent) => {
|
||||
this.resize.next(event.target);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'charterregular';
|
||||
src: url('./assets/fonts/charter_regular-webfont.eot');
|
||||
src: url('./assets/fonts/charter_regular-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('./assets/fonts/charter_regular-webfont.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.traefik-blue {
|
||||
color: #00B1FF;
|
||||
}
|
||||
|
||||
.traefik-text {
|
||||
font-family: 'charterregular', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.panel-body .panel-table__servers,
|
||||
.panel-body .panel-table__routes {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tabset-row__providers {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
td, th {
|
||||
word-wrap: break-word;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/** @ngInject */
|
||||
function VersionController($scope, Version) {
|
||||
Version.get(function (version) {
|
||||
$scope.version = version;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = VersionController;
|
|
@ -1,11 +0,0 @@
|
|||
'use strict';
|
||||
var angular = require('angular');
|
||||
var traefikCoreVersion = require('../core/version.resource');
|
||||
var VersionController = require('./version.controller');
|
||||
|
||||
var traefikVersion = 'traefik.version';
|
||||
module.exports = traefikVersion;
|
||||
|
||||
angular
|
||||
.module(traefikVersion, [traefikCoreVersion])
|
||||
.controller('VersionController', VersionController);
|
Loading…
Add table
Add a link
Reference in a new issue