feat(webui): migrate to a work in progress webui
This commit is contained in:
parent
3cfbe7cf6d
commit
2d54065082
23 changed files with 18 additions and 2392 deletions
|
@ -25,3 +25,13 @@ html
|
||||||
font-family: $open-sans
|
font-family: $open-sans
|
||||||
height: 100%
|
height: 100%
|
||||||
background: $background
|
background: $background
|
||||||
|
|
||||||
|
.wip
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
height: 80vh
|
||||||
|
|
||||||
|
.title
|
||||||
|
font-size: 4em
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
||||||
import { async, TestBed } from '@angular/core/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [AppComponent],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should create the app', async(() => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.debugElement.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
}));
|
|
||||||
});
|
|
|
@ -3,8 +3,12 @@ import { Component } from '@angular/core';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: `
|
template: `
|
||||||
<app-header></app-header>
|
<main class="wip">
|
||||||
<router-outlet></router-outlet>
|
<img src="./assets/images/traefik.logo.svg" alt="logo" />
|
||||||
|
<header>
|
||||||
|
<h1 class="title">Work in progress...</h1>
|
||||||
|
</header>
|
||||||
|
</main>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class AppComponent {}
|
export class AppComponent {}
|
||||||
|
|
|
@ -3,46 +3,11 @@ import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { AppComponent } from './app.component';
|
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 { HumanReadableFilterPipe } from './pipes/humanreadable.filter.pipe';
|
|
||||||
import { KeysPipe } from './pipes/keys.pipe';
|
|
||||||
import { ApiService } from './services/api.service';
|
|
||||||
import { WindowService } from './services/window.service';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [AppComponent],
|
||||||
AppComponent,
|
imports: [BrowserModule, CommonModule, HttpClientModule, FormsModule],
|
||||||
HeaderComponent,
|
|
||||||
ProvidersComponent,
|
|
||||||
HealthComponent,
|
|
||||||
LineChartComponent,
|
|
||||||
BarChartComponent,
|
|
||||||
KeysPipe,
|
|
||||||
FrontendFilterPipe,
|
|
||||||
BackendFilterPipe,
|
|
||||||
HumanReadableFilterPipe,
|
|
||||||
LetDirective
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
CommonModule,
|
|
||||||
HttpClientModule,
|
|
||||||
FormsModule,
|
|
||||||
RouterModule.forRoot([
|
|
||||||
{ path: '', component: ProvidersComponent, pathMatch: 'full' },
|
|
||||||
{ path: 'status', component: HealthComponent }
|
|
||||||
])
|
|
||||||
],
|
|
||||||
providers: [ApiService, WindowService],
|
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { WindowService } from '../../services/window.service';
|
|
||||||
import { BarChartComponent } from './bar-chart.component';
|
|
||||||
|
|
||||||
describe('BarChartComponent', () => {
|
|
||||||
let component: BarChartComponent;
|
|
||||||
let fixture: ComponentFixture<BarChartComponent>;
|
|
||||||
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [BarChartComponent],
|
|
||||||
providers: [{ provide: WindowService, useInstance: {} }]
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(BarChartComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initially go to loading state', () => {
|
|
||||||
expect(component.loading).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,141 +0,0 @@
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
ElementRef,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
SimpleChanges
|
|
||||||
} from '@angular/core';
|
|
||||||
import { axisBottom, axisLeft, max, scaleBand, scaleLinear, select } from 'd3';
|
|
||||||
import { format } from 'd3-format';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import { WindowService } from '../../services/window.service';
|
|
||||||
|
|
||||||
@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;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
margin = { top: 40, right: 40, bottom: 40, left: 40 };
|
|
||||||
loading: boolean;
|
|
||||||
data: any[];
|
|
||||||
previousData: 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), 1000);
|
|
||||||
|
|
||||||
this.windowService.resize.subscribe(w => this.draw());
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
|
||||||
if (!this.value || !this.svg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEqual(this.previousData, this.value)) {
|
|
||||||
this.previousData = _.cloneDeep(this.value);
|
|
||||||
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 {
|
|
||||||
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.y.domain([0, max(this.data, (d: any) => d.count)]);
|
|
||||||
|
|
||||||
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)
|
|
||||||
.tickFormat(format('~s'))
|
|
||||||
.tickSize(-this.width)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean previous graph
|
|
||||||
this.g.selectAll('.bar').remove();
|
|
||||||
|
|
||||||
const bars = this.g.selectAll('.bar').data(this.data);
|
|
||||||
|
|
||||||
bars
|
|
||||||
.enter()
|
|
||||||
.append('rect')
|
|
||||||
.attr('class', 'bar')
|
|
||||||
.style(
|
|
||||||
'fill',
|
|
||||||
(d: any) =>
|
|
||||||
'hsl(' + Math.floor(((d.code - 100) * 310) / 427 + 50) + ', 50%, 50%)'
|
|
||||||
)
|
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
<div class="line-chart" [class.is-hidden]="loading"></div>
|
|
||||||
<div class="loading-text line-chart-loading" [class.is-hidden]="!loading">
|
|
||||||
<span>
|
|
||||||
<span>Loading, please wait...</span>
|
|
||||||
<img src="./assets/images/loader.svg" class="main-loader" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
|
@ -1,240 +0,0 @@
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
ElementRef,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
SimpleChanges
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
axisBottom,
|
|
||||||
axisLeft,
|
|
||||||
curveLinear,
|
|
||||||
easeLinear,
|
|
||||||
line,
|
|
||||||
max,
|
|
||||||
min,
|
|
||||||
range,
|
|
||||||
scaleLinear,
|
|
||||||
scaleTime,
|
|
||||||
select,
|
|
||||||
timeFormat,
|
|
||||||
timeSecond
|
|
||||||
} from 'd3';
|
|
||||||
import { WindowService } from '../../services/window.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-line-chart',
|
|
||||||
templateUrl: 'line-chart.component.html'
|
|
||||||
})
|
|
||||||
export class LineChartComponent implements OnChanges, OnInit {
|
|
||||||
@Input() value: { count: number; date: string };
|
|
||||||
|
|
||||||
firstDisplay: boolean;
|
|
||||||
dirty: boolean;
|
|
||||||
lineChartEl: HTMLElement;
|
|
||||||
loadingEl: 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.loadingEl = this.elementRef.nativeElement.querySelector(
|
|
||||||
'.line-chart-loading'
|
|
||||||
);
|
|
||||||
this.limit = 40;
|
|
||||||
|
|
||||||
// related to the Observable.timer(0, 3000) in health component
|
|
||||||
this.duration = 3000;
|
|
||||||
|
|
||||||
this.now = new Date(Date.now() - this.duration);
|
|
||||||
|
|
||||||
this.options = {
|
|
||||||
title: '',
|
|
||||||
color: '#3A84C5'
|
|
||||||
};
|
|
||||||
|
|
||||||
this.firstDisplay = true;
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
this.windowService.resize.subscribe(w => {
|
|
||||||
if (this.svg) {
|
|
||||||
this.dirty = true;
|
|
||||||
this.loading = true;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
// When the lineChartEl is not displayed (is-hidden), width and length are equal to 0.
|
|
||||||
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;
|
|
||||||
|
|
||||||
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('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 - 10]);
|
|
||||||
this.y = scaleLinear().range([this.height, 0]);
|
|
||||||
|
|
||||||
this.x.domain([
|
|
||||||
(this.now as any) - (this.limit - 2),
|
|
||||||
(this.now as any) - this.duration
|
|
||||||
]);
|
|
||||||
this.y.domain([0, max(this.data, (d: any) => d)]);
|
|
||||||
|
|
||||||
this.line = line()
|
|
||||||
.x((d: any, i: number) =>
|
|
||||||
this.x((this.now as any) - (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([
|
|
||||||
(this.now as any) - (this.limit - 2) * this.duration,
|
|
||||||
(this.now as any) - 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.firstDisplay || this.dirty ? 0 : this.duration)
|
|
||||||
.ease(easeLinear)
|
|
||||||
.call(
|
|
||||||
axisBottom(this.x)
|
|
||||||
.tickSize(-this.height)
|
|
||||||
.ticks(timeSecond, 5)
|
|
||||||
.tickFormat(timeFormat('%H:%M:%S'))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.xAxis
|
|
||||||
.transition()
|
|
||||||
.duration(0)
|
|
||||||
.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(
|
|
||||||
(this.now as any) - (this.limit - 1) * this.duration
|
|
||||||
)})`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.firstDisplay = false;
|
|
||||||
this.dirty = false;
|
|
||||||
|
|
||||||
if (this.loading) {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data.shift();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
<nav
|
|
||||||
class="navbar is-fixed-top is-transparent"
|
|
||||||
role="navigation"
|
|
||||||
aria-label="main navigation"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<div class="navbar-brand">
|
|
||||||
<a class="navbar-item" routerLink="/" (click)="burger = false">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div class="navbar-end">
|
|
||||||
<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>
|
|
|
@ -1,24 +0,0 @@
|
||||||
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;
|
|
||||||
burger: boolean;
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
<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" [title]="exactTotalResponseTime">{{
|
|
||||||
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" [title]="exactAverageResponseTime">{{
|
|
||||||
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; trackBy: trackRecentErrors">
|
|
||||||
<td>
|
|
||||||
<span class="tag is-info" [title]="entry.status">{{
|
|
||||||
entry.status_code
|
|
||||||
}}</span
|
|
||||||
> <span class="is-hidden-mobile is-hidden-desktop-only">{{
|
|
||||||
entry.status
|
|
||||||
}}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="tag">{{ entry.method }}</span
|
|
||||||
> <span>{{ entry.host }}{{ entry.path }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="!recentErrors?.length">
|
|
||||||
<td colspan="3">
|
|
||||||
<p class="text-muted text-center">No entries</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,83 +0,0 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import { distanceInWordsStrict, format, subSeconds } from 'date-fns';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import { Subscription, timer } from 'rxjs';
|
|
||||||
import { mergeMap, timeInterval } from 'rxjs/operators';
|
|
||||||
import { ApiService } from '../../services/api.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-health',
|
|
||||||
templateUrl: 'health.component.html'
|
|
||||||
})
|
|
||||||
export class HealthComponent implements OnInit, OnDestroy {
|
|
||||||
sub: Subscription;
|
|
||||||
recentErrors: any;
|
|
||||||
previousRecentErrors: any;
|
|
||||||
pid: number;
|
|
||||||
uptime: string;
|
|
||||||
uptimeSince: string;
|
|
||||||
averageResponseTime: string;
|
|
||||||
exactAverageResponseTime: string;
|
|
||||||
totalResponseTime: string;
|
|
||||||
exactTotalResponseTime: string;
|
|
||||||
codeCount: number;
|
|
||||||
totalCodeCount: number;
|
|
||||||
chartValue: any;
|
|
||||||
statusCodeValue: any;
|
|
||||||
|
|
||||||
constructor(private apiService: ApiService) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.sub = timer(0, 3000)
|
|
||||||
.pipe(
|
|
||||||
timeInterval(),
|
|
||||||
mergeMap(() => this.apiService.fetchHealthStatus())
|
|
||||||
)
|
|
||||||
.subscribe(data => {
|
|
||||||
if (data) {
|
|
||||||
if (!_.isEqual(this.previousRecentErrors, data.recent_errors)) {
|
|
||||||
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).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),
|
|
||||||
'YYYY-MM-DD HH:mm:ss Z'
|
|
||||||
);
|
|
||||||
this.totalResponseTime = distanceInWordsStrict(
|
|
||||||
subSeconds(new Date(), data.total_response_time_sec),
|
|
||||||
new Date()
|
|
||||||
);
|
|
||||||
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.totalCodeCount = data.total_count;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
if (this.sub) {
|
|
||||||
this.sub.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trackRecentErrors(index, item): string {
|
|
||||||
return item.status_code + item.method + item.host + item.path + item.time;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,58 +0,0 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import { Subscription, timer } from 'rxjs';
|
|
||||||
import { mergeMap, timeInterval } from 'rxjs/operators';
|
|
||||||
import { ApiService } from '../../services/api.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-providers',
|
|
||||||
templateUrl: 'providers.component.html'
|
|
||||||
})
|
|
||||||
export class ProvidersComponent implements OnInit, OnDestroy {
|
|
||||||
sub: Subscription;
|
|
||||||
maxItem: number;
|
|
||||||
keys: string[];
|
|
||||||
previousKeys: string[];
|
|
||||||
previousData: any;
|
|
||||||
providers: any;
|
|
||||||
tab: string;
|
|
||||||
keyword: string;
|
|
||||||
|
|
||||||
constructor(private apiService: ApiService) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.maxItem = 100;
|
|
||||||
this.keyword = '';
|
|
||||||
this.sub = timer(0, 2000)
|
|
||||||
.pipe(
|
|
||||||
timeInterval(),
|
|
||||||
mergeMap(() => this.apiService.fetchProviders())
|
|
||||||
)
|
|
||||||
.subscribe(data => {
|
|
||||||
if (!_.isEqual(this.previousData, data)) {
|
|
||||||
this.previousData = _.cloneDeep(data);
|
|
||||||
this.providers = data;
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
trackItem(tab): (index, item) => string {
|
|
||||||
return (index, item): string => tab + '-' + item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
if (this.sub) {
|
|
||||||
this.sub.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { HumanReadableFilterPipe } from './humanreadable.filter.pipe';
|
|
||||||
|
|
||||||
describe('HumanReadableFilterPipe', () => {
|
|
||||||
const pipe = new HumanReadableFilterPipe();
|
|
||||||
|
|
||||||
const datatable = [
|
|
||||||
{
|
|
||||||
given: '180000000000',
|
|
||||||
expected: '180s'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
given: '4096.0',
|
|
||||||
expected: '4096ns'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
given: '7200000000000',
|
|
||||||
expected: '120m'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
given: '1337',
|
|
||||||
expected: '1337ns'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
given: 'traefik',
|
|
||||||
expected: 'traefik'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
given: '-23',
|
|
||||||
expected: '-23'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
given: '0',
|
|
||||||
expected: '0'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
datatable.forEach(item => {
|
|
||||||
it(item.given + ' should be transformed to ' + item.expected, () => {
|
|
||||||
expect(pipe.transform(item.given)).toEqual(item.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('create an instance', () => {
|
|
||||||
expect(pipe).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HumanReadableFilterPipe converts a time period in nanoseconds to a human-readable
|
|
||||||
* string.
|
|
||||||
*/
|
|
||||||
@Pipe({ name: 'humanreadable' })
|
|
||||||
export class HumanReadableFilterPipe implements PipeTransform {
|
|
||||||
transform(value): any {
|
|
||||||
let result = '';
|
|
||||||
const powerOf10 = Math.floor(Math.log10(value));
|
|
||||||
|
|
||||||
if (powerOf10 > 11) {
|
|
||||||
result = value / (60 * Math.pow(10, 9)) + 'm';
|
|
||||||
} else if (powerOf10 > 9) {
|
|
||||||
result = value / Math.pow(10, 9) + 's';
|
|
||||||
} else if (powerOf10 > 6) {
|
|
||||||
result = value / Math.pow(10, 6) + 'ms';
|
|
||||||
} else if (value > 0) {
|
|
||||||
result = Math.floor(value) + 'ns';
|
|
||||||
} else {
|
|
||||||
result = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core';
|
|
||||||
|
|
||||||
@Pipe({ name: 'keys' })
|
|
||||||
export class KeysPipe implements PipeTransform {
|
|
||||||
transform(value, args: string[]): any {
|
|
||||||
return Object.keys(value);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
import {
|
|
||||||
HttpClient,
|
|
||||||
HttpErrorResponse,
|
|
||||||
HttpHeaders
|
|
||||||
} from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Observable, EMPTY, of } from 'rxjs';
|
|
||||||
import { catchError, map, retry } from 'rxjs/operators';
|
|
||||||
|
|
||||||
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 }).pipe(
|
|
||||||
retry(4),
|
|
||||||
catchError((err: HttpErrorResponse) => {
|
|
||||||
console.error(
|
|
||||||
`[version] returned code ${err.status}, body was: ${err.error}`
|
|
||||||
);
|
|
||||||
return EMPTY;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchHealthStatus(): Observable<any> {
|
|
||||||
return this.http.get('../health', { headers: this.headers }).pipe(
|
|
||||||
retry(2),
|
|
||||||
catchError((err: HttpErrorResponse) => {
|
|
||||||
console.error(
|
|
||||||
`[health] returned code ${err.status}, body was: ${err.error}`
|
|
||||||
);
|
|
||||||
return EMPTY;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchProviders(): Observable<any> {
|
|
||||||
return this.http.get('../api/providers', { headers: this.headers }).pipe(
|
|
||||||
retry(2),
|
|
||||||
catchError((err: HttpErrorResponse) => {
|
|
||||||
console.error(
|
|
||||||
`[providers] returned code ${err.status}, body was: ${err.error}`
|
|
||||||
);
|
|
||||||
return of<any>({});
|
|
||||||
}),
|
|
||||||
map((data: any): ProviderType => this.parseProviders(data))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseProviders(data: any): ProviderType {
|
|
||||||
return Object.keys(data)
|
|
||||||
.filter(value => value !== 'acme' && value !== 'ACME')
|
|
||||||
.reduce((acc, curr) => {
|
|
||||||
acc[curr] = {};
|
|
||||||
|
|
||||||
acc[curr].frontends = this.toArray(data[curr].frontends, 'id').map(
|
|
||||||
frontend => {
|
|
||||||
frontend.routes = this.toArray(frontend.routes, 'id');
|
|
||||||
frontend.errors = this.toArray(frontend.errors, 'id');
|
|
||||||
if (frontend.headers) {
|
|
||||||
frontend.headers.customRequestHeaders = this.toHeaderArray(
|
|
||||||
frontend.headers.customRequestHeaders
|
|
||||||
);
|
|
||||||
frontend.headers.customResponseHeaders = this.toHeaderArray(
|
|
||||||
frontend.headers.customResponseHeaders
|
|
||||||
);
|
|
||||||
frontend.headers.sslProxyHeaders = this.toHeaderArray(
|
|
||||||
frontend.headers.sslProxyHeaders
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (frontend.ratelimit && frontend.ratelimit.rateset) {
|
|
||||||
frontend.ratelimit.rateset = this.toArray(
|
|
||||||
frontend.ratelimit.rateset,
|
|
||||||
'id'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return frontend;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
acc[curr].backends = this.toArray(data[curr].backends, 'id').map(
|
|
||||||
backend => {
|
|
||||||
backend.servers = this.toArray(backend.servers, 'id');
|
|
||||||
return backend;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { EventManager } from '@angular/platform-browser';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WindowService {
|
|
||||||
resize: Subject<any>;
|
|
||||||
|
|
||||||
constructor(private eventManager: EventManager) {
|
|
||||||
this.resize = new Subject();
|
|
||||||
this.eventManager.addGlobalEventListener(
|
|
||||||
'window',
|
|
||||||
'resize',
|
|
||||||
(event: UIEvent) => {
|
|
||||||
this.resize.next(event.target);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue