New web ui

This commit is contained in:
Jan Kuri 2018-04-27 13:12:04 +02:00 committed by Traefiker Bot
parent e09d5cb4ec
commit 9c651ae913
105 changed files with 7314 additions and 5514 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@
/traefik /traefik
/traefik.toml /traefik.toml
/static/ /static/
/webui/.tmp/
.vscode/ .vscode/
/site/ /site/
*.log *.log

View file

@ -14,9 +14,19 @@ type DashboardHandler struct{}
// AddRoutes add dashboard routes on a router // AddRoutes add dashboard routes on a router
func (g DashboardHandler) AddRoutes(router *mux.Router) { func (g DashboardHandler) AddRoutes(router *mux.Router) {
// Expose dashboard // Expose dashboard
router.Methods(http.MethodGet).Path("/").HandlerFunc(func(response http.ResponseWriter, request *http.Request) { router.Methods(http.MethodGet).
Path("/").
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", 302) http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", 302)
}) })
router.Methods(http.MethodGet).PathPrefix("/dashboard/").
router.Methods(http.MethodGet).
Path("/dashboard/status").
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
http.Redirect(response, request, "/dashboard/", 302)
})
router.Methods(http.MethodGet).
PathPrefix("/dashboard/").
Handler(http.StripPrefix("/dashboard/", http.FileServer(&assetfs.AssetFS{Asset: genstatic.Asset, AssetInfo: genstatic.AssetInfo, AssetDir: genstatic.AssetDir, Prefix: "static"}))) Handler(http.StripPrefix("/dashboard/", http.FileServer(&assetfs.AssetFS{Asset: genstatic.Asset, AssetInfo: genstatic.AssetInfo, AssetDir: genstatic.AssetDir, Prefix: "static"})))
} }

63
webui/.angular-cli.json Normal file
View file

@ -0,0 +1,63 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "webui"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles/app.sass"
],
"scripts": [
"../node_modules/@fortawesome/fontawesome/index.js",
"../node_modules/@fortawesome/fontawesome-free-solid/index.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "sass",
"component": {}
}
}

View file

@ -1,3 +0,0 @@
{
"presets": ["es2015"]
}

View file

@ -1,13 +1,13 @@
# http://editorconfig.org # Editor configuration, see http://editorconfig.org
root = true root = true
[*] [*]
charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true
[*.md] [*.md]
max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false

View file

@ -1 +0,0 @@
* text=auto

48
webui/.gitignore vendored
View file

@ -1,6 +1,44 @@
.tmp/ # See http://help.github.com/ignore-files/ for more about ignoring files.
coverage/
dist/
node_modules/
.sass-cache/
# compiled output
/dist
/dist-server
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db

View file

@ -1,20 +0,0 @@
{
"generator-fountain-angular1": {
"version": "0.6.0",
"props": {
"resolved": "/Users/micael/Documents/zenika/fountain/generator-fountain-angular1/generators/app/index.js",
"namespace": "fountain-angular1:app",
"argv": {
"remain": [],
"cooked": [],
"original": []
},
"framework": "angular1",
"modules": "webpack",
"css": "scss",
"js": "js",
"sample": "hello",
"router": "none"
}
}
}

View file

@ -19,4 +19,4 @@ RUN yarn install
COPY . $WEBUI_DIR/ COPY . $WEBUI_DIR/
EXPOSE 3000 3001 8080 EXPOSE 8080

View file

@ -1,12 +0,0 @@
const conf = require('./gulp.conf');
module.exports = function () {
return {
server: {
baseDir: [
conf.paths.dist
]
},
open: false
};
};

View file

@ -1,28 +0,0 @@
const conf = require('./gulp.conf');
const proxy = require('http-proxy-middleware');
const apiProxy = proxy('/api', {
target: 'http://localhost:8080',
changeOrigin: true
});
const healthProxy = proxy('/health', {
target: 'http://localhost:8080',
changeOrigin: true
});
module.exports = function () {
return {
server: {
baseDir: [
conf.paths.tmp,
conf.paths.src
],
middleware: [
apiProxy,
healthProxy
]
},
open: false
};
};

View file

@ -1,47 +0,0 @@
'use strict';
/**
* This file contains the variables used in other gulp files
* which defines tasks
* By design, we only put there very generic config values
* which are used in several places to keep good readability
* of the tasks
*/
const path = require('path');
const gutil = require('gulp-util');
exports.ngModule = 'traefik';
/**
* The main paths of your project handle these with care
*/
exports.paths = {
src: 'src',
dist: '../static',
tmp: '.tmp',
e2e: 'e2e',
tasks: 'gulp_tasks'
};
exports.path = {};
for (const pathName in exports.paths) {
if (exports.paths.hasOwnProperty(pathName)) {
exports.path[pathName] = function pathJoin() {
const pathValue = exports.paths[pathName];
const funcArgs = Array.prototype.slice.call(arguments);
const joinArgs = [pathValue].concat(funcArgs);
return path.join.apply(this, joinArgs);
};
}
}
/**
* Common implementation for an error handler of a Gulp plugin
*/
exports.errorHandler = function (title) {
return function (err) {
gutil.log(gutil.colors.red(`[${title}]`), err.toString());
this.emit('end');
};
};

View file

@ -1,55 +0,0 @@
const conf = require('./gulp.conf');
module.exports = function (config) {
const configuration = {
basePath: '../',
singleRun: false,
autoWatch: true,
logLevel: 'INFO',
junitReporter: {
outputDir: 'test-reports'
},
browsers: [
'PhantomJS'
],
frameworks: [
'jasmine'
],
files: [
'node_modules/es6-shim/es6-shim.js',
conf.path.src('index.spec.js'),
conf.path.src('**/*.html')
],
preprocessors: {
[conf.path.src('index.spec.js')]: [
'webpack'
],
[conf.path.src('**/*.html')]: [
'ng-html2js'
]
},
ngHtml2JsPreprocessor: {
stripPrefix: `${conf.paths.src}/`
},
reporters: ['progress', 'coverage'],
coverageReporter: {
type: 'html',
dir: 'coverage/'
},
webpack: require('./webpack-test.conf'),
webpackMiddleware: {
noInfo: true
},
plugins: [
require('karma-jasmine'),
require('karma-junit-reporter'),
require('karma-coverage'),
require('karma-phantomjs-launcher'),
require('karma-phantomjs-shim'),
require('karma-ng-html2js-preprocessor'),
require('karma-webpack')
]
};
config.set(configuration);
};

View file

@ -1,55 +0,0 @@
const conf = require('./gulp.conf');
module.exports = function (config) {
const configuration = {
basePath: '../',
singleRun: true,
autoWatch: false,
logLevel: 'INFO',
junitReporter: {
outputDir: 'test-reports'
},
browsers: [
'PhantomJS'
],
frameworks: [
'jasmine'
],
files: [
'node_modules/es6-shim/es6-shim.js',
conf.path.src('index.spec.js'),
conf.path.src('**/*.html')
],
preprocessors: {
[conf.path.src('index.spec.js')]: [
'webpack'
],
[conf.path.src('**/*.html')]: [
'ng-html2js'
]
},
ngHtml2JsPreprocessor: {
stripPrefix: `${conf.paths.src}/`
},
reporters: ['progress', 'coverage'],
coverageReporter: {
type: 'html',
dir: 'coverage/'
},
webpack: require('./webpack-test.conf'),
webpackMiddleware: {
noInfo: true
},
plugins: [
require('karma-jasmine'),
require('karma-junit-reporter'),
require('karma-coverage'),
require('karma-phantomjs-launcher'),
require('karma-phantomjs-shim'),
require('karma-ng-html2js-preprocessor'),
require('karma-webpack')
]
};
config.set(configuration);
};

View file

@ -1,75 +0,0 @@
const webpack = require('webpack');
const conf = require('./gulp.conf');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const SplitByPathPlugin = require('webpack-split-by-path');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const autoprefixer = require('autoprefixer');
module.exports = {
module: {
preLoaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint'
}
],
loaders: [
{
test: /.json$/,
loaders: [
'json'
]
},
{
test: /\.(css|scss)$/,
loaders: ExtractTextPlugin.extract('style', 'css?minimize!sass', 'postcss')
},
{
test: /\.js$/,
exclude: /node_modules/,
loaders: [
'babel-loader',
'ng-annotate'
]
},
{
test: /.html$/,
loaders: [
'html'
]
},
{
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000'
}
]
},
plugins: [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.NoErrorsPlugin(),
new HtmlWebpackPlugin({
template: conf.path.src('index.html'),
inject: true
}),
new webpack.optimize.UglifyJsPlugin({
compress: {unused: true, dead_code: true} // eslint-disable-line camelcase
}),
new SplitByPathPlugin([{
name: 'vendor',
path: path.join(__dirname, '../node_modules')
}]),
new ExtractTextPlugin('./index-[contenthash].css')
],
postcss: () => [autoprefixer],
output: {
path: path.join(process.cwd(), conf.paths.dist),
filename: './[name]-[hash].js'
},
entry: {
app: `./${conf.path.src('index')}`
}
};

View file

@ -1,41 +0,0 @@
module.exports = {
module: {
preLoaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint'
}
],
loaders: [
{
test: /.json$/,
loaders: [
'json'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
loaders: [
'ng-annotate'
]
},
{
test: /.html$/,
loaders: [
'html'
]
},
{
test: /\.js$/,
exclude: /(node_modules|.*\.spec\.js)/,
loader: 'isparta'
}
]
},
plugins: [],
debug: true,
devtool: 'cheap-module-eval-source-map'
};

View file

@ -1,61 +0,0 @@
const webpack = require('webpack');
const conf = require('./gulp.conf');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const autoprefixer = require('autoprefixer');
module.exports = {
module: {
loaders: [
{
test: /.json$/,
loaders: [
'json'
]
},
{
test: /\.(css|scss)$/,
loaders: [
'style',
'css',
'sass',
'postcss'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
loaders: [
'ng-annotate'
]
},
{
test: /.html$/,
loaders: [
'html'
]
},
{
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000'
}
]
},
plugins: [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.NoErrorsPlugin(),
new HtmlWebpackPlugin({
template: conf.path.src('index.html'),
inject: true
})
],
postcss: () => [autoprefixer],
debug: true,
devtool: 'cheap-module-eval-source-map',
output: {
path: path.join(process.cwd(), conf.paths.tmp),
filename: 'index.js'
},
entry: `./${conf.path.src('index')}`
};

View file

@ -1,21 +0,0 @@
const gulp = require('gulp');
const browserSync = require('browser-sync');
const spa = require('browser-sync-spa');
const browserSyncConf = require('../conf/browsersync.conf');
const browserSyncDistConf = require('../conf/browsersync-dist.conf');
browserSync.use(spa());
gulp.task('browsersync', browserSyncServe);
gulp.task('browsersync:dist', browserSyncDist);
function browserSyncServe(done) {
browserSync.init(browserSyncConf());
done();
}
function browserSyncDist(done) {
browserSync.init(browserSyncDistConf());
done();
}

View file

@ -1,25 +0,0 @@
const path = require('path');
const gulp = require('gulp');
const karma = require('karma');
gulp.task('karma:single-run', karmaSingleRun);
gulp.task('karma:auto-run', karmaAutoRun);
function karmaFinishHandler(done) {
return failCount => {
done(failCount ? new Error(`Failed ${failCount} tests.`) : null);
};
}
function karmaSingleRun(done) {
const configFile = path.join(process.cwd(), 'conf', 'karma.conf.js');
const karmaServer = new karma.Server({configFile}, karmaFinishHandler(done));
karmaServer.start();
}
function karmaAutoRun(done) {
const configFile = path.join(process.cwd(), 'conf', 'karma-auto.conf.js');
const karmaServer = new karma.Server({configFile}, karmaFinishHandler(done));
karmaServer.start();
}

View file

@ -1,25 +0,0 @@
const path = require('path');
const gulp = require('gulp');
const del = require('del');
const filter = require('gulp-filter');
const conf = require('../conf/gulp.conf');
gulp.task('clean', clean);
gulp.task('other', other);
function clean() {
return del([conf.paths.tmp]);
}
function other() {
const fileFilter = filter(file => file.stat.isFile());
return gulp.src([
path.join(conf.paths.src, '/**/*'),
path.join(`!${conf.paths.src}`, '/**/*.{scss,js,html}')
])
.pipe(fileFilter)
.pipe(gulp.dest(conf.paths.dist));
}

View file

@ -1,48 +0,0 @@
/* eslint angular/module-getter:0 */
const gulp = require('gulp');
const gutil = require('gulp-util');
const webpack = require('webpack');
const webpackConf = require('../conf/webpack.conf');
const webpackDistConf = require('../conf/webpack-dist.conf');
const browsersync = require('browser-sync');
gulp.task('webpack:dev', done => {
webpackWrapper(false, webpackConf, done);
});
gulp.task('webpack:watch', done => {
webpackWrapper(true, webpackConf, done);
});
gulp.task('webpack:dist', done => {
webpackWrapper(false, webpackDistConf, done);
});
function webpackWrapper(watch, conf, done) {
const webpackBundler = webpack(conf);
const webpackChangeHandler = (err, stats) => {
if (err) {
conf.errorHandler('Webpack')(err);
}
gutil.log(stats.toString({
colors: true,
chunks: false,
hash: false,
version: false
}));
if (done) {
done();
done = null;
} else {
browsersync.reload();
}
};
if (watch) {
webpackBundler.watch(200, webpackChangeHandler);
} else {
webpackBundler.run(webpackChangeHandler);
}
}

View file

@ -1,29 +0,0 @@
const gulp = require('gulp');
const HubRegistry = require('gulp-hub');
const browserSync = require('browser-sync');
const conf = require('./conf/gulp.conf');
// Load some files into the registry
const hub = new HubRegistry([conf.path.tasks('*.js')]);
// Tell gulp to use the tasks just loaded
gulp.registry(hub);
gulp.task('build', gulp.series(gulp.parallel('other', 'webpack:dist')));
gulp.task('test', gulp.series('karma:single-run'));
gulp.task('test:auto', gulp.series('karma:auto-run'));
gulp.task('serve', gulp.series('webpack:watch', 'watch', 'browsersync'));
gulp.task('serve:dist', gulp.series('default', 'browsersync:dist'));
gulp.task('default', gulp.series('clean', 'build'));
gulp.task('watch', watch);
function reloadBrowserSync(cb) {
browserSync.reload();
cb();
}
function watch(done) {
gulp.watch(conf.path.src('app/**/*.html'), reloadBrowserSync);
done();
}

33
webui/karma.conf.js Normal file
View file

@ -0,0 +1,33 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View file

@ -1,103 +1,58 @@
{ {
"name": "traefik", "name": "traefik",
"version": "2.0.0", "version": "3.0.0",
"homepage": "http://traefik.io",
"authors": [ "authors": [
"Fernandez Ludovic <lfernandez.dev@gmail.com>", "Fernandez Ludovic <lfernandez.dev@gmail.com>",
"Micaël Mbagira <micael.mbagira@icloud.com>" "Micaël Mbagira <micael.mbagira@icloud.com>",
"Jan Kuri <jan@bleenco.com>"
], ],
"description": "Front end for Træfik",
"license": "MIT", "license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build --prod --no-delete-output-path --output-path ../static/",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"dependencies": { "dependencies": {
"angular": "^1.4.2", "@angular/animations": "^5.2.0",
"angular-animate": "^1.5.8", "@angular/common": "^5.2.0",
"angular-aria": "^1.5.8", "@angular/compiler": "^5.2.0",
"angular-cookies": "^1.5.8", "@angular/core": "^5.2.0",
"angular-messages": "^1.5.8", "@angular/forms": "^5.2.0",
"angular-nvd3": "^1.0.8", "@angular/http": "^5.2.0",
"angular-resource": "^1.5.8", "@angular/platform-browser": "^5.2.0",
"angular-sanitize": "^1.5.8", "@angular/platform-browser-dynamic": "^5.2.0",
"angular-ui-bootstrap": "^2.0.0", "@angular/router": "^5.2.0",
"angular-ui-router": "^0.3.1", "@fortawesome/fontawesome": "^1.1.5",
"animate.css": "^3.4.0", "@fortawesome/fontawesome-free-solid": "^5.0.10",
"bootstrap": "^3.3.6", "bulma": "^0.6.2",
"http-status-codes": "^1.3.0", "core-js": "^2.4.1",
"d3": "^4.13.0",
"date-fns": "^1.29.0",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"moment": "^2.14.1", "rxjs": "^5.5.6",
"nvd3": "^1.8.4" "zone.js": "^0.8.19"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "^1.4.2", "@angular/cli": "~1.7.2",
"autoprefixer": "^6.2.2", "@angular/compiler-cli": "^5.2.0",
"babel-core": "^6.24.1", "@angular/language-service": "^5.2.0",
"babel-loader": "^7.0.0", "@types/jasmine": "~2.8.3",
"babel-preset-es2015": "^6.24.1", "@types/jasminewd2": "~2.0.2",
"browser-sync": "^2.9.11", "@types/node": "~6.0.60",
"browser-sync-spa": "^1.0.3", "codelyzer": "^4.0.1",
"css-loader": "^0.23.1", "jasmine-core": "~2.8.0",
"del": "^2.0.2", "jasmine-spec-reporter": "~4.2.1",
"es6-shim": "^0.35.0", "karma": "~2.0.0",
"eslint": "^2.11.0", "karma-chrome-launcher": "~2.2.0",
"eslint-config-angular": "^0.5.0", "karma-coverage-istanbul-reporter": "^1.2.1",
"eslint-config-xo-space": "^0.12.0", "karma-jasmine": "~1.1.0",
"eslint-loader": "^1.3.0", "karma-jasmine-html-reporter": "^0.2.2",
"eslint-plugin-angular": "^1.3.0", "protractor": "~5.1.2",
"extract-text-webpack-plugin": "^1.0.1", "ts-node": "~4.1.0",
"file-loader": "^0.9.0", "tslint": "~5.9.1",
"gulp": "gulpjs/gulp#4ed9a4a3275559c73a396eff7e1fde3824951ebb", "typescript": "~2.5.3"
"gulp-angular-filesort": "^1.1.1",
"gulp-angular-templatecache": "^1.8.0",
"gulp-filter": "^4.0.0",
"gulp-htmlmin": "^1.3.0",
"gulp-hub": "frankwallis/gulp-hub#d461b9c700df9010d0a8694e4af1fb96d9f38bf4",
"gulp-insert": "^0.5.0",
"gulp-ng-annotate": "^1.1.0",
"gulp-sass": "^2.1.1",
"gulp-util": "^3.0.7",
"html-loader": "^0.4.3",
"html-webpack-plugin": "^2.9.0",
"http-proxy-middleware": "^0.17.4",
"isparta-loader": "^2.0.0",
"jasmine": "^2.4.1",
"json-loader": "^0.5.4",
"karma": "^0.13.14",
"karma-angular-filesort": "^1.0.0",
"karma-coverage": "^0.5.3",
"karma-jasmine": "^0.3.8",
"karma-junit-reporter": "^0.4.2",
"karma-ng-html2js-preprocessor": "^0.2.0",
"karma-phantomjs-launcher": "^1.0.0",
"karma-phantomjs-shim": "^1.1.2",
"karma-webpack": "^1.7.0",
"ng-annotate-loader": "^0.0.10",
"node-sass": "^3.4.2",
"phantomjs-prebuilt": "^2.1.6",
"postcss-loader": "^0.8.0",
"sass-loader": "^3.1.2",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "2.1.0-beta.15",
"webpack-split-by-path": "^0.0.10"
},
"scripts": {
"build": "gulp",
"serve": "gulp serve",
"serve:dist": "gulp serve:dist",
"test": "gulp test",
"test:auto": "gulp test:auto"
},
"eslintConfig": {
"globals": {
"expect": true
},
"root": true,
"env": {
"browser": true,
"jasmine": true
},
"extends": [
"angular",
"xo-space"
]
} }
} }

28
webui/protractor.conf.js Normal file
View file

@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

10
webui/proxy.conf.json Normal file
View file

@ -0,0 +1,10 @@
{
"/api": {
"target": "http://localhost:8080",
"secure": false
},
"/health": {
"target": "http://localhost:8080",
"secure": false
}
}

View file

@ -19,7 +19,7 @@ make generate-webui # Generate static contents in `traefik/static/` folder.
## How to build (only for frontends developer) ## How to build (only for frontends developer)
- prerequisite: [Node 4+](https://nodejs.org) [yarn](https://yarnpkg.com/) - prerequisite: [Node 6+](https://nodejs.org) [yarn](https://yarnpkg.com/)
Note: In case of conflict with the Apache Hadoop Yarn Command Line Interface, use the `yarnpkg` Note: In case of conflict with the Apache Hadoop Yarn Command Line Interface, use the `yarnpkg`
alias. alias.
@ -51,29 +51,15 @@ make generate-webui # Generate static contents in `traefik/static/` folder.
- Go to the directory `webui` - Go to the directory `webui`
- Edit files in `webui/src` - Edit files in `webui/src`
- Run in development mode : - Run in development mode :
- `yarn run serve` - `yarn start`
- Træfik API connections are defined in:
- `webui/src/app/core/health.resource.js`
- `webui/src/app/core/providers.resource.js`
- The pages contents are in the directory `webui/src/app/sections`.
## Libraries ## Libraries
- [Node](https://nodejs.org) - [Node](https://nodejs.org)
- [Yarn](https://yarnpkg.com/) - [Yarn](https://yarnpkg.com/)
- [Generator FountainJS](https://github.com/FountainJS/generator-fountain-webapp)
- [Webpack](https://github.com/webpack/webpack) - [Webpack](https://github.com/webpack/webpack)
- [AngularJS](https://docs.angularjs.org/api) - [Angular](https://angular.io)
- [UI Router](https://github.com/angular-ui/ui-router) - [Bulma](https://bulma.io)
- [UI Router - Documentation](https://github.com/angular-ui/ui-router/wiki)
- [Bootstrap](https://getbootstrap.com)
- [Angular Bootstrap](https://angular-ui.github.io/bootstrap)
- [D3](https://d3js.org) - [D3](https://d3js.org)
- [D3 - Documentation](https://github.com/mbostock/d3/wiki) - [D3 - Documentation](https://github.com/mbostock/d3/wiki)
- [NVD3](http://nvd3.org)
- [Angular nvD3](https://krispo.github.io/angular-nvd3)

View file

@ -1,13 +0,0 @@
{
"extends": "eslint:recommended",
"plugins": ["angular"],
"env": {
"browser": true,
"jasmine": true
},
"globals": {
"angular": true,
"module": true,
"inject": true
}
}

View file

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});

View file

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-header></app-header>
<router-outlet></router-outlet>
`
})
export class AppComponent { }

View file

@ -0,0 +1,43 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { ApiService } from './services/api.service';
import { WindowService } from './services/window.service';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';
import { ProvidersComponent } from './components/providers/providers.component';
import { HealthComponent } from './components/health/health.component';
import { LineChartComponent } from './charts/line-chart/line-chart.component';
import { BarChartComponent } from './charts/bar-chart/bar-chart.component';
import { KeysPipe } from './pipes/keys.pipe';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
ProvidersComponent,
HealthComponent,
LineChartComponent,
BarChartComponent,
KeysPipe
],
imports: [
BrowserModule,
CommonModule,
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: ProvidersComponent, pathMatch: 'full' },
{ path: 'status', component: HealthComponent }
])
],
providers: [
ApiService,
WindowService
],
bootstrap: [AppComponent]
})
export class AppModule { }

View file

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

View file

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BarChartComponent } from './bar-chart.component';
describe('BarChartComponent', () => {
let component: BarChartComponent;
let fixture: ComponentFixture<BarChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BarChartComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BarChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,114 @@
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
import { WindowService } from '../../services/window.service';
import {
min,
max,
easeLinear,
select,
axisLeft,
axisBottom,
scaleBand,
scaleLinear
} from 'd3';
@Component({
selector: 'app-bar-chart',
templateUrl: './bar-chart.component.html'
})
export class BarChartComponent implements OnInit, OnChanges {
@Input() value: any;
barChartEl: HTMLElement;
svg: any;
x: any;
y: any;
g: any;
bars: any;
width: number;
height: number;
margin = { top: 40, right: 40, bottom: 40, left: 40 };
loading: boolean;
data: any[];
constructor(public elementRef: ElementRef, public windowService: WindowService) {
this.loading = true;
}
ngOnInit() {
this.barChartEl = this.elementRef.nativeElement.querySelector('.bar-chart');
this.setup();
setTimeout(() => this.loading = false, 4000);
this.windowService.resize.subscribe(w => this.draw());
}
ngOnChanges(changes: SimpleChanges) {
if (!this.value || !this.svg) {
return;
}
this.data = this.value;
this.draw();
}
setup(): void {
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
this.svg = select(this.barChartEl).append('svg')
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom);
this.g = this.svg.append('g')
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
this.x = scaleBand().padding(0.05);
this.y = scaleLinear();
this.g.append('g')
.attr('class', 'axis axis--x');
this.g.append('g')
.attr('class', 'axis axis--y');
}
draw(): void {
this.x.domain(this.data.map((d: any) => d.code));
this.y.domain([0, max(this.data, (d: any) => d.count)]);
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
this.svg
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom);
this.x.rangeRound([0, this.width]);
this.y.rangeRound([this.height, 0]);
this.g.select('.axis--x')
.attr('transform', `translate(0, ${this.height})`)
.call(axisBottom(this.x));
this.g.select('.axis--y')
.call(axisLeft(this.y).tickSize(-this.width));
const bars = this.g.selectAll('.bar').data(this.data);
bars.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', (d: any) => d.code)
.attr('y', (d: any) => d.count)
.attr('width', this.x.bandwidth())
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
bars.attr('x', (d: any) => this.x(d.code))
.attr('y', (d: any) => this.y(d.count))
.attr('width', this.x.bandwidth())
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
bars.exit().remove();
}
}

View file

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

View file

@ -0,0 +1,162 @@
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
import { WindowService } from '../../services/window.service';
import {
range,
scaleTime,
scaleLinear,
min,
max,
curveLinear,
line,
easeLinear,
select,
axisLeft,
axisBottom,
timeSecond,
timeFormat
} from 'd3';
@Component({
selector: 'app-line-chart',
templateUrl: 'line-chart.component.html'
})
export class LineChartComponent implements OnChanges, OnInit {
@Input() value: { count: number, date: string };
lineChartEl: HTMLElement;
svg: any;
g: any;
line: any;
path: any;
x: any;
y: any;
data: number[];
now: Date;
duration: number;
limit: number;
options: any;
xAxis: any;
yAxis: any;
height: number;
width: number;
margin = { top: 40, right: 40, bottom: 60, left: 60 };
loading = true;
constructor(private elementRef: ElementRef, public windowService: WindowService) { }
ngOnInit() {
this.lineChartEl = this.elementRef.nativeElement.querySelector('.line-chart');
this.limit = 40;
this.duration = 3000;
this.now = new Date(Date.now() - this.duration);
this.options = {
title: '',
color: '#3A84C5'
};
this.render();
setTimeout(() => this.loading = false, 4000);
this.windowService.resize.subscribe(w => {
if (this.svg) {
const el = this.lineChartEl.querySelector('svg');
el.parentNode.removeChild(el);
this.render();
}
});
}
render() {
this.width = this.lineChartEl.clientWidth - this.margin.left - this.margin.right;
this.height = this.lineChartEl.clientHeight - this.margin.top - this.margin.bottom;
this.svg = select(this.lineChartEl).append('svg')
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
if (!this.data) {
this.data = range(this.limit).map(i => 0);
}
this.x = scaleTime().range([0, this.width]);
this.y = scaleLinear().range([this.height, 0]);
this.x.domain([<any>this.now - (this.limit - 2), <any>this.now - this.duration]);
this.y.domain([0, max(this.data, (d: any) => d)]);
this.line = line()
.x((d: any, i: number) => this.x(<any>this.now - (this.limit - 1 - i) * this.duration))
.y((d: any) => this.y(d))
.curve(curveLinear);
this.svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', this.width)
.attr('height', this.height);
this.xAxis = this.svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0, ${this.height})`)
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')));
this.yAxis = this.svg.append('g')
.attr('class', 'y axis')
.call(axisLeft(this.y).tickSize(-this.width));
this.path = this.svg.append('g')
.attr('clip-path', 'url(#clip)')
.append('path')
.data([this.data])
.attr('class', 'line');
}
ngOnChanges(changes: SimpleChanges) {
if (!this.value || !this.svg) {
return;
}
this.updateData(this.value.count);
}
updateData = (value: number) => {
this.data.push(value * 1000000);
this.now = new Date();
this.x.domain([<any>this.now - (this.limit - 2) * this.duration, <any>this.now - this.duration]);
const minv = min(this.data, (d: any) => d) > 0 ? min(this.data, (d: any) => d) - 4 : 0;
const maxv = max(this.data, (d: any) => d) + 4;
this.y.domain([minv, maxv]);
this.xAxis
.transition()
.duration(this.duration)
.ease(easeLinear)
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')))
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
this.yAxis
.transition()
.duration(500)
.ease(easeLinear)
.call(axisLeft(this.y).tickSize(-this.width));
this.path
.transition()
.duration(0)
.attr('d', this.line(this.data))
.attr('transform', null)
.transition()
.duration(this.duration)
.ease(easeLinear)
.attr('transform', `translate(${this.x(<any>this.now - (this.limit - 1) * this.duration)})`);
this.data.shift();
}
}

View file

@ -0,0 +1,29 @@
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-menu">
<div class="navbar-brand">
<a class="navbar-item" routerLink="/">
<img src="./assets/images/traefik.logo.svg" alt="Traefik" class="navbar-logo">
</a>
</div>
<div class="navbar-start">
<div class="navbar-menu">
<a class="navbar-item" routerLink="/" routerLinkActive="is-active" [routerLinkActiveOptions]="{ exact: true }">
Providers
</a>
<a class="navbar-item" routerLink="/status" routerLinkActive="is-active">
Health
</a>
</div>
</div>
<div class="navbar-end is-hidden-mobile">
<a class="navbar-item" [href]="releaseLink" target="_blank">
{{ version }} / {{ codename }}
</a>
<a class="navbar-item" href="https://docs.traefik.io" target="_blank">
Documentation
</a>
</div>
</div>
</div>
</nav>

View file

@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-header',
templateUrl: 'header.component.html'
})
export class HeaderComponent implements OnInit {
version: string;
codename: string;
releaseLink: string;
constructor(private apiService: ApiService) { }
ngOnInit() {
this.apiService.fetchVersion()
.subscribe(data => {
this.version = data.Version;
this.codename = data.Codename;
this.releaseLink = 'https://github.com/containous/traefik/tree/' + data.Version;
});
}
}

View file

@ -0,0 +1,104 @@
<div class="container">
<div class="content">
<div class="columns is-multiline">
<div class="column is-12">
<div class="content-item">
<div class="content-item-data">
<div class="columns">
<div class="column is-4">
<div class="item-data border-right">
<span class="data-grey">Total Response Time</span>
<span class="data-blue">{{ totalResponseTime }}</span>
</div>
</div>
<div class="column is-4">
<div class="item-data border-right">
<span class="data-grey">Total Code Count</span>
<span class="data-blue">{{ totalCodeCount }}</span>
</div>
</div>
<div class="column is-4">
<div class="item-data">
<span class="data-grey">Uptime Since <br/>{{ uptimeSince }}</span>
<span class="data-blue">{{ uptime }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="content-item">
<div class="content-item-data">
<div class="columns">
<div class="column is-4">
<div class="item-data border-right">
<span class="data-grey">Average Response Time</span>
<span class="data-blue">{{ averageResponseTime }}</span>
</div>
</div>
<div class="column is-4">
<div class="item-data border-right">
<span class="data-grey">Code Count</span>
<span class="data-blue">{{ codeCount }}</span>
</div>
</div>
<div class="column is-4">
<div class="item-data">
<span class="data-grey">PID</span>
<span class="data-blue">{{ pid }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column is-12">
<div class="columns">
<div class="column is-6">
<div class="content-item">
<h2>Average Response Time (µs)</h2>
<app-line-chart [value]="chartValue"></app-line-chart>
</div>
</div>
<div class="column is-6">
<div class="content-item">
<h2>Total Status Code Count</h2>
<app-bar-chart [value]="statusCodeValue"></app-bar-chart>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="content" *ngIf="recentErrors">
<div class="content-item">
<h2>Recent HTTP Errors</h2>
<table class="table is-fullwidth">
<tr>
<td>Status</td>
<td>Request</td>
<td>Time</td>
</tr>
<tr *ngFor="let entry of recentErrors">
<td>
<span class="tag is-info">{{ entry.status_code }}</span>&nbsp;<span>{{ entry.status }}</span>
</td>
<td>
<span class="tag">{{ entry.method }}</span>&nbsp;<a>{{ entry.host }}{{ entry.path }}</a>
</td>
<td>
<span>{{ entry.time }}</span>
</td>
</tr>
<tr *ngIf="!recentErrors?.length">
<td colspan="3">
<p class="text-muted text-center">No entries</p>
</td>
</tr>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,57 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/timeInterval';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/map';
import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
@Component({
selector: 'app-health',
templateUrl: 'health.component.html'
})
export class HealthComponent implements OnInit, OnDestroy {
sub: Subscription;
recentErrors: any;
pid: number;
uptime: string;
uptimeSince: string;
averageResponseTime: string;
totalResponseTime: string;
codeCount: number;
totalCodeCount: number;
chartValue: any;
statusCodeValue: any;
constructor(private apiService: ApiService) { }
ngOnInit() {
this.sub = Observable.timer(0, 3000)
.timeInterval()
.mergeMap(() => this.apiService.fetchHealthStatus())
.subscribe(data => {
if (data) {
this.recentErrors = data.recent_errors;
this.chartValue = { count: data.average_response_time_sec, date: data.time };
this.statusCodeValue = Object.keys(data.total_status_code_count)
.map(key => ({ code: key, count: data.total_status_code_count[key] }));
this.pid = data.pid;
this.uptime = distanceInWordsStrict(subSeconds(new Date(), data.uptime_sec), new Date());
this.uptimeSince = format(subSeconds(new Date(), data.uptime_sec), 'MM/DD/YYYY HH:mm:ss');
this.totalResponseTime = data.total_response_time;
this.averageResponseTime = data.average_response_time;
this.codeCount = data.count;
this.totalCodeCount = data.total_count;
}
});
}
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe();
}
}
}

View file

@ -0,0 +1,566 @@
<div class="container">
<div class="content">
<div class="columns is-multiline" *ngIf="keys?.length">
<div class="column is-12">
<div class="search-container">
<span class="icon"><i class="fas fa-search"></i></span>
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword" (ngModelChange)="filter()">
</div>
<div class="tabs" *ngIf="keys?.length">
<ul>
<li *ngFor="let provider of keys" [class.is-active]="tab === provider" (click)="tab = provider">
<a>{{ provider }}</a>
</li>
</ul>
</div>
<div *ngIf="keys?.length">
<div class="columns">
<!-- Frontends -->
<div class="column is-6">
<h2 class="subtitle"><span class="tag is-info">{{ providers[tab]?.frontends.length }}</span> Frontends</h2>
<div class="message" *ngFor="let p of providers[tab]?.frontends; let i = index;">
<div class="message-header">
<h2>
<div>
<i class="icon fas fa-globe"></i>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-info">{{ p.id }}</span>
</div>
</div>
</div>
</div>
<div *ngIf="p.backend">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<a class="tags has-addons" [href]="'#' + p.backend">
<span class="tag is-light">Backend</span>
<span class="tag is-primary">{{ p.backend }}</span>
</a>
</div>
</div>
</div>
</h2>
</div>
<div class="message-body">
<div class="tabs is-fullwidth is-small is-boxed">
<ul>
<li [class.is-active]="p.section !== 'details'" (click)="p.section = 'main'"><a>Main</a></li>
<li [class.is-active]="p.section === 'details'" (click)="p.section = 'details'"><a>Details</a></li>
</ul>
</div>
<!-- Main -->
<div *ngIf="p.section !== 'details'">
<div *ngIf="p.routes && p.routes.length">
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr>
<td>Route Rule</td>
</tr>
<tr *ngFor="let route of p.routes; let ri = index;">
<td><span class="has-text-grey" title="{{ route.title }}">{{ route.rule }}</span></td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="p.entryPoints && p.entryPoints.length">
<hr>
<div class="columns">
<div class="column is-3">
<h2>Entry Points</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags">
<span class="tag is-info" *ngFor="let ep of p.entryPoints; let ri = index;">{{ ep }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Details -->
<div *ngIf="p.section === 'details'">
<div>
<div class="columns">
<div class="column is-3">
<h2>Misc.</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Priority</span>
<span class="tag is-info">{{ p.priority }}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Host Header</span>
<span class="tag is-info">{{ p.passHostHeader }}</span>
</div>
</div>
<div class="control" *ngIf="p.passTLSCert">
<div class="tags has-addons">
<span class="tag is-light">TLS Cert</span>
<span class="tag is-info">{{ p.passTLSCert }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="p.basicAuth && p.basicAuth.length">
<hr/>
<h2>Basic Authentication</h2>
<div class="tags padding-5-10">
<span class="tag is-info" *ngFor="let auth of p.basicAuth; let ri = index;">{{ auth }}</span>
</div>
</div>
<div *ngIf="p.errors">
<hr/>
<h2>Error Pages</h2>
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr>
<td>Backend</td>
<td>Query</td>
<td>Status</td>
</tr>
<tr *ngFor="let key of p.errors | keys">
<td><span class="has-text-grey-light">{{ p.errors[key].backend }}</span></td>
<td><span class="has-text-grey">{{ p.errors[key].query }}</span></td>
<td>
<span class="tag is-light" *ngFor="let state of p.errors[key].status">{{ state }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="p.whiteList">
<hr/>
<div class="columns is-gapless is-multiline is-mobile">
<div class="column is-half">
<h2>Whitelist</h2>
</div>
<div class="column is-half">
<div class="field">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">useXForwardedFor</span>
<span class="tag is-info">{{ p.whiteList.useXForwardedFor }}</span>
</div>
</div>
</div>
</div>
<div class="column">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags">
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange; let ri = index;">{{ wlRange }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="p.headers">
<hr/>
<h2>Headers</h2>
<div class="columns is-multiline">
<div class="column is-12" *ngIf="p.headers.customRequestHeaders">
<h2>Custom Request Headers</h2>
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr *ngFor="let key of p.headers.customRequestHeaders | keys">
<td><span class="has-text-grey-light">{{ key }}</span></td>
<td><span class="has-text-grey">{{ p.headers.customRequestHeaders[key] }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="column is-12" *ngIf="p.headers.customResponseHeaders">
<h2>Custom Response Headers</h2>
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr *ngFor="let key of p.headers.customResponseHeaders | keys">
<td><span class="has-text-grey-light">{{ key }}</span></td>
<td><span class="has-text-grey">{{ p.headers.customResponseHeaders[key] }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="column is-12">
<h2>Secure</h2>
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr *ngIf="p.headers.browserXssFilter">
<td><span class="has-text-grey">Browser XSS Filter</span></td>
<td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td>
</tr>
<tr *ngIf="p.headers.contentSecurityPolicy">
<td><span class="has-text-grey">Content Security Policy</span></td>
<td><span class="has-text-grey">{{ p.headers.contentSecurityPolicy }}</span></td>
</tr>
<tr *ngIf="p.headers.contentTypeNoSniff">
<td><span class="has-text-grey">Content Type (No sniff)</span></td>
<td><span class="has-text-grey">{{ p.headers.contentTypeNoSniff }}</span></td>
</tr>
<tr *ngIf="p.headers.customFrameOptionsValue">
<td><span class="has-text-grey">Custom Frame Options Value</span></td>
<td><span class="has-text-grey">{{ p.headers.customFrameOptionsValue }}</span></td>
</tr>
<tr *ngIf="p.headers.forceSTSHeader">
<td><span class="has-text-grey">Force STS Header</span></td>
<td><span class="has-text-grey">{{ p.headers.forceSTSHeader }}</span></td>
</tr>
<tr *ngIf="p.headers.frameDeny">
<td><span class="has-text-grey">Frame Deny</span></td>
<td><span class="has-text-grey">{{ p.headers.frameDeny }}</span></td>
</tr>
<tr *ngIf="p.headers.isDevelopment">
<td><span class="has-text-grey">Is Development</span></td>
<td><span class="has-text-grey">{{ p.headers.isDevelopment }}</span></td>
</tr>
<tr *ngIf="p.headers.publicKey">
<td><span class="has-text-grey">Public Key</span></td>
<td><span class="has-text-grey">{{ p.headers.publicKey }}</span></td>
</tr>
<tr *ngIf="p.headers.referrerPolicy">
<td><span class="has-text-grey">Referrer Policy</span></td>
<td><span class="has-text-grey">{{ p.headers.referrerPolicy }}</span></td>
</tr>
<tr *ngIf="p.headers.sslHost">
<td><span class="has-text-grey">SSL Host</span></td>
<td><span class="has-text-grey">{{ p.headers.sslHost }}</span></td>
</tr>
<tr *ngIf="p.headers.sslRedirect">
<td><span class="has-text-grey">SSL Redirect</span></td>
<td><span class="has-text-grey">{{ p.headers.sslRedirect }}</span></td>
</tr>
<tr *ngIf="p.headers.sslTemporaryRedirect">
<td><span class="has-text-grey">SSL Temporary Redirect</span></td>
<td><span class="has-text-grey">{{ p.headers.sslTemporaryRedirect }}</span></td>
</tr>
<tr *ngIf="p.headers.stsIncludeSubdomains">
<td><span class="has-text-grey">STS Include Subdomains</span></td>
<td><span class="has-text-grey">{{ p.headers.stsIncludeSubdomains }}</span></td>
</tr>
<tr *ngIf="p.headers.stsPreload">
<td><span class="has-text-grey">STS Preload</span></td>
<td><span class="has-text-grey">{{ p.headers.stsPreload }}</span></td>
</tr>
<tr *ngIf="p.headers.stsSeconds">
<td><span class="has-text-grey">STS Seconds</span></td>
<td><span class="has-text-grey">{{ p.headers.stsSeconds }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="column is-12" *ngIf="p.headers.allowedHosts">
<h2>Allowed Hosts</h2>
<div class="tags-list">
<span class="tag is-light" *ngFor="let host of p.headers.allowedHosts">{{ host }}</span>
</div>
</div>
<div class="column is-12" *ngIf="p.headers.sslProxyHeaders">
<h2>SSL Proxy Headers</h2>
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr *ngFor="let key of p.headers.sslProxyHeaders | keys">
<td><span class="has-text-grey-light">{{ key }}</span></td>
<td><span class="has-text-grey">{{ p.headers.sslProxyHeaders[key] }}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="column is-12" *ngIf="p.headers.hostsProxyHeaders">
<h2>Hosts Proxy Headers</h2>
<div class="tags-list">
<span class="tag is-light" *ngFor="let h of p.headers.hostsProxyHeaders">{{ h }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Backends -->
<div class="column is-6">
<h2 class="subtitle"><span class="tag is-primary">{{ providers[tab]?.backends.length }}</span> Backends</h2>
<div class="message" *ngFor="let p of providers[tab]?.backends; let i = index;">
<div class="message-header">
<h2 [id]="p.id">
<div>
<i class="icon fas fa-server"></i>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-primary">{{ p.id }}</span>
</div>
</div>
</div>
</div>
</h2>
</div>
<div class="message-body">
<div class="tabs is-fullwidth is-small is-boxed">
<ul>
<li [class.is-active]="p.section !== 'details'" (click)="p.section = 'main'"><a>Main</a></li>
<li [class.is-active]="p.section === 'details'" (click)="p.section = 'details'"><a>Details</a></li>
</ul>
</div>
<!-- Main -->
<div *ngIf="p.section !== 'details'">
<table class="table is-fullwidth is-hoverable">
<tbody>
<tr>
<td>Server</td>
<td>Weight</td>
</tr>
<tr *ngFor="let server of p.servers; let ri = index;">
<td><a href="{{ server.url }}" title="{{ server.title }}">{{ server.url }}</a></td>
<td><span class="has-text-grey">{{ server.weight }}</span></td>
</tr>
</tbody>
</table>
</div>
<!-- Details -->
<div *ngIf="p.section === 'details'">
<div *ngIf="p.loadBalancer">
<div class="columns">
<div class="column is-3">
<h2>Load Balancer</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Method</span>
<span class="tag is-info">{{ p.loadBalancer.method }}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons" *ngIf="p.loadBalancer.stickiness || p.loadBalancer.sticky">
<span class="tag is-light">Stickiness</span>
<span class="tag is-info">true</span>
</div>
</div>
<div class="control" *ngIf="p.loadBalancer.stickiness">
<div class="tags has-addons">
<span class="tag is-light">Cookie Name</span>
<span class="tag is-info">{{ p.loadBalancer.stickiness.cookieName }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="p.maxConn">
<hr/>
<div class="columns">
<div class="column is-3">
<h2>Max Connections</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Amount</span>
<span class="tag is-info">{{ p.maxConn.amount }}</span>
</div>
</div>
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Extractor Function</span>
<span class="tag is-info">{{ p.maxConn.extractorFunc }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="p.circuitBreaker">
<hr/>
<div class="columns">
<div class="column is-3">
<h2>Circuit Breaker</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Expression</span>
<span class="tag is-info">{{ p.circuitBreaker.expression }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="p.healthCheck">
<hr/>
<div class="columns">
<div class="column is-3">
<h2>Health Check</h2>
</div>
<div class="column is-9">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Path</span>
<span class="tag is-info">{{ p.healthCheck.path }}</span>
</div>
</div>
<div class="control" *ngIf="p.healthCheck.port">
<div class="tags has-addons">
<span class="tag is-light">Port</span>
<span class="tag is-info">{{ p.healthCheck.port }}</span>
</div>
</div>
<div class="control" *ngIf="p.healthCheck.interval">
<div class="tags has-addons">
<span class="tag is-light">Interval</span>
<span class="tag is-info">{{ p.healthCheck.interval }}</span>
</div>
</div>
<div class="control" *ngIf="p.healthCheck.hostname">
<div class="tags has-addons">
<span class="tag is-light">Hostname</span>
<span class="tag is-info">{{ p.healthCheck.hostname }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="p.buffering">
<hr>
<div class="columns list-title">
<div class="column is-12">
<h2>Buffering</h2>
</div>
</div>
<div class="list-item">
<div class="columns">
<div class="column is-4">
<span>Request Body Bytes</span>
</div>
<div class="column is-4">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Max</span>
<span class="tag is-info">{{ p.buffering.maxRequestBodyBytes }}</span>
</div>
</div>
</div>
</div>
<div class="column is-4">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Men</span>
<span class="tag is-info">{{ p.buffering.memRequestBodyBytes }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="list-item">
<div class="columns">
<div class="column is-4">
<span>Response Body Bytes</span>
</div>
<div class="column is-4">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Max</span>
<span class="tag is-info">{{ p.buffering.maxResponseBodyBytes }}</span>
</div>
</div>
</div>
</div>
<div class="column is-4">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="tags has-addons">
<span class="tag is-light">Men</span>
<span class="tag is-info">{{ p.buffering.memResponseBodyBytes }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="list-item">
<div class="columns">
<div class="column is-4">
<span>Retry Expression</span>
</div>
<div class="column is-8">
<span class="tag is-info">{{ p.buffering.retryExpression }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns" *ngIf="!keys || !keys.length">
<div class="column is-12">
<div class="notification">
No providers found.
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,59 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import * as _ from "lodash";
@Component({
selector: 'app-providers',
templateUrl: 'providers.component.html'
})
export class ProvidersComponent implements OnInit, OnDestroy {
sub: Subscription;
keys: string[];
data: any;
previousData: any;
providers: any;
tab: string;
keyword: string;
constructor(private apiService: ApiService) { }
ngOnInit() {
this.keyword = '';
this.sub = Observable.timer(0, 2000)
.timeInterval()
.mergeMap(() => this.apiService.fetchProviders())
.subscribe(data => {
if (!_.isEqual(this.previousData, data)) {
this.previousData = _.cloneDeep(data);
this.data = data;
this.providers = data;
this.keys = Object.keys(this.providers);
this.tab = this.keys[0];
}
});
}
filter(): void {
const keyword = this.keyword.toLowerCase();
this.providers = Object.keys(this.data)
.filter(value => value !== 'acme' && value !== 'ACME')
.reduce((acc, curr) => {
return Object.assign(acc, {
[curr]: {
backends: this.data[curr].backends.filter(d => d.id.toLowerCase().includes(keyword)),
frontends: this.data[curr].frontends.filter(d => {
return d.id.toLowerCase().includes(keyword) || d.backend.toLowerCase().includes(keyword);
})
}
});
}, {});
}
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe();
}
}
}

View file

@ -1,14 +0,0 @@
'use strict';
var angular = require('angular');
var traefikCoreHealth = 'traefik.core.health';
module.exports = traefikCoreHealth;
angular
.module(traefikCoreHealth, ['ngResource'])
.factory('Health', Health);
/** @ngInject */
function Health($resource) {
return $resource('../health');
}

View file

@ -1,52 +0,0 @@
'use strict';
var angular = require('angular');
var traefikCoreProvider = 'traefik.core.provider';
module.exports = traefikCoreProvider;
angular
.module(traefikCoreProvider, ['ngResource'])
.factory('Providers', Providers);
/** @ngInject */
function Providers($resource, $q) {
const resourceProvider = $resource('../api/providers');
return {
get: function () {
return $q((resolve, reject) => {
resourceProvider.get()
.$promise
.then((rawProviders) => {
delete rawProviders.acme;
delete rawProviders.ACME;
for (let providerName in rawProviders) {
if (rawProviders.hasOwnProperty(providerName)) {
if (!providerName.startsWith('$')) {
// BackEnds mapping
let bckends = rawProviders[providerName].backends || {};
rawProviders[providerName].backends = Object.keys(bckends)
.map(key => {
const goodBackend = bckends[key];
goodBackend.backendId = key;
return goodBackend;
});
// FrontEnds mapping
let frtends = rawProviders[providerName].frontends || {};
rawProviders[providerName].frontends = Object.keys(frtends)
.map(key => {
const goodFrontend = frtends[key];
goodFrontend.frontendId = key;
return goodFrontend;
});
}
}
}
resolve(rawProviders);
})
.catch(reject);
});
}
};
}

View file

@ -1,14 +0,0 @@
'use strict';
var angular = require('angular');
var traefikCoreVersion = 'traefik.core.version';
module.exports = traefikCoreVersion;
angular
.module(traefikCoreVersion, ['ngResource'])
.factory('Version', Version);
/** @ngInject */
function Version($resource) {
return $resource('../api/version');
}

View file

@ -1,34 +0,0 @@
/**
* If you want to override some bootstrap variables, you have to change values here.
* The list of variables are listed here bower_components/bootstrap-sass/assets/stylesheets/bootstrap/_variables.scss
*/
$navbar-inverse-link-color: #5AADBB;
/**
* Do not remove the comments below. It's the markers used by wiredep to inject
* sass dependencies when defined in the bower.json of your dependencies
*/
// bower:scss
// endbower
.browsehappy {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
.thumbnail {
height: 200px;
img.pull-right {
width: 50px;
}
}
/**
* Do not remove the comments below. It's the markers used by gulp-inject to inject
* all your sass files automatically
*/
// injector
// endinjector

View file

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

View file

@ -1,238 +0,0 @@
'use strict';
var d3 = require('d3'),
moment = require('moment'),
HttpStatus = require('http-status-codes');
/** @ngInject */
function HealthController($scope, $interval, $log, Health) {
var vm = this;
vm.graph = {
averageResponseTime: {},
totalStatusCodeCount: {}
};
vm.graph.totalStatusCodeCount.options = {
"chart": {
type: 'discreteBarChart',
tooltip: {
contentGenerator: function (e) {
var d = e.data;
return d.label + " " + d.text;
}
},
height: 200,
margin: {
top: 20,
right: 20,
bottom: 40,
left: 55
},
x: function (d) {
return d.label;
},
y: function (d) {
return d.value;
},
showValues: true,
valueFormat: function (d) {
return d3.format('d')(d);
},
yAxis: {
axisLabelDistance: 30,
tickFormat: d3.format('d')
}
},
"title": {
"enable": true,
"text": "Total Status Code Count",
"css": {
"textAlign": "center"
}
}
};
vm.graph.totalStatusCodeCount.data = [
{
key: "Total Status Code Count",
values: [
{
"label": "200",
"value": 0
}
]
}
];
/**
* Update Total Status Code Count graph
*
* @param {Object} totalStatusCodeCount Object from API
*/
function updateTotalStatusCodeCount(totalStatusCodeCount) {
// extract values
vm.graph.totalStatusCodeCount.data[0].values = [];
for (var code in totalStatusCodeCount) {
if (totalStatusCodeCount.hasOwnProperty(code)) {
var statusCodeText = "";
try {
statusCodeText = HttpStatus.getStatusText(code);
} catch (e) {
// HttpStatus.getStatusText throws error on unknown codes
statusCodeText = "Unknown status code";
}
vm.graph.totalStatusCodeCount.data[0].values.push({
label: code,
value: totalStatusCodeCount[code],
text: statusCodeText
});
}
}
// Update Total Status Code Count graph render
if (vm.graph.totalStatusCodeCount.api) {
vm.graph.totalStatusCodeCount.api.update();
} else {
$log.error('fail');
}
}
vm.graph.averageResponseTime.options = {
chart: {
type: 'lineChart',
height: 200,
margin: {
top: 20,
right: 40,
bottom: 40,
left: 55
},
x: function (d) {
return d.x;
},
y: function (d) {
return d.y;
},
useInteractiveGuideline: true,
xAxis: {
tickFormat: function (d) {
return d3.time.format('%X')(new Date(d));
}
},
yAxis: {
tickFormat: function (d) {
return d3.format(',.1f')(d);
}
},
forceY: [0., 1.], // This prevents the chart from showing -1 on Oy when all the input data points
// have y = 0. It won't disable the automatic adjustment of the max value.
duration: 0 // Bug: Markers will not be drawn if you set this to some other value...
},
"title": {
"enable": true,
"text": "Average response time",
"css": {
"textAlign": "center"
}
}
};
var initialPoint = {
x: Date.now() - 3000,
y: 0
};
vm.graph.averageResponseTime.data = [
{
values: [initialPoint],
key: 'Average response time (ms)',
type: 'line',
color: '#2ca02c'
}
];
/**
* Update average response time graph
*
* @param {Number} x Coordinate X
* @param {Number} y Coordinate Y
*/
function updateAverageResponseTimeGraph(x, y) {
// x multiply 1000 by because unix time is in seconds and JS Date are in milliseconds
var data = {
x: x * 1000,
y: y * 1000
};
vm.graph.averageResponseTime.data[0].values.push(data);
// limit graph entries
if (vm.graph.averageResponseTime.data[0].values.length > 100) {
vm.graph.averageResponseTime.data[0].values.shift();
}
// Update Average Response Time graph render
if (vm.graph.averageResponseTime.api) {
vm.graph.averageResponseTime.api.update();
}
}
/**
* Format the timestamp as "x seconds ago", etc.
*
* @param {String} t Timestamp returned from the API
*/
function formatTimestamp(t) {
return moment(t, "YYYY-MM-DDTHH:mm:ssZ").fromNow();
}
/**
* Load all graph's datas
*
* @param {Object} health Health data from server
*/
function loadData(health) {
// Load datas and update Average Response Time graph render
updateAverageResponseTimeGraph(health.unixtime, health.average_response_time_sec);
// Load datas and update Total Status Code Count graph render
updateTotalStatusCodeCount(health.total_status_code_count);
// Format the timestamps
if (health.recent_errors) {
angular.forEach(health.recent_errors, function(i) {
i.time_formatted = formatTimestamp(i.time);
});
}
// set data's view
vm.health = health;
}
/**
* Action when load datas failed
*
* @param {Object} error Error state object
*/
function erroData(error) {
vm.health = {};
$log.error(error);
}
// first load
Health.get(loadData, erroData);
// Auto refresh data
var intervalId = $interval(function () {
Health.get(loadData, erroData);
}, 3000);
// Stop auto refresh when page change
$scope.$on('$destroy', function () {
$interval.cancel(intervalId);
});
}
module.exports = HealthController;

View file

@ -1,73 +0,0 @@
<div>
<h1 class="text-danger">
<span class="glyphicon glyphicon-heart" aria-hidden="true"></span> Health
</h1>
<div class="row">
<div class="col-md-6">
<div>
<nvd3 options="healthCtrl.graph.averageResponseTime.options" data="healthCtrl.graph.averageResponseTime.data" api="healthCtrl.graph.averageResponseTime.api"></nvd3>
</div>
<ul class="list-group">
<li class="list-group-item">
<span>Total response time :</span><span class="badge">{{healthCtrl.health.total_response_time}}</span>
</li>
</ul>
<ul class="list-group">
<li class="list-group-item">
<span>PID :</span><span class="badge">{{healthCtrl.health.pid}}</span>
</li>
<li class="list-group-item">
<span>Uptime :</span><span class="badge">{{healthCtrl.health.uptime}}</span>
</li>
</ul>
</div>
<div class="col-md-6">
<div>
<nvd3 options="healthCtrl.graph.totalStatusCodeCount.options" data="healthCtrl.graph.totalStatusCodeCount.data" api="healthCtrl.graph.totalStatusCodeCount.api"></nvd3>
</div>
<ul class="list-group">
<li class="list-group-item">
<span>Total count :</span><span class="badge">{{healthCtrl.health.total_count}}</span>
</li>
<li class="list-group-item">
<span>Count :</span><span class="badge">{{healthCtrl.health.count}}</span>
</li>
</ul>
</div>
</div>
<div ng-if="healthCtrl.health.recent_errors">
<h3>Recent HTTP Errors</h3>
<table class="table table-striped table-bordered">
<tr>
<td>Status</td>
<td>Request</td>
<td>Time</td>
</tr>
<tr ng-repeat="entry in healthCtrl.health.recent_errors"
ng-class="{'text-danger': entry.status_code >= 500}">
<td>{{ entry.status_code }} &mdash; {{ entry.status }}</td>
<td>
<span class="badge">{{ entry.method }}</span>
&nbsp;
{{ entry.host }}{{ entry.path }}
</td>
<td>
<span title="{{ entry.time }}">
{{ entry.time_formatted }}
</span>
</td>
</tr>
<tr ng-if="healthCtrl.health.recent_errors.length == 0">
<td colspan="3">
<p class="text-muted text-center">No entries</p>
</td>
</tr>
</table>
</div>
</div>

View file

@ -1,24 +0,0 @@
'use strict';
var angular = require('angular');
var traefikCoreHealth = require('../../core/health.resource');
var HealthController = require('./health.controller');
var traefikSectionHealth = 'traefik.section.health';
module.exports = traefikSectionHealth;
angular
.module(traefikSectionHealth, [traefikCoreHealth])
.controller('HealthController', HealthController)
.config(config);
/** @ngInject */
function config($stateProvider) {
$stateProvider.state('health', {
url: '/health',
template: require('./health.html'),
controller: 'HealthController',
controllerAs: 'healthCtrl'
});
}

View file

@ -1,20 +0,0 @@
'use strict';
function backendMonitor() {
return {
restrict: 'EA',
template: require('./backend-monitor.html'),
controller: BackendMonitorController,
controllerAs: 'backendCtrl',
bindToController: true,
scope: {
backend: '='
}
};
}
function BackendMonitorController() {
// Nothing
}
module.exports = backendMonitor;

View file

@ -1,23 +0,0 @@
<div class="panel panel-success">
<div class="panel-heading">
<strong><span class="glyphicon glyphicon-tasks" aria-hidden="true"></span> {{backendCtrl.backend.backendId}}</strong>
</div>
<div class="panel-body">
<table class="panel-table__servers table table-striped table-hover">
<tr>
<td><em>Server</em></td>
<td><em>URL</em></td>
<td><em>Weight</em></td>
</tr>
<tr data-ng-repeat="(serverId, server) in backendCtrl.backend.servers">
<td>{{serverId}}</td>
<td><code><a data-ng-href="{{server.url}}">{{server.url}}</a></code></td>
<td>{{server.weight}}</td>
</tr>
</table>
</div>
<div class="panel-footer" data-ng-show="backendCtrl.backend.loadBalancer || backendCtrl.backend.circuitBreaker">
<span data-ng-show="backendCtrl.backend.loadBalancer" class="label label-success">Load Balancer: {{backendCtrl.backend.loadBalancer.method}}</span>
<span data-ng-show="backendCtrl.backend.circuitBreaker" class="label label-success">Circuit Breaker: {{backendCtrl.backend.circuitBreaker.expression}}</span>
</div>
</div>

View file

@ -1,10 +0,0 @@
'use strict';
var angular = require('angular');
var backendMonitor = require('./backend-monitor.directive');
var traefikBackendMonitor = 'traefik.section.providers.backend-monitor';
module.exports = traefikBackendMonitor;
angular
.module(traefikBackendMonitor, [])
.directive('backendMonitor', backendMonitor);

View file

@ -1,20 +0,0 @@
'use strict';
function frontendMonitor() {
return {
restrict: 'EA',
template: require('./frontend-monitor.html'),
controller: FrontendMonitorController,
controllerAs: 'frontendCtrl',
bindToController: true,
scope: {
frontend: '='
}
};
}
function FrontendMonitorController() {
// Nothing
}
module.exports = frontendMonitor;

View file

@ -1,29 +0,0 @@
<div class="panel panel-warning">
<div class="panel-heading">
<strong><span class="glyphicon glyphicon-globe" aria-hidden="true"></span> {{frontendCtrl.frontend.frontendId}}</strong>
</div>
<div class="panel-body">
<table class="panel-table__routes table table-striped table-hover">
<tr>
<td><em>Route</em></td>
<td><em>Rule</em></td>
</tr>
<tr data-ng-repeat="(routeId, route) in frontendCtrl.frontend.routes">
<td>{{routeId}}</td>
<td><code>{{route.rule}}</code></td>
</tr>
</table>
</div>
<div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer">
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints">
<span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last">&nbsp;</span>
</span>
<span data-ng-show="frontendCtrl.frontend.redirect" class="label label-success">Redirect to {{frontendCtrl.frontend.redirect}}</span>
<span class="label label-warning" role="button" data-toggle="collapse" href="#{{frontendCtrl.frontend.backend}}" aria-expanded="false">Backend:{{frontendCtrl.frontend.backend}}</span>
<span data-ng-show="frontendCtrl.frontend.passHostHeader" class="label label-warning">PassHostHeader</span>
<span data-ng-repeat="whitelistSourceRange in frontendCtrl.frontend.whitelistSourceRange">
<span class="label label-warning">Whitelist {{ whitelistSourceRange }}</span>
</span>
<span data-ng-show="frontendCtrl.frontend.priority" class="label label-warning">Priority:{{frontendCtrl.frontend.priority}}</span>
</div>
</div>

View file

@ -1,10 +0,0 @@
'use strict';
var angular = require('angular');
var frontendMonitor = require('./frontend-monitor.directive');
var traefikFrontendMonitor = 'traefik.section.providers.frontend-monitor';
module.exports = traefikFrontendMonitor;
angular
.module(traefikFrontendMonitor, [])
.directive('frontendMonitor', frontendMonitor);

View file

@ -1,33 +0,0 @@
'use strict';
var _ = require('lodash');
/** @ngInject */
function ProvidersController($scope, $interval, $log, Providers) {
const vm = this;
function loadProviders() {
Providers
.get()
.then(providers => {
if (!_.isEqual(vm.previousProviders, providers)) {
vm.providers = providers;
vm.previousProviders = _.cloneDeep(providers);
}
})
.catch(error => {
vm.providers = {};
$log.error(error);
});
}
loadProviders();
const intervalId = $interval(loadProviders, 2000);
$scope.$on('$destroy', function () {
$interval.cancel(intervalId);
});
}
module.exports = ProvidersController;

View file

@ -1,22 +0,0 @@
<div>
<div><input type="text" data-ng-model="providersCtrl.providerFilter" placeholder="Filter" class="form-control"></div>
<br>
<uib-tabset>
<uib-tab data-ng-repeat="(providerId, provider) in providersCtrl.providers" heading="{{providerId}}">
<div class="row tabset-row__providers">
<div class="col-md-6">
<div data-ng-repeat="frontend in provider.frontends | filter: providersCtrl.providerFilter ">
<frontend-monitor data-provider-id="providerId" data-frontend="frontend"></frontend-monitor>
</div>
</div>
<div class="col-md-6">
<div data-ng-repeat="backend in provider.backends | filter: providersCtrl.providerFilter">
<backend-monitor data-provider-id="providerId" data-backend="backend"></backend-monitor>
</div>
</div>
</div>
</uib-tab>
</uib-tabset>
</div>

View file

@ -1,30 +0,0 @@
'use strict';
var angular = require('angular');
var traefikCoreProvider = require('../../core/providers.resource');
var ProvidersController = require('./providers.controller');
var traefikBackendMonitor = require('./backend-monitor/backend-monitor.module');
var traefikFrontendMonitor = require('./frontend-monitor/frontend-monitor.module');
var traefikSectionProviders = 'traefik.section.providers';
module.exports = traefikSectionProviders;
angular
.module(traefikSectionProviders, [
traefikCoreProvider,
traefikBackendMonitor,
traefikFrontendMonitor
])
.config(config)
.controller('ProvidersController', ProvidersController);
/** @ngInject */
function config($stateProvider) {
$stateProvider.state('provider', {
url: '/',
template: require('./providers.html'),
controller: 'ProvidersController',
controllerAs: 'providersCtrl'
});
}

View file

@ -1,24 +0,0 @@
'use strict';
var angular = require('angular');
require('nvd3');
var ndv3 = require('angular-nvd3');
var traefikSectionHealth = require('./health/health.module');
var traefikSectionProviders = require('./providers/providers.module');
var traefikSection = 'traefik.section';
module.exports = traefikSection;
angular
.module(traefikSection, [
'ui.router',
'ui.bootstrap',
ndv3,
traefikSectionProviders,
traefikSectionHealth
])
.config(config);
/** @ngInject */
function config($urlRouterProvider) {
$urlRouterProvider.otherwise('/');
}

View file

@ -0,0 +1,88 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/retry';
export interface ProviderType {
[provider: string]: {
backends: any;
frontends: any;
};
}
@Injectable()
export class ApiService {
headers: HttpHeaders;
constructor(private http: HttpClient) {
this.headers = new HttpHeaders({
'Access-Control-Allow-Origin': '*'
});
}
fetchVersion(): Observable<any> {
return this.http.get(`/api/version`, { headers: this.headers })
.retry(4)
.catch((err: HttpErrorResponse) => {
console.error(`[version] returned code ${err.status}, body was: ${err.error}`);
return Observable.empty<any>();
});
}
fetchHealthStatus(): Observable<any> {
return this.http.get(`/health`, { headers: this.headers })
.retry(2)
.catch((err: HttpErrorResponse) => {
console.error(`[health] returned code ${err.status}, body was: ${err.error}`);
return Observable.empty<any>();
});
}
fetchProviders(): Observable<any> {
return this.http.get(`/api/providers`, { headers: this.headers })
.retry(2)
.catch((err: HttpErrorResponse) => {
console.error(`[providers] returned code ${err.status}, body was: ${err.error}`);
return Observable.of<any>({});
})
.map(this.parseProviders);
}
parseProviders(data: any): ProviderType {
return Object.keys(data)
.filter(value => value !== 'acme' && value !== 'ACME')
.reduce((acc, curr) => {
acc[curr] = {
backends: Object.keys(data[curr].backends || {}).map(key => {
data[curr].backends[key].id = key;
data[curr].backends[key].servers = Object.keys(data[curr].backends[key].servers || {}).map(server => {
return {
title: server,
url: data[curr].backends[key].servers[server].url,
weight: data[curr].backends[key].servers[server].weight
};
});
return data[curr].backends[key];
}),
frontends: Object.keys(data[curr].frontends || {}).map(key => {
data[curr].frontends[key].id = key;
data[curr].frontends[key].routes = Object.keys(data[curr].frontends[key].routes || {}).map(route => {
return {
title: route,
rule: data[curr].frontends[key].routes[route].rule
};
});
return data[curr].frontends[key];
}),
};
return acc;
}, {});
}
}

View file

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { Subject } from 'rxjs/Subject';
@Injectable()
export class WindowService {
resize: Subject<any>;
constructor(private eventManager: EventManager) {
this.resize = new Subject();
this.eventManager.addGlobalEventListener('window', 'resize', this.onResize);
}
onResize = (event: UIEvent) => {
this.resize.next(event.target);
}
}

View file

@ -1,34 +0,0 @@
@font-face {
font-family: 'charterregular';
src: url('./assets/fonts/charter_regular-webfont.eot');
src: url('./assets/fonts/charter_regular-webfont.eot?#iefix') format('embedded-opentype'),
url('./assets/fonts/charter_regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.traefik-blue {
color: #00B1FF;
}
.traefik-text {
font-family: 'charterregular', Arial, sans-serif;
}
.panel-body .panel-table__servers,
.panel-body .panel-table__routes {
margin-bottom: 0;
}
.tabset-row__providers {
margin-top: 3rem;
}
table {
table-layout: fixed;
}
td, th {
word-wrap: break-word;
}

View file

@ -1,10 +0,0 @@
'use strict';
/** @ngInject */
function VersionController($scope, Version) {
Version.get(function (version) {
$scope.version = version;
});
}
module.exports = VersionController;

View file

@ -1,11 +0,0 @@
'use strict';
var angular = require('angular');
var traefikCoreVersion = require('../core/version.resource');
var VersionController = require('./version.controller');
var traefikVersion = 'traefik.version';
module.exports = traefikVersion;
angular
.module(traefikVersion, [traefikCoreVersion])
.controller('VersionController', VersionController);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,30 @@
<svg version="1.1" id="L5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve">
<circle fill="#0294FF" stroke="none" cx="6" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 15 ; 0 -15; 0 15"
repeatCount="indefinite"
begin="0.1"/>
</circle>
<circle fill="#0294FF" stroke="none" cx="30" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 10 ; 0 -10; 0 10"
repeatCount="indefinite"
begin="0.2"/>
</circle>
<circle fill="#0294FF" stroke="none" cx="54" cy="50" r="6">
<animateTransform
attributeName="transform"
dur="1s"
type="translate"
values="0 5 ; 0 -5; 0 5"
repeatCount="indefinite"
begin="0.3"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xml:space="preserve"
enable-background="new 0 0 595.28 841.89"
viewBox="0 0 340 456.33044"
height="486.75247"
width="362.66666"
y="0px"
x="0px"
id="Calque_1"
version="1.1"><metadata
id="metadata3280"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3278" /><path
style="fill:#c9781f"
id="path3156"
d="m 65.412121,155.07154 c 0,0 24.66784,-21.47409 97.677319,-21.47409 66.97804,0 85.80047,15.19398 104.91761,21.47409 l -99.73639,48.73542 z" /><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3158"><path
style="fill:#f6d2a2"
id="path3160"
d="m 118.946,476.458 c 0.707,14.572 15.264,7.83 21.858,3.274 6.259,-4.325 8.089,-0.73 8.638,-9.266 0.36,-5.61 1.007,-11.22 0.688,-16.853 -9.464,-0.858 -19.759,1.396 -27.518,7.033 -3.996,2.905 -11.49,12.174 -3.666,15.812" /><path
style="fill:#c6b198"
id="path3162"
d="m 118.946,476.458 c 2.119,-0.788 4.364,-1.348 5.802,-3.264" /><path
style="fill:#37abc8"
id="path3164"
d="M 152.588,302.861 C 96.804,287.174 138.284,216.207 183.08,245.397 Z" /><path
style="fill:#37abc8"
id="path3166"
d="m 400.436,240.071 c 44.155,-31.014 84.056,38.959 32.74,56.565 z" /><path
style="fill:#f6d2a2"
id="path3168"
d="m 409.934,655.8 c 11.216,6.94 31.716,27.923 14.891,38.098 -16.166,14.802 -25.214,-16.247 -39.403,-20.549 6.111,-8.298 13.856,-15.865 24.512,-17.549 z" /><path
style="fill:none"
id="path3170"
d="m 424.825,693.897 c -2.494,-4.96 -3.332,-10.748 -7.496,-14.746" /><path
style="fill:#f6d2a2"
id="path3172"
d="m 209.561,679.514 c -13.164,2.037 -20.574,13.914 -31.548,19.945 -10.341,6.166 -14.297,-1.974 -15.229,-3.627 -1.621,-0.739 -1.485,0.688 -3.987,-1.831 -9.587,-15.13 9.989,-26.189 20.182,-33.705 14.198,-2.871 23.096,9.438 30.582,19.218 z" /><path
style="fill:none"
id="path3174"
d="m 162.785,695.831 c 0.501,-5.766 5.074,-9.628 7.251,-14.504" /><path
style="fill:#077e91"
id="path3176"
d="m 154.916,283.26 c -7.36,-3.893 -12.759,-9.18 -8.257,-17.693 4.168,-7.88 11.911,-7.025 19.271,-3.132 z" /><path
style="fill:#077e91"
id="path3178"
d="m 421.549,275.859 c 7.36,-3.893 12.759,-9.18 8.257,-17.693 -4.168,-7.881 -11.91,-7.025 -19.271,-3.132 z" /><path
style="fill:#f6d2a2"
id="path3180"
d="m 472.21,474.607 c -0.707,14.572 -15.264,7.83 -21.858,3.274 -6.259,-4.325 -8.089,-0.73 -8.638,-9.265 -0.36,-5.61 -1.007,-11.22 -0.688,-16.853 9.464,-0.858 19.759,1.396 27.518,7.033 3.996,2.904 11.49,12.174 3.666,15.811" /><path
style="fill:#c6b198"
id="path3182"
d="m 472.21,474.607 c -2.119,-0.788 -4.364,-1.348 -5.802,-3.264" /><g
id="g3184"><path
style="fill:#37abc8"
id="path3186"
d="m 289.988,210.595 c 55.847,0 108.2,7.987 135.492,61.642 24.496,60.141 15.785,124.993 19.521,188.553 3.208,54.577 10.322,117.629 -14.997,168.205 -26.635,53.21 -93.191,66.595 -148.026,64.634 -43.071,-1.541 -95.101,-15.593 -119.409,-54.944 -28.519,-46.165 -15.017,-114.81 -12.946,-166.179 2.454,-60.849 -16.482,-121.882 3.508,-181.425 20.737,-61.765 76.665,-75.724 136.857,-80.486" /></g><path
style="fill:#ffffff"
id="path3188"
d="m 299.847,285.567 c 10.027,58.288 105.304,42.877 91.619,-15.91 -12.271,-52.716 -94.951,-38.124 -91.619,15.91" /><path
style="fill:#ffffff"
id="path3190"
d="m 185.992,294.994 c 12.996,50.745 94.24,37.753 91.178,-13.149 -3.669,-60.964 -103.603,-49.2 -91.178,13.149" /><path
style="fill:#ffffff"
id="path3192"
d="m 318.343,353.511 c 0.044,7.79 1.843,15.403 0.289,24.148 -1.935,3.656 -5.729,4.043 -9.001,5.52 -4.524,-0.71 -8.328,-3.68 -10.143,-7.912 -1.161,-9.202 0.433,-18.111 0.726,-27.316 z" /><g
id="g3194"><ellipse
id="ellipse3196"
ry="14.86"
rx="13.719"
cy="286.71799"
cx="208.39999" /><ellipse
style="fill:#ffffff"
id="ellipse3198"
ry="3.777"
rx="3.234"
cy="290.07101"
cx="214.64" /></g><g
id="g3200"><ellipse
id="ellipse3202"
ry="14.86"
rx="13.491"
cy="283.017"
cx="323.34799" /><ellipse
style="fill:#ffffff"
id="ellipse3204"
ry="3.777"
rx="3.181"
cy="286.371"
cx="329.48499" /></g><path
style="fill:#ffffff"
id="path3206"
d="m 279.137,354.685 c -5.986,14.507 3.338,43.515 19.579,22.119 -1.161,-9.202 0.433,-18.111 0.726,-27.316 z" /><g
id="g3208"><path
style="fill:#f6d2a2"
id="path3210"
d="m 278.185,326.748 c -11.156,0.951 -20.276,14.216 -14.475,24.71 7.682,13.9 24.828,-1.23 35.507,0.188 12.291,0.252 22.361,12.996 32.233,2.304 10.979,-11.892 -4.727,-23.474 -17.002,-28.652 z" /></g></g><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3212"><path
style="fill:#ef9325"
id="path3214"
d="m 135.236,375.349 c 0,0 3.532,38.261 3.29,54.754 -0.242,16.492 15.56,6.079 16.38,32.016 0.82,25.937 -9.572,21.018 -15.047,33.713 -5.475,12.694 -9.196,72.145 -9.196,72.145 0,0 5.618,11.108 23.166,21.11 17.548,10.002 45.765,15.62 70.198,14.513 24.433,-1.107 41.885,-6.341 46.249,-9.236 4.363,-2.894 11.605,-12.05 14.329,-19.131 2.724,-7.081 8.415,-65.402 8.018,-98.775 -0.397,-33.373 -5.911,-64.166 -5.911,-64.166 z" /><path
style="fill:#e5e5e5"
id="path3216"
d="m 290.007,537.841 c -71.952,8.032 -155.439,-14.172 -155.439,-14.172 0,0 1.226,-22.521 6.626,-30.447 74.833,23.185 150.329,15.834 150.329,15.834 1.12,10.514 0.331,18.872 -1.516,28.785 z" /><path
style="fill:#e5e5e5"
id="path3218"
d="m 280.95,582.228 c -71.952,8.032 -150.287,-15.064 -150.287,-15.064 0,0 -0.251,-21.631 2.607,-31.13 74.833,23.185 154.763,17.407 154.763,17.407 -0.673,11.413 -2.278,22.646 -7.083,28.787 z" /></g><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3220"><path
style="fill:#ef9325"
id="path3222"
d="m 452.156,375.349 c 0,0 -0.488,38.261 -0.243,54.754 0.245,16.492 -9.578,6.079 -10.409,32.016 -0.831,25.937 9.692,21.018 15.236,33.713 5.544,12.694 9.312,72.145 9.312,72.145 0,0 -5.689,11.108 -23.457,21.11 -17.768,10.002 -46.34,15.62 -71.08,14.513 -24.74,-1.107 -42.411,-6.341 -46.83,-9.236 -4.419,-2.895 -11.751,-12.05 -14.509,-19.131 -2.758,-7.081 -8.521,-65.402 -8.119,-98.775 0.402,-33.373 5.985,-64.166 5.985,-64.166 z" /><path
style="fill:#e5e5e5"
id="path3224"
d="m 304.706,537.841 c 72.856,8.032 157.392,-14.172 157.392,-14.172 0,0 -1.242,-22.521 -6.709,-30.447 -75.774,23.185 -152.218,15.834 -152.218,15.834 -1.134,10.514 -0.335,18.872 1.535,28.785 z" /><path
style="fill:#e5e5e5"
id="path3226"
d="m 313.876,582.228 c 72.856,8.032 152.175,-15.064 152.175,-15.064 0,0 0.254,-21.631 -2.64,-31.13 -75.774,23.185 -156.707,17.407 -156.707,17.407 0.682,11.413 2.307,22.646 7.172,28.787 z" /></g><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3228"><path
style="fill:#d2e261"
id="path3230"
d="m 481.221,445.525 c -2.264,4.272 -6.745,6.334 -10.009,4.604 l -2.615,-1.385 c -3.264,-1.729 -4.076,-6.595 -1.812,-10.867 l 50.985,-96.234 c 2.263,-4.272 6.745,-6.334 10.009,-4.604 l 2.615,1.385 c 3.264,1.729 4.076,6.595 1.812,10.867 z" /><path
id="path3232"
d="m 457.143,490.972 c -1.319,2.489 -5.034,3.105 -8.298,1.375 l -2.615,-1.385 c -3.264,-1.729 -4.842,-5.149 -3.523,-7.638 l 23.92,-45.149 c 1.319,-2.489 5.034,-3.105 8.298,-1.375 l 2.615,1.385 c 3.264,1.729 4.842,5.149 3.523,7.638 z" /><path
style="fill:#9b9b9b"
id="path3234"
d="m 478.411,436.54 -2.615,-1.385 c -3.264,-1.729 -6.604,-1.823 -7.459,-0.21 l -1.529,2.886 c 0.855,-1.614 4.194,-1.52 7.459,0.21 l 2.615,1.385 c 3.264,1.729 5.218,4.44 4.363,6.053 l 1.529,-2.886 c 0.855,-1.613 -1.099,-4.323 -4.363,-6.053 z" /></g><ellipse
style="fill:#f6d2a2"
id="ellipse3236"
ry="8.6829996"
rx="11.224"
cy="462.66101"
cx="456.83801"
transform="matrix(0.59911638,0.2229746,-0.2229746,0.59911638,100.48325,-168.1724)" /><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3238"><path
style="fill:#d2e261"
id="path3240"
d="m 111.779,447.078 c 2.065,4.372 6.447,6.637 9.787,5.059 l 2.675,-1.264 c 3.34,-1.578 4.374,-6.401 2.309,-10.773 L 80.032,341.629 c -2.065,-4.372 -6.447,-6.637 -9.787,-5.059 l -2.675,1.264 c -3.34,1.578 -4.374,6.401 -2.309,10.773 z" /><path
id="path3242"
d="m 133.748,493.582 c 1.203,2.547 4.886,3.332 8.227,1.755 l 2.675,-1.264 c 3.34,-1.578 5.073,-4.922 3.869,-7.469 l -21.825,-46.199 c -1.203,-2.547 -4.886,-3.332 -8.227,-1.755 l -2.675,1.264 c -3.34,1.578 -5.073,4.922 -3.869,7.469 z" /><path
style="fill:#9b9b9b"
id="path3244"
d="m 114.998,438.232 2.675,-1.264 c 3.34,-1.578 6.68,-1.519 7.46,0.132 l 1.395,2.954 c -0.78,-1.651 -4.12,-1.71 -7.46,-0.133 l -2.675,1.264 c -3.34,1.578 -5.416,4.196 -4.636,5.847 l -1.395,-2.954 c -0.779,-1.65 1.296,-4.268 4.636,-5.846 z" /></g><ellipse
style="fill:#f6d2a2"
id="ellipse3246"
ry="8.6829996"
rx="11.224"
cy="463.922"
cx="137.56"
transform="matrix(-0.59911638,0.2229746,-0.2229746,-0.59911638,252.76813,458.95304)" /><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3248"><g
id="g3250"><path
style="fill:#960000"
id="path3252"
d="m 159.132,324.732 c -2.218,4.033 -7.45,5.414 -11.687,3.084 l -4.873,-2.679 c -4.237,-2.33 -5.873,-7.488 -3.656,-11.521 l 50.714,-92.23 c 2.218,-4.033 7.45,-5.414 11.687,-3.084 l 4.873,2.679 c 4.237,2.33 5.33,6.689 3.656,11.521 -17.765,51.258 -50.714,92.23 -50.714,92.23 z" /><path
style="fill:#595959"
id="path3254"
d="m 172.547,272.051 c 15.422,-28.047 25.555,-52.169 23.141,-54.905 l 0.057,-0.103 c -0.066,-0.036 -0.136,-0.06 -0.202,-0.096 -0.008,-0.005 -0.01,-0.021 -0.019,-0.026 l -0.007,0.014 c -23.699,-12.841 -55.583,-0.124 -71.334,28.522 -15.751,28.646 -9.41,62.381 14.128,75.514 l -0.007,0.014 c 0.009,0.005 0.023,-0.002 0.032,0.002 0.066,0.037 0.123,0.083 0.189,0.119 l 0.057,-0.103 c 3.602,0.573 18.543,-20.905 33.965,-48.952 z" /></g><g
id="g3256"><path
style="fill:#960000"
id="path3258"
d="m 426.693,324.925 c 2.1,4.095 7.291,5.627 11.593,3.42 l 4.948,-2.538 c 4.302,-2.206 6.087,-7.315 3.987,-11.41 L 399.19,220.742 c -2.1,-4.095 -7.291,-5.627 -11.593,-3.42 l -4.948,2.538 c -4.302,2.206 -5.521,6.533 -3.987,11.41 16.279,51.749 48.031,93.655 48.031,93.655 z" /><path
style="fill:#595959"
id="path3260"
d="m 414.804,271.879 c -14.606,-28.48 -24.039,-52.885 -21.547,-55.55 l -0.054,-0.105 c 0.067,-0.034 0.138,-0.056 0.205,-0.09 0.009,-0.005 0.011,-0.021 0.02,-0.025 l 0.007,0.014 c 24.059,-12.152 55.563,1.48 70.481,30.569 14.918,29.089 7.606,62.627 -16.301,75.075 l 0.007,0.014 c -0.009,0.004 -0.023,-0.003 -0.032,10e-4 -0.067,0.035 -0.125,0.08 -0.192,0.114 l -0.054,-0.105 c -3.618,0.468 -17.934,-21.432 -32.54,-49.912 z" /></g><path
style="fill:#353535"
id="path3262"
d="m 462.36,259.869 c 0,0 -17.746,-44.446 -38.326,-67.945 -20.58,-23.498 -221.937,-31.512 -255.003,0 -33.066,31.512 -45.533,67.945 -45.533,67.945 v -9.844 c 0,0 15.295,-43.268 45.533,-67.945 30.238,-24.677 228.946,-23.378 254.582,0 25.635,23.378 38.747,67.945 38.747,67.945 z" /><ellipse
style="fill:#960000"
id="ellipse3264"
ry="16.975"
rx="6.9489999"
cy="248.211"
cx="462.09201"
transform="matrix(-0.8898,0.4563,-0.4563,-0.8898,986.5333,258.1984)" /><ellipse
style="fill:#960000"
id="ellipse3266"
ry="16.975"
rx="6.9489999"
cy="247.02901"
cx="125.962"
transform="matrix(0.8763,0.4818,-0.4818,0.8763,134.6121,-30.1261)" /></g><g
transform="matrix(0.63926204,0,0,0.63926204,-21.039129,-84.874827)"
id="g3268"><path
style="opacity:0.6;fill:#ffffff"
id="path3270"
d="M 386.49,250.492 H 349.285 319.791 274.555 249 198.68 c -33.72,0 -33.604,89.606 -1.945,94.771 l 65.466,-4.21 c 7.915,0 23.719,-30.655 26.684,-33.065 2.965,-2.41 12.729,-2.704 16.575,0 3.846,2.704 12.593,29.976 20.507,29.976 l 65.466,7.298 c 30.579,-14.072 22.87,-94.77 -4.943,-94.77 z" /><path
style="opacity:0.5;fill:#ffffff"
id="path3272"
d="m 248.545,269.019 c -20.685,-0.462 -53.05,-0.274 -70.729,-0.13 -2.964,9.719 -6.269,22.024 -4.63,33.972 16.056,0 50.96,0.078 75.782,0.078 14.946,0 27.512,2.82 39.198,5.875 0.288,-0.368 0.53,-0.639 0.719,-0.792 2.965,-2.41 12.729,-2.704 16.575,0 0.995,0.7 2.323,3.058 3.874,6.149 5.967,1.258 11.943,2.127 18.153,2.205 16.915,0.214 56.557,0.146 82.066,0.074 3.249,-10.857 0.853,-22.495 0.125,-33.432 -24.906,0 -64.487,-0.562 -83.463,-0.562 -26.738,-0.001 -50.083,-12.821 -77.67,-13.437 z" /></g><path
id="path3274"
d="m 155.89646,123.63135 c -0.52803,-12.35565 23.02878,-13.90011 25.81085,-3.55813 2.77504,10.31833 -24.65122,12.71492 -25.81085,3.55813 -0.92565,-7.31124 0,0 0,0 z" /><g
transform="matrix(0.16363642,0,0,0.16363643,2.8667513,26.432443)"
id="g4788"><g
transform="matrix(4.1803662,0,0,4.1803662,-117.56255,1825.0688)"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:40px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans';letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none"
id="text3009"><path
d="m 119.09119,162.63228 h 4.14844 c 1.68746,0 3.04684,-0.0469 4.07813,-0.14063 1.07808,-0.0937 2.13277,-0.30468 3.16406,-0.63281 1.07808,-0.32812 1.87495,-0.84375 2.39062,-1.54687 0.51558,-0.75 0.77339,-1.71094 0.77344,-2.88282 -5e-5,-1.31249 -0.44536,-2.46093 -1.33594,-3.44531 -0.89067,-0.98437 -2.10942,-1.47655 -3.65625,-1.47656 h -0.42187 l -4.78125,0.21094 h -0.77344 c -2.95316,0 -5.10941,-1.05468 -6.46875,-3.16407 -1.3594,-2.15623 -2.03909,-5.64842 -2.03906,-10.47656 v -38.32031 h 12.9375 c 3.0937,6e-5 4.64058,-1.335877 4.64062,-4.007814 -4e-5,-1.265559 -0.39848,-2.296808 -1.19531,-3.09375 -0.75004,-0.796806 -1.89848,-1.195243 -3.44531,-1.195313 h -12.9375 V 72.069778 c -3e-5,-1.640533 -0.46878,-2.88272 -1.40625,-3.726562 -0.93753,-0.890531 -2.1094,-1.335843 -3.51563,-1.335938 -1.50002,9.5e-5 -2.88283,0.539157 -4.14843,1.617188 -1.21877,1.078217 -1.87502,2.367278 -1.96875,3.867187 l -1.68751,19.96875 h -8.64844 c -1.546881,7e-5 -2.718754,0.398507 -3.515625,1.195313 -0.796878,0.750067 -1.195315,1.734441 -1.195313,2.953125 -2e-6,1.265689 0.398435,2.2735 1.195313,3.023437 0.843745,0.750062 2.039057,1.125062 3.585937,1.125002 h 8.578128 v 40.5 c -1e-5,7.26564 1.59374,12.65626 4.78126,16.17187 3.18747,3.46875 7.47653,5.20313 12.86718,5.20313"
style="font-size:144px;font-family:Nunito;-inkscape-font-specification:Nunito;fill:#333333"
id="path3014" /><path
d="m 154.80994,161.6479 c 1.73436,0 3.23436,-0.51562 4.5,-1.54687 1.31248,-1.03125 1.96873,-2.48437 1.96875,-4.35938 v -36.21093 c -2e-5,-5.48433 1.73435,-9.86714 5.20313,-13.14844 3.51559,-3.28119 8.43746,-4.92182 14.76562,-4.92188 1.49996,6e-5 2.6484,-0.49212 3.44532,-1.476559 0.79682,-1.031187 1.19526,-2.249936 1.19531,-3.65625 -5e-5,-1.499933 -0.44536,-2.812432 -1.33594,-3.9375 -0.84379,-1.12493 -2.01567,-1.687429 -3.51562,-1.6875 -4.96879,7.1e-5 -9.23441,1.476632 -12.79688,4.429687 -3.51565,2.906314 -5.8594,6.539122 -7.03125,10.898442 l 0.0703,-8.437504 c -2e-5,-1.781184 -0.63283,-3.163995 -1.89843,-4.148438 -1.21877,-1.03118 -2.69534,-1.546805 -4.42969,-1.546875 -1.73439,7e-5 -3.23439,0.515695 -4.5,1.546875 -1.26564,0.984443 -1.89845,2.414129 -1.89844,4.289063 v 58.007809 c -1e-5,1.87501 0.60936,3.32813 1.82813,4.35938 1.21873,1.03125 2.69529,1.54687 4.42968,1.54687"
style="font-size:144px;font-family:Nunito;-inkscape-font-specification:Nunito;fill:#333333"
id="path3016" /><path
d="m 326.8058,161.6479 c 1.73435,0 3.23435,-0.53906 4.5,-1.61718 1.2656,-1.125 1.89841,-2.67187 1.89844,-4.64063 v -54.63281 h 11.88281 c 2.99996,6e-5 4.49996,-1.359314 4.5,-4.078127 -4e-5,-1.218683 -0.37504,-2.226495 -1.125,-3.023437 -0.70317,-0.796806 -1.82817,-1.195243 -3.375,-1.195313 h -11.88281 v -5.484375 c -3e-5,-3.796796 0.14059,-6.796793 0.42187,-9 0.3281,-2.249914 0.91404,-3.960849 1.75782,-5.132812 0.89059,-1.21866 1.8984,-1.992097 3.02343,-2.320313 1.12497,-0.374908 2.69528,-0.562408 4.71094,-0.5625 h 5.97656 c 1.40621,9.2e-5 2.48433,-0.468657 3.23438,-1.40625 0.79682,-0.937406 1.19526,-2.062404 1.19531,-3.375 -5e-5,-1.312402 -0.39849,-2.437401 -1.19531,-3.375 -0.75005,-0.984274 -1.80474,-1.476461 -3.16406,-1.476562 h -8.57813 c -6.28128,1.01e-4 -11.20315,1.781349 -14.76562,5.34375 -3.56252,3.515717 -5.34377,9.468836 -5.34375,17.859375 v 8.929687 h -9.21094 c -1.54688,7e-5 -2.69532,0.398507 -3.44531,1.195313 -0.75001,0.796942 -1.12501,1.804754 -1.125,3.023437 -1e-5,1.171939 0.37499,2.156313 1.125,2.953125 0.74999,0.750062 1.89843,1.125062 3.44531,1.125002 h 9.21094 v 54.63281 c -2e-5,1.96876 0.60935,3.51563 1.82812,4.64063 1.26561,1.07812 2.76561,1.61718 4.5,1.61718"
style="font-size:144px;font-family:Nunito;-inkscape-font-specification:Nunito;fill:#333333"
id="path3020" /><path
d="m 371.17299,75.515091 c 2.48435,8.6e-5 4.47654,-0.703038 5.97656,-2.109375 1.49998,-1.453035 2.24998,-3.328033 2.25,-5.625 -2e-5,-2.343654 -0.75002,-4.218652 -2.25,-5.625 -1.50002,-1.453024 -3.46877,-2.179586 -5.90625,-2.179688 -2.48439,1.02e-4 -4.50001,0.726664 -6.04687,2.179688 -1.50001,1.453223 -2.25001,3.328221 -2.25,5.625 -1e-5,2.296967 0.74999,4.171965 2.25,5.625 1.49998,1.406337 3.49217,2.109461 5.97656,2.109375 m -0.0703,86.132809 c 1.73435,0 3.23435,-0.5625 4.5,-1.6875 1.31247,-1.17187 1.96872,-2.74218 1.96875,-4.71093 V 98.085403 c -3e-5,-1.968684 -0.63284,-3.515557 -1.89844,-4.640625 -1.21877,-1.12493 -2.6719,-1.68743 -4.35938,-1.6875 -1.73439,7e-5 -3.25782,0.56257 -4.57031,1.6875 -1.26564,1.125068 -1.89845,2.671941 -1.89844,4.640625 v 57.164067 c -1e-5,2.0625 0.60937,3.65625 1.82813,4.78125 1.21873,1.07812 2.6953,1.61718 4.42969,1.61718"
style="font-size:144px;font-family:Nunito;-inkscape-font-specification:Nunito;fill:#333333"
id="path3022" /><path
d="m 404.92299,161.6479 c 1.73436,0 3.23435,-0.5625 4.5,-1.6875 1.31248,-1.12499 1.96873,-2.6953 1.96875,-4.71093 v -30.65625 l 33.04687,35.15625 c 1.17182,1.3125 2.57807,1.96875 4.21875,1.96875 1.54682,0 2.90619,-0.58594 4.07813,-1.75782 1.21868,-1.17187 1.82806,-2.5078 1.82812,-4.00781 -6e-5,-1.35937 -0.51569,-2.57812 -1.54687,-3.65625 l -27.91406,-29.39062 25.52343,-22.85157 c 1.12494,-1.078059 1.68744,-2.27337 1.6875,-3.585934 -6e-5,-1.453058 -0.586,-2.765557 -1.75781,-3.9375 -1.12506,-1.171805 -2.43756,-1.757742 -3.9375,-1.757813 -1.17193,7.1e-5 -2.27349,0.445383 -3.30469,1.335938 L 411.39174,121.35884 V 67.569778 c -2e-5,-1.968654 -0.63284,-3.515527 -1.89844,-4.640625 -1.21877,-1.1249 -2.67189,-1.687399 -4.35937,-1.6875 -1.73439,1.01e-4 -3.25783,0.5626 -4.57032,1.6875 -1.26563,1.125098 -1.89844,2.671971 -1.89843,4.640625 v 87.679692 c -1e-5,2.0625 0.60936,3.65625 1.82812,4.78125 1.21874,1.07812 2.6953,1.61718 4.42969,1.61718"
style="font-size:144px;font-family:Nunito;-inkscape-font-specification:Nunito;fill:#333333"
id="path3024" /></g><path
style="display:inline;fill:#37abc8;fill-opacity:1;stroke:none"
d="m 776.62606,2500.7168 c -33.86547,-2.9196 -59.74328,-13.1348 -78.56521,-31.0137 -19.14975,-18.1909 -27.36664,-41.9069 -24.12384,-69.6286 3.73164,-31.9013 18.89136,-54.4293 45.47029,-67.5719 20.48497,-10.1294 40.92086,-13.9825 74.2015,-13.9904 23.6457,-0.01 43.45939,1.9263 73.67896,7.1839 7.4724,1.3001 13.94181,2.3644 14.37649,2.3653 0.63211,0 1.13342,-2.3782 2.5045,-11.8865 2.48071,-17.2034 2.92533,-23.1396 2.24949,-30.0309 -0.78666,-8.0217 -1.81332,-11.8425 -4.85232,-18.0592 -7.8409,-16.0392 -26.4539,-25.8526 -56.3431,-29.7057 -8.54805,-1.1019 -28.27642,-1.2561 -36.83948,-0.2884 -14.34651,1.622 -22.35237,3.6887 -40.20696,10.3786 -10.44586,3.9141 -10.4936,3.9262 -14.66034,3.6816 -7.16306,-0.4197 -12.05304,-3.6628 -15.18945,-10.0734 -1.29168,-2.6399 -1.5188,-3.7297 -1.51885,-7.2868 0,-5.3399 1.39069,-8.1747 6.23314,-12.7045 10.85909,-10.1583 29.13255,-18.8091 47.57591,-22.5234 11.80945,-2.3782 17.62271,-2.8782 33.18165,-2.8543 16.09952,0.042 26.39396,0.9338 40.7011,3.5942 23.79706,4.4245 41.86649,12.2234 55.49189,23.9501 4.04288,3.4794 10.13915,10.3276 12.75137,14.3237 0.79841,1.2215 1.6209,2.221 1.8277,2.221 0.20693,0 1.52767,-1.4677 2.9352,-3.2611 20.63182,-26.2903 55.38062,-41.1712 96.0916,-41.1503 28.7948,0 55.5598,7.2994 75.5111,20.5528 15.8247,10.5123 28.6803,26.7063 33.8736,42.6702 2.9819,9.1659 3.4352,12.5529 3.4627,25.8661 0.016,8.2838 -0.2116,13.8713 -0.6736,16.4602 -4.6692,26.1661 -15.7909,44.2934 -35.0951,57.2012 -15.53,10.384 -35.0343,16.4601 -60.5435,18.8601 -12.8411,1.2081 -36.32755,1.094 -51.54251,-0.2509 -12.9128,-1.1411 -28.76889,-3.149 -35.53309,-4.4992 -5.6619,-1.1304 -25.0506,-4.3677 -25.21024,-4.2092 -0.0703,0.071 -0.99285,5.8692 -2.04985,12.888 -1.68766,11.2067 -1.92673,14.003 -1.96214,22.9511 -0.0376,9.0977 0.0915,10.6938 1.18915,14.8925 3.52969,13.5013 11.30818,22.5326 25.42034,29.5146 13.88254,6.8684 28.43586,9.8406 50.42569,10.2988 9.73545,0.2048 14.72115,0.062 20.81185,-0.5827 14.7651,-1.5656 21.4933,-3.2833 41.1098,-10.4948 4.8858,-1.7964 9.872,-3.4309 11.0805,-3.6324 2.5898,-0.4318 7.4198,0.4599 10.2348,1.89 5.081,2.5805 9.3499,9.4627 9.3233,15.0301 -0.029,6.5042 -2.2232,10.4438 -8.6626,15.5672 -15.3968,12.2502 -35.0556,19.7627 -58.5228,22.3646 -8.3681,0.928 -27.4738,1.0647 -36.70232,0.2633 -25.98905,-2.2573 -44.03272,-6.7024 -60.52259,-14.9089 -12.86662,-6.4034 -22.68346,-14.3474 -30.01545,-24.2895 -1.80157,-2.443 -3.33187,-4.5027 -3.40069,-4.578 -0.069,-0.075 -1.26635,1.3361 -2.66126,3.1353 -6.16583,7.9536 -15.59468,16.5025 -24.80897,22.4937 -13.30221,8.6492 -32.39892,15.2475 -51.72224,17.8715 -5.76268,0.7822 -24.88234,1.4276 -29.78512,1.005 z m 33.18166,-37.2321 c 15.77633,-3.4964 29.83536,-11.3772 40.04156,-22.4447 9.30988,-10.0952 16.94591,-25.2189 21.14838,-41.8852 2.36889,-9.395 6.30655,-35.5428 5.43369,-36.082 -0.34447,-0.2132 -2.42546,-0.5497 -4.62386,-0.7487 -2.19846,-0.2007 -8.46495,-0.9389 -13.92555,-1.645 -32.43526,-4.195 -52.33389,-5.8249 -63.48931,-5.1995 -21.20826,1.188 -29.85413,3.0395 -42.58748,9.1207 -6.5233,3.1152 -11.02568,6.4959 -14.5881,10.953 -5.11092,6.3951 -8.19582,12.7501 -10.34612,21.3132 -1.5648,6.2312 -1.72448,22.5471 -0.27214,27.804 2.44652,8.8552 5.35078,13.8671 11.88143,20.5034 8.94252,9.0869 20.73103,15.1488 35.35353,18.18 7.47705,1.5501 7.25577,1.535 19.77505,1.3465 9.08402,-0.1379 12.48275,-0.393 16.19892,-1.2157 z M 1016.4745,2340.883 c 13.1943,-1.1642 21.3072,-3.2126 31.2752,-7.8955 8.2903,-3.8952 13.1088,-7.8762 17.6686,-14.5978 6.7131,-9.8962 9.3272,-19.2614 9.2601,-33.1758 -0.048,-9.9175 -0.8885,-14.0431 -4.3122,-21.1631 -7.6497,-15.908 -25.6199,-27.3028 -49.1887,-31.1902 -5.8812,-0.9698 -21.66658,-0.7976 -27.69493,0.3011 -11.5973,2.116 -21.40043,5.9729 -30.04639,11.8207 -15.97364,10.8042 -26.99764,27.0905 -33.13643,48.9538 -2.41127,8.5877 -3.05405,11.884 -5.20388,26.687 l -1.98309,13.6552 2.65642,0.2926 c 8.02308,0.8896 26.11491,3.0802 36.62189,4.4346 6.6102,0.852 14.72275,1.7612 18.02786,2.0208 9.55476,0.75 26.68545,0.6802 36.05555,-0.1463 v 0 z"
id="path4507" /></g></svg>

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View file

@ -0,0 +1,8 @@
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
production: false
};

BIN
webui/src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,53 +1,13 @@
<!doctype html> <!doctype html>
<html ng-app="traefik"> <html class="has-navbar-fixed-top">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Træfik</title> <title>Traefik</title>
<meta name="description" content=""> <base href="./">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="traefik.icon.png" /> <link rel="icon" type="image/x-icon" href="./assets/images/traefik.icon.png">
</head> </head>
<body>
<app-root></app-root>
<body> </body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<div class="container">
<header>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand traefik-text" ui-sref="provider"><img height="16" src="traefik.icon.png"/></a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a ui-sref="provider" class="active">Providers</a></li>
<li><a ui-sref="health">Health</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a ng-controller="VersionController" href="https://github.com/containous/traefik/tree/{{version.Version}}" target="_blank">
<small>{{version.Version}} / {{version.Codename}}</small>
</a>
</li>
<li>
<a href="https://docs.traefik.io" target="_blank">Documentation</a>
</li>
<li>
<a href="https://traefik.io" target="_blank"><span class="traefik-blue">traefik.io</span></a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<main class="main">
<div data-ui-view></div>
</main>
</div>
</body>
</html> </html>

View file

@ -1,48 +0,0 @@
'use strict';
var angular = require('angular');
var ngAnimate = require('angular-animate');
var ngCookies = require('angular-cookies');
var ngSanitize = require('angular-sanitize');
var ngMessages = require('angular-messages');
var ngAria = require('angular-aria');
var ngResource = require('angular-resource');
var uiRouter = require('angular-ui-router');
var uiBootstrap = require('angular-ui-bootstrap');
var moment = require('moment');
var traefikSection = require('./app/sections/sections');
var traefikVersion = require('./app/version/version.module');
require('./index.scss');
require('animate.css/animate.css');
require('nvd3/build/nv.d3.css');
require('bootstrap/dist/css/bootstrap.css');
var app = 'traefik';
module.exports = app;
angular
.module(app, [
ngAnimate,
ngCookies,
ngSanitize,
ngMessages,
ngAria,
ngResource,
uiRouter,
uiBootstrap,
traefikSection,
traefikVersion
])
.run(runBlock)
.constant('moment', moment)
.config(config);
/** @ngInject */
function config($logProvider) {
// Enable log
$logProvider.debugEnabled(true);
}
/** @ngInject */
function runBlock($log) {
$log.debug('runBlock end');
}

View file

@ -1,3 +0,0 @@
$icon-font-path: "../bower_components/bootstrap-sass/assets/fonts/bootstrap/";
@import 'app/index.scss';
@import 'app/traefik.scss';

12
webui/src/main.ts Normal file
View file

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));

79
webui/src/polyfills.ts Normal file
View file

@ -0,0 +1,79 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';
/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect';
/**
* Required to support Web Animations `@angular/platform-browser/animations`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
*/
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

26
webui/src/styles/app.sass Normal file
View file

@ -0,0 +1,26 @@
@charset "utf-8"
@import 'typography'
@import 'colors'
@import '../../node_modules/bulma/sass/utilities/all'
@import '../../node_modules/bulma/sass/base/all'
@import '../../node_modules/bulma/sass/grid/columns'
@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 'nav'
@import 'content'
@import 'message'
@import 'label'
@import 'charts'
@import 'helper'
html
font-family: $open-sans
height: 100%
background: $background

View file

@ -0,0 +1,54 @@
.line-chart
width: 100%
height: 320px
background-color: $white
svg
font: 10px sans-serif
.line
fill: none
stroke: $blue
stroke-width: 3px
shape-rendering: geometricPrecision
.axis line, .axis path
stroke: $text
opacity: .2
shape-rendering: crispEdges
fill: none
.axis path
display: none
.axis text
fill: $text
.bar-chart
width: 100%
height: 320px
background-color: $white
.bar
fill: rgba($blue, 0.91)
&:hover
fill: lighten($blue, 10)
.axis text
fill: $text
font: 10px sans-serif
.axis line, .axis path
fill: none
opacity: .2
stroke: $text
.axis--x
text
font: 12px sans-serif
path
display: none

View file

@ -0,0 +1,34 @@
$background: #F1F5F7
$border: #DFE2E5
$border-secondary: #E6EAEE
$text: #7F8FA4
$text-dark: #3D495C
$text-grey: #8A9AAE
$color: #354052
$color-secondary: #7F8FA4
$color-disabled: #C2CAD4
$color-search: #A1A7AF
$border: #DFE2E5
$divider: #DFE2E5
$border-blue: #2EA2F8
$border-light: #f1f3f5
$blue: #0294FF
$blue-secondary: #1991EB
$blue-background: #2EA1F8
$green: #39B54A
$green-secondary: #45B854
$green-bg: #36AF47
$red: #f03e3e
$red-secondary: #ED1C24
$yellow: #ffd43b
$yellow-secondary: #F7981C
$grey: #7F8FA4
$grey-light: #adb5bd
$orange-secondary: #FD9A18
$grey-background: #E8EAF1
$grey-color: #8D909F
$turquoise: #01BCD4

View file

@ -0,0 +1,142 @@
.content
background: transparent
margin: 40px 0
.subtitle
font-size: 15px
text-transform: uppercase
color: $black
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
.content-item
background: $white
border: 1px solid $border-secondary
margin: 10px 0
border-radius: 4px
box-shadow: 1px 2px 5px rgba($border, 0.4)
h2
color: $text-dark
font-size: 14px
padding: 20px 20px 0 20px
font-weight: $weight-semibold
.content-item-data
padding: 10px 20px
.item-data
display: flex
align-items: center
justify-content: space-between
padding: 5px 10px
&.border-right
border-right: 1px solid #DFE3E9
.data-blue
color: $blue
font-size: 22px
font-weight: $weight-semibold
.data-grey
color: $grey
font-size: 12px
font-weight: $weight-light
.widget-item
min-height: 80px
padding: 20px
h1
color: $text-dark
font-size: 18px
font-weight: $weight-light
img
width: 40px
heught: 40px
display: block
float: left
margin-right: 10px
span
font-size: 13px
display: block
&.mtop12
margin-top: 12px
.loading-text
height: 320px
display: flex
align-items: center
justify-content: center
.main-loader
width: 70px
display: block
margin: 15px auto
.search-container
height: 50px
background: $white
border-radius: 4px
color: $black
margin: 10px 0
display: flex
align-items: center
position: relative
box-shadow: 1px 2px 5px rgba($border, 0.4)
border: 1px solid $border-secondary
.icon
position: absolute
left: 10px
top: 13px
input
font-size: 16px
color: $text
width: 100%
height: 48px
padding-left: 50px
border: none
outline: none
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

View file

@ -0,0 +1,2 @@
.padding-5-10
padding: 5px 10px

View file

@ -0,0 +1,29 @@
.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

@ -0,0 +1,89 @@
.message
display: block
font-size: 14px
margin: 20px 0 30px 0
border: 1px solid $border
background: $white
border-radius: 4px
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
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
.tags-list
margin: 5px 10px
.control
width: 100%
margin: 5px 0
.tags
width: 100%
.tag
width: 50%
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

16
webui/src/styles/nav.sass Normal file
View file

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

View file

@ -0,0 +1,14 @@
=font-face($family, $path, $weight: normal, $style: normal)
@font-face
font-family: $family
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')
$open-sans: 'Open Sans', sans-serif

20
webui/src/test.ts Normal file
View file

@ -0,0 +1,20 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View file

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "es2015",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

Some files were not shown because too many files have changed in this diff Show more