Fix webui

This commit is contained in:
Ludovic Fernandez 2018-05-14 19:46:03 +02:00 committed by Traefiker Bot
parent 67847c3117
commit b72937e8fb
28 changed files with 696 additions and 610 deletions

View file

@ -8,7 +8,7 @@
"root": "src", "root": "src",
"outDir": "dist", "outDir": "dist",
"assets": [ "assets": [
"assets", "assets/images",
"favicon.ico" "favicon.ico"
], ],
"index": "index.html", "index": "index.html",
@ -19,7 +19,7 @@
"testTsconfig": "tsconfig.spec.json", "testTsconfig": "tsconfig.spec.json",
"prefix": "app", "prefix": "app",
"styles": [ "styles": [
"styles/app.sass" "app.sass"
], ],
"scripts": [ "scripts": [
"../node_modules/@fortawesome/fontawesome/index.js", "../node_modules/@fortawesome/fontawesome/index.js",

View file

@ -27,7 +27,7 @@
"@angular/router": "^5.2.0", "@angular/router": "^5.2.0",
"@fortawesome/fontawesome": "^1.1.5", "@fortawesome/fontawesome": "^1.1.5",
"@fortawesome/fontawesome-free-solid": "^5.0.10", "@fortawesome/fontawesome-free-solid": "^5.0.10",
"bulma": "^0.6.2", "bulma": "^0.7.0",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"d3": "^4.13.0", "d3": "^4.13.0",
"date-fns": "^1.29.0", "date-fns": "^1.29.0",

27
webui/src/app.sass Normal file
View file

@ -0,0 +1,27 @@
@charset "utf-8"
@import 'styles/typography'
@import 'styles/variables'
@import 'styles/colors'
@import '~bulma/sass/utilities/all'
@import '~bulma/sass/base/all'
@import '~bulma/sass/grid/all'
@import '~bulma/sass/elements/container'
@import '~bulma/sass/elements/tag'
@import '~bulma/sass/elements/other'
@import '~bulma/sass/elements/box'
@import '~bulma/sass/elements/form'
@import '~bulma/sass/elements/table'
@import '~bulma/sass/components/navbar'
@import '~bulma/sass/components/tabs'
@import '~bulma/sass/elements/notification'
@import 'styles/nav'
@import 'styles/content'
@import 'styles/message'
@import 'styles/charts'
@import 'styles/helper'
html
font-family: $open-sans
height: 100%
background: $background

View file

@ -1,4 +1,4 @@
import { TestBed, async } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {

View file

@ -1,18 +1,21 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { BarChartComponent } from './charts/bar-chart/bar-chart.component';
import { LineChartComponent } from './charts/line-chart/line-chart.component';
import { HeaderComponent } from './components/header/header.component';
import { HealthComponent } from './components/health/health.component';
import { ProvidersComponent } from './components/providers/providers.component';
import { LetDirective } from './directives/let.directive';
import { BackendFilterPipe } from './pipes/backend.filter.pipe';
import { FrontendFilterPipe } from './pipes/frontend.filter.pipe';
import { KeysPipe } from './pipes/keys.pipe';
import { ApiService } from './services/api.service'; import { ApiService } from './services/api.service';
import { WindowService } from './services/window.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({ @NgModule({
declarations: [ declarations: [
@ -22,7 +25,10 @@ import { KeysPipe } from './pipes/keys.pipe';
HealthComponent, HealthComponent,
LineChartComponent, LineChartComponent,
BarChartComponent, BarChartComponent,
KeysPipe KeysPipe,
FrontendFilterPipe,
BackendFilterPipe,
LetDirective
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -30,8 +36,8 @@ import { KeysPipe } from './pipes/keys.pipe';
HttpClientModule, HttpClientModule,
FormsModule, FormsModule,
RouterModule.forRoot([ RouterModule.forRoot([
{ path: '', component: ProvidersComponent, pathMatch: 'full' }, {path: '', component: ProvidersComponent, pathMatch: 'full'},
{ path: 'status', component: HealthComponent } {path: 'status', component: HealthComponent}
]) ])
], ],
providers: [ providers: [

View file

@ -1,15 +1,7 @@
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core'; import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { axisBottom, axisLeft, easeLinear, max, min, scaleBand, scaleLinear, select } from 'd3';
import * as _ from 'lodash';
import { WindowService } from '../../services/window.service'; import { WindowService } from '../../services/window.service';
import {
min,
max,
easeLinear,
select,
axisLeft,
axisBottom,
scaleBand,
scaleLinear
} from 'd3';
@Component({ @Component({
selector: 'app-bar-chart', selector: 'app-bar-chart',
@ -23,12 +15,12 @@ export class BarChartComponent implements OnInit, OnChanges {
x: any; x: any;
y: any; y: any;
g: any; g: any;
bars: any;
width: number; width: number;
height: number; height: number;
margin = { top: 40, right: 40, bottom: 40, left: 40 }; margin = {top: 40, right: 40, bottom: 40, left: 40};
loading: boolean; loading: boolean;
data: any[]; data: any[];
previousData: any[];
constructor(public elementRef: ElementRef, public windowService: WindowService) { constructor(public elementRef: ElementRef, public windowService: WindowService) {
this.loading = true; this.loading = true;
@ -37,7 +29,7 @@ export class BarChartComponent implements OnInit, OnChanges {
ngOnInit() { ngOnInit() {
this.barChartEl = this.elementRef.nativeElement.querySelector('.bar-chart'); this.barChartEl = this.elementRef.nativeElement.querySelector('.bar-chart');
this.setup(); this.setup();
setTimeout(() => this.loading = false, 4000); setTimeout(() => this.loading = false, 1000);
this.windowService.resize.subscribe(w => this.draw()); this.windowService.resize.subscribe(w => this.draw());
} }
@ -47,15 +39,20 @@ export class BarChartComponent implements OnInit, OnChanges {
return; return;
} }
this.data = this.value; if (!_.isEqual(this.previousData, this.value)) {
this.draw(); this.previousData = _.cloneDeep(this.value);
this.data = this.value;
this.draw();
}
} }
setup(): void { setup(): void {
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right; this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom; this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
this.svg = select(this.barChartEl).append('svg') this.svg = select(this.barChartEl)
.append('svg')
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom); .attr('height', this.height + this.margin.top + this.margin.bottom);
@ -73,11 +70,16 @@ export class BarChartComponent implements OnInit, OnChanges {
} }
draw(): void { draw(): void {
if (this.barChartEl.clientWidth === 0 || this.barChartEl.clientHeight === 0) {
this.previousData = [];
} else {
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
}
this.x.domain(this.data.map((d: any) => d.code)); this.x.domain(this.data.map((d: any) => d.code));
this.y.domain([0, max(this.data, (d: any) => d.count)]); 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 this.svg
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
@ -93,17 +95,16 @@ export class BarChartComponent implements OnInit, OnChanges {
this.g.select('.axis--y') this.g.select('.axis--y')
.call(axisLeft(this.y).tickSize(-this.width)); .call(axisLeft(this.y).tickSize(-this.width));
// Clean previous graph
this.g.selectAll('.bar').remove();
const bars = this.g.selectAll('.bar').data(this.data); const bars = this.g.selectAll('.bar').data(this.data);
bars.enter() bars.enter()
.append('rect') .append('rect')
.attr('class', 'bar') .attr('class', 'bar')
.attr('x', (d: any) => d.code) .style('fill', (d: any) => 'hsl(' + Math.floor(((d.code - 100) * 310 / 427) + 50) + ', 50%, 50%)')
.attr('y', (d: any) => d.count) .attr('x', (d: any) => this.x(d.code))
.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('y', (d: any) => this.y(d.count))
.attr('width', this.x.bandwidth()) .attr('width', this.x.bandwidth())
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count)); .attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));

View file

@ -1,5 +1,5 @@
<div class="line-chart" [class.is-hidden]="loading"></div> <div class="line-chart" [class.is-hidden]="loading"></div>
<div class="loading-text" [class.is-hidden]="!loading"> <div class="loading-text line-chart-loading" [class.is-hidden]="!loading">
<span> <span>
<span>Loading, please wait...</span> <span>Loading, please wait...</span>
<img src="./assets/images/loader.svg" class="main-loader"> <img src="./assets/images/loader.svg" class="main-loader">

View file

@ -1,20 +1,20 @@
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core'; import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { WindowService } from '../../services/window.service';
import { import {
range,
scaleTime,
scaleLinear,
min,
max,
curveLinear,
line,
easeLinear,
select,
axisLeft,
axisBottom, axisBottom,
timeSecond, axisLeft,
timeFormat curveLinear,
easeLinear,
line,
max,
min,
range,
scaleLinear,
scaleTime,
select,
timeFormat,
timeSecond
} from 'd3'; } from 'd3';
import { WindowService } from '../../services/window.service';
@Component({ @Component({
selector: 'app-line-chart', selector: 'app-line-chart',
@ -23,7 +23,10 @@ import {
export class LineChartComponent implements OnChanges, OnInit { export class LineChartComponent implements OnChanges, OnInit {
@Input() value: { count: number, date: string }; @Input() value: { count: number, date: string };
firstDisplay: boolean;
dirty: boolean;
lineChartEl: HTMLElement; lineChartEl: HTMLElement;
loadingEl: HTMLElement;
svg: any; svg: any;
g: any; g: any;
line: any; line: any;
@ -39,15 +42,19 @@ export class LineChartComponent implements OnChanges, OnInit {
yAxis: any; yAxis: any;
height: number; height: number;
width: number; width: number;
margin = { top: 40, right: 40, bottom: 60, left: 60 }; margin = {top: 40, right: 40, bottom: 60, left: 60};
loading = true; loading = true;
constructor(private elementRef: ElementRef, public windowService: WindowService) { } constructor(private elementRef: ElementRef, public windowService: WindowService) { }
ngOnInit() { ngOnInit() {
this.lineChartEl = this.elementRef.nativeElement.querySelector('.line-chart'); this.lineChartEl = this.elementRef.nativeElement.querySelector('.line-chart');
this.loadingEl = this.elementRef.nativeElement.querySelector('.line-chart-loading');
this.limit = 40; this.limit = 40;
// related to the Observable.timer(0, 3000) in health component
this.duration = 3000; this.duration = 3000;
this.now = new Date(Date.now() - this.duration); this.now = new Date(Date.now() - this.duration);
this.options = { this.options = {
@ -55,22 +62,37 @@ export class LineChartComponent implements OnChanges, OnInit {
color: '#3A84C5' color: '#3A84C5'
}; };
this.firstDisplay = true;
this.render(); this.render();
setTimeout(() => this.loading = false, 4000);
this.windowService.resize.subscribe(w => { this.windowService.resize.subscribe(w => {
if (this.svg) { if (this.svg) {
const el = this.lineChartEl.querySelector('svg'); this.dirty = true;
el.parentNode.removeChild(el); this.loading = true;
this.render(); this.render();
} }
}); });
} }
render() { render() {
this.width = this.lineChartEl.clientWidth - this.margin.left - this.margin.right; // When the lineChartEl is not displayed (is-hidden), width and length are equal to 0.
this.height = this.lineChartEl.clientHeight - this.margin.top - this.margin.bottom; let elt;
if (this.lineChartEl.clientWidth === 0 || this.lineChartEl.clientHeight === 0) {
elt = this.loadingEl;
} else {
elt = this.lineChartEl;
}
this.width = elt.clientWidth - this.margin.left - this.margin.right;
this.height = elt.clientHeight - this.margin.top - this.margin.bottom;
this.svg = select(this.lineChartEl).append('svg')
const el = this.lineChartEl.querySelector('svg');
if (el) {
el.parentNode.removeChild(el);
}
this.svg = select(this.lineChartEl)
.append('svg')
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom) .attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g') .append('g')
@ -80,7 +102,7 @@ export class LineChartComponent implements OnChanges, OnInit {
this.data = range(this.limit).map(i => 0); this.data = range(this.limit).map(i => 0);
} }
this.x = scaleTime().range([0, this.width]); this.x = scaleTime().range([0, this.width - 10]);
this.y = scaleLinear().range([this.height, 0]); this.y = scaleLinear().range([this.height, 0]);
this.x.domain([<any>this.now - (this.limit - 2), <any>this.now - this.duration]); this.x.domain([<any>this.now - (this.limit - 2), <any>this.now - this.duration]);
@ -91,7 +113,9 @@ export class LineChartComponent implements OnChanges, OnInit {
.y((d: any) => this.y(d)) .y((d: any) => this.y(d))
.curve(curveLinear); .curve(curveLinear);
this.svg.append('defs').append('clipPath') this.svg
.append('defs')
.append('clipPath')
.attr('id', 'clip') .attr('id', 'clip')
.append('rect') .append('rect')
.attr('width', this.width) .attr('width', this.width)
@ -121,7 +145,7 @@ export class LineChartComponent implements OnChanges, OnInit {
this.updateData(this.value.count); this.updateData(this.value.count);
} }
updateData = (value: number) => { updateData(value: number) {
this.data.push(value * 1000000); this.data.push(value * 1000000);
this.now = new Date(); this.now = new Date();
@ -132,9 +156,13 @@ export class LineChartComponent implements OnChanges, OnInit {
this.xAxis this.xAxis
.transition() .transition()
.duration(this.duration) .duration(this.firstDisplay || this.dirty ? 0 : this.duration)
.ease(easeLinear) .ease(easeLinear)
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S'))) .call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')));
this.xAxis
.transition()
.duration(0)
.selectAll('text') .selectAll('text')
.style('text-anchor', 'end') .style('text-anchor', 'end')
.attr('dx', '-.8em') .attr('dx', '-.8em')
@ -157,6 +185,13 @@ export class LineChartComponent implements OnChanges, OnInit {
.ease(easeLinear) .ease(easeLinear)
.attr('transform', `translate(${this.x(<any>this.now - (this.limit - 1) * this.duration)})`); .attr('transform', `translate(${this.x(<any>this.now - (this.limit - 1) * this.duration)})`);
this.firstDisplay = false;
this.dirty = false;
if (this.loading) {
this.loading = false;
}
this.data.shift(); this.data.shift();
} }
} }

View file

@ -1,22 +1,27 @@
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation"> <nav class="navbar is-fixed-top is-transparent" role="navigation" aria-label="main navigation">
<div class="container"> <div class="container">
<div class="navbar-menu">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" routerLink="/"> <a class="navbar-item" routerLink="/" (click)="burger = false">
<img src="./assets/images/traefik.logo.svg" alt="Traefik" class="navbar-logo"> <img src="./assets/images/traefik.logo.svg" alt="Traefik" class="navbar-logo">
</a>
<div class="navbar-burger burger" data-target="navbarMain" (click)="burger = !burger" [class.is-active]="burger">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</div>
</div>
<div id="navbarMain" class="navbar-menu" [class.is-active]="burger">
<div class="navbar-start">
<a class="navbar-item" routerLink="/" routerLinkActive="is-active" [routerLinkActiveOptions]="{ exact: true }" (click)="burger = false">
Providers
</a>
<a class="navbar-item" routerLink="/status" routerLinkActive="is-active" (click)="burger = false">
Health
</a> </a>
</div> </div>
<div class="navbar-start"> <div class="navbar-end">
<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"> <a class="navbar-item" [href]="releaseLink" target="_blank">
{{ version }} / {{ codename }} {{ version }} / {{ codename }}
</a> </a>
@ -25,5 +30,6 @@
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>

View file

@ -9,6 +9,7 @@ export class HeaderComponent implements OnInit {
version: string; version: string;
codename: string; codename: string;
releaseLink: string; releaseLink: string;
burger: boolean;
constructor(private apiService: ApiService) { } constructor(private apiService: ApiService) { }

View file

@ -9,7 +9,7 @@
<div class="column is-4"> <div class="column is-4">
<div class="item-data border-right"> <div class="item-data border-right">
<span class="data-grey">Total Response Time</span> <span class="data-grey">Total Response Time</span>
<span class="data-blue">{{ totalResponseTime }}</span> <span class="data-blue" [title]="exactTotalResponseTime">{{ totalResponseTime }}</span>
</div> </div>
</div> </div>
<div class="column is-4"> <div class="column is-4">
@ -33,7 +33,7 @@
<div class="column is-4"> <div class="column is-4">
<div class="item-data border-right"> <div class="item-data border-right">
<span class="data-grey">Average Response Time</span> <span class="data-grey">Average Response Time</span>
<span class="data-blue">{{ averageResponseTime }}</span> <span class="data-blue" [title]="exactAverageResponseTime">{{ averageResponseTime }}</span>
</div> </div>
</div> </div>
<div class="column is-4"> <div class="column is-4">
@ -82,15 +82,15 @@
<td>Request</td> <td>Request</td>
<td>Time</td> <td>Time</td>
</tr> </tr>
<tr *ngFor="let entry of recentErrors"> <tr *ngFor="let entry of recentErrors; trackBy: trackRecentErrors;">
<td> <td>
<span class="tag is-info">{{ entry.status_code }}</span>&nbsp;<span>{{ entry.status }}</span> <span class="tag is-info" [title]="entry.status">{{ entry.status_code }}</span>&nbsp;<span class="is-hidden-mobile is-hidden-desktop-only">{{ entry.status }}</span>
</td> </td>
<td> <td>
<span class="tag">{{ entry.method }}</span>&nbsp;<a>{{ entry.host }}{{ entry.path }}</a> <span class="tag">{{ entry.method }}</span>&nbsp;<span>{{ entry.host }}{{ entry.path }}</span>
</td> </td>
<td> <td>
<span>{{ entry.time }}</span> <span [title]="entry.time | date:'yyyy-MM-dd HH:mm:ss:SSS a z'">{{ entry.time | date:'yyyy-MM-dd HH:mm:ss a z' }}</span>
</td> </td>
</tr> </tr>
<tr *ngIf="!recentErrors?.length"> <tr *ngIf="!recentErrors?.length">

View file

@ -1,12 +1,13 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ApiService } from '../../services/api.service'; import { distanceInWordsStrict, format, subSeconds } from 'date-fns';
import * as _ from 'lodash';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/timeInterval';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/timer'; import { ApiService } from '../../services/api.service';
import 'rxjs/add/operator/timeInterval';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/map';
import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
@Component({ @Component({
selector: 'app-health', selector: 'app-health',
@ -15,11 +16,14 @@ import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
export class HealthComponent implements OnInit, OnDestroy { export class HealthComponent implements OnInit, OnDestroy {
sub: Subscription; sub: Subscription;
recentErrors: any; recentErrors: any;
previousRecentErrors: any;
pid: number; pid: number;
uptime: string; uptime: string;
uptimeSince: string; uptimeSince: string;
averageResponseTime: string; averageResponseTime: string;
exactAverageResponseTime: string;
totalResponseTime: string; totalResponseTime: string;
exactTotalResponseTime: string;
codeCount: number; codeCount: number;
totalCodeCount: number; totalCodeCount: number;
chartValue: any; chartValue: any;
@ -33,16 +37,22 @@ export class HealthComponent implements OnInit, OnDestroy {
.mergeMap(() => this.apiService.fetchHealthStatus()) .mergeMap(() => this.apiService.fetchHealthStatus())
.subscribe(data => { .subscribe(data => {
if (data) { if (data) {
this.recentErrors = data.recent_errors; if (!_.isEqual(this.previousRecentErrors, data.recent_errors)) {
this.chartValue = { count: data.average_response_time_sec, date: data.time }; this.previousRecentErrors = _.cloneDeep(data.recent_errors);
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) this.statusCodeValue = Object.keys(data.total_status_code_count)
.map(key => ({ code: key, count: data.total_status_code_count[key] })); .map(key => ({code: key, count: data.total_status_code_count[key]}));
this.pid = data.pid; this.pid = data.pid;
this.uptime = distanceInWordsStrict(subSeconds(new Date(), data.uptime_sec), new Date()); 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.uptimeSince = format(subSeconds(new Date(), data.uptime_sec), 'YYYY-MM-DD HH:mm:ss Z');
this.totalResponseTime = data.total_response_time; this.totalResponseTime = distanceInWordsStrict(subSeconds(new Date(), data.total_response_time_sec), new Date());
this.averageResponseTime = data.average_response_time; this.exactTotalResponseTime = data.total_response_time;
this.averageResponseTime = Math.floor(data.average_response_time_sec * 1000) + ' ms';
this.exactAverageResponseTime = data.average_response_time;
this.codeCount = data.count; this.codeCount = data.count;
this.totalCodeCount = data.total_count; this.totalCodeCount = data.total_count;
} }
@ -54,4 +64,8 @@ export class HealthComponent implements OnInit, OnDestroy {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
} }
trackRecentErrors(index, item): string {
return item.status_code + item.method + item.host + item.path + item.time;
}
} }

View file

@ -5,8 +5,9 @@
<div class="column is-12"> <div class="column is-12">
<div class="search-container"> <div class="search-container">
<span class="icon"><i class="fas fa-search"></i></span> <span class="icon search-button" *ngIf="!keyword"><i class="fas fa-search"></i></span>
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword" (ngModelChange)="filter()"> <a class="delete search-button" *ngIf="keyword" (click)="keyword = ''"></a>
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword">
</div> </div>
<div class="tabs" *ngIf="keys?.length"> <div class="tabs" *ngIf="keys?.length">
@ -20,30 +21,17 @@
<div *ngIf="keys?.length"> <div *ngIf="keys?.length">
<div class="columns"> <div class="columns">
<!-- Frontends --> <!-- Frontends -->
<div class="column is-6"> <div class="column is-6" *appLet="providers[tab]?.frontends | frontendFilter:keyword as frontends">
<h2 class="subtitle"><span class="tag is-info">{{ providers[tab]?.frontends.length }}</span> Frontends</h2> <h2 class="subtitle"><span class="tag is-info">{{ frontends.length }}</span><span class="subtitle-name">Frontends</span></h2>
<div class="message" *ngFor="let p of providers[tab]?.frontends; let i = index;">
<div class="message-header"> <div *ngIf="frontends.length < maxItem">
<div class="message" *ngFor="let p of frontends; trackBy: trackItem(tab)">
<div class="message-header" [class.has-background-info]="p.backend" [class.has-background-danger]="!p.backend">
<h2> <h2>
<i class="icon fas fa-globe has-text-white"></i>
<div> <div>
<i class="icon fas fa-globe"></i> <span class="has-text-white" [class.is-info]="p.backend" [class.is-danger]="!p.backend">{{ p.id }}</span>
<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> </div>
</h2> </h2>
</div> </div>
@ -57,16 +45,16 @@
</div> </div>
<!-- Main --> <!-- Main -->
<div *ngIf="p.section !== 'details'"> <div *ngIf="p.section !== 'details'" class="section-container">
<div *ngIf="p.routes && p.routes.length"> <div *ngIf="p.routes && p.routes.length" class="section-line">
<div>
<h2>Route Rule</h2>
</div>
<table class="table is-fullwidth is-hoverable"> <table class="table is-fullwidth is-hoverable">
<tbody> <tbody>
<tr> <tr *ngFor="let route of p.routes">
<td>Route Rule</td> <td><code class="has-text-grey" [title]="route.id">{{ route.rule }}</code></td>
</tr>
<tr *ngFor="let route of p.routes; let ri = index;">
<td><code class="has-text-grey" title="{{ route.title }}">{{ route.rule }}</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -74,15 +62,15 @@
<div *ngIf="p.entryPoints && p.entryPoints.length"> <div *ngIf="p.entryPoints && p.entryPoints.length">
<hr> <hr>
<div class="columns"> <div class="columns section-line">
<div class="column is-3"> <div class="column is-3">
<h2>Entry Points</h2> <h2 class="section-line-header">Entry Points</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
<div class="control"> <div class="control">
<div class="tags"> <div class="tags">
<span class="tag is-info" *ngFor="let ep of p.entryPoints; let ri = index;">{{ ep }}</span> <span class="tag is-info" *ngFor="let ep of p.entryPoints">{{ ep }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -90,19 +78,34 @@
</div> </div>
</div> </div>
<div *ngIf="p.backend">
<hr>
<div class="columns section-line">
<div class="column is-2">
<h2 class="section-line-header">Backend</h2>
</div>
<div class="column is-10">
<div class="field">
<i class="icon fas fa-server has-text-primary" title="Backend"></i>
<span class="has-text-primary">{{ p.backend }}</span>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Details --> <!-- Details -->
<div *ngIf="p.section === 'details'"> <div *ngIf="p.section === 'details'" class="section-container">
<div> <div class="section-line">
<div class="columns"> <div class="columns">
<div class="column is-3"> <div class="column is-3">
<h2>Misc.</h2> <h2 class="section-line-header">Misc.</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
<div class="control"> <div class="control" *ngIf="p.priority">
<div class="tags has-addons"> <div class="tags has-addons">
<span class="tag is-light">Priority</span> <span class="tag is-light">Priority</span>
<span class="tag is-info">{{ p.priority }}</span> <span class="tag is-info">{{ p.priority }}</span>
@ -111,7 +114,7 @@
<div class="control"> <div class="control">
<div class="tags has-addons"> <div class="tags has-addons">
<span class="tag is-light">Host Header</span> <span class="tag is-light">Host Header</span>
<span class="tag is-info">{{ p.passHostHeader }}</span> <span class="tag is-info">{{ !!p.passHostHeader }}</span>
</div> </div>
</div> </div>
<div class="control" *ngIf="p.passTLSCert"> <div class="control" *ngIf="p.passTLSCert">
@ -127,9 +130,9 @@
<div *ngIf="p.redirect"> <div *ngIf="p.redirect">
<hr> <hr>
<div class="columns"> <div class="columns section-line">
<div class="column is-3"> <div class="column is-3">
<h2>Redirect</h2> <h2 class="section-line-header">Redirect</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline" *ngIf="p.redirect.entryPoint"> <div class="field is-grouped is-grouped-multiline" *ngIf="p.redirect.entryPoint">
@ -160,45 +163,49 @@
<div *ngIf="p.basicAuth && p.basicAuth.length"> <div *ngIf="p.basicAuth && p.basicAuth.length">
<hr/> <hr/>
<h2>Basic Authentication</h2> <div class="section-line">
<div class="tags padding-5-10"> <h2 class="section-line-header">Basic Authentication</h2>
<span class="tag is-info" *ngFor="let auth of p.basicAuth; let ri = index;">{{ auth }}</span> <div class="tags padding-5-10">
<span class="tag is-info" *ngFor="let auth of p.basicAuth">{{ auth }}</span>
</div>
</div> </div>
</div> </div>
<div *ngIf="p.errors"> <div *ngIf="p.errors?.length">
<hr/> <hr/>
<h2>Error Pages</h2> <div class="section-line">
<table class="table is-fullwidth is-hoverable"> <h2 class="section-line-header">Error Pages</h2>
<tbody> <table class="table is-fullwidth is-hoverable">
<tr> <tbody>
<td>Backend</td> <tr>
<td>Query</td> <td>Backend</td>
<td>Status</td> <td>Query</td>
</tr> <td>Status</td>
<tr *ngFor="let key of p.errors | keys"> </tr>
<td><span class="has-text-grey-light">{{ p.errors[key].backend }}</span></td> <tr *ngFor="let entry of p.errors">
<td><span class="has-text-grey">{{ p.errors[key].query }}</span></td> <td><span class="has-text-grey-light">{{ entry.backend }}</span></td>
<td> <td><span class="has-text-grey">{{ entry.query }}</span></td>
<span class="tag is-light" *ngFor="let state of p.errors[key].status">{{ state }}</span> <td>
</td> <span class="tag is-light" *ngFor="let state of entry.status">{{ state }}</span>
</tr> </td>
</tbody> </tr>
</table> </tbody>
</table>
</div>
</div> </div>
<div *ngIf="p.whiteList"> <div *ngIf="p.whiteList">
<hr/> <hr/>
<div class="columns is-gapless is-multiline is-mobile"> <div class="columns is-gapless is-multiline is-mobile section-line">
<div class="column is-half"> <div class="column is-half">
<h2>Whitelist</h2> <h2 class="section-line-header">Whitelist</h2>
</div> </div>
<div class="column is-half"> <div class="column is-half">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<div class="tags has-addons"> <div class="tags has-addons">
<span class="tag is-light">useXForwardedFor</span> <span class="tag is-light">useXForwardedFor</span>
<span class="tag is-info">{{ p.whiteList.useXForwardedFor }}</span> <span class="tag is-info">{{ !!p.whiteList.useXForwardedFor }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -207,7 +214,7 @@
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
<div class="control"> <div class="control">
<div class="tags"> <div class="tags">
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange; let ri = index;">{{ wlRange }}</span> <span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange">{{ wlRange }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -217,126 +224,137 @@
<div *ngIf="p.headers"> <div *ngIf="p.headers">
<hr/> <hr/>
<h2>Headers</h2> <div class="section-line">
<div class="columns is-multiline"> <h2 class="section-line-header">Headers</h2>
<div class="columns is-multiline">
<div class="column is-12" *ngIf="p.headers.customRequestHeaders"> <div class="column is-12" *ngIf="p.headers.customRequestHeaders?.length">
<h2>Custom Request Headers</h2> <table class="table is-fullwidth is-hoverable table-fixed-break">
<table class="table is-fullwidth is-hoverable"> <tbody>
<tbody> <tr>
<tr *ngFor="let key of p.headers.customRequestHeaders | keys"> <td colspan="2">Custom Request Headers</td>
<td><span class="has-text-grey-light">{{ key }}</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.customRequestHeaders[key] }}</span></td> <tr *ngFor="let header of p.headers.customRequestHeaders">
</tr> <td><span class="has-text-grey-light">{{ header.name }}</span></td>
</tbody> <td><span class="has-text-grey">{{ header.value }}</span></td>
</table> </tr>
</div> </tbody>
</table>
</div>
<div class="column is-12" *ngIf="p.headers.customResponseHeaders"> <div class="column is-12" *ngIf="p.headers.customResponseHeaders?.length">
<h2>Custom Response Headers</h2> <table class="table is-fullwidth is-hoverable table-fixed-break">
<table class="table is-fullwidth is-hoverable"> <tbody>
<tbody> <tr>
<tr *ngFor="let key of p.headers.customResponseHeaders | keys"> <td colspan="2">Custom Response Headers</td>
<td><span class="has-text-grey-light">{{ key }}</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.customResponseHeaders[key] }}</span></td> <tr *ngFor="let header of p.headers.customResponseHeaders">
</tr> <td><span class="has-text-grey-light">{{ header.name }}</span></td>
</tbody> <td><span class="has-text-grey">{{ header.value }}</span></td>
</table> </tr>
</div> </tbody>
</table>
</div>
<div class="column is-12"> <div class="column is-12">
<h2>Secure</h2> <table class="table is-fullwidth is-hoverable table-fixed-break">
<table class="table is-fullwidth is-hoverable"> <tbody>
<tbody> <tr>
<tr *ngIf="p.headers.browserXssFilter"> <td colspan="2">Secure</td>
<td><span class="has-text-grey">Browser XSS Filter</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td> <tr *ngIf="p.headers.browserXssFilter">
</tr> <td><span class="has-text-grey">Browser XSS Filter</span></td>
<tr *ngIf="p.headers.contentSecurityPolicy"> <td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td>
<td><span class="has-text-grey">Content Security Policy</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.contentSecurityPolicy }}</span></td> <tr *ngIf="p.headers.contentSecurityPolicy">
</tr> <td><span class="has-text-grey">Content Security Policy</span></td>
<tr *ngIf="p.headers.contentTypeNoSniff"> <td><span class="has-text-grey">{{ p.headers.contentSecurityPolicy }}</span></td>
<td><span class="has-text-grey">Content Type (No sniff)</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.contentTypeNoSniff }}</span></td> <tr *ngIf="p.headers.contentTypeNoSniff">
</tr> <td><span class="has-text-grey">Content Type (No sniff)</span></td>
<tr *ngIf="p.headers.customFrameOptionsValue"> <td><span class="has-text-grey">{{ p.headers.contentTypeNoSniff }}</span></td>
<td><span class="has-text-grey">Custom Frame Options Value</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.customFrameOptionsValue }}</span></td> <tr *ngIf="p.headers.customFrameOptionsValue">
</tr> <td><span class="has-text-grey">Custom Frame Options Value</span></td>
<tr *ngIf="p.headers.forceSTSHeader"> <td><span class="has-text-grey">{{ p.headers.customFrameOptionsValue }}</span></td>
<td><span class="has-text-grey">Force STS Header</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.forceSTSHeader }}</span></td> <tr *ngIf="p.headers.forceSTSHeader">
</tr> <td><span class="has-text-grey">Force STS Header</span></td>
<tr *ngIf="p.headers.frameDeny"> <td><span class="has-text-grey">{{ p.headers.forceSTSHeader }}</span></td>
<td><span class="has-text-grey">Frame Deny</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.frameDeny }}</span></td> <tr *ngIf="p.headers.frameDeny">
</tr> <td><span class="has-text-grey">Frame Deny</span></td>
<tr *ngIf="p.headers.isDevelopment"> <td><span class="has-text-grey">{{ p.headers.frameDeny }}</span></td>
<td><span class="has-text-grey">Is Development</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.isDevelopment }}</span></td> <tr *ngIf="p.headers.isDevelopment">
</tr> <td><span class="has-text-grey">Is Development</span></td>
<tr *ngIf="p.headers.publicKey"> <td><span class="has-text-grey">{{ p.headers.isDevelopment }}</span></td>
<td><span class="has-text-grey">Public Key</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.publicKey }}</span></td> <tr *ngIf="p.headers.publicKey">
</tr> <td><span class="has-text-grey">Public Key</span></td>
<tr *ngIf="p.headers.referrerPolicy"> <td><span class="has-text-grey">{{ p.headers.publicKey }}</span></td>
<td><span class="has-text-grey">Referrer Policy</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.referrerPolicy }}</span></td> <tr *ngIf="p.headers.referrerPolicy">
</tr> <td><span class="has-text-grey">Referrer Policy</span></td>
<tr *ngIf="p.headers.sslHost"> <td><span class="has-text-grey">{{ p.headers.referrerPolicy }}</span></td>
<td><span class="has-text-grey">SSL Host</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.sslHost }}</span></td> <tr *ngIf="p.headers.sslHost">
</tr> <td><span class="has-text-grey">SSL Host</span></td>
<tr *ngIf="p.headers.sslRedirect"> <td><span class="has-text-grey">{{ p.headers.sslHost }}</span></td>
<td><span class="has-text-grey">SSL Redirect</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.sslRedirect }}</span></td> <tr *ngIf="p.headers.sslRedirect">
</tr> <td><span class="has-text-grey">SSL Redirect</span></td>
<tr *ngIf="p.headers.sslTemporaryRedirect"> <td><span class="has-text-grey">{{ p.headers.sslRedirect }}</span></td>
<td><span class="has-text-grey">SSL Temporary Redirect</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.sslTemporaryRedirect }}</span></td> <tr *ngIf="p.headers.sslTemporaryRedirect">
</tr> <td><span class="has-text-grey">SSL Temporary Redirect</span></td>
<tr *ngIf="p.headers.stsIncludeSubdomains"> <td><span class="has-text-grey">{{ p.headers.sslTemporaryRedirect }}</span></td>
<td><span class="has-text-grey">STS Include Subdomains</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.stsIncludeSubdomains }}</span></td> <tr *ngIf="p.headers.stsIncludeSubdomains">
</tr> <td><span class="has-text-grey">STS Include Subdomains</span></td>
<tr *ngIf="p.headers.stsPreload"> <td><span class="has-text-grey">{{ p.headers.stsIncludeSubdomains }}</span></td>
<td><span class="has-text-grey">STS Preload</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.stsPreload }}</span></td> <tr *ngIf="p.headers.stsPreload">
</tr> <td><span class="has-text-grey">STS Preload</span></td>
<tr *ngIf="p.headers.stsSeconds"> <td><span class="has-text-grey">{{ p.headers.stsPreload }}</span></td>
<td><span class="has-text-grey">STS Seconds</span></td> </tr>
<td><span class="has-text-grey">{{ p.headers.stsSeconds }}</span></td> <tr *ngIf="p.headers.stsSeconds">
</tr> <td><span class="has-text-grey">STS Seconds</span></td>
</tbody> <td><span class="has-text-grey">{{ p.headers.stsSeconds }}</span></td>
</table> </tr>
</div> </tbody>
</table>
</div>
<div class="column is-12" *ngIf="p.headers.allowedHosts"> <div class="column is-12" *ngIf="p.headers.sslProxyHeaders?.length">
<h2>Allowed Hosts</h2> <table class="table is-fullwidth is-hoverable table-fixed-break">
<div class="tags-list"> <tbody>
<span class="tag is-light" *ngFor="let host of p.headers.allowedHosts">{{ host }}</span> <tr>
<td colspan="2">SSL Proxy Headers</td>
</tr>
<tr *ngFor="let header of p.headers.sslProxyHeaders">
<td><span class="has-text-grey-light">{{ header.name }}</span></td>
<td><span class="has-text-grey">{{ header.value }}</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.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 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>
@ -344,23 +362,33 @@
</div> </div>
</div> </div>
</div>
<div *ngIf="frontends.length > maxItem">
<div class="message">
<div class="message-header has-background-warning has-text-black">
Too many frontends to display, please add a filter.
</div>
</div>
</div>
</div> </div>
<!-- Backends --> <!-- Backends -->
<div class="column is-6"> <div class="column is-6" *appLet="providers[tab]?.backends | backendFilter:keyword as backends">
<h2 class="subtitle"><span class="tag is-primary">{{ providers[tab]?.backends.length }}</span> Backends</h2> <h2 class="subtitle"><span class="tag is-primary">{{ backends.length }}</span><span class="subtitle-name">Backends</span></h2>
<div class="message" *ngFor="let p of providers[tab]?.backends; let i = index;">
<div class="message-header"> <div *ngIf="backends.length < maxItem">
<h2 [id]="p.id">
<div class="message" *ngFor="let p of backends; trackBy: trackItem(tab);">
<div class="message-header" [class.has-background-primary]="p.servers?.length" [class.has-background-danger]="!p.servers?.length">
<h2>
<i class="icon fas fa-server has-text-white"></i>
<div> <div>
<i class="icon fas fa-server"></i> <span class="has-text-white">{{ p.id }}</span>
<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> </div>
</h2> </h2>
</div> </div>
@ -374,28 +402,34 @@
</div> </div>
<!-- Main --> <!-- Main -->
<div *ngIf="p.section !== 'details'"> <div *ngIf="p.section !== 'details'" class="section-container">
<table class="table is-fullwidth is-hoverable"> <div class="section-line">
<tbody> <table class="table is-fullwidth is-hoverable table-fixed">
<tr> <colgroup>
<td>Server</td> <col class="table-col-75">
<td>Weight</td> <col>
</tr> </colgroup>
<tr *ngFor="let server of p.servers; let ri = index;"> <tbody>
<td><a href="{{ server.url }}" title="{{ server.title }}">{{ server.url }}</a></td> <tr>
<td><span class="has-text-grey">{{ server.weight }}</span></td> <td>Server</td>
</tr> <td>Weight</td>
</tbody> </tr>
</table> <tr *ngFor="let server of p.servers">
<td class="table-cell-limited"><a href="{{ server.url }}" [title]="server.id">{{ server.url }}</a></td>
<td><span class="has-text-grey">{{ server.weight }}</span></td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- Details --> <!-- Details -->
<div *ngIf="p.section === 'details'"> <div *ngIf="p.section === 'details'" class="section-container">
<div *ngIf="p.loadBalancer"> <div *ngIf="p.loadBalancer" class="section-line">
<div class="columns"> <div class="columns">
<div class="column is-3"> <div class="column is-3">
<h2>Load Balancer</h2> <h2 class="section-line-header">Load Balancer</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
@ -424,9 +458,9 @@
<div *ngIf="p.maxConn"> <div *ngIf="p.maxConn">
<hr/> <hr/>
<div class="columns"> <div class="columns section-line">
<div class="column is-3"> <div class="column is-3">
<h2>Max Connections</h2> <h2 class="section-line-header">Max Connections</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
@ -449,9 +483,9 @@
<div *ngIf="p.circuitBreaker"> <div *ngIf="p.circuitBreaker">
<hr/> <hr/>
<div class="columns"> <div class="columns section-line">
<div class="column is-3"> <div class="column is-3">
<h2>Circuit Breaker</h2> <h2 class="section-line-header">Circuit Breaker</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
@ -468,9 +502,9 @@
<div *ngIf="p.healthCheck"> <div *ngIf="p.healthCheck">
<hr/> <hr/>
<div class="columns"> <div class="columns section-line">
<div class="column is-3"> <div class="column is-3">
<h2>Health Check</h2> <h2 class="section-line-header">Health Check</h2>
</div> </div>
<div class="column is-9"> <div class="column is-9">
<div class="field is-grouped is-grouped-multiline"> <div class="field is-grouped is-grouped-multiline">
@ -505,81 +539,79 @@
<div *ngIf="p.buffering"> <div *ngIf="p.buffering">
<hr> <hr>
<div class="columns list-title"> <div class="section-line">
<div class="column is-12"> <h2 class="section-line-header">Buffering</h2>
<h2>Buffering</h2> <table class="table is-fullwidth is-hoverable table-fixedd">
</div> <tbody>
</div> <tr>
<div class="list-item"> <td><span class="has-text-grey">Request Body Bytes</span></td>
<div class="columns"> <td>
<div class="column is-4"> <div class="field is-grouped is-grouped-multiline">
<span>Request Body Bytes</span> <div class="control">
</div> <div class="tags has-addons">
<div class="column is-4"> <span class="tag is-light">Max</span>
<div class="field is-grouped is-grouped-multiline"> <span class="tag is-info">{{ p.buffering.maxRequestBodyBytes }}</span>
<div class="control"> </div>
<div class="tags has-addons"> </div>
<span class="tag is-light">Max</span>
<span class="tag is-info">{{ p.buffering.maxRequestBodyBytes }}</span>
</div> </div>
</div> </td>
</div> <td>
</div> <div class="field is-grouped is-grouped-multiline">
<div class="column is-4"> <div class="control">
<div class="field is-grouped is-grouped-multiline"> <div class="tags has-addons">
<div class="control"> <span class="tag is-light">Men</span>
<div class="tags has-addons"> <span class="tag is-info">{{ p.buffering.memRequestBodyBytes }}</span>
<span class="tag is-light">Men</span> </div>
<span class="tag is-info">{{ p.buffering.memRequestBodyBytes }}</span> </div>
</div> </div>
</div> </td>
</div> </tr>
</div> <tr>
</div> <td><span class="has-text-grey">Response Body Bytes</span></td>
</div> <td>
<div class="list-item"> <div class="field is-grouped is-grouped-multiline">
<div class="columns"> <div class="control">
<div class="column is-4"> <div class="tags has-addons">
<span>Response Body Bytes</span> <span class="tag is-light">Max</span>
</div> <span class="tag is-info">{{ p.buffering.maxResponseBodyBytes }}</span>
<div class="column is-4"> </div>
<div class="field is-grouped is-grouped-multiline"> </div>
<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> </td>
</div> <td>
</div> <div class="field is-grouped is-grouped-multiline">
<div class="column is-4"> <div class="control">
<div class="field is-grouped is-grouped-multiline"> <div class="tags has-addons">
<div class="control"> <span class="tag is-light">Men</span>
<div class="tags has-addons"> <span class="tag is-info">{{ p.buffering.memResponseBodyBytes }}</span>
<span class="tag is-light">Men</span> </div>
<span class="tag is-info">{{ p.buffering.memResponseBodyBytes }}</span> </div>
</div> </div>
</div> </td>
</div> </tr>
</div> <tr>
</div> <td class="has-text-grey">Retry Expression</td>
</div> <td colspan="2"><span class="tag is-info">{{ p.buffering.retryExpression }}</span></td>
<div class="list-item"> </tr>
<div class="columns"> </tbody>
<div class="column is-4"> </table>
<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>
<div *ngIf="backends.length > maxItem">
<div class="message">
<div class="message-header has-background-warning has-text-black">
Too many backends to display, please add a filter.
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ApiService } from '../../services/api.service'; import * as _ from 'lodash';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import * as _ from "lodash"; import { Subscription } from 'rxjs/Subscription';
import { ApiService } from '../../services/api.service';
@Component({ @Component({
selector: 'app-providers', selector: 'app-providers',
@ -10,8 +10,9 @@ import * as _ from "lodash";
}) })
export class ProvidersComponent implements OnInit, OnDestroy { export class ProvidersComponent implements OnInit, OnDestroy {
sub: Subscription; sub: Subscription;
maxItem: number;
keys: string[]; keys: string[];
data: any; previousKeys: string[];
previousData: any; previousData: any;
providers: any; providers: any;
tab: string; tab: string;
@ -20,6 +21,7 @@ export class ProvidersComponent implements OnInit, OnDestroy {
constructor(private apiService: ApiService) { } constructor(private apiService: ApiService) { }
ngOnInit() { ngOnInit() {
this.maxItem = 100;
this.keyword = ''; this.keyword = '';
this.sub = Observable.timer(0, 2000) this.sub = Observable.timer(0, 2000)
.timeInterval() .timeInterval()
@ -27,28 +29,23 @@ export class ProvidersComponent implements OnInit, OnDestroy {
.subscribe(data => { .subscribe(data => {
if (!_.isEqual(this.previousData, data)) { if (!_.isEqual(this.previousData, data)) {
this.previousData = _.cloneDeep(data); this.previousData = _.cloneDeep(data);
this.data = data;
this.providers = data; this.providers = data;
this.keys = Object.keys(this.providers);
this.tab = this.keys[0]; const keys = Object.keys(this.providers);
if (!_.isEqual(this.previousKeys, keys)) {
this.keys = keys;
// keep current tab or set to the first tab
if (!this.tab || (this.tab && !this.keys.includes(this.tab))) {
this.tab = this.keys[0];
}
}
} }
}); });
} }
filter(): void { trackItem(tab): (index, item) => string {
const keyword = this.keyword.toLowerCase(); return (index, item): string => tab + '-' + item.id;
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() { ngOnDestroy() {

View file

@ -0,0 +1,21 @@
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
interface LetContext<T> {
appLet: T;
}
@Directive({
selector: '[appLet]'
})
export class LetDirective<T> {
private _context: LetContext<T> = {appLet: null};
constructor(_viewContainer: ViewContainerRef, _templateRef: TemplateRef<LetContext<T>>) {
_viewContainer.createEmbeddedView(_templateRef, this._context);
}
@Input()
set appLet(value: T) {
this._context.appLet = value;
}
}

View file

@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'backendFilter',
pure: false
})
export class BackendFilterPipe implements PipeTransform {
transform(items: any[], filter: string): any {
if (!items || !filter) {
return items;
}
const keyword = filter.toLowerCase();
return items.filter(d => d.id.toLowerCase().includes(keyword)
|| d.servers.some(r => r.url.toLowerCase().includes(keyword)));
}
}

View file

@ -0,0 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'frontendFilter',
pure: false
})
export class FrontendFilterPipe implements PipeTransform {
transform(items: any[], filter: string): any {
if (!items || !filter) {
return items;
}
const keyword = filter.toLowerCase();
return items.filter(d => d.id.toLowerCase().includes(keyword)
|| d.backend.toLowerCase().includes(keyword)
|| d.routes.some(r => r.rule.toLowerCase().includes(keyword)));
}
}

View file

@ -1,6 +1,6 @@
import { PipeTransform, Pipe } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'keys' }) @Pipe({name: 'keys'})
export class KeysPipe implements PipeTransform { export class KeysPipe implements PipeTransform {
transform(value, args: string[]): any { transform(value, args: string[]): any {
return Object.keys(value); return Object.keys(value);

View file

@ -1,11 +1,11 @@
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; 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/empty';
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/retry'; import 'rxjs/add/operator/retry';
import { Observable } from 'rxjs/Observable';
export interface ProviderType { export interface ProviderType {
[provider: string]: { [provider: string]: {
@ -25,7 +25,7 @@ export class ApiService {
} }
fetchVersion(): Observable<any> { fetchVersion(): Observable<any> {
return this.http.get(`/api/version`, { headers: this.headers }) return this.http.get('../api/version', {headers: this.headers})
.retry(4) .retry(4)
.catch((err: HttpErrorResponse) => { .catch((err: HttpErrorResponse) => {
console.error(`[version] returned code ${err.status}, body was: ${err.error}`); console.error(`[version] returned code ${err.status}, body was: ${err.error}`);
@ -34,7 +34,7 @@ export class ApiService {
} }
fetchHealthStatus(): Observable<any> { fetchHealthStatus(): Observable<any> {
return this.http.get(`/health`, { headers: this.headers }) return this.http.get('../health', {headers: this.headers})
.retry(2) .retry(2)
.catch((err: HttpErrorResponse) => { .catch((err: HttpErrorResponse) => {
console.error(`[health] returned code ${err.status}, body was: ${err.error}`); console.error(`[health] returned code ${err.status}, body was: ${err.error}`);
@ -43,46 +43,53 @@ export class ApiService {
} }
fetchProviders(): Observable<any> { fetchProviders(): Observable<any> {
return this.http.get(`/api/providers`, { headers: this.headers }) return this.http.get('../api/providers', {headers: this.headers})
.retry(2) .retry(2)
.catch((err: HttpErrorResponse) => { .catch((err: HttpErrorResponse) => {
console.error(`[providers] returned code ${err.status}, body was: ${err.error}`); console.error(`[providers] returned code ${err.status}, body was: ${err.error}`);
return Observable.of<any>({}); return Observable.of<any>({});
}) })
.map(this.parseProviders); .map((data: any): ProviderType => this.parseProviders(data));
} }
parseProviders(data: any): ProviderType { parseProviders(data: any): ProviderType {
return Object.keys(data) return Object.keys(data)
.filter(value => value !== 'acme' && value !== 'ACME') .filter(value => value !== 'acme' && value !== 'ACME')
.reduce((acc, curr) => { .reduce((acc, curr) => {
acc[curr] = { acc[curr] = {};
backends: Object.keys(data[curr].backends || {}).map(key => {
data[curr].backends[key].id = key; acc[curr].frontends = this.toArray(data[curr].frontends, 'id')
data[curr].backends[key].servers = Object.keys(data[curr].backends[key].servers || {}).map(server => { .map(frontend => {
return { frontend.routes = this.toArray(frontend.routes, 'id');
title: server, frontend.errors = this.toArray(frontend.errors, 'id');
url: data[curr].backends[key].servers[server].url, if (frontend.headers) {
weight: data[curr].backends[key].servers[server].weight frontend.headers.customRequestHeaders = this.toHeaderArray(frontend.headers.customRequestHeaders);
}; frontend.headers.customResponseHeaders = this.toHeaderArray(frontend.headers.customResponseHeaders);
frontend.headers.sslProxyHeaders = this.toHeaderArray(frontend.headers.sslProxyHeaders);
}
return frontend;
}); });
return data[curr].backends[key]; acc[curr].backends = this.toArray(data[curr].backends, 'id')
}), .map(backend => {
frontends: Object.keys(data[curr].frontends || {}).map(key => { backend.servers = this.toArray(backend.servers, 'id');
data[curr].frontends[key].id = key; return backend;
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; return acc;
}, {}); }, {});
} }
toHeaderArray(data: any): any[] {
return Object.keys(data || {}).map(key => ({name: key, value: data[key]}));
}
toArray(data: any, fieldKeyName: string): any[] {
return Object.keys(data || {}).map(key => {
data[key][fieldKeyName] = key;
return data[key];
});
}
} }

View file

@ -1,22 +1,23 @@
@charset "utf-8" @charset "utf-8"
@import 'typography' @import 'typography'
@import 'variables'
@import 'colors' @import 'colors'
@import '../../node_modules/bulma/sass/utilities/all' @import '~bulma/sass/utilities/all'
@import '../../node_modules/bulma/sass/base/all' @import '~bulma/sass/base/all'
@import '../../node_modules/bulma/sass/grid/all' @import '~bulma/sass/grid/all'
@import '../../node_modules/bulma/sass/elements/container' @import '~bulma/sass/elements/container'
@import '../../node_modules/bulma/sass/elements/tag' @import '~bulma/sass/elements/tag'
@import '../../node_modules/bulma/sass/elements/box' @import '~bulma/sass/elements/other'
@import '../../node_modules/bulma/sass/elements/form' @import '~bulma/sass/elements/box'
@import '../../node_modules/bulma/sass/elements/table' @import '~bulma/sass/elements/form'
@import '../../node_modules/bulma/sass/components/navbar' @import '~bulma/sass/elements/table'
@import '../../node_modules/bulma/sass/components/tabs' @import '~bulma/sass/components/navbar'
@import '../../node_modules/bulma/sass/elements/notification' @import '~bulma/sass/components/tabs'
@import '~bulma/sass/elements/notification'
@import 'nav' @import 'nav'
@import 'content' @import 'content'
@import 'message' @import 'message'
@import 'label'
@import 'charts' @import 'charts'
@import 'helper' @import 'helper'

View file

@ -30,12 +30,6 @@
height: 320px height: 320px
background-color: $white background-color: $white
.bar
fill: rgba($blue, 0.91)
&:hover
fill: lighten($blue, 10)
.axis text .axis text
fill: $text fill: $text
font: 10px sans-serif font: 10px sans-serif

View file

@ -1,46 +1,21 @@
.content .content
background: transparent background: transparent
margin: 40px 0 margin: 2rem 0
.subtitle .subtitle
font-size: 15px
text-transform: uppercase
color: $black color: $black
font-size: 0.9rem
font-weight: $weight-bold font-weight: $weight-bold
text-transform: uppercase text-transform: uppercase
margin: 10px 0 0 0
.list-title .subtitle-name
color: $text-dark padding-left: 0.5rem
weight: $weight-semibold
margin: 5px 0 0 0
.list-item
width: 100%
display: block
align-items: center
font-size: 12px
padding: 6px 10px
border-top: 1px solid $border-light
.columns
.column
display: flex
align-items: center
.icon
width: 22px
height: 22px
display: block
float: left
margin-right: 10px
.content-item .content-item
background: $white background: $white
border: 1px solid $border-secondary border: 1px solid $border-secondary
margin: 10px 0 margin: 10px 0
border-radius: 4px border-radius: $traefik-border-radius
box-shadow: 1px 2px 5px rgba($border, 0.4) box-shadow: 1px 2px 5px rgba($border, 0.4)
h2 h2
@ -82,7 +57,7 @@
img img
width: 40px width: 40px
heught: 40px height: 40px
display: block display: block
float: left float: left
margin-right: 10px margin-right: 10px
@ -106,37 +81,27 @@
margin: 15px auto margin: 15px auto
.search-container .search-container
height: 50px
background: $white background: $white
border-radius: 4px
color: $black color: $black
margin: 10px 0
display: flex display: flex
align-items: center align-items: center
position: relative border-radius: $traefik-border-radius
box-shadow: 1px 2px 5px rgba($border, 0.4) box-shadow: 1px 2px 5px rgba($border, 0.4)
border: 1px solid $border-secondary border: 1px solid $border-secondary
position: relative
height: 3rem
.icon .search-button
position: absolute position: absolute
left: 10px left: 1rem
top: 13px top: 0.8rem
input input
font-size: 16px
color: $text color: $text
width: 100%
height: 48px
padding-left: 50px
border: none border: none
border-radius: $traefik-border-radius
outline: none outline: none
font-size: 1rem
font-weight: $weight-light font-weight: $weight-light
border-radius: 4px width: 100%
padding-left: 2.8rem
.notification
background: $white
border-radius: 4px
color: $text
font-size: 16px
box-shadow: 1px 2px 5px rgba($border, 0.4)
border: 1px solid $border-secondary

View file

@ -1,29 +0,0 @@
.label
padding: 5px 10px
background: $white
color: $color
font-size: 12px
font-family: $weight-semibold
width: 100%
display: flex
align-items: center
justify-content: center
border: 1px solid $border
background: linear-gradient(0deg, #F2F4F7 0%, #FFFFFF 100%)
&.green
background: $green-secondary
&.red
background: $red-secondary
&.yellow
background: $yellow-secondary
&.blue
background: $blue-secondary
span
display: inline-flex
float: left
align-items: center

View file

@ -1,89 +1,65 @@
.message .message
display: block display: block
font-size: 14px font-size: 0.8rem
margin: 20px 0 30px 0 margin: 1rem 0 1.5rem 0
padding-bottom: 0.3rem
border: 1px solid $border border: 1px solid $border
background: $white background: $white
border-radius: 4px border-radius: $traefik-border-radius
box-shadow: 1px 2px 5px rgba($border, 0.4) box-shadow: 1px 2px 5px rgba($border, 0.4)
.message-header .message-header
color: $color-secondary color: $color-secondary
border-bottom: 1px solid $border-secondary border-bottom: 1px solid $border-secondary
padding: 20px 10px padding: 0.6rem
background: #f8f9fa border-top-left-radius: $traefik-border-radius
border-top-left-radius: 4px border-top-right-radius: $traefik-border-radius
border-top-right-radius: 4px
.icon
display: block
float: left
width: 1.4rem
height: 1.4rem
margin-right: 0.5rem
h2 h2
font-size: 14px
weight: $weight-bold
display: flex display: flex
justify-content: space-between
&.red
background: rgba($red-secondary, 0.4)
border-bottom: 1px solid $red-secondary
color: $red-secondary
p
color: $red-secondary
&.green
background-color: rgba($green-secondary, 0.4)
border-bottom: 1px solid $green-secondary
color: $green-secondary
p
color: darken($green-secondary, 10) !important
&.orange
background-color: rgba($orange-secondary, 0.4)
border-bottom: 1px solid $orange-secondary
color: $orange-secondary
p
color: $orange-secondary
&.blue
background-color: rgba($blue-background, 0.4)
border-bottom: 1px solid $blue-background
color: $blue-background
p
color: $blue-background !important
img img
margin-right: 15px margin-right: 15px
.message-body .message-body
.field .tabs
margin: 5px 10px margin-bottom: 0.5rem
padding-bottom: 10px
.tags-list .section-container
margin: 5px 10px padding: 0.3em 0 0 0
.control .section-line
width: 100% padding: 0 0.75em
margin: 5px 0
.tags .section-line-header
width: 100% padding: 0.2em 0 0 0
.tag // required for small screen (without -> table overlapping)
width: 50% .table-fixed
table-layout: fixed
// required for small screen (without -> table overlapping)
.table-fixed-break
table-layout: fixed
word-wrap: break-word
.table-cell-limited
overflow: hidden
text-overflow: ellipsis
.table-col-75
width: 75%
h2 h2
margin: 10px 10px 0 10px
color: $black color: $black
hr hr
margin: 5px 0 margin: 5px 0
.message-subheader
border-bottom: 1px solid $border-secondary
padding: 10px
margin-bottom: 5px

View file

@ -1,16 +1,12 @@
.navbar .navbar
border-bottom: 1px solid $border border-bottom: 1px solid $border
box-shadow: 1px 2px 5px rgba($border, 0.4) box-shadow: 1px 2px 5px rgba($border, 0.4)
height: 60px
.navbar-item .navbar-item
font-size: 13px font-size: 0.8rem
text-transform: uppercase text-transform: uppercase
font-weight: $weight-semibold font-weight: $weight-semibold
.navbar-logo .navbar-logo
width: 40px width: 40px
min-height: 40px min-height: 40px
&:hover
background: transparent

View file

@ -1,14 +1,14 @@
=font-face($family, $path, $weight: normal, $style: normal) =font-face($family, $path, $weight: normal, $style: normal)
@font-face @font-face
font-family: $family font-family: $family
src: url('#{$path}.ttf') format('truetype') src: url('./#{$path}.ttf') format('truetype')
font-weight: $weight font-weight: $weight
font-style: $style font-style: $style
+font-face('Open Sans', '/assets/fonts/OpenSans-Light', 300, 'light') +font-face('Open Sans', 'assets/fonts/OpenSans-Light', 300, 'light')
+font-face('Open Sans', '/assets/fonts/OpenSans-Regular', 400, 'regular') +font-face('Open Sans', 'assets/fonts/OpenSans-Regular', 400, 'regular')
+font-face('Open Sans', '/assets/fonts/OpenSans-Semibold', 600, 'semibold') +font-face('Open Sans', 'assets/fonts/OpenSans-Semibold', 600, 'semibold')
+font-face('Open Sans', '/assets/fonts/OpenSans-Bold', 700, 'bold') +font-face('Open Sans', 'assets/fonts/OpenSans-Bold', 700, 'bold')
+font-face('Open Sans', '/assets/fonts/OpenSans-ExtraBold', 800, 'extrabold') +font-face('Open Sans', 'assets/fonts/OpenSans-ExtraBold', 800, 'extrabold')
$open-sans: 'Open Sans', sans-serif $open-sans: 'Open Sans', sans-serif

View file

@ -0,0 +1 @@
$traefik-border-radius: 4px

View file

@ -1031,9 +1031,9 @@ builtin-status-codes@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bulma@^0.6.2: bulma@^0.7.0:
version "0.6.2" version "0.7.1"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.6.2.tgz#f4b1d11d5acc51a79644eb0a2b0b10649d3d71f5" resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.1.tgz#73c2e3b2930c90cc272029cbd19918b493fca486"
bytes@3.0.0: bytes@3.0.0:
version "3.0.0" version "3.0.0"