Merge branch 'v1.6' into master

This commit is contained in:
Fernandez Ludovic 2018-05-15 10:43:27 +02:00
commit 4eda1e1bd4
50 changed files with 763 additions and 650 deletions

View file

@ -1,5 +1,20 @@
# Change Log # Change Log
## [v1.6.1](https://github.com/containous/traefik/tree/v1.6.1) (2018-05-14)
[All Commits](https://github.com/containous/traefik/compare/v1.6.0...v1.6.1)
**Bug fixes:**
- **[acme]** Add missing deprecation info in CLI help. ([#3291](https://github.com/containous/traefik/pull/3291) by [ldez](https://github.com/ldez))
- **[docker,marathon,rancher]** Fix segment backend name ([#3317](https://github.com/containous/traefik/pull/3317) by [ldez](https://github.com/ldez))
- **[logs,middleware]** Error when accesslog and error pages ([#3314](https://github.com/containous/traefik/pull/3314) by [ldez](https://github.com/ldez))
- **[middleware,tracing]** Fix wrong tag in forward span in tracing middleware ([#3279](https://github.com/containous/traefik/pull/3279) by [mmatur](https://github.com/mmatur))
- **[webui]** Fix webui ([#3299](https://github.com/containous/traefik/pull/3299) by [ldez](https://github.com/ldez))
**Documentation:**
- **[k8s]** Add Documentation update for Kubernetes Ingress ([#3294](https://github.com/containous/traefik/pull/3294) by [dtomcej](https://github.com/dtomcej))
- **[tls]** Enhance entry point TLS CLI reference. ([#3290](https://github.com/containous/traefik/pull/3290) by [ldez](https://github.com/ldez))
- Typo in documentation ([#3261](https://github.com/containous/traefik/pull/3261) by [blakethepatton](https://github.com/blakethepatton))
## [v1.6.0](https://github.com/containous/traefik/tree/v1.6.0) (2018-04-30) ## [v1.6.0](https://github.com/containous/traefik/tree/v1.6.0) (2018-04-30)
[Commits](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.6.0) [Commits](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.6.0)
[Commits pre RC](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.6.0-rc1) [Commits pre RC](https://github.com/containous/traefik/compare/v1.5.0-rc1...v1.6.0-rc1)

View file

@ -14,7 +14,7 @@
Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.
Træfik integrates with your existing infrastructure components ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), ...) and configures itself automatically and dynamically. Træfik integrates with your existing infrastructure components ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), ...) and configures itself automatically and dynamically.
Telling Træfik where your orchestrator is could be the _only_ configuration step you need to do. Pointing Træfik at your orchestrator should be the _only_ configuration step you need.
--- ---

View file

@ -41,15 +41,15 @@ type ACME struct {
Email string `description:"Email address used for registration"` Email string `description:"Email address used for registration"`
Domains []types.Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` Domains []types.Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
Storage string `description:"File or key used for certificates storage."` Storage string `description:"File or key used for certificates storage."`
StorageFile string // deprecated StorageFile string // Deprecated
OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` //deprecated OnDemand bool `description:"(Deprecated) Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` //deprecated
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
CAServer string `description:"CA server to use."` CAServer string `description:"CA server to use."`
EntryPoint string `description:"Entrypoint to proxy acme challenge to."` EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"` DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"`
HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"` HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"`
DNSProvider string `description:"Activate DNS-01 Challenge (Deprecated)"` // deprecated DNSProvider string `description:"(Deprecated) Activate DNS-01 Challenge"` // Deprecated
DelayDontCheckDNS flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` // deprecated DelayDontCheckDNS flaeg.Duration `description:"(Deprecated) Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` // Deprecated
ACMELogging bool `description:"Enable debug logging of ACME actions."` ACMELogging bool `description:"Enable debug logging of ACME actions."`
client *acme.Client client *acme.Client
defaultCertificate *tls.Certificate defaultCertificate *tls.Certificate

View file

@ -105,13 +105,13 @@ type GlobalConfiguration struct {
// WebCompatibility is a configuration to handle compatibility with deprecated web provider options // WebCompatibility is a configuration to handle compatibility with deprecated web provider options
type WebCompatibility struct { type WebCompatibility struct {
Address string `description:"Web administration port" export:"true"` Address string `description:"(Deprecated) Web administration port" export:"true"`
CertFile string `description:"SSL certificate" export:"true"` CertFile string `description:"(Deprecated) SSL certificate" export:"true"`
KeyFile string `description:"SSL certificate" export:"true"` KeyFile string `description:"(Deprecated) SSL certificate" export:"true"`
ReadOnly bool `description:"Enable read only API" export:"true"` ReadOnly bool `description:"(Deprecated) Enable read only API" export:"true"`
Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"` Statistics *types.Statistics `description:"(Deprecated) Enable more detailed statistics" export:"true"`
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"` Metrics *types.Metrics `description:"(Deprecated) Enable a metrics exporter" export:"true"`
Path string `description:"Root path for dashboard and API" export:"true"` Path string `description:"(Deprecated) Root path for dashboard and API" export:"true"`
Auth *types.Auth `export:"true"` Auth *types.Auth `export:"true"`
Debug bool `export:"true"` Debug bool `export:"true"`
} }

View file

@ -174,7 +174,7 @@ func TestEntryPoints_Set(t *testing.T) {
name: "all parameters camelcase", name: "all parameters camelcase",
expression: "Name:foo " + expression: "Name:foo " +
"Address::8000 " + "Address::8000 " +
"TLS:goo,gii " + "TLS:goo,gii;foo,fii " +
"TLS " + "TLS " +
"TLS.MinVersion:VersionTLS11 " + "TLS.MinVersion:VersionTLS11 " +
"TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA " + "TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA " +
@ -211,6 +211,10 @@ func TestEntryPoints_Set(t *testing.T) {
CertFile: tls.FileOrContent("goo"), CertFile: tls.FileOrContent("goo"),
KeyFile: tls.FileOrContent("gii"), KeyFile: tls.FileOrContent("gii"),
}, },
{
CertFile: tls.FileOrContent("foo"),
KeyFile: tls.FileOrContent("fii"),
},
}, },
ClientCA: tls.ClientCA{ ClientCA: tls.ClientCA{
Files: []string{"car"}, Files: []string{"car"},
@ -280,7 +284,7 @@ func TestEntryPoints_Set(t *testing.T) {
name: "all parameters lowercase", name: "all parameters lowercase",
expression: "Name:foo " + expression: "Name:foo " +
"address::8000 " + "address::8000 " +
"tls:goo,gii " + "tls:goo,gii;foo,fii " +
"tls " + "tls " +
"tls.minversion:VersionTLS11 " + "tls.minversion:VersionTLS11 " +
"tls.ciphersuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA " + "tls.ciphersuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA " +
@ -315,6 +319,10 @@ func TestEntryPoints_Set(t *testing.T) {
CertFile: tls.FileOrContent("goo"), CertFile: tls.FileOrContent("goo"),
KeyFile: tls.FileOrContent("gii"), KeyFile: tls.FileOrContent("gii"),
}, },
{
CertFile: tls.FileOrContent("foo"),
KeyFile: tls.FileOrContent("fii"),
},
}, },
ClientCA: tls.ClientCA{ ClientCA: tls.ClientCA{
Files: []string{"car"}, Files: []string{"car"},

View file

@ -288,12 +288,12 @@ Segment labels override the default behavior.
| Label | Description | | Label | Description |
|---------------------------------------------------------------------------|-------------------------------------------------------------| |---------------------------------------------------------------------------|-------------------------------------------------------------|
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` | | `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` | | `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` | | `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
| `traefik.<segment_name>.weight=10` | Same as `traefik.weight` | | `traefik.<segment_name>.weight=10` | Same as `traefik.weight` |
| `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` |
| `traefik.<segment_name>.frontend.backend=BACKEND` | Same as `traefik.frontend.backend` |
| `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` | | `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` |
| `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` | | `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` |
| `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` | | `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |

View file

@ -137,7 +137,7 @@ The following general annotations are applicable on the Ingress object:
| `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. | | `traefik.ingress.kubernetes.io/redirect-replacement: http://mydomain/$1` | Redirect to another URL for that frontend. Must be set with `traefik.ingress.kubernetes.io/redirect-regex`. |
| `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. | | `traefik.ingress.kubernetes.io/rewrite-target: /users` | Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. |
| `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. | | `traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip` | Override the default frontend rule type. Default: `PathPrefix`. |
| `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access. all source IPs are permitted if the list is empty or a single range is ill-formatted. | | `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access. all source IPs are permitted if the list is empty or a single range is ill-formatted. Please note, you may have to set `service.spec.externalTrafficPolicy` to the value `Local` to preserve the source IP of the request for filtering. Please see [this link](https://kubernetes.io/docs/tutorials/services/source-ip/) for more information.|
| `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) | | `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) |
<1> `traefik.ingress.kubernetes.io/error-pages` example: <1> `traefik.ingress.kubernetes.io/error-pages` example:

View file

@ -259,13 +259,13 @@ Segment labels override the default behavior.
| Label | Description | | Label | Description |
|---------------------------------------------------------------------------|-------------------------------------------------------------| |---------------------------------------------------------------------------|-------------------------------------------------------------|
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` | | `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
| `traefik.<segment_name>.portIndex=1` | Same as `traefik.portIndex` | | `traefik.<segment_name>.portIndex=1` | Same as `traefik.portIndex` |
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` | | `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` | | `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
| `traefik.<segment_name>.weight=10` | Same as `traefik.weight` | | `traefik.<segment_name>.weight=10` | Same as `traefik.weight` |
| `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` |
| `traefik.<segment_name>.frontend.backend=BACKEND` | Same as `traefik.frontend.backend` |
| `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` | | `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` |
| `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` | | `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` |
| `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` | | `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |

View file

@ -226,12 +226,12 @@ Segment labels override the default behavior.
| Label | Description | | Label | Description |
|---------------------------------------------------------------------------|-------------------------------------------------------------| |---------------------------------------------------------------------------|-------------------------------------------------------------|
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` | | `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` | | `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` | | `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
| `traefik.<segment_name>.weight=10` | Same as `traefik.weight` | | `traefik.<segment_name>.weight=10` | Same as `traefik.weight` |
| `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` |
| `traefik.<segment_name>.frontend.backend=BACKEND` | Same as `traefik.frontend.backend` |
| `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` | | `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` |
| `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` | | `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` |
| `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` | | `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |

View file

@ -106,7 +106,7 @@ traefik:
```ini ```ini
Name:foo Name:foo
Address::80 Address::80
TLS:goo,gii TLS:/my/path/foo.cert,/my/path/foo.key;/my/path/goo.cert,/my/path/goo.key;/my/path/hoo.cert,/my/path/hoo.key
TLS TLS
TLS.MinVersion:VersionTLS11 TLS.MinVersion:VersionTLS11
TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384 TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384

View file

@ -12,7 +12,7 @@
Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.
Træfik integrates with your existing infrastructure components ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), ...) and configures itself automatically and dynamically. Træfik integrates with your existing infrastructure components ([Docker](https://www.docker.com/), [Swarm mode](https://docs.docker.com/engine/swarm/), [Kubernetes](https://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Rancher](https://rancher.com), [Amazon ECS](https://aws.amazon.com/ecs), ...) and configures itself automatically and dynamically.
Telling Træfik where your orchestrator is could be the _only_ configuration step you need to do. Pointing Træfik at your orchestrator should be the _only_ configuration step you need.
## Overview ## Overview

View file

@ -101,19 +101,25 @@ func openAccessLogFile(filePath string) (*os.File, error) {
return file, nil return file, nil
} }
// GetLogDataTable gets the request context object that contains logging data. This accretes // GetLogDataTable gets the request context object that contains logging data.
// data as the request passes through the middleware chain. // This creates data as the request passes through the middleware chain.
func GetLogDataTable(req *http.Request) *LogData { func GetLogDataTable(req *http.Request) *LogData {
return req.Context().Value(DataTableKey).(*LogData) if ld, ok := req.Context().Value(DataTableKey).(*LogData); ok {
return ld
}
log.Errorf("%s is nil", DataTableKey)
return &LogData{Core: make(CoreLogData)}
} }
func (l *LogHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) { func (l *LogHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
now := time.Now().UTC() now := time.Now().UTC()
core := make(CoreLogData)
core := CoreLogData{
StartUTC: now,
StartLocal: now.Local(),
}
logDataTable := &LogData{Core: core, Request: req.Header} logDataTable := &LogData{Core: core, Request: req.Header}
core[StartUTC] = now
core[StartLocal] = now.Local()
reqWithDataTable := req.WithContext(context.WithValue(req.Context(), DataTableKey, logDataTable)) reqWithDataTable := req.WithContext(context.WithValue(req.Context(), DataTableKey, logDataTable))

View file

@ -43,8 +43,6 @@ func (sb *SaveBackend) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
table.Core[OriginContentSize] = crw.Size() table.Core[OriginContentSize] = crw.Size()
} }
//-------------------------------------------------------------------------------------------------
// SaveFrontend sends the frontend name to the logger. These are sometimes used with a corresponding // SaveFrontend sends the frontend name to the logger. These are sometimes used with a corresponding
// SaveBackend handler, but not always. For example, redirected requests don't reach a backend. // SaveBackend handler, but not always. For example, redirected requests don't reach a backend.
type SaveFrontend struct { type SaveFrontend struct {

View file

@ -99,7 +99,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
utils.CopyHeaders(pageReq.Header, req.Header) utils.CopyHeaders(pageReq.Header, req.Header)
utils.CopyHeaders(w.Header(), recorder.Header()) utils.CopyHeaders(w.Header(), recorder.Header())
w.WriteHeader(recorder.GetCode()) w.WriteHeader(recorder.GetCode())
h.backendHandler.ServeHTTP(w, pageReq)
h.backendHandler.ServeHTTP(w, pageReq.WithContext(req.Context()))
return return
} }
} }

View file

@ -33,7 +33,7 @@ func (f *forwarderMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
span.SetTag("frontend.name", f.frontend) span.SetTag("frontend.name", f.frontend)
span.SetTag("backend.name", f.backend) span.SetTag("backend.name", f.backend)
ext.HTTPMethod.Set(span, r.Method) ext.HTTPMethod.Set(span, r.Method)
ext.HTTPUrl.Set(span, r.URL.String()) ext.HTTPUrl.Set(span, fmt.Sprintf("%s%s", r.URL.String(), r.RequestURI))
span.SetTag("http.host", r.Host) span.SetTag("http.host", r.Host)
InjectRequestHeaders(r) InjectRequestHeaders(r)

View file

@ -262,7 +262,7 @@ func isBackendLBSwarm(container dockerData) bool {
} }
func getSegmentBackendName(container dockerData) string { func getSegmentBackendName(container dockerData) string {
if value := label.GetStringValue(container.SegmentLabels, label.TraefikFrontendBackend, ""); len(value) > 0 { if value := label.GetStringValue(container.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(container.ServiceName + "-" + value) return provider.Normalize(container.ServiceName + "-" + value)
} }

View file

@ -253,7 +253,7 @@ func TestSegmentBuildConfiguration(t *testing.T) {
"traefik.sauternes.port": "2503", "traefik.sauternes.port": "2503",
"traefik.sauternes.protocol": "https", "traefik.sauternes.protocol": "https",
"traefik.sauternes.weight": "80", "traefik.sauternes.weight": "80",
"traefik.sauternes.frontend.backend": "foobar", "traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.passHostHeader": "false", "traefik.sauternes.frontend.passHostHeader": "false",
"traefik.sauternes.frontend.rule": "Path:/mypath", "traefik.sauternes.frontend.rule": "Path:/mypath",
"traefik.sauternes.frontend.priority": "5000", "traefik.sauternes.frontend.priority": "5000",

View file

@ -88,7 +88,7 @@ func extractServicePortV1(labelName string) []string {
// Extract backend from labels for a given service and a given docker container // Extract backend from labels for a given service and a given docker container
// Deprecated // Deprecated
func getServiceBackendNameV1(container dockerData, serviceName string) string { func getServiceBackendNameV1(container dockerData, serviceName string) string {
if value, ok := getServiceLabelsV1(container, serviceName)[label.SuffixFrontendBackend]; ok { if value, ok := getServiceLabelsV1(container, serviceName)[label.SuffixBackend]; ok {
return provider.Normalize(container.ServiceName + "-" + value) return provider.Normalize(container.ServiceName + "-" + value)
} }
return provider.Normalize(container.ServiceName + "-" + getBackendNameV1(container) + "-" + serviceName) return provider.Normalize(container.ServiceName + "-" + getBackendNameV1(container) + "-" + serviceName)

View file

@ -162,7 +162,7 @@ func TestDockerServiceBuildConfigurationV1(t *testing.T) {
"traefik.service.port": "2503", "traefik.service.port": "2503",
"traefik.service.protocol": "https", "traefik.service.protocol": "https",
"traefik.service.weight": "80", "traefik.service.weight": "80",
"traefik.service.frontend.backend": "foobar", "traefik.service.backend": "foobar",
"traefik.service.frontend.passHostHeader": "false", "traefik.service.frontend.passHostHeader": "false",
"traefik.service.frontend.rule": "Path:/mypath", "traefik.service.frontend.rule": "Path:/mypath",
"traefik.service.frontend.priority": "5000", "traefik.service.frontend.priority": "5000",
@ -595,7 +595,7 @@ func TestDockerGetServiceBackendNameV1(t *testing.T) {
}, },
{ {
container: containerJSON(labels(map[string]string{ container: containerJSON(labels(map[string]string{
"traefik.myservice.frontend.backend": "custom-backend", "traefik.myservice.backend": "custom-backend",
})), })),
expected: "fake-custom-backend", expected: "fake-custom-backend",
}, },

View file

@ -59,6 +59,7 @@ func GetBoolValue(labels map[string]string, labelName string, defaultValue bool)
if err == nil { if err == nil {
return v return v
} }
log.Errorf("Unable to parse %q: %q, falling back to %v. %v", labelName, rawValue, defaultValue, err)
} }
return defaultValue return defaultValue
} }

View file

@ -35,7 +35,6 @@ const (
SuffixBackendBufferingRetryExpression = SuffixBackendBuffering + ".retryExpression" SuffixBackendBufferingRetryExpression = SuffixBackendBuffering + ".retryExpression"
SuffixFrontend = "frontend" SuffixFrontend = "frontend"
SuffixFrontendAuthBasic = "frontend.auth.basic" SuffixFrontendAuthBasic = "frontend.auth.basic"
SuffixFrontendBackend = "frontend.backend"
SuffixFrontendEntryPoints = "frontend.entryPoints" SuffixFrontendEntryPoints = "frontend.entryPoints"
SuffixFrontendHeaders = "frontend.headers." SuffixFrontendHeaders = "frontend.headers."
SuffixFrontendRequestHeaders = SuffixFrontendHeaders + "customRequestHeaders" SuffixFrontendRequestHeaders = SuffixFrontendHeaders + "customRequestHeaders"
@ -105,7 +104,6 @@ const (
TraefikBackendBufferingRetryExpression = Prefix + SuffixBackendBufferingRetryExpression TraefikBackendBufferingRetryExpression = Prefix + SuffixBackendBufferingRetryExpression
TraefikFrontend = Prefix + SuffixFrontend TraefikFrontend = Prefix + SuffixFrontend
TraefikFrontendAuthBasic = Prefix + SuffixFrontendAuthBasic TraefikFrontendAuthBasic = Prefix + SuffixFrontendAuthBasic
TraefikFrontendBackend = Prefix + SuffixFrontendBackend
TraefikFrontendEntryPoints = Prefix + SuffixFrontendEntryPoints TraefikFrontendEntryPoints = Prefix + SuffixFrontendEntryPoints
TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader
TraefikFrontendPassTLSCert = Prefix + SuffixFrontendPassTLSCert TraefikFrontendPassTLSCert = Prefix + SuffixFrontendPassTLSCert

View file

@ -150,7 +150,7 @@ func getBackendName(service rancherData) string {
} }
func getSegmentBackendName(service rancherData) string { func getSegmentBackendName(service rancherData) string {
if value := label.GetStringValue(service.SegmentLabels, label.TraefikFrontendBackend, ""); len(value) > 0 { if value := label.GetStringValue(service.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(service.Name + "-" + value) return provider.Normalize(service.Name + "-" + value)
} }

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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