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

@ -112,7 +112,7 @@ Although traefik will connect directly to the endpoints (pods), it still checks
If the service port defined in the ingress spec is 443, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically. If the service port defined in the ingress spec is 443, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically.
!!! note !!! note
Please note that by enabling TLS communication between traefik and your pods, you will have to have trusted certificates that have the proper trust chain and IP subject name. Please note that by enabling TLS communication between traefik and your pods, you will have to have trusted certificates that have the proper trust chain and IP subject name.
If this is not an option, you may need to skip TLS certificate verification. If this is not an option, you may need to skip TLS certificate verification.
See the [insecureSkipVerify](/configuration/commons/#main-section) setting for more details. See the [insecureSkipVerify](/configuration/commons/#main-section) setting for more details.
@ -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:
@ -262,4 +262,4 @@ More information are available in the [User Guide](/user-guide/kubernetes/#add-
!!! note !!! note
Only TLS certificates provided by users can be stored in Kubernetes Secrets. Only TLS certificates provided by users can be stored in Kubernetes Secrets.
[Let's Encrypt](https://letsencrypt.org) certificates cannot be managed in Kubernets Secrets yet. [Let's Encrypt](https://letsencrypt.org) certificates cannot be managed in Kubernets Secrets yet.

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"