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",
"outDir": "dist",
"assets": [
"assets",
"assets/images",
"favicon.ico"
],
"index": "index.html",
@ -19,7 +19,7 @@
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles/app.sass"
"app.sass"
],
"scripts": [
"../node_modules/@fortawesome/fontawesome/index.js",

View file

@ -27,7 +27,7 @@
"@angular/router": "^5.2.0",
"@fortawesome/fontawesome": "^1.1.5",
"@fortawesome/fontawesome-free-solid": "^5.0.10",
"bulma": "^0.6.2",
"bulma": "^0.7.0",
"core-js": "^2.4.1",
"d3": "^4.13.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';
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 { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
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 { WindowService } from './services/window.service';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';
import { ProvidersComponent } from './components/providers/providers.component';
import { HealthComponent } from './components/health/health.component';
import { LineChartComponent } from './charts/line-chart/line-chart.component';
import { BarChartComponent } from './charts/bar-chart/bar-chart.component';
import { KeysPipe } from './pipes/keys.pipe';
@NgModule({
declarations: [
@ -22,7 +25,10 @@ import { KeysPipe } from './pipes/keys.pipe';
HealthComponent,
LineChartComponent,
BarChartComponent,
KeysPipe
KeysPipe,
FrontendFilterPipe,
BackendFilterPipe,
LetDirective
],
imports: [
BrowserModule,
@ -30,8 +36,8 @@ import { KeysPipe } from './pipes/keys.pipe';
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: ProvidersComponent, pathMatch: 'full' },
{ path: 'status', component: HealthComponent }
{path: '', component: ProvidersComponent, pathMatch: 'full'},
{path: 'status', component: HealthComponent}
])
],
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 {
min,
max,
easeLinear,
select,
axisLeft,
axisBottom,
scaleBand,
scaleLinear
} from 'd3';
@Component({
selector: 'app-bar-chart',
@ -23,12 +15,12 @@ export class BarChartComponent implements OnInit, OnChanges {
x: any;
y: any;
g: any;
bars: any;
width: number;
height: number;
margin = { top: 40, right: 40, bottom: 40, left: 40 };
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;
@ -37,7 +29,7 @@ export class BarChartComponent implements OnInit, OnChanges {
ngOnInit() {
this.barChartEl = this.elementRef.nativeElement.querySelector('.bar-chart');
this.setup();
setTimeout(() => this.loading = false, 4000);
setTimeout(() => this.loading = false, 1000);
this.windowService.resize.subscribe(w => this.draw());
}
@ -47,15 +39,20 @@ export class BarChartComponent implements OnInit, OnChanges {
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')
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);
@ -73,11 +70,16 @@ export class BarChartComponent implements OnInit, OnChanges {
}
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.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
this.svg
.attr('width', this.width + this.margin.left + this.margin.right)
@ -93,17 +95,16 @@ export class BarChartComponent implements OnInit, OnChanges {
this.g.select('.axis--y')
.call(axisLeft(this.y).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')
.attr('x', (d: any) => d.code)
.attr('y', (d: any) => d.count)
.attr('width', this.x.bandwidth())
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
bars.attr('x', (d: any) => this.x(d.code))
.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));

View file

@ -1,5 +1,5 @@
<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>Loading, please wait...</span>
<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 { WindowService } from '../../services/window.service';
import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
range,
scaleTime,
scaleLinear,
min,
max,
curveLinear,
line,
easeLinear,
select,
axisLeft,
axisBottom,
timeSecond,
timeFormat
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',
@ -23,7 +23,10 @@ import {
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;
@ -39,15 +42,19 @@ export class LineChartComponent implements OnChanges, OnInit {
yAxis: any;
height: number;
width: number;
margin = { top: 40, right: 40, bottom: 60, left: 60 };
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 = {
@ -55,22 +62,37 @@ export class LineChartComponent implements OnChanges, OnInit {
color: '#3A84C5'
};
this.firstDisplay = true;
this.render();
setTimeout(() => this.loading = false, 4000);
this.windowService.resize.subscribe(w => {
if (this.svg) {
const el = this.lineChartEl.querySelector('svg');
el.parentNode.removeChild(el);
this.dirty = true;
this.loading = true;
this.render();
}
});
}
render() {
this.width = this.lineChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.lineChartEl.clientHeight - this.margin.top - this.margin.bottom;
// 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;
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('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
@ -80,7 +102,7 @@ export class LineChartComponent implements OnChanges, OnInit {
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.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))
.curve(curveLinear);
this.svg.append('defs').append('clipPath')
this.svg
.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', this.width)
@ -121,7 +145,7 @@ export class LineChartComponent implements OnChanges, OnInit {
this.updateData(this.value.count);
}
updateData = (value: number) => {
updateData(value: number) {
this.data.push(value * 1000000);
this.now = new Date();
@ -132,9 +156,13 @@ export class LineChartComponent implements OnChanges, OnInit {
this.xAxis
.transition()
.duration(this.duration)
.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')))
.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')
@ -157,6 +185,13 @@ export class LineChartComponent implements OnChanges, OnInit {
.ease(easeLinear)
.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();
}
}

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="navbar-menu">
<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">
</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">
<div class="navbar-menu">
<a class="navbar-item" routerLink="/" routerLinkActive="is-active" [routerLinkActiveOptions]="{ exact: true }">
<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">
<a class="navbar-item" routerLink="/status" routerLinkActive="is-active" (click)="burger = false">
Health
</a>
</div>
</div>
<div class="navbar-end is-hidden-mobile">
<div class="navbar-end">
<a class="navbar-item" [href]="releaseLink" target="_blank">
{{ version }} / {{ codename }}
</a>
@ -25,5 +30,6 @@
</a>
</div>
</div>
</div>
</nav>

View file

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

View file

@ -9,7 +9,7 @@
<div class="column is-4">
<div class="item-data border-right">
<span class="data-grey">Total Response Time</span>
<span class="data-blue">{{ totalResponseTime }}</span>
<span class="data-blue" [title]="exactTotalResponseTime">{{ totalResponseTime }}</span>
</div>
</div>
<div class="column is-4">
@ -33,7 +33,7 @@
<div class="column is-4">
<div class="item-data border-right">
<span class="data-grey">Average Response Time</span>
<span class="data-blue">{{ averageResponseTime }}</span>
<span class="data-blue" [title]="exactAverageResponseTime">{{ averageResponseTime }}</span>
</div>
</div>
<div class="column is-4">
@ -82,15 +82,15 @@
<td>Request</td>
<td>Time</td>
</tr>
<tr *ngFor="let entry of recentErrors">
<tr *ngFor="let entry of recentErrors; trackBy: trackRecentErrors;">
<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>
<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>
<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>
</tr>
<tr *ngIf="!recentErrors?.length">

View file

@ -1,12 +1,13 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Component, OnDestroy, OnInit } from '@angular/core';
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 { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/timeInterval';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/map';
import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-health',
@ -15,11 +16,14 @@ import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
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;
@ -33,16 +37,22 @@ export class HealthComponent implements OnInit, OnDestroy {
.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.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] }));
.map(key => ({code: key, count: data.total_status_code_count[key]}));
this.pid = data.pid;
this.uptime = distanceInWordsStrict(subSeconds(new Date(), data.uptime_sec), new Date());
this.uptimeSince = format(subSeconds(new Date(), data.uptime_sec), 'MM/DD/YYYY HH:mm:ss');
this.totalResponseTime = data.total_response_time;
this.averageResponseTime = data.average_response_time;
this.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;
}
@ -54,4 +64,8 @@ export class HealthComponent implements OnInit, OnDestroy {
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="search-container">
<span class="icon"><i class="fas fa-search"></i></span>
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword" (ngModelChange)="filter()">
<span class="icon search-button" *ngIf="!keyword"><i class="fas fa-search"></i></span>
<a class="delete search-button" *ngIf="keyword" (click)="keyword = ''"></a>
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword">
</div>
<div class="tabs" *ngIf="keys?.length">
@ -20,30 +21,17 @@
<div *ngIf="keys?.length">
<div class="columns">
<!-- Frontends -->
<div class="column is-6">
<h2 class="subtitle"><span class="tag is-info">{{ providers[tab]?.frontends.length }}</span> Frontends</h2>
<div class="message" *ngFor="let p of providers[tab]?.frontends; let i = index;">
<div class="message-header">
<div class="column is-6" *appLet="providers[tab]?.frontends | frontendFilter:keyword as frontends">
<h2 class="subtitle"><span class="tag is-info">{{ frontends.length }}</span><span class="subtitle-name">Frontends</span></h2>
<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>
<i class="icon fas fa-globe has-text-white"></i>
<div>
<i class="icon fas fa-globe"></i>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-info">{{ p.id }}</span>
</div>
</div>
</div>
</div>
<div *ngIf="p.backend">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<a class="tags has-addons" [href]="'#' + p.backend">
<span class="tag is-light">Backend</span>
<span class="tag is-primary">{{ p.backend }}</span>
</a>
</div>
</div>
<span class="has-text-white" [class.is-info]="p.backend" [class.is-danger]="!p.backend">{{ p.id }}</span>
</div>
</h2>
</div>
@ -57,16 +45,16 @@
</div>
<!-- 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">
<tbody>
<tr>
<td>Route Rule</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 *ngFor="let route of p.routes">
<td><code class="has-text-grey" [title]="route.id">{{ route.rule }}</code></td>
</tr>
</tbody>
</table>
@ -74,15 +62,15 @@
<div *ngIf="p.entryPoints && p.entryPoints.length">
<hr>
<div class="columns">
<div class="columns section-line">
<div class="column is-3">
<h2>Entry Points</h2>
<h2 class="section-line-header">Entry Points</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags">
<span class="tag is-info" *ngFor="let ep of p.entryPoints; let ri = index;">{{ ep }}</span>
<span class="tag is-info" *ngFor="let ep of p.entryPoints">{{ ep }}</span>
</div>
</div>
</div>
@ -90,19 +78,34 @@
</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>
<!-- Details -->
<div *ngIf="p.section === 'details'">
<div *ngIf="p.section === 'details'" class="section-container">
<div>
<div class="section-line">
<div class="columns">
<div class="column is-3">
<h2>Misc.</h2>
<h2 class="section-line-header">Misc.</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="control" *ngIf="p.priority">
<div class="tags has-addons">
<span class="tag is-light">Priority</span>
<span class="tag is-info">{{ p.priority }}</span>
@ -111,7 +114,7 @@
<div class="control">
<div class="tags has-addons">
<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 class="control" *ngIf="p.passTLSCert">
@ -127,9 +130,9 @@
<div *ngIf="p.redirect">
<hr>
<div class="columns">
<div class="columns section-line">
<div class="column is-3">
<h2>Redirect</h2>
<h2 class="section-line-header">Redirect</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline" *ngIf="p.redirect.entryPoint">
@ -160,15 +163,18 @@
<div *ngIf="p.basicAuth && p.basicAuth.length">
<hr/>
<h2>Basic Authentication</h2>
<div class="section-line">
<h2 class="section-line-header">Basic Authentication</h2>
<div class="tags padding-5-10">
<span class="tag is-info" *ngFor="let auth of p.basicAuth; let ri = index;">{{ auth }}</span>
<span class="tag is-info" *ngFor="let auth of p.basicAuth">{{ auth }}</span>
</div>
</div>
</div>
<div *ngIf="p.errors">
<div *ngIf="p.errors?.length">
<hr/>
<h2>Error Pages</h2>
<div class="section-line">
<h2 class="section-line-header">Error Pages</h2>
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr>
@ -176,29 +182,30 @@
<td>Query</td>
<td>Status</td>
</tr>
<tr *ngFor="let key of p.errors | keys">
<td><span class="has-text-grey-light">{{ p.errors[key].backend }}</span></td>
<td><span class="has-text-grey">{{ p.errors[key].query }}</span></td>
<tr *ngFor="let entry of p.errors">
<td><span class="has-text-grey-light">{{ entry.backend }}</span></td>
<td><span class="has-text-grey">{{ entry.query }}</span></td>
<td>
<span class="tag is-light" *ngFor="let state of p.errors[key].status">{{ state }}</span>
<span class="tag is-light" *ngFor="let state of entry.status">{{ state }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div *ngIf="p.whiteList">
<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">
<h2>Whitelist</h2>
<h2 class="section-line-header">Whitelist</h2>
</div>
<div class="column is-half">
<div class="field">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">useXForwardedFor</span>
<span class="tag is-info">{{ p.whiteList.useXForwardedFor }}</span>
<span class="tag is-info">{{ !!p.whiteList.useXForwardedFor }}</span>
</div>
</div>
</div>
@ -207,7 +214,7 @@
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags">
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange; let ri = index;">{{ wlRange }}</span>
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange">{{ wlRange }}</span>
</div>
</div>
</div>
@ -217,37 +224,44 @@
<div *ngIf="p.headers">
<hr/>
<h2>Headers</h2>
<div class="section-line">
<h2 class="section-line-header">Headers</h2>
<div class="columns is-multiline">
<div class="column is-12" *ngIf="p.headers.customRequestHeaders">
<h2>Custom Request Headers</h2>
<table class="table is-fullwidth is-hoverable">
<div class="column is-12" *ngIf="p.headers.customRequestHeaders?.length">
<table class="table is-fullwidth is-hoverable table-fixed-break">
<tbody>
<tr *ngFor="let key of p.headers.customRequestHeaders | keys">
<td><span class="has-text-grey-light">{{ key }}</span></td>
<td><span class="has-text-grey">{{ p.headers.customRequestHeaders[key] }}</span></td>
<tr>
<td colspan="2">Custom Request Headers</td>
</tr>
<tr *ngFor="let header of p.headers.customRequestHeaders">
<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.customResponseHeaders">
<h2>Custom Response Headers</h2>
<table class="table is-fullwidth is-hoverable">
<div class="column is-12" *ngIf="p.headers.customResponseHeaders?.length">
<table class="table is-fullwidth is-hoverable table-fixed-break">
<tbody>
<tr *ngFor="let key of p.headers.customResponseHeaders | keys">
<td><span class="has-text-grey-light">{{ key }}</span></td>
<td><span class="has-text-grey">{{ p.headers.customResponseHeaders[key] }}</span></td>
<tr>
<td colspan="2">Custom Response Headers</td>
</tr>
<tr *ngFor="let header of p.headers.customResponseHeaders">
<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">
<h2>Secure</h2>
<table class="table is-fullwidth is-hoverable">
<table class="table is-fullwidth is-hoverable table-fixed-break">
<tbody>
<tr>
<td colspan="2">Secure</td>
</tr>
<tr *ngIf="p.headers.browserXssFilter">
<td><span class="has-text-grey">Browser XSS Filter</span></td>
<td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td>
@ -312,6 +326,20 @@
</table>
</div>
<div class="column is-12" *ngIf="p.headers.sslProxyHeaders?.length">
<table class="table is-fullwidth is-hoverable table-fixed-break">
<tbody>
<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">
@ -319,18 +347,6 @@
</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">
@ -338,29 +354,41 @@
</div>
</div>
</div>
</div>
</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>
<!-- Backends -->
<div class="column is-6">
<h2 class="subtitle"><span class="tag is-primary">{{ providers[tab]?.backends.length }}</span> Backends</h2>
<div class="message" *ngFor="let p of providers[tab]?.backends; let i = index;">
<div class="message-header">
<h2 [id]="p.id">
<div class="column is-6" *appLet="providers[tab]?.backends | backendFilter:keyword as backends">
<h2 class="subtitle"><span class="tag is-primary">{{ backends.length }}</span><span class="subtitle-name">Backends</span></h2>
<div *ngIf="backends.length < maxItem">
<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>
<i class="icon fas fa-server"></i>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-primary">{{ p.id }}</span>
</div>
</div>
</div>
<span class="has-text-white">{{ p.id }}</span>
</div>
</h2>
</div>
@ -374,28 +402,34 @@
</div>
<!-- Main -->
<div *ngIf="p.section !== 'details'">
<table class="table is-fullwidth is-hoverable">
<div *ngIf="p.section !== 'details'" class="section-container">
<div class="section-line">
<table class="table is-fullwidth is-hoverable table-fixed">
<colgroup>
<col class="table-col-75">
<col>
</colgroup>
<tbody>
<tr>
<td>Server</td>
<td>Weight</td>
</tr>
<tr *ngFor="let server of p.servers; let ri = index;">
<td><a href="{{ server.url }}" title="{{ server.title }}">{{ server.url }}</a></td>
<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>
<!-- 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="column is-3">
<h2>Load Balancer</h2>
<h2 class="section-line-header">Load Balancer</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
@ -424,9 +458,9 @@
<div *ngIf="p.maxConn">
<hr/>
<div class="columns">
<div class="columns section-line">
<div class="column is-3">
<h2>Max Connections</h2>
<h2 class="section-line-header">Max Connections</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
@ -449,9 +483,9 @@
<div *ngIf="p.circuitBreaker">
<hr/>
<div class="columns">
<div class="columns section-line">
<div class="column is-3">
<h2>Circuit Breaker</h2>
<h2 class="section-line-header">Circuit Breaker</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
@ -468,9 +502,9 @@
<div *ngIf="p.healthCheck">
<hr/>
<div class="columns">
<div class="columns section-line">
<div class="column is-3">
<h2>Health Check</h2>
<h2 class="section-line-header">Health Check</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
@ -505,17 +539,13 @@
<div *ngIf="p.buffering">
<hr>
<div class="columns list-title">
<div class="column is-12">
<h2>Buffering</h2>
</div>
</div>
<div class="list-item">
<div class="columns">
<div class="column is-4">
<span>Request Body Bytes</span>
</div>
<div class="column is-4">
<div class="section-line">
<h2 class="section-line-header">Buffering</h2>
<table class="table is-fullwidth is-hoverable table-fixedd">
<tbody>
<tr>
<td><span class="has-text-grey">Request Body Bytes</span></td>
<td>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
@ -524,8 +554,8 @@
</div>
</div>
</div>
</div>
<div class="column is-4">
</td>
<td>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
@ -534,15 +564,11 @@
</div>
</div>
</div>
</div>
</div>
</div>
<div class="list-item">
<div class="columns">
<div class="column is-4">
<span>Response Body Bytes</span>
</div>
<div class="column is-4">
</td>
</tr>
<tr>
<td><span class="has-text-grey">Response Body Bytes</span></td>
<td>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
@ -551,8 +577,8 @@
</div>
</div>
</div>
</div>
<div class="column is-4">
</td>
<td>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
@ -561,25 +587,31 @@
</div>
</div>
</div>
</td>
</tr>
<tr>
<td class="has-text-grey">Retry Expression</td>
<td colspan="2"><span class="tag is-info">{{ p.buffering.retryExpression }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="list-item">
<div class="columns">
<div class="column is-4">
<span>Retry Expression</span>
</div>
<div class="column is-8">
<span class="tag is-info">{{ p.buffering.retryExpression }}</span>
</div>
</div>
</div>
</div>
</div>
<div *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>

View file

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

View file

@ -1,11 +1,11 @@
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/retry';
import { Observable } from 'rxjs/Observable';
export interface ProviderType {
[provider: string]: {
@ -25,7 +25,7 @@ export class ApiService {
}
fetchVersion(): Observable<any> {
return this.http.get(`/api/version`, { headers: this.headers })
return this.http.get('../api/version', {headers: this.headers})
.retry(4)
.catch((err: HttpErrorResponse) => {
console.error(`[version] returned code ${err.status}, body was: ${err.error}`);
@ -34,7 +34,7 @@ export class ApiService {
}
fetchHealthStatus(): Observable<any> {
return this.http.get(`/health`, { headers: this.headers })
return this.http.get('../health', {headers: this.headers})
.retry(2)
.catch((err: HttpErrorResponse) => {
console.error(`[health] returned code ${err.status}, body was: ${err.error}`);
@ -43,46 +43,53 @@ export class ApiService {
}
fetchProviders(): Observable<any> {
return this.http.get(`/api/providers`, { headers: this.headers })
return this.http.get('../api/providers', {headers: this.headers})
.retry(2)
.catch((err: HttpErrorResponse) => {
console.error(`[providers] returned code ${err.status}, body was: ${err.error}`);
return Observable.of<any>({});
})
.map(this.parseProviders);
.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] = {
backends: Object.keys(data[curr].backends || {}).map(key => {
data[curr].backends[key].id = key;
data[curr].backends[key].servers = Object.keys(data[curr].backends[key].servers || {}).map(server => {
return {
title: server,
url: data[curr].backends[key].servers[server].url,
weight: data[curr].backends[key].servers[server].weight
};
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);
}
return frontend;
});
return data[curr].backends[key];
}),
frontends: Object.keys(data[curr].frontends || {}).map(key => {
data[curr].frontends[key].id = key;
data[curr].frontends[key].routes = Object.keys(data[curr].frontends[key].routes || {}).map(route => {
return {
title: route,
rule: data[curr].frontends[key].routes[route].rule
};
acc[curr].backends = this.toArray(data[curr].backends, 'id')
.map(backend => {
backend.servers = this.toArray(backend.servers, 'id');
return backend;
});
return data[curr].frontends[key];
}),
};
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"
@import 'typography'
@import 'variables'
@import 'colors'
@import '../../node_modules/bulma/sass/utilities/all'
@import '../../node_modules/bulma/sass/base/all'
@import '../../node_modules/bulma/sass/grid/all'
@import '../../node_modules/bulma/sass/elements/container'
@import '../../node_modules/bulma/sass/elements/tag'
@import '../../node_modules/bulma/sass/elements/box'
@import '../../node_modules/bulma/sass/elements/form'
@import '../../node_modules/bulma/sass/elements/table'
@import '../../node_modules/bulma/sass/components/navbar'
@import '../../node_modules/bulma/sass/components/tabs'
@import '../../node_modules/bulma/sass/elements/notification'
@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 'nav'
@import 'content'
@import 'message'
@import 'label'
@import 'charts'
@import 'helper'

View file

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

View file

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

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
display: block
font-size: 14px
margin: 20px 0 30px 0
font-size: 0.8rem
margin: 1rem 0 1.5rem 0
padding-bottom: 0.3rem
border: 1px solid $border
background: $white
border-radius: 4px
border-radius: $traefik-border-radius
box-shadow: 1px 2px 5px rgba($border, 0.4)
.message-header
color: $color-secondary
border-bottom: 1px solid $border-secondary
padding: 20px 10px
background: #f8f9fa
border-top-left-radius: 4px
border-top-right-radius: 4px
padding: 0.6rem
border-top-left-radius: $traefik-border-radius
border-top-right-radius: $traefik-border-radius
.icon
display: block
float: left
width: 1.4rem
height: 1.4rem
margin-right: 0.5rem
h2
font-size: 14px
weight: $weight-bold
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
margin-right: 15px
.message-body
.field
margin: 5px 10px
padding-bottom: 10px
.tabs
margin-bottom: 0.5rem
.tags-list
margin: 5px 10px
.section-container
padding: 0.3em 0 0 0
.control
width: 100%
margin: 5px 0
.section-line
padding: 0 0.75em
.tags
width: 100%
.section-line-header
padding: 0.2em 0 0 0
.tag
width: 50%
// required for small screen (without -> table overlapping)
.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
margin: 10px 10px 0 10px
color: $black
hr
margin: 5px 0
.message-subheader
border-bottom: 1px solid $border-secondary
padding: 10px
margin-bottom: 5px

View file

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

View file

@ -1,14 +1,14 @@
=font-face($family, $path, $weight: normal, $style: normal)
@font-face
font-family: $family
src: url('#{$path}.ttf') format('truetype')
src: url('./#{$path}.ttf') format('truetype')
font-weight: $weight
font-style: $style
+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-Semibold', 600, 'semibold')
+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-Light', 300, 'light')
+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-Bold', 700, 'bold')
+font-face('Open Sans', 'assets/fonts/OpenSans-ExtraBold', 800, 'extrabold')
$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"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bulma@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.6.2.tgz#f4b1d11d5acc51a79644eb0a2b0b10649d3d71f5"
bulma@^0.7.0:
version "0.7.1"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.1.tgz#73c2e3b2930c90cc272029cbd19918b493fca486"
bytes@3.0.0:
version "3.0.0"