Merge branch 'v1.6' into master
This commit is contained in:
commit
4eda1e1bd4
50 changed files with 763 additions and 650 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
|||
# 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)
|
||||
[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)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -41,15 +41,15 @@ type ACME struct {
|
|||
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'"`
|
||||
Storage string `description:"File or key used for certificates storage."`
|
||||
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
|
||||
StorageFile string // 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."`
|
||||
CAServer string `description:"CA server to use."`
|
||||
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
||||
DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"`
|
||||
HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"`
|
||||
DNSProvider string `description:"Activate DNS-01 Challenge (Deprecated)"` // deprecated
|
||||
DelayDontCheckDNS flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` // deprecated
|
||||
DNSProvider string `description:"(Deprecated) Activate DNS-01 Challenge"` // 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."`
|
||||
client *acme.Client
|
||||
defaultCertificate *tls.Certificate
|
||||
|
|
|
@ -105,13 +105,13 @@ type GlobalConfiguration struct {
|
|||
|
||||
// WebCompatibility is a configuration to handle compatibility with deprecated web provider options
|
||||
type WebCompatibility struct {
|
||||
Address string `description:"Web administration port" export:"true"`
|
||||
CertFile string `description:"SSL certificate" export:"true"`
|
||||
KeyFile string `description:"SSL certificate" export:"true"`
|
||||
ReadOnly bool `description:"Enable read only API" export:"true"`
|
||||
Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"`
|
||||
Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"`
|
||||
Path string `description:"Root path for dashboard and API" export:"true"`
|
||||
Address string `description:"(Deprecated) Web administration port" export:"true"`
|
||||
CertFile string `description:"(Deprecated) SSL certificate" export:"true"`
|
||||
KeyFile string `description:"(Deprecated) SSL certificate" export:"true"`
|
||||
ReadOnly bool `description:"(Deprecated) Enable read only API" export:"true"`
|
||||
Statistics *types.Statistics `description:"(Deprecated) Enable more detailed statistics" export:"true"`
|
||||
Metrics *types.Metrics `description:"(Deprecated) Enable a metrics exporter" export:"true"`
|
||||
Path string `description:"(Deprecated) Root path for dashboard and API" export:"true"`
|
||||
Auth *types.Auth `export:"true"`
|
||||
Debug bool `export:"true"`
|
||||
}
|
||||
|
|
|
@ -174,7 +174,7 @@ func TestEntryPoints_Set(t *testing.T) {
|
|||
name: "all parameters camelcase",
|
||||
expression: "Name:foo " +
|
||||
"Address::8000 " +
|
||||
"TLS:goo,gii " +
|
||||
"TLS:goo,gii;foo,fii " +
|
||||
"TLS " +
|
||||
"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 " +
|
||||
|
@ -211,6 +211,10 @@ func TestEntryPoints_Set(t *testing.T) {
|
|||
CertFile: tls.FileOrContent("goo"),
|
||||
KeyFile: tls.FileOrContent("gii"),
|
||||
},
|
||||
{
|
||||
CertFile: tls.FileOrContent("foo"),
|
||||
KeyFile: tls.FileOrContent("fii"),
|
||||
},
|
||||
},
|
||||
ClientCA: tls.ClientCA{
|
||||
Files: []string{"car"},
|
||||
|
@ -280,7 +284,7 @@ func TestEntryPoints_Set(t *testing.T) {
|
|||
name: "all parameters lowercase",
|
||||
expression: "Name:foo " +
|
||||
"address::8000 " +
|
||||
"tls:goo,gii " +
|
||||
"tls:goo,gii;foo,fii " +
|
||||
"tls " +
|
||||
"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 " +
|
||||
|
@ -315,6 +319,10 @@ func TestEntryPoints_Set(t *testing.T) {
|
|||
CertFile: tls.FileOrContent("goo"),
|
||||
KeyFile: tls.FileOrContent("gii"),
|
||||
},
|
||||
{
|
||||
CertFile: tls.FileOrContent("foo"),
|
||||
KeyFile: tls.FileOrContent("fii"),
|
||||
},
|
||||
},
|
||||
ClientCA: tls.ClientCA{
|
||||
Files: []string{"car"},
|
||||
|
|
|
@ -288,12 +288,12 @@ Segment labels override the default behavior.
|
|||
|
||||
| Label | Description |
|
||||
|---------------------------------------------------------------------------|-------------------------------------------------------------|
|
||||
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
|
||||
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
|
||||
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
|
||||
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
|
||||
| `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.backend=BACKEND` | Same as `traefik.frontend.backend` |
|
||||
| `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>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |
|
||||
|
|
|
@ -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.
|
||||
|
||||
!!! 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.
|
||||
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/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/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) |
|
||||
|
||||
<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
|
||||
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.
|
||||
|
|
|
@ -259,13 +259,13 @@ Segment labels override the default behavior.
|
|||
|
||||
| Label | Description |
|
||||
|---------------------------------------------------------------------------|-------------------------------------------------------------|
|
||||
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
|
||||
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
|
||||
| `traefik.<segment_name>.portIndex=1` | Same as `traefik.portIndex` |
|
||||
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
|
||||
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
|
||||
| `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.backend=BACKEND` | Same as `traefik.frontend.backend` |
|
||||
| `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>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |
|
||||
|
|
|
@ -226,12 +226,12 @@ Segment labels override the default behavior.
|
|||
|
||||
| Label | Description |
|
||||
|---------------------------------------------------------------------------|-------------------------------------------------------------|
|
||||
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
|
||||
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
|
||||
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
|
||||
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
|
||||
| `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.backend=BACKEND` | Same as `traefik.frontend.backend` |
|
||||
| `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>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |
|
||||
|
|
|
@ -106,7 +106,7 @@ traefik:
|
|||
```ini
|
||||
Name:foo
|
||||
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.MinVersion:VersionTLS11
|
||||
TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
|
@ -101,19 +101,25 @@ func openAccessLogFile(filePath string) (*os.File, error) {
|
|||
return file, nil
|
||||
}
|
||||
|
||||
// GetLogDataTable gets the request context object that contains logging data. This accretes
|
||||
// data as the request passes through the middleware chain.
|
||||
// GetLogDataTable gets the request context object that contains logging data.
|
||||
// This creates data as the request passes through the middleware chain.
|
||||
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) {
|
||||
now := time.Now().UTC()
|
||||
core := make(CoreLogData)
|
||||
|
||||
core := CoreLogData{
|
||||
StartUTC: now,
|
||||
StartLocal: now.Local(),
|
||||
}
|
||||
|
||||
logDataTable := &LogData{Core: core, Request: req.Header}
|
||||
core[StartUTC] = now
|
||||
core[StartLocal] = now.Local()
|
||||
|
||||
reqWithDataTable := req.WithContext(context.WithValue(req.Context(), DataTableKey, logDataTable))
|
||||
|
||||
|
|
|
@ -43,8 +43,6 @@ func (sb *SaveBackend) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|||
table.Core[OriginContentSize] = crw.Size()
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
|
||||
// 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.
|
||||
type SaveFrontend struct {
|
||||
|
|
|
@ -99,7 +99,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
|
|||
utils.CopyHeaders(pageReq.Header, req.Header)
|
||||
utils.CopyHeaders(w.Header(), recorder.Header())
|
||||
w.WriteHeader(recorder.GetCode())
|
||||
h.backendHandler.ServeHTTP(w, pageReq)
|
||||
|
||||
h.backendHandler.ServeHTTP(w, pageReq.WithContext(req.Context()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func (f *forwarderMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
|
|||
span.SetTag("frontend.name", f.frontend)
|
||||
span.SetTag("backend.name", f.backend)
|
||||
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)
|
||||
|
||||
InjectRequestHeaders(r)
|
||||
|
|
|
@ -262,7 +262,7 @@ func isBackendLBSwarm(container dockerData) bool {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -253,7 +253,7 @@ func TestSegmentBuildConfiguration(t *testing.T) {
|
|||
"traefik.sauternes.port": "2503",
|
||||
"traefik.sauternes.protocol": "https",
|
||||
"traefik.sauternes.weight": "80",
|
||||
"traefik.sauternes.frontend.backend": "foobar",
|
||||
"traefik.sauternes.backend": "foobar",
|
||||
"traefik.sauternes.frontend.passHostHeader": "false",
|
||||
"traefik.sauternes.frontend.rule": "Path:/mypath",
|
||||
"traefik.sauternes.frontend.priority": "5000",
|
||||
|
|
|
@ -88,7 +88,7 @@ func extractServicePortV1(labelName string) []string {
|
|||
// Extract backend from labels for a given service and a given docker container
|
||||
// Deprecated
|
||||
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 + "-" + getBackendNameV1(container) + "-" + serviceName)
|
||||
|
|
|
@ -162,7 +162,7 @@ func TestDockerServiceBuildConfigurationV1(t *testing.T) {
|
|||
"traefik.service.port": "2503",
|
||||
"traefik.service.protocol": "https",
|
||||
"traefik.service.weight": "80",
|
||||
"traefik.service.frontend.backend": "foobar",
|
||||
"traefik.service.backend": "foobar",
|
||||
"traefik.service.frontend.passHostHeader": "false",
|
||||
"traefik.service.frontend.rule": "Path:/mypath",
|
||||
"traefik.service.frontend.priority": "5000",
|
||||
|
@ -595,7 +595,7 @@ func TestDockerGetServiceBackendNameV1(t *testing.T) {
|
|||
},
|
||||
{
|
||||
container: containerJSON(labels(map[string]string{
|
||||
"traefik.myservice.frontend.backend": "custom-backend",
|
||||
"traefik.myservice.backend": "custom-backend",
|
||||
})),
|
||||
expected: "fake-custom-backend",
|
||||
},
|
||||
|
|
|
@ -59,6 +59,7 @@ func GetBoolValue(labels map[string]string, labelName string, defaultValue bool)
|
|||
if err == nil {
|
||||
return v
|
||||
}
|
||||
log.Errorf("Unable to parse %q: %q, falling back to %v. %v", labelName, rawValue, defaultValue, err)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ const (
|
|||
SuffixBackendBufferingRetryExpression = SuffixBackendBuffering + ".retryExpression"
|
||||
SuffixFrontend = "frontend"
|
||||
SuffixFrontendAuthBasic = "frontend.auth.basic"
|
||||
SuffixFrontendBackend = "frontend.backend"
|
||||
SuffixFrontendEntryPoints = "frontend.entryPoints"
|
||||
SuffixFrontendHeaders = "frontend.headers."
|
||||
SuffixFrontendRequestHeaders = SuffixFrontendHeaders + "customRequestHeaders"
|
||||
|
@ -105,7 +104,6 @@ const (
|
|||
TraefikBackendBufferingRetryExpression = Prefix + SuffixBackendBufferingRetryExpression
|
||||
TraefikFrontend = Prefix + SuffixFrontend
|
||||
TraefikFrontendAuthBasic = Prefix + SuffixFrontendAuthBasic
|
||||
TraefikFrontendBackend = Prefix + SuffixFrontendBackend
|
||||
TraefikFrontendEntryPoints = Prefix + SuffixFrontendEntryPoints
|
||||
TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader
|
||||
TraefikFrontendPassTLSCert = Prefix + SuffixFrontendPassTLSCert
|
||||
|
|
|
@ -150,7 +150,7 @@ func getBackendName(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)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"assets/images",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
|
@ -19,7 +19,7 @@
|
|||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles/app.sass"
|
||||
"app.sass"
|
||||
],
|
||||
"scripts": [
|
||||
"../node_modules/@fortawesome/fontawesome/index.js",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"@angular/router": "^5.2.0",
|
||||
"@fortawesome/fontawesome": "^1.1.5",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.10",
|
||||
"bulma": "^0.6.2",
|
||||
"bulma": "^0.7.0",
|
||||
"core-js": "^2.4.1",
|
||||
"d3": "^4.13.0",
|
||||
"date-fns": "^1.29.0",
|
||||
|
|
27
webui/src/app.sass
Normal file
27
webui/src/app.sass
Normal 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
|
|
@ -1,4 +1,4 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BarChartComponent } from './charts/bar-chart/bar-chart.component';
|
||||
import { LineChartComponent } from './charts/line-chart/line-chart.component';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { HealthComponent } from './components/health/health.component';
|
||||
import { ProvidersComponent } from './components/providers/providers.component';
|
||||
import { LetDirective } from './directives/let.directive';
|
||||
import { BackendFilterPipe } from './pipes/backend.filter.pipe';
|
||||
import { FrontendFilterPipe } from './pipes/frontend.filter.pipe';
|
||||
import { KeysPipe } from './pipes/keys.pipe';
|
||||
import { ApiService } from './services/api.service';
|
||||
import { WindowService } from './services/window.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { ProvidersComponent } from './components/providers/providers.component';
|
||||
import { HealthComponent } from './components/health/health.component';
|
||||
import { LineChartComponent } from './charts/line-chart/line-chart.component';
|
||||
import { BarChartComponent } from './charts/bar-chart/bar-chart.component';
|
||||
import { KeysPipe } from './pipes/keys.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -22,7 +25,10 @@ import { KeysPipe } from './pipes/keys.pipe';
|
|||
HealthComponent,
|
||||
LineChartComponent,
|
||||
BarChartComponent,
|
||||
KeysPipe
|
||||
KeysPipe,
|
||||
FrontendFilterPipe,
|
||||
BackendFilterPipe,
|
||||
LetDirective
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -30,8 +36,8 @@ import { KeysPipe } from './pipes/keys.pipe';
|
|||
HttpClientModule,
|
||||
FormsModule,
|
||||
RouterModule.forRoot([
|
||||
{ path: '', component: ProvidersComponent, pathMatch: 'full' },
|
||||
{ path: 'status', component: HealthComponent }
|
||||
{path: '', component: ProvidersComponent, pathMatch: 'full'},
|
||||
{path: 'status', component: HealthComponent}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { axisBottom, axisLeft, easeLinear, max, min, scaleBand, scaleLinear, select } from 'd3';
|
||||
import * as _ from 'lodash';
|
||||
import { WindowService } from '../../services/window.service';
|
||||
import {
|
||||
min,
|
||||
max,
|
||||
easeLinear,
|
||||
select,
|
||||
axisLeft,
|
||||
axisBottom,
|
||||
scaleBand,
|
||||
scaleLinear
|
||||
} from 'd3';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-chart',
|
||||
|
@ -23,12 +15,12 @@ export class BarChartComponent implements OnInit, OnChanges {
|
|||
x: any;
|
||||
y: any;
|
||||
g: any;
|
||||
bars: any;
|
||||
width: number;
|
||||
height: number;
|
||||
margin = { top: 40, right: 40, bottom: 40, left: 40 };
|
||||
margin = {top: 40, right: 40, bottom: 40, left: 40};
|
||||
loading: boolean;
|
||||
data: any[];
|
||||
previousData: any[];
|
||||
|
||||
constructor(public elementRef: ElementRef, public windowService: WindowService) {
|
||||
this.loading = true;
|
||||
|
@ -37,7 +29,7 @@ export class BarChartComponent implements OnInit, OnChanges {
|
|||
ngOnInit() {
|
||||
this.barChartEl = this.elementRef.nativeElement.querySelector('.bar-chart');
|
||||
this.setup();
|
||||
setTimeout(() => this.loading = false, 4000);
|
||||
setTimeout(() => this.loading = false, 1000);
|
||||
|
||||
this.windowService.resize.subscribe(w => this.draw());
|
||||
}
|
||||
|
@ -47,15 +39,20 @@ export class BarChartComponent implements OnInit, OnChanges {
|
|||
return;
|
||||
}
|
||||
|
||||
this.data = this.value;
|
||||
this.draw();
|
||||
if (!_.isEqual(this.previousData, this.value)) {
|
||||
this.previousData = _.cloneDeep(this.value);
|
||||
this.data = this.value;
|
||||
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
|
||||
this.svg = select(this.barChartEl).append('svg')
|
||||
this.svg = select(this.barChartEl)
|
||||
.append('svg')
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.top + this.margin.bottom);
|
||||
|
||||
|
@ -73,11 +70,16 @@ export class BarChartComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
draw(): void {
|
||||
if (this.barChartEl.clientWidth === 0 || this.barChartEl.clientHeight === 0) {
|
||||
this.previousData = [];
|
||||
} else {
|
||||
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
}
|
||||
|
||||
this.x.domain(this.data.map((d: any) => d.code));
|
||||
this.y.domain([0, max(this.data, (d: any) => d.count)]);
|
||||
|
||||
this.width = this.barChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.barChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
|
||||
this.svg
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
|
@ -93,17 +95,16 @@ export class BarChartComponent implements OnInit, OnChanges {
|
|||
this.g.select('.axis--y')
|
||||
.call(axisLeft(this.y).tickSize(-this.width));
|
||||
|
||||
// Clean previous graph
|
||||
this.g.selectAll('.bar').remove();
|
||||
|
||||
const bars = this.g.selectAll('.bar').data(this.data);
|
||||
|
||||
bars.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'bar')
|
||||
.attr('x', (d: any) => d.code)
|
||||
.attr('y', (d: any) => d.count)
|
||||
.attr('width', this.x.bandwidth())
|
||||
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
|
||||
|
||||
bars.attr('x', (d: any) => this.x(d.code))
|
||||
.style('fill', (d: any) => 'hsl(' + Math.floor(((d.code - 100) * 310 / 427) + 50) + ', 50%, 50%)')
|
||||
.attr('x', (d: any) => this.x(d.code))
|
||||
.attr('y', (d: any) => this.y(d.count))
|
||||
.attr('width', this.x.bandwidth())
|
||||
.attr('height', (d: any) => (this.height - this.y(d.count)) < 0 ? 0 : this.height - this.y(d.count));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="line-chart" [class.is-hidden]="loading"></div>
|
||||
<div class="loading-text" [class.is-hidden]="!loading">
|
||||
<div class="loading-text line-chart-loading" [class.is-hidden]="!loading">
|
||||
<span>
|
||||
<span>Loading, please wait...</span>
|
||||
<img src="./assets/images/loader.svg" class="main-loader">
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { Component, Input, OnInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { WindowService } from '../../services/window.service';
|
||||
import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import {
|
||||
range,
|
||||
scaleTime,
|
||||
scaleLinear,
|
||||
min,
|
||||
max,
|
||||
curveLinear,
|
||||
line,
|
||||
easeLinear,
|
||||
select,
|
||||
axisLeft,
|
||||
axisBottom,
|
||||
timeSecond,
|
||||
timeFormat
|
||||
axisLeft,
|
||||
curveLinear,
|
||||
easeLinear,
|
||||
line,
|
||||
max,
|
||||
min,
|
||||
range,
|
||||
scaleLinear,
|
||||
scaleTime,
|
||||
select,
|
||||
timeFormat,
|
||||
timeSecond
|
||||
} from 'd3';
|
||||
import { WindowService } from '../../services/window.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-line-chart',
|
||||
|
@ -23,7 +23,10 @@ import {
|
|||
export class LineChartComponent implements OnChanges, OnInit {
|
||||
@Input() value: { count: number, date: string };
|
||||
|
||||
firstDisplay: boolean;
|
||||
dirty: boolean;
|
||||
lineChartEl: HTMLElement;
|
||||
loadingEl: HTMLElement;
|
||||
svg: any;
|
||||
g: any;
|
||||
line: any;
|
||||
|
@ -39,15 +42,19 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
yAxis: any;
|
||||
height: number;
|
||||
width: number;
|
||||
margin = { top: 40, right: 40, bottom: 60, left: 60 };
|
||||
margin = {top: 40, right: 40, bottom: 60, left: 60};
|
||||
loading = true;
|
||||
|
||||
constructor(private elementRef: ElementRef, public windowService: WindowService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.lineChartEl = this.elementRef.nativeElement.querySelector('.line-chart');
|
||||
this.loadingEl = this.elementRef.nativeElement.querySelector('.line-chart-loading');
|
||||
this.limit = 40;
|
||||
|
||||
// related to the Observable.timer(0, 3000) in health component
|
||||
this.duration = 3000;
|
||||
|
||||
this.now = new Date(Date.now() - this.duration);
|
||||
|
||||
this.options = {
|
||||
|
@ -55,22 +62,37 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
color: '#3A84C5'
|
||||
};
|
||||
|
||||
this.firstDisplay = true;
|
||||
this.render();
|
||||
setTimeout(() => this.loading = false, 4000);
|
||||
|
||||
this.windowService.resize.subscribe(w => {
|
||||
if (this.svg) {
|
||||
const el = this.lineChartEl.querySelector('svg');
|
||||
el.parentNode.removeChild(el);
|
||||
this.dirty = true;
|
||||
this.loading = true;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
this.width = this.lineChartEl.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = this.lineChartEl.clientHeight - this.margin.top - this.margin.bottom;
|
||||
// When the lineChartEl is not displayed (is-hidden), width and length are equal to 0.
|
||||
let elt;
|
||||
if (this.lineChartEl.clientWidth === 0 || this.lineChartEl.clientHeight === 0) {
|
||||
elt = this.loadingEl;
|
||||
} else {
|
||||
elt = this.lineChartEl;
|
||||
}
|
||||
this.width = elt.clientWidth - this.margin.left - this.margin.right;
|
||||
this.height = elt.clientHeight - this.margin.top - this.margin.bottom;
|
||||
|
||||
this.svg = select(this.lineChartEl).append('svg')
|
||||
|
||||
const el = this.lineChartEl.querySelector('svg');
|
||||
if (el) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
|
||||
this.svg = select(this.lineChartEl)
|
||||
.append('svg')
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.top + this.margin.bottom)
|
||||
.append('g')
|
||||
|
@ -80,7 +102,7 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
this.data = range(this.limit).map(i => 0);
|
||||
}
|
||||
|
||||
this.x = scaleTime().range([0, this.width]);
|
||||
this.x = scaleTime().range([0, this.width - 10]);
|
||||
this.y = scaleLinear().range([this.height, 0]);
|
||||
|
||||
this.x.domain([<any>this.now - (this.limit - 2), <any>this.now - this.duration]);
|
||||
|
@ -91,7 +113,9 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
.y((d: any) => this.y(d))
|
||||
.curve(curveLinear);
|
||||
|
||||
this.svg.append('defs').append('clipPath')
|
||||
this.svg
|
||||
.append('defs')
|
||||
.append('clipPath')
|
||||
.attr('id', 'clip')
|
||||
.append('rect')
|
||||
.attr('width', this.width)
|
||||
|
@ -121,7 +145,7 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
this.updateData(this.value.count);
|
||||
}
|
||||
|
||||
updateData = (value: number) => {
|
||||
updateData(value: number) {
|
||||
this.data.push(value * 1000000);
|
||||
this.now = new Date();
|
||||
|
||||
|
@ -132,9 +156,13 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
|
||||
this.xAxis
|
||||
.transition()
|
||||
.duration(this.duration)
|
||||
.duration(this.firstDisplay || this.dirty ? 0 : this.duration)
|
||||
.ease(easeLinear)
|
||||
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')))
|
||||
.call(axisBottom(this.x).tickSize(-this.height).ticks(timeSecond, 5).tickFormat(timeFormat('%H:%M:%S')));
|
||||
|
||||
this.xAxis
|
||||
.transition()
|
||||
.duration(0)
|
||||
.selectAll('text')
|
||||
.style('text-anchor', 'end')
|
||||
.attr('dx', '-.8em')
|
||||
|
@ -157,6 +185,13 @@ export class LineChartComponent implements OnChanges, OnInit {
|
|||
.ease(easeLinear)
|
||||
.attr('transform', `translate(${this.x(<any>this.now - (this.limit - 1) * this.duration)})`);
|
||||
|
||||
this.firstDisplay = false;
|
||||
this.dirty = false;
|
||||
|
||||
if (this.loading) {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
this.data.shift();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<nav class="navbar is-fixed-top is-transparent" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" routerLink="/">
|
||||
<img src="./assets/images/traefik.logo.svg" alt="Traefik" class="navbar-logo">
|
||||
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" routerLink="/" (click)="burger = false">
|
||||
<img src="./assets/images/traefik.logo.svg" alt="Traefik" class="navbar-logo">
|
||||
</a>
|
||||
<div class="navbar-burger burger" data-target="navbarMain" (click)="burger = !burger" [class.is-active]="burger">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbarMain" class="navbar-menu" [class.is-active]="burger">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" routerLink="/" routerLinkActive="is-active" [routerLinkActiveOptions]="{ exact: true }" (click)="burger = false">
|
||||
Providers
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="/status" routerLinkActive="is-active" (click)="burger = false">
|
||||
Health
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-menu">
|
||||
<a class="navbar-item" routerLink="/" routerLinkActive="is-active" [routerLinkActiveOptions]="{ exact: true }">
|
||||
Providers
|
||||
</a>
|
||||
<a class="navbar-item" routerLink="/status" routerLinkActive="is-active">
|
||||
Health
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end is-hidden-mobile">
|
||||
<div class="navbar-end">
|
||||
<a class="navbar-item" [href]="releaseLink" target="_blank">
|
||||
{{ version }} / {{ codename }}
|
||||
</a>
|
||||
|
@ -25,5 +30,6 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -9,6 +9,7 @@ export class HeaderComponent implements OnInit {
|
|||
version: string;
|
||||
codename: string;
|
||||
releaseLink: string;
|
||||
burger: boolean;
|
||||
|
||||
constructor(private apiService: ApiService) { }
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="column is-4">
|
||||
<div class="item-data border-right">
|
||||
<span class="data-grey">Total Response Time</span>
|
||||
<span class="data-blue">{{ totalResponseTime }}</span>
|
||||
<span class="data-blue" [title]="exactTotalResponseTime">{{ totalResponseTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
|
@ -33,7 +33,7 @@
|
|||
<div class="column is-4">
|
||||
<div class="item-data border-right">
|
||||
<span class="data-grey">Average Response Time</span>
|
||||
<span class="data-blue">{{ averageResponseTime }}</span>
|
||||
<span class="data-blue" [title]="exactAverageResponseTime">{{ averageResponseTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
|
@ -82,15 +82,15 @@
|
|||
<td>Request</td>
|
||||
<td>Time</td>
|
||||
</tr>
|
||||
<tr *ngFor="let entry of recentErrors">
|
||||
<tr *ngFor="let entry of recentErrors; trackBy: trackRecentErrors;">
|
||||
<td>
|
||||
<span class="tag is-info">{{ entry.status_code }}</span> <span>{{ entry.status }}</span>
|
||||
<span class="tag is-info" [title]="entry.status">{{ entry.status_code }}</span> <span class="is-hidden-mobile is-hidden-desktop-only">{{ entry.status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag">{{ entry.method }}</span> <a>{{ entry.host }}{{ entry.path }}</a>
|
||||
<span class="tag">{{ entry.method }}</span> <span>{{ entry.host }}{{ entry.path }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ entry.time }}</span>
|
||||
<span [title]="entry.time | date:'yyyy-MM-dd HH:mm:ss:SSS a z'">{{ entry.time | date:'yyyy-MM-dd HH:mm:ss a z' }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="!recentErrors?.length">
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { distanceInWordsStrict, format, subSeconds } from 'date-fns';
|
||||
import * as _ from 'lodash';
|
||||
import 'rxjs/add/observable/timer';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import 'rxjs/add/operator/timeInterval';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import 'rxjs/add/observable/timer';
|
||||
import 'rxjs/add/operator/timeInterval';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-health',
|
||||
|
@ -15,11 +16,14 @@ import { format, distanceInWordsStrict, subSeconds } from 'date-fns';
|
|||
export class HealthComponent implements OnInit, OnDestroy {
|
||||
sub: Subscription;
|
||||
recentErrors: any;
|
||||
previousRecentErrors: any;
|
||||
pid: number;
|
||||
uptime: string;
|
||||
uptimeSince: string;
|
||||
averageResponseTime: string;
|
||||
exactAverageResponseTime: string;
|
||||
totalResponseTime: string;
|
||||
exactTotalResponseTime: string;
|
||||
codeCount: number;
|
||||
totalCodeCount: number;
|
||||
chartValue: any;
|
||||
|
@ -33,16 +37,22 @@ export class HealthComponent implements OnInit, OnDestroy {
|
|||
.mergeMap(() => this.apiService.fetchHealthStatus())
|
||||
.subscribe(data => {
|
||||
if (data) {
|
||||
this.recentErrors = data.recent_errors;
|
||||
this.chartValue = { count: data.average_response_time_sec, date: data.time };
|
||||
if (!_.isEqual(this.previousRecentErrors, data.recent_errors)) {
|
||||
this.previousRecentErrors = _.cloneDeep(data.recent_errors);
|
||||
this.recentErrors = data.recent_errors;
|
||||
}
|
||||
|
||||
this.chartValue = {count: data.average_response_time_sec, date: data.time};
|
||||
this.statusCodeValue = Object.keys(data.total_status_code_count)
|
||||
.map(key => ({ code: key, count: data.total_status_code_count[key] }));
|
||||
.map(key => ({code: key, count: data.total_status_code_count[key]}));
|
||||
|
||||
this.pid = data.pid;
|
||||
this.uptime = distanceInWordsStrict(subSeconds(new Date(), data.uptime_sec), new Date());
|
||||
this.uptimeSince = format(subSeconds(new Date(), data.uptime_sec), 'MM/DD/YYYY HH:mm:ss');
|
||||
this.totalResponseTime = data.total_response_time;
|
||||
this.averageResponseTime = data.average_response_time;
|
||||
this.uptimeSince = format(subSeconds(new Date(), data.uptime_sec), 'YYYY-MM-DD HH:mm:ss Z');
|
||||
this.totalResponseTime = distanceInWordsStrict(subSeconds(new Date(), data.total_response_time_sec), new Date());
|
||||
this.exactTotalResponseTime = data.total_response_time;
|
||||
this.averageResponseTime = Math.floor(data.average_response_time_sec * 1000) + ' ms';
|
||||
this.exactAverageResponseTime = data.average_response_time;
|
||||
this.codeCount = data.count;
|
||||
this.totalCodeCount = data.total_count;
|
||||
}
|
||||
|
@ -54,4 +64,8 @@ export class HealthComponent implements OnInit, OnDestroy {
|
|||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
trackRecentErrors(index, item): string {
|
||||
return item.status_code + item.method + item.host + item.path + item.time;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
<div class="column is-12">
|
||||
|
||||
<div class="search-container">
|
||||
<span class="icon"><i class="fas fa-search"></i></span>
|
||||
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword" (ngModelChange)="filter()">
|
||||
<span class="icon search-button" *ngIf="!keyword"><i class="fas fa-search"></i></span>
|
||||
<a class="delete search-button" *ngIf="keyword" (click)="keyword = ''"></a>
|
||||
<input type="text" placeholder="Filter by name or id ..." [(ngModel)]="keyword">
|
||||
</div>
|
||||
|
||||
<div class="tabs" *ngIf="keys?.length">
|
||||
|
@ -20,30 +21,17 @@
|
|||
<div *ngIf="keys?.length">
|
||||
<div class="columns">
|
||||
<!-- Frontends -->
|
||||
<div class="column is-6">
|
||||
<h2 class="subtitle"><span class="tag is-info">{{ providers[tab]?.frontends.length }}</span> Frontends</h2>
|
||||
<div class="message" *ngFor="let p of providers[tab]?.frontends; let i = index;">
|
||||
<div class="message-header">
|
||||
<div class="column is-6" *appLet="providers[tab]?.frontends | frontendFilter:keyword as frontends">
|
||||
<h2 class="subtitle"><span class="tag is-info">{{ frontends.length }}</span><span class="subtitle-name">Frontends</span></h2>
|
||||
|
||||
<div *ngIf="frontends.length < maxItem">
|
||||
|
||||
<div class="message" *ngFor="let p of frontends; trackBy: trackItem(tab)">
|
||||
<div class="message-header" [class.has-background-info]="p.backend" [class.has-background-danger]="!p.backend">
|
||||
<h2>
|
||||
<i class="icon fas fa-globe has-text-white"></i>
|
||||
<div>
|
||||
<i class="icon fas fa-globe"></i>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-info">{{ p.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="p.backend">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<a class="tags has-addons" [href]="'#' + p.backend">
|
||||
<span class="tag is-light">Backend</span>
|
||||
<span class="tag is-primary">{{ p.backend }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<span class="has-text-white" [class.is-info]="p.backend" [class.is-danger]="!p.backend">{{ p.id }}</span>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
@ -57,16 +45,16 @@
|
|||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div *ngIf="p.section !== 'details'">
|
||||
<div *ngIf="p.section !== 'details'" class="section-container">
|
||||
|
||||
<div *ngIf="p.routes && p.routes.length">
|
||||
<div *ngIf="p.routes && p.routes.length" class="section-line">
|
||||
<div>
|
||||
<h2>Route Rule</h2>
|
||||
</div>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Route Rule</td>
|
||||
</tr>
|
||||
<tr *ngFor="let route of p.routes; let ri = index;">
|
||||
<td><code class="has-text-grey" title="{{ route.title }}">{{ route.rule }}</code></td>
|
||||
<tr *ngFor="let route of p.routes">
|
||||
<td><code class="has-text-grey" [title]="route.id">{{ route.rule }}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -74,15 +62,15 @@
|
|||
|
||||
<div *ngIf="p.entryPoints && p.entryPoints.length">
|
||||
<hr>
|
||||
<div class="columns">
|
||||
<div class="columns section-line">
|
||||
<div class="column is-3">
|
||||
<h2>Entry Points</h2>
|
||||
<h2 class="section-line-header">Entry Points</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags">
|
||||
<span class="tag is-info" *ngFor="let ep of p.entryPoints; let ri = index;">{{ ep }}</span>
|
||||
<span class="tag is-info" *ngFor="let ep of p.entryPoints">{{ ep }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -90,19 +78,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.backend">
|
||||
<hr>
|
||||
<div class="columns section-line">
|
||||
<div class="column is-2">
|
||||
<h2 class="section-line-header">Backend</h2>
|
||||
</div>
|
||||
<div class="column is-10">
|
||||
<div class="field">
|
||||
<i class="icon fas fa-server has-text-primary" title="Backend"></i>
|
||||
<span class="has-text-primary">{{ p.backend }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div *ngIf="p.section === 'details'">
|
||||
<div *ngIf="p.section === 'details'" class="section-container">
|
||||
|
||||
<div>
|
||||
<div class="section-line">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Misc.</h2>
|
||||
<h2 class="section-line-header">Misc.</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="control" *ngIf="p.priority">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Priority</span>
|
||||
<span class="tag is-info">{{ p.priority }}</span>
|
||||
|
@ -111,7 +114,7 @@
|
|||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Host Header</span>
|
||||
<span class="tag is-info">{{ p.passHostHeader }}</span>
|
||||
<span class="tag is-info">{{ !!p.passHostHeader }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" *ngIf="p.passTLSCert">
|
||||
|
@ -127,9 +130,9 @@
|
|||
|
||||
<div *ngIf="p.redirect">
|
||||
<hr>
|
||||
<div class="columns">
|
||||
<div class="columns section-line">
|
||||
<div class="column is-3">
|
||||
<h2>Redirect</h2>
|
||||
<h2 class="section-line-header">Redirect</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline" *ngIf="p.redirect.entryPoint">
|
||||
|
@ -160,45 +163,49 @@
|
|||
|
||||
<div *ngIf="p.basicAuth && p.basicAuth.length">
|
||||
<hr/>
|
||||
<h2>Basic Authentication</h2>
|
||||
<div class="tags padding-5-10">
|
||||
<span class="tag is-info" *ngFor="let auth of p.basicAuth; let ri = index;">{{ auth }}</span>
|
||||
<div class="section-line">
|
||||
<h2 class="section-line-header">Basic Authentication</h2>
|
||||
<div class="tags padding-5-10">
|
||||
<span class="tag is-info" *ngFor="let auth of p.basicAuth">{{ auth }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.errors">
|
||||
<div *ngIf="p.errors?.length">
|
||||
<hr/>
|
||||
<h2>Error Pages</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Query</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
<tr *ngFor="let key of p.errors | keys">
|
||||
<td><span class="has-text-grey-light">{{ p.errors[key].backend }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.errors[key].query }}</span></td>
|
||||
<td>
|
||||
<span class="tag is-light" *ngFor="let state of p.errors[key].status">{{ state }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="section-line">
|
||||
<h2 class="section-line-header">Error Pages</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Query</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
<tr *ngFor="let entry of p.errors">
|
||||
<td><span class="has-text-grey-light">{{ entry.backend }}</span></td>
|
||||
<td><span class="has-text-grey">{{ entry.query }}</span></td>
|
||||
<td>
|
||||
<span class="tag is-light" *ngFor="let state of entry.status">{{ state }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="p.whiteList">
|
||||
<hr/>
|
||||
<div class="columns is-gapless is-multiline is-mobile">
|
||||
<div class="columns is-gapless is-multiline is-mobile section-line">
|
||||
<div class="column is-half">
|
||||
<h2>Whitelist</h2>
|
||||
<h2 class="section-line-header">Whitelist</h2>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">useXForwardedFor</span>
|
||||
<span class="tag is-info">{{ p.whiteList.useXForwardedFor }}</span>
|
||||
<span class="tag is-info">{{ !!p.whiteList.useXForwardedFor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -207,7 +214,7 @@
|
|||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags">
|
||||
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange; let ri = index;">{{ wlRange }}</span>
|
||||
<span class="tag is-info" *ngFor="let wlRange of p.whiteList.sourceRange">{{ wlRange }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -217,126 +224,137 @@
|
|||
|
||||
<div *ngIf="p.headers">
|
||||
<hr/>
|
||||
<h2>Headers</h2>
|
||||
<div class="columns is-multiline">
|
||||
<div class="section-line">
|
||||
<h2 class="section-line-header">Headers</h2>
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.customRequestHeaders">
|
||||
<h2>Custom Request Headers</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngFor="let key of p.headers.customRequestHeaders | keys">
|
||||
<td><span class="has-text-grey-light">{{ key }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customRequestHeaders[key] }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-12" *ngIf="p.headers.customRequestHeaders?.length">
|
||||
<table class="table is-fullwidth is-hoverable table-fixed-break">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2">Custom Request Headers</td>
|
||||
</tr>
|
||||
<tr *ngFor="let header of p.headers.customRequestHeaders">
|
||||
<td><span class="has-text-grey-light">{{ header.name }}</span></td>
|
||||
<td><span class="has-text-grey">{{ header.value }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.customResponseHeaders">
|
||||
<h2>Custom Response Headers</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngFor="let key of p.headers.customResponseHeaders | keys">
|
||||
<td><span class="has-text-grey-light">{{ key }}</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customResponseHeaders[key] }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-12" *ngIf="p.headers.customResponseHeaders?.length">
|
||||
<table class="table is-fullwidth is-hoverable table-fixed-break">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2">Custom Response Headers</td>
|
||||
</tr>
|
||||
<tr *ngFor="let header of p.headers.customResponseHeaders">
|
||||
<td><span class="has-text-grey-light">{{ header.name }}</span></td>
|
||||
<td><span class="has-text-grey">{{ header.value }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<h2>Secure</h2>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr *ngIf="p.headers.browserXssFilter">
|
||||
<td><span class="has-text-grey">Browser XSS Filter</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.contentSecurityPolicy">
|
||||
<td><span class="has-text-grey">Content Security Policy</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.contentSecurityPolicy }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.contentTypeNoSniff">
|
||||
<td><span class="has-text-grey">Content Type (No sniff)</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.contentTypeNoSniff }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.customFrameOptionsValue">
|
||||
<td><span class="has-text-grey">Custom Frame Options Value</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customFrameOptionsValue }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.forceSTSHeader">
|
||||
<td><span class="has-text-grey">Force STS Header</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.forceSTSHeader }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.frameDeny">
|
||||
<td><span class="has-text-grey">Frame Deny</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.frameDeny }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.isDevelopment">
|
||||
<td><span class="has-text-grey">Is Development</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.isDevelopment }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.publicKey">
|
||||
<td><span class="has-text-grey">Public Key</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.publicKey }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.referrerPolicy">
|
||||
<td><span class="has-text-grey">Referrer Policy</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.referrerPolicy }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslHost">
|
||||
<td><span class="has-text-grey">SSL Host</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslHost }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslRedirect">
|
||||
<td><span class="has-text-grey">SSL Redirect</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslRedirect }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslTemporaryRedirect">
|
||||
<td><span class="has-text-grey">SSL Temporary Redirect</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslTemporaryRedirect }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsIncludeSubdomains">
|
||||
<td><span class="has-text-grey">STS Include Subdomains</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsIncludeSubdomains }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsPreload">
|
||||
<td><span class="has-text-grey">STS Preload</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsPreload }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsSeconds">
|
||||
<td><span class="has-text-grey">STS Seconds</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsSeconds }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<table class="table is-fullwidth is-hoverable table-fixed-break">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2">Secure</td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.browserXssFilter">
|
||||
<td><span class="has-text-grey">Browser XSS Filter</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.browserXssFilter }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.contentSecurityPolicy">
|
||||
<td><span class="has-text-grey">Content Security Policy</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.contentSecurityPolicy }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.contentTypeNoSniff">
|
||||
<td><span class="has-text-grey">Content Type (No sniff)</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.contentTypeNoSniff }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.customFrameOptionsValue">
|
||||
<td><span class="has-text-grey">Custom Frame Options Value</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.customFrameOptionsValue }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.forceSTSHeader">
|
||||
<td><span class="has-text-grey">Force STS Header</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.forceSTSHeader }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.frameDeny">
|
||||
<td><span class="has-text-grey">Frame Deny</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.frameDeny }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.isDevelopment">
|
||||
<td><span class="has-text-grey">Is Development</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.isDevelopment }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.publicKey">
|
||||
<td><span class="has-text-grey">Public Key</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.publicKey }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.referrerPolicy">
|
||||
<td><span class="has-text-grey">Referrer Policy</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.referrerPolicy }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslHost">
|
||||
<td><span class="has-text-grey">SSL Host</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslHost }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslRedirect">
|
||||
<td><span class="has-text-grey">SSL Redirect</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslRedirect }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.sslTemporaryRedirect">
|
||||
<td><span class="has-text-grey">SSL Temporary Redirect</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.sslTemporaryRedirect }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsIncludeSubdomains">
|
||||
<td><span class="has-text-grey">STS Include Subdomains</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsIncludeSubdomains }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsPreload">
|
||||
<td><span class="has-text-grey">STS Preload</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsPreload }}</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="p.headers.stsSeconds">
|
||||
<td><span class="has-text-grey">STS Seconds</span></td>
|
||||
<td><span class="has-text-grey">{{ p.headers.stsSeconds }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.allowedHosts">
|
||||
<h2>Allowed Hosts</h2>
|
||||
<div class="tags-list">
|
||||
<span class="tag is-light" *ngFor="let host of p.headers.allowedHosts">{{ host }}</span>
|
||||
<div class="column is-12" *ngIf="p.headers.sslProxyHeaders?.length">
|
||||
<table class="table is-fullwidth is-hoverable table-fixed-break">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2">SSL Proxy Headers</td>
|
||||
</tr>
|
||||
<tr *ngFor="let header of p.headers.sslProxyHeaders">
|
||||
<td><span class="has-text-grey-light">{{ header.name }}</span></td>
|
||||
<td><span class="has-text-grey">{{ header.value }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" *ngIf="p.headers.allowedHosts">
|
||||
<h2>Allowed Hosts</h2>
|
||||
<div class="tags-list">
|
||||
<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 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>
|
||||
|
||||
|
@ -344,23 +362,33 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div *ngIf="frontends.length > maxItem">
|
||||
|
||||
<div class="message">
|
||||
<div class="message-header has-background-warning has-text-black">
|
||||
Too many frontends to display, please add a filter.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Backends -->
|
||||
<div class="column is-6">
|
||||
<h2 class="subtitle"><span class="tag is-primary">{{ providers[tab]?.backends.length }}</span> Backends</h2>
|
||||
<div class="message" *ngFor="let p of providers[tab]?.backends; let i = index;">
|
||||
<div class="message-header">
|
||||
<h2 [id]="p.id">
|
||||
<div class="column is-6" *appLet="providers[tab]?.backends | backendFilter:keyword as backends">
|
||||
<h2 class="subtitle"><span class="tag is-primary">{{ backends.length }}</span><span class="subtitle-name">Backends</span></h2>
|
||||
|
||||
<div *ngIf="backends.length < maxItem">
|
||||
|
||||
<div class="message" *ngFor="let p of backends; trackBy: trackItem(tab);">
|
||||
<div class="message-header" [class.has-background-primary]="p.servers?.length" [class.has-background-danger]="!p.servers?.length">
|
||||
<h2>
|
||||
<i class="icon fas fa-server has-text-white"></i>
|
||||
<div>
|
||||
<i class="icon fas fa-server"></i>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-primary">{{ p.id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="has-text-white">{{ p.id }}</span>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
@ -374,28 +402,34 @@
|
|||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div *ngIf="p.section !== 'details'">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Server</td>
|
||||
<td>Weight</td>
|
||||
</tr>
|
||||
<tr *ngFor="let server of p.servers; let ri = index;">
|
||||
<td><a href="{{ server.url }}" title="{{ server.title }}">{{ server.url }}</a></td>
|
||||
<td><span class="has-text-grey">{{ server.weight }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div *ngIf="p.section !== 'details'" class="section-container">
|
||||
<div class="section-line">
|
||||
<table class="table is-fullwidth is-hoverable table-fixed">
|
||||
<colgroup>
|
||||
<col class="table-col-75">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Server</td>
|
||||
<td>Weight</td>
|
||||
</tr>
|
||||
<tr *ngFor="let server of p.servers">
|
||||
<td class="table-cell-limited"><a href="{{ server.url }}" [title]="server.id">{{ server.url }}</a></td>
|
||||
<td><span class="has-text-grey">{{ server.weight }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div *ngIf="p.section === 'details'">
|
||||
<div *ngIf="p.section === 'details'" class="section-container">
|
||||
|
||||
<div *ngIf="p.loadBalancer">
|
||||
<div *ngIf="p.loadBalancer" class="section-line">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<h2>Load Balancer</h2>
|
||||
<h2 class="section-line-header">Load Balancer</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
|
@ -424,9 +458,9 @@
|
|||
|
||||
<div *ngIf="p.maxConn">
|
||||
<hr/>
|
||||
<div class="columns">
|
||||
<div class="columns section-line">
|
||||
<div class="column is-3">
|
||||
<h2>Max Connections</h2>
|
||||
<h2 class="section-line-header">Max Connections</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
|
@ -449,9 +483,9 @@
|
|||
|
||||
<div *ngIf="p.circuitBreaker">
|
||||
<hr/>
|
||||
<div class="columns">
|
||||
<div class="columns section-line">
|
||||
<div class="column is-3">
|
||||
<h2>Circuit Breaker</h2>
|
||||
<h2 class="section-line-header">Circuit Breaker</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
|
@ -468,9 +502,9 @@
|
|||
|
||||
<div *ngIf="p.healthCheck">
|
||||
<hr/>
|
||||
<div class="columns">
|
||||
<div class="columns section-line">
|
||||
<div class="column is-3">
|
||||
<h2>Health Check</h2>
|
||||
<h2 class="section-line-header">Health Check</h2>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
|
@ -505,81 +539,79 @@
|
|||
|
||||
<div *ngIf="p.buffering">
|
||||
<hr>
|
||||
<div class="columns list-title">
|
||||
<div class="column is-12">
|
||||
<h2>Buffering</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<span>Request Body Bytes</span>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Max</span>
|
||||
<span class="tag is-info">{{ p.buffering.maxRequestBodyBytes }}</span>
|
||||
<div class="section-line">
|
||||
<h2 class="section-line-header">Buffering</h2>
|
||||
<table class="table is-fullwidth is-hoverable table-fixedd">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="has-text-grey">Request Body Bytes</span></td>
|
||||
<td>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Max</span>
|
||||
<span class="tag is-info">{{ p.buffering.maxRequestBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Men</span>
|
||||
<span class="tag is-info">{{ p.buffering.memRequestBodyBytes }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Men</span>
|
||||
<span class="tag is-info">{{ p.buffering.memRequestBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<span>Response Body Bytes</span>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Max</span>
|
||||
<span class="tag is-info">{{ p.buffering.maxResponseBodyBytes }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="has-text-grey">Response Body Bytes</span></td>
|
||||
<td>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Max</span>
|
||||
<span class="tag is-info">{{ p.buffering.maxResponseBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Men</span>
|
||||
<span class="tag is-info">{{ p.buffering.memResponseBodyBytes }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-light">Men</span>
|
||||
<span class="tag is-info">{{ p.buffering.memResponseBodyBytes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<span>Retry Expression</span>
|
||||
</div>
|
||||
<div class="column is-8">
|
||||
<span class="tag is-info">{{ p.buffering.retryExpression }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="has-text-grey">Retry Expression</td>
|
||||
<td colspan="2"><span class="tag is-info">{{ p.buffering.retryExpression }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import * as _ from 'lodash';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import * as _ from "lodash";
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-providers',
|
||||
|
@ -10,8 +10,9 @@ import * as _ from "lodash";
|
|||
})
|
||||
export class ProvidersComponent implements OnInit, OnDestroy {
|
||||
sub: Subscription;
|
||||
maxItem: number;
|
||||
keys: string[];
|
||||
data: any;
|
||||
previousKeys: string[];
|
||||
previousData: any;
|
||||
providers: any;
|
||||
tab: string;
|
||||
|
@ -20,6 +21,7 @@ export class ProvidersComponent implements OnInit, OnDestroy {
|
|||
constructor(private apiService: ApiService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.maxItem = 100;
|
||||
this.keyword = '';
|
||||
this.sub = Observable.timer(0, 2000)
|
||||
.timeInterval()
|
||||
|
@ -27,28 +29,23 @@ export class ProvidersComponent implements OnInit, OnDestroy {
|
|||
.subscribe(data => {
|
||||
if (!_.isEqual(this.previousData, data)) {
|
||||
this.previousData = _.cloneDeep(data);
|
||||
this.data = data;
|
||||
this.providers = data;
|
||||
this.keys = Object.keys(this.providers);
|
||||
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 {
|
||||
const keyword = this.keyword.toLowerCase();
|
||||
this.providers = Object.keys(this.data)
|
||||
.filter(value => value !== 'acme' && value !== 'ACME')
|
||||
.reduce((acc, curr) => {
|
||||
return Object.assign(acc, {
|
||||
[curr]: {
|
||||
backends: this.data[curr].backends.filter(d => d.id.toLowerCase().includes(keyword)),
|
||||
frontends: this.data[curr].frontends.filter(d => {
|
||||
return d.id.toLowerCase().includes(keyword) || d.backend.toLowerCase().includes(keyword);
|
||||
})
|
||||
}
|
||||
});
|
||||
}, {});
|
||||
trackItem(tab): (index, item) => string {
|
||||
return (index, item): string => tab + '-' + item.id;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
21
webui/src/app/directives/let.directive.ts
Normal file
21
webui/src/app/directives/let.directive.ts
Normal 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;
|
||||
}
|
||||
}
|
17
webui/src/app/pipes/backend.filter.pipe.ts
Normal file
17
webui/src/app/pipes/backend.filter.pipe.ts
Normal 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)));
|
||||
}
|
||||
}
|
18
webui/src/app/pipes/frontend.filter.pipe.ts
Normal file
18
webui/src/app/pipes/frontend.filter.pipe.ts
Normal 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)));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { PipeTransform, Pipe } from '@angular/core';
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'keys' })
|
||||
@Pipe({name: 'keys'})
|
||||
export class KeysPipe implements PipeTransform {
|
||||
transform(value, args: string[]): any {
|
||||
return Object.keys(value);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/observable/empty';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/retry';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
export interface ProviderType {
|
||||
[provider: string]: {
|
||||
|
@ -25,7 +25,7 @@ export class ApiService {
|
|||
}
|
||||
|
||||
fetchVersion(): Observable<any> {
|
||||
return this.http.get(`/api/version`, { headers: this.headers })
|
||||
return this.http.get('../api/version', {headers: this.headers})
|
||||
.retry(4)
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(`[version] returned code ${err.status}, body was: ${err.error}`);
|
||||
|
@ -34,7 +34,7 @@ export class ApiService {
|
|||
}
|
||||
|
||||
fetchHealthStatus(): Observable<any> {
|
||||
return this.http.get(`/health`, { headers: this.headers })
|
||||
return this.http.get('../health', {headers: this.headers})
|
||||
.retry(2)
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(`[health] returned code ${err.status}, body was: ${err.error}`);
|
||||
|
@ -43,46 +43,53 @@ export class ApiService {
|
|||
}
|
||||
|
||||
fetchProviders(): Observable<any> {
|
||||
return this.http.get(`/api/providers`, { headers: this.headers })
|
||||
return this.http.get('../api/providers', {headers: this.headers})
|
||||
.retry(2)
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
console.error(`[providers] returned code ${err.status}, body was: ${err.error}`);
|
||||
return Observable.of<any>({});
|
||||
})
|
||||
.map(this.parseProviders);
|
||||
.map((data: any): ProviderType => this.parseProviders(data));
|
||||
}
|
||||
|
||||
parseProviders(data: any): ProviderType {
|
||||
return Object.keys(data)
|
||||
.filter(value => value !== 'acme' && value !== 'ACME')
|
||||
.reduce((acc, curr) => {
|
||||
acc[curr] = {
|
||||
backends: Object.keys(data[curr].backends || {}).map(key => {
|
||||
data[curr].backends[key].id = key;
|
||||
data[curr].backends[key].servers = Object.keys(data[curr].backends[key].servers || {}).map(server => {
|
||||
return {
|
||||
title: server,
|
||||
url: data[curr].backends[key].servers[server].url,
|
||||
weight: data[curr].backends[key].servers[server].weight
|
||||
};
|
||||
acc[curr] = {};
|
||||
|
||||
acc[curr].frontends = this.toArray(data[curr].frontends, 'id')
|
||||
.map(frontend => {
|
||||
frontend.routes = this.toArray(frontend.routes, 'id');
|
||||
frontend.errors = this.toArray(frontend.errors, 'id');
|
||||
if (frontend.headers) {
|
||||
frontend.headers.customRequestHeaders = this.toHeaderArray(frontend.headers.customRequestHeaders);
|
||||
frontend.headers.customResponseHeaders = this.toHeaderArray(frontend.headers.customResponseHeaders);
|
||||
frontend.headers.sslProxyHeaders = this.toHeaderArray(frontend.headers.sslProxyHeaders);
|
||||
}
|
||||
return frontend;
|
||||
});
|
||||
|
||||
return data[curr].backends[key];
|
||||
}),
|
||||
frontends: Object.keys(data[curr].frontends || {}).map(key => {
|
||||
data[curr].frontends[key].id = key;
|
||||
data[curr].frontends[key].routes = Object.keys(data[curr].frontends[key].routes || {}).map(route => {
|
||||
return {
|
||||
title: route,
|
||||
rule: data[curr].frontends[key].routes[route].rule
|
||||
};
|
||||
acc[curr].backends = this.toArray(data[curr].backends, 'id')
|
||||
.map(backend => {
|
||||
backend.servers = this.toArray(backend.servers, 'id');
|
||||
return backend;
|
||||
});
|
||||
|
||||
return data[curr].frontends[key];
|
||||
}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
toHeaderArray(data: any): any[] {
|
||||
return Object.keys(data || {}).map(key => ({name: key, value: data[key]}));
|
||||
}
|
||||
|
||||
toArray(data: any, fieldKeyName: string): any[] {
|
||||
return Object.keys(data || {}).map(key => {
|
||||
data[key][fieldKeyName] = key;
|
||||
return data[key];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
@charset "utf-8"
|
||||
|
||||
@import 'typography'
|
||||
@import 'variables'
|
||||
@import 'colors'
|
||||
@import '../../node_modules/bulma/sass/utilities/all'
|
||||
@import '../../node_modules/bulma/sass/base/all'
|
||||
@import '../../node_modules/bulma/sass/grid/all'
|
||||
@import '../../node_modules/bulma/sass/elements/container'
|
||||
@import '../../node_modules/bulma/sass/elements/tag'
|
||||
@import '../../node_modules/bulma/sass/elements/box'
|
||||
@import '../../node_modules/bulma/sass/elements/form'
|
||||
@import '../../node_modules/bulma/sass/elements/table'
|
||||
@import '../../node_modules/bulma/sass/components/navbar'
|
||||
@import '../../node_modules/bulma/sass/components/tabs'
|
||||
@import '../../node_modules/bulma/sass/elements/notification'
|
||||
@import '~bulma/sass/utilities/all'
|
||||
@import '~bulma/sass/base/all'
|
||||
@import '~bulma/sass/grid/all'
|
||||
@import '~bulma/sass/elements/container'
|
||||
@import '~bulma/sass/elements/tag'
|
||||
@import '~bulma/sass/elements/other'
|
||||
@import '~bulma/sass/elements/box'
|
||||
@import '~bulma/sass/elements/form'
|
||||
@import '~bulma/sass/elements/table'
|
||||
@import '~bulma/sass/components/navbar'
|
||||
@import '~bulma/sass/components/tabs'
|
||||
@import '~bulma/sass/elements/notification'
|
||||
@import 'nav'
|
||||
@import 'content'
|
||||
@import 'message'
|
||||
@import 'label'
|
||||
@import 'charts'
|
||||
@import 'helper'
|
||||
|
||||
|
|
|
@ -30,12 +30,6 @@
|
|||
height: 320px
|
||||
background-color: $white
|
||||
|
||||
.bar
|
||||
fill: rgba($blue, 0.91)
|
||||
|
||||
&:hover
|
||||
fill: lighten($blue, 10)
|
||||
|
||||
.axis text
|
||||
fill: $text
|
||||
font: 10px sans-serif
|
||||
|
|
|
@ -1,46 +1,21 @@
|
|||
.content
|
||||
background: transparent
|
||||
margin: 40px 0
|
||||
margin: 2rem 0
|
||||
|
||||
.subtitle
|
||||
font-size: 15px
|
||||
text-transform: uppercase
|
||||
color: $black
|
||||
font-size: 0.9rem
|
||||
font-weight: $weight-bold
|
||||
text-transform: uppercase
|
||||
margin: 10px 0 0 0
|
||||
|
||||
.list-title
|
||||
color: $text-dark
|
||||
weight: $weight-semibold
|
||||
margin: 5px 0 0 0
|
||||
|
||||
.list-item
|
||||
width: 100%
|
||||
display: block
|
||||
align-items: center
|
||||
font-size: 12px
|
||||
padding: 6px 10px
|
||||
border-top: 1px solid $border-light
|
||||
|
||||
.columns
|
||||
|
||||
.column
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.icon
|
||||
width: 22px
|
||||
height: 22px
|
||||
display: block
|
||||
float: left
|
||||
margin-right: 10px
|
||||
.subtitle-name
|
||||
padding-left: 0.5rem
|
||||
|
||||
.content-item
|
||||
background: $white
|
||||
border: 1px solid $border-secondary
|
||||
margin: 10px 0
|
||||
border-radius: 4px
|
||||
border-radius: $traefik-border-radius
|
||||
box-shadow: 1px 2px 5px rgba($border, 0.4)
|
||||
|
||||
h2
|
||||
|
@ -82,7 +57,7 @@
|
|||
|
||||
img
|
||||
width: 40px
|
||||
heught: 40px
|
||||
height: 40px
|
||||
display: block
|
||||
float: left
|
||||
margin-right: 10px
|
||||
|
@ -106,37 +81,27 @@
|
|||
margin: 15px auto
|
||||
|
||||
.search-container
|
||||
height: 50px
|
||||
background: $white
|
||||
border-radius: 4px
|
||||
color: $black
|
||||
margin: 10px 0
|
||||
display: flex
|
||||
align-items: center
|
||||
position: relative
|
||||
border-radius: $traefik-border-radius
|
||||
box-shadow: 1px 2px 5px rgba($border, 0.4)
|
||||
border: 1px solid $border-secondary
|
||||
position: relative
|
||||
height: 3rem
|
||||
|
||||
.icon
|
||||
.search-button
|
||||
position: absolute
|
||||
left: 10px
|
||||
top: 13px
|
||||
left: 1rem
|
||||
top: 0.8rem
|
||||
|
||||
input
|
||||
font-size: 16px
|
||||
color: $text
|
||||
width: 100%
|
||||
height: 48px
|
||||
padding-left: 50px
|
||||
border: none
|
||||
border-radius: $traefik-border-radius
|
||||
outline: none
|
||||
font-size: 1rem
|
||||
font-weight: $weight-light
|
||||
border-radius: 4px
|
||||
|
||||
.notification
|
||||
background: $white
|
||||
border-radius: 4px
|
||||
color: $text
|
||||
font-size: 16px
|
||||
box-shadow: 1px 2px 5px rgba($border, 0.4)
|
||||
border: 1px solid $border-secondary
|
||||
width: 100%
|
||||
padding-left: 2.8rem
|
||||
|
|
|
@ -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
|
|
@ -1,89 +1,65 @@
|
|||
.message
|
||||
display: block
|
||||
font-size: 14px
|
||||
margin: 20px 0 30px 0
|
||||
font-size: 0.8rem
|
||||
margin: 1rem 0 1.5rem 0
|
||||
padding-bottom: 0.3rem
|
||||
border: 1px solid $border
|
||||
background: $white
|
||||
border-radius: 4px
|
||||
border-radius: $traefik-border-radius
|
||||
box-shadow: 1px 2px 5px rgba($border, 0.4)
|
||||
|
||||
.message-header
|
||||
color: $color-secondary
|
||||
border-bottom: 1px solid $border-secondary
|
||||
padding: 20px 10px
|
||||
background: #f8f9fa
|
||||
border-top-left-radius: 4px
|
||||
border-top-right-radius: 4px
|
||||
padding: 0.6rem
|
||||
border-top-left-radius: $traefik-border-radius
|
||||
border-top-right-radius: $traefik-border-radius
|
||||
|
||||
.icon
|
||||
display: block
|
||||
float: left
|
||||
width: 1.4rem
|
||||
height: 1.4rem
|
||||
margin-right: 0.5rem
|
||||
|
||||
h2
|
||||
font-size: 14px
|
||||
weight: $weight-bold
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
|
||||
&.red
|
||||
background: rgba($red-secondary, 0.4)
|
||||
border-bottom: 1px solid $red-secondary
|
||||
color: $red-secondary
|
||||
|
||||
p
|
||||
color: $red-secondary
|
||||
|
||||
&.green
|
||||
background-color: rgba($green-secondary, 0.4)
|
||||
border-bottom: 1px solid $green-secondary
|
||||
color: $green-secondary
|
||||
|
||||
p
|
||||
color: darken($green-secondary, 10) !important
|
||||
|
||||
&.orange
|
||||
background-color: rgba($orange-secondary, 0.4)
|
||||
border-bottom: 1px solid $orange-secondary
|
||||
color: $orange-secondary
|
||||
|
||||
p
|
||||
color: $orange-secondary
|
||||
|
||||
&.blue
|
||||
background-color: rgba($blue-background, 0.4)
|
||||
border-bottom: 1px solid $blue-background
|
||||
color: $blue-background
|
||||
|
||||
p
|
||||
color: $blue-background !important
|
||||
|
||||
img
|
||||
margin-right: 15px
|
||||
|
||||
.message-body
|
||||
|
||||
.field
|
||||
margin: 5px 10px
|
||||
padding-bottom: 10px
|
||||
.tabs
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
.tags-list
|
||||
margin: 5px 10px
|
||||
.section-container
|
||||
padding: 0.3em 0 0 0
|
||||
|
||||
.control
|
||||
width: 100%
|
||||
margin: 5px 0
|
||||
.section-line
|
||||
padding: 0 0.75em
|
||||
|
||||
.tags
|
||||
width: 100%
|
||||
.section-line-header
|
||||
padding: 0.2em 0 0 0
|
||||
|
||||
.tag
|
||||
width: 50%
|
||||
// required for small screen (without -> table overlapping)
|
||||
.table-fixed
|
||||
table-layout: fixed
|
||||
|
||||
// required for small screen (without -> table overlapping)
|
||||
.table-fixed-break
|
||||
table-layout: fixed
|
||||
word-wrap: break-word
|
||||
|
||||
.table-cell-limited
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
|
||||
.table-col-75
|
||||
width: 75%
|
||||
|
||||
h2
|
||||
margin: 10px 10px 0 10px
|
||||
color: $black
|
||||
|
||||
hr
|
||||
margin: 5px 0
|
||||
|
||||
.message-subheader
|
||||
border-bottom: 1px solid $border-secondary
|
||||
padding: 10px
|
||||
margin-bottom: 5px
|
||||
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
.navbar
|
||||
border-bottom: 1px solid $border
|
||||
box-shadow: 1px 2px 5px rgba($border, 0.4)
|
||||
height: 60px
|
||||
|
||||
.navbar-item
|
||||
font-size: 13px
|
||||
font-size: 0.8rem
|
||||
text-transform: uppercase
|
||||
font-weight: $weight-semibold
|
||||
|
||||
.navbar-logo
|
||||
width: 40px
|
||||
min-height: 40px
|
||||
|
||||
&:hover
|
||||
background: transparent
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
=font-face($family, $path, $weight: normal, $style: normal)
|
||||
@font-face
|
||||
font-family: $family
|
||||
src: url('#{$path}.ttf') format('truetype')
|
||||
src: url('./#{$path}.ttf') format('truetype')
|
||||
font-weight: $weight
|
||||
font-style: $style
|
||||
|
||||
+font-face('Open Sans', '/assets/fonts/OpenSans-Light', 300, 'light')
|
||||
+font-face('Open Sans', '/assets/fonts/OpenSans-Regular', 400, 'regular')
|
||||
+font-face('Open Sans', '/assets/fonts/OpenSans-Semibold', 600, 'semibold')
|
||||
+font-face('Open Sans', '/assets/fonts/OpenSans-Bold', 700, 'bold')
|
||||
+font-face('Open Sans', '/assets/fonts/OpenSans-ExtraBold', 800, 'extrabold')
|
||||
+font-face('Open Sans', 'assets/fonts/OpenSans-Light', 300, 'light')
|
||||
+font-face('Open Sans', 'assets/fonts/OpenSans-Regular', 400, 'regular')
|
||||
+font-face('Open Sans', 'assets/fonts/OpenSans-Semibold', 600, 'semibold')
|
||||
+font-face('Open Sans', 'assets/fonts/OpenSans-Bold', 700, 'bold')
|
||||
+font-face('Open Sans', 'assets/fonts/OpenSans-ExtraBold', 800, 'extrabold')
|
||||
|
||||
$open-sans: 'Open Sans', sans-serif
|
||||
|
|
1
webui/src/styles/variables.sass
Normal file
1
webui/src/styles/variables.sass
Normal file
|
@ -0,0 +1 @@
|
|||
$traefik-border-radius: 4px
|
|
@ -1031,9 +1031,9 @@ builtin-status-codes@^3.0.0:
|
|||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
|
||||
bulma@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.6.2.tgz#f4b1d11d5acc51a79644eb0a2b0b10649d3d71f5"
|
||||
bulma@^0.7.0:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.1.tgz#73c2e3b2930c90cc272029cbd19918b493fca486"
|
||||
|
||||
bytes@3.0.0:
|
||||
version "3.0.0"
|
||||
|
|
Loading…
Reference in a new issue