mirror of
https://github.com/containous/traefik.git
synced 2025-11-17 08:23:51 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ad42cd0ec |
@@ -239,8 +239,6 @@ linters:
|
||||
text: ' always receives '
|
||||
linters:
|
||||
- unparam
|
||||
- path: pkg/server/service/bufferpool.go
|
||||
text: 'SA6002: argument should be pointer-like to avoid allocations'
|
||||
- path: pkg/server/middleware/middlewares.go
|
||||
text: Function 'buildConstructor' has too many statements
|
||||
linters:
|
||||
@@ -316,8 +314,12 @@ linters:
|
||||
text: 'the methods of "wasmMiddlewareBuilder" use pointer receiver and non-pointer receiver.'
|
||||
linters:
|
||||
- recvcheck
|
||||
- path: pkg/server/service/bufferpool.go
|
||||
text: 'SA6002: argument should be pointer-like to avoid allocations'
|
||||
- path: pkg/proxy/httputil/bufferpool.go
|
||||
text: 'SA6002: argument should be pointer-like to avoid allocations'
|
||||
- path: pkg/udp/conn.go
|
||||
text: 'SA6002: argument should be pointer-like to avoid allocations'
|
||||
- path: integration/integration_test.go
|
||||
text: 'var (gatewayAPIConformanceRunTest|traefikVersion) is unused'
|
||||
- path: pkg/server/router/router.go
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,23 +1,3 @@
|
||||
## [v3.6.1](https://github.com/traefik/traefik/tree/v3.6.1) (2025-11-13)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.6.0...v3.6.1)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[docker]** Auto-negotiate Docker API Version ([#12256](https://github.com/traefik/traefik/pull/12256) by [felixbuenemann](https://github.com/felixbuenemann))
|
||||
- **[server]** Fix multi-layer routing with models ([#12258](https://github.com/traefik/traefik/pull/12258) by [juliens](https://github.com/juliens))
|
||||
- **[udp]** Revert "Avoid allocations in readLoop by using sync.Pool" ([#12267](https://github.com/traefik/traefik/pull/12267) by [kevinpollet](https://github.com/kevinpollet))
|
||||
- **[webui]** Fix blocked navigation on Safari ([#12231](https://github.com/traefik/traefik/pull/12231) by [gndz07](https://github.com/gndz07))
|
||||
- **[webui]** Restore remote Upgrade to Hub button web component ([#12219](https://github.com/traefik/traefik/pull/12219) by [gndz07](https://github.com/gndz07))
|
||||
|
||||
**Documentation:**
|
||||
- **[k8s]** Fix Nginx provider documentation ([#12266](https://github.com/traefik/traefik/pull/12266) by [nmengin](https://github.com/nmengin))
|
||||
- **[k8s]** Fix Gateway API version and the list of features supported ([#12254](https://github.com/traefik/traefik/pull/12254) by [nmengin](https://github.com/nmengin))
|
||||
|
||||
## [v2.11.31](https://github.com/traefik/traefik/tree/v2.11.31) (2025-11-13)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v2.11.30...v2.11.31)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[docker,docker/swarm]** Auto-negotiate Docker API version ([#12262](https://github.com/traefik/traefik/pull/12262) by [kevinpollet](https://github.com/kevinpollet))
|
||||
|
||||
## [v3.6.0](https://github.com/traefik/traefik/tree/v3.6.0) (2025-11-07)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.5.0-rc1...v3.6.0)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ specification from the Kubernetes Special Interest Groups (SIGs).
|
||||
|
||||
This provider supports Standard version [v1.4.0](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.4.0) of the Gateway API specification.
|
||||
|
||||
It fully supports all `HTTPRoute` core and some extended features, like `BackendTLSPolicy`, and `GRPCRoute` resources from the [Standard channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels), as well as `TCPRoute`, and `TLSRoute` resources from the [Experimental channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels).
|
||||
It fully supports all HTTP core and some extended features, as well as the `TCPRoute` and `TLSRoute` resources from the [Experimental channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels).
|
||||
|
||||
For more details, check out the conformance [report](https://github.com/kubernetes-sigs/gateway-api/tree/main/conformance/reports/v1.4.0/traefik-traefik).
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ description: "The Kubernetes Gateway API can be used as a provider for routing a
|
||||
When using the Kubernetes Gateway API provider, Traefik leverages the Gateway API Custom Resource Definitions (CRDs) to obtain its routing configuration.
|
||||
For detailed information on the Gateway API concepts and resources, refer to the official [documentation](https://gateway-api.sigs.k8s.io/).
|
||||
|
||||
The Kubernetes Gateway API provider supports version [v1.4.0](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.4.0) of the specification.
|
||||
The Kubernetes Gateway API provider supports version [v1.2.1](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.2.1) of the specification.
|
||||
|
||||
It fully supports all `HTTPRoute` core and some extended features, like `BackendTLSPolicy`, and `GRPCRoute` resources from the [Standard channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels), as well as `TCPRoute`, and `TLSRoute` resources from the [Experimental channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels).
|
||||
|
||||
For more details, check out the conformance [report](https://github.com/kubernetes-sigs/gateway-api/tree/main/conformance/reports/v1.4.0/traefik-traefik).
|
||||
It fully supports all `HTTPRoute` core and some extended features, like `GRPCRoute`, as well as the `TCPRoute` and `TLSRoute` resources from the [Experimental channel](https://gateway-api.sigs.k8s.io/concepts/versioning/?h=#release-channels).
|
||||
|
||||
For more details, check out the conformance [report](https://github.com/kubernetes-sigs/gateway-api/tree/main/conformance/reports/v1.2.1/traefik-traefik).
|
||||
|
||||
## Deploying a Gateway
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ Limitations or behavioral differences are indicated where relevant.
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioproxy-ssl-secret" href="#opt-nginx-ingress-kubernetes-ioproxy-ssl-secret" title="#opt-nginx-ingress-kubernetes-ioproxy-ssl-secret">`nginx.ingress.kubernetes.io/proxy-ssl-secret`</a> | |
|
||||
| <a id="opt-nginx-ingress-kubernetes-ioservice-upstream" href="#opt-nginx-ingress-kubernetes-ioservice-upstream" title="#opt-nginx-ingress-kubernetes-ioservice-upstream">`nginx.ingress.kubernetes.io/service-upstream`</a> | |
|
||||
|
||||
**Unsupported NGINX Annotations**
|
||||
### Unsupported NGINX Annotations
|
||||
|
||||
!!! question "Want to Add Support for More Annotations?"
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/safe"
|
||||
)
|
||||
|
||||
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use.
|
||||
const DockerAPIVersion = "1.24"
|
||||
|
||||
const dockerName = "docker"
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
@@ -51,6 +54,7 @@ func (p *Provider) Init() error {
|
||||
}
|
||||
|
||||
func (p *Provider) createClient(ctx context.Context) (*client.Client, error) {
|
||||
p.ClientConfig.apiVersion = DockerAPIVersion
|
||||
return createClient(ctx, p.ClientConfig)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/safe"
|
||||
)
|
||||
|
||||
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use.
|
||||
const SwarmAPIVersion = "1.24"
|
||||
|
||||
const swarmName = "swarm"
|
||||
|
||||
var _ provider.Provider = (*SwarmProvider)(nil)
|
||||
@@ -55,6 +58,7 @@ func (p *SwarmProvider) Init() error {
|
||||
}
|
||||
|
||||
func (p *SwarmProvider) createClient(ctx context.Context) (*client.Client, error) {
|
||||
p.ClientConfig.apiVersion = SwarmAPIVersion
|
||||
return createClient(ctx, p.ClientConfig)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ func parseContainer(container containertypes.InspectResponse) dockerData {
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
apiVersion string
|
||||
|
||||
Username string `description:"Username for Basic HTTP authentication." json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty"`
|
||||
Password string `description:"Password for Basic HTTP authentication." json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty"`
|
||||
Endpoint string `description:"Docker server endpoint. Can be a TCP or a Unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
@@ -121,9 +123,8 @@ func createClient(ctx context.Context, cfg ClientConfig) (*client.Client, error)
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
client.FromEnv,
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithHTTPHeaders(httpHeaders))
|
||||
client.WithHTTPHeaders(httpHeaders),
|
||||
client.WithVersion(cfg.apiVersion))
|
||||
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
@@ -175,11 +175,9 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
if cfg.HTTP != nil && len(cfg.HTTP.Models) > 0 {
|
||||
rts := make(map[string]*dynamic.Router)
|
||||
|
||||
modelRouterNames := make(map[string][]string)
|
||||
for name, rt := range cfg.HTTP.Routers {
|
||||
// Only root routers can have models applied.
|
||||
if rt.ParentRefs != nil {
|
||||
rts[name] = rt
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -235,9 +233,7 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
rtName := name
|
||||
if len(eps) > 1 {
|
||||
rtName = epName + "-" + name
|
||||
modelRouterNames[name] = append(modelRouterNames[name], rtName)
|
||||
}
|
||||
|
||||
rts[rtName] = cp
|
||||
} else {
|
||||
router.EntryPoints = append(router.EntryPoints, epName)
|
||||
@@ -247,26 +243,6 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
for _, rt := range rts {
|
||||
if rt.ParentRefs == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var parentRefs []string
|
||||
for _, ref := range rt.ParentRefs {
|
||||
// Only add the initial parent ref if it still exists.
|
||||
if _, ok := rts[ref]; ok {
|
||||
parentRefs = append(parentRefs, ref)
|
||||
}
|
||||
|
||||
if names, ok := modelRouterNames[ref]; ok {
|
||||
parentRefs = append(parentRefs, names...)
|
||||
}
|
||||
}
|
||||
|
||||
rt.ParentRefs = parentRefs
|
||||
}
|
||||
|
||||
cfg.HTTP.Routers = rts
|
||||
}
|
||||
|
||||
|
||||
@@ -810,414 +810,6 @@ func Test_applyModel(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with parentRefs, parent not split",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: make(map[string]*dynamic.Model),
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: make(map[string]*dynamic.Model),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with parentRefs, parent split by model",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"websecure", "web"},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
Middlewares: []string{"test"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"websecure-parent": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
Middlewares: []string{"test"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent", "websecure-parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
Middlewares: []string{"test"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple child routers with parentRefs, parent split by model",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"websecure", "web"},
|
||||
},
|
||||
"child1": {
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
"child2": {
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
Middlewares: []string{"auth"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"websecure-parent": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
Middlewares: []string{"auth"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"child1": {
|
||||
ParentRefs: []string{"parent", "websecure-parent"},
|
||||
},
|
||||
"child2": {
|
||||
ParentRefs: []string{"parent", "websecure-parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
Middlewares: []string{"auth"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with parentRefs to non-existing parent",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"child": {
|
||||
ParentRefs: []string{"nonexistent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: make(map[string]*dynamic.Model),
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"child": {
|
||||
ParentRefs: []string{"nonexistent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: make(map[string]*dynamic.Model),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with multiple parentRefs, some split",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent1": {
|
||||
EntryPoints: []string{"websecure", "web"},
|
||||
},
|
||||
"parent2": {
|
||||
EntryPoints: []string{"web"},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent1", "parent2"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent1": {
|
||||
EntryPoints: []string{"web"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"websecure-parent1": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"parent2": {
|
||||
EntryPoints: []string{"web"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent1", "websecure-parent1", "parent2"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with multiple parentRefs, all split",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent1": {
|
||||
EntryPoints: []string{"websecure", "web"},
|
||||
},
|
||||
"parent2": {
|
||||
EntryPoints: []string{"web"},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent1", "parent2"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"web@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
"websecure@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"web-parent1": {
|
||||
EntryPoints: []string{"web"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"websecure-parent1": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"parent2": {
|
||||
EntryPoints: []string{"web"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"websecure-parent1", "web-parent1", "parent2"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"web@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
"websecure@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with parentRefs, parent split into three routers",
|
||||
input: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"websecure", "web", "admin"},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
"admin@internal": {
|
||||
Middlewares: []string{"admin-auth"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"websecure-parent": {
|
||||
EntryPoints: []string{"websecure"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"admin-parent": {
|
||||
EntryPoints: []string{"admin"},
|
||||
Middlewares: []string{"admin-auth"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
AccessLogs: pointer(true),
|
||||
Metrics: pointer(true),
|
||||
Tracing: pointer(true),
|
||||
TraceVerbosity: otypes.MinimalVerbosity,
|
||||
},
|
||||
},
|
||||
"child": {
|
||||
ParentRefs: []string{"parent", "websecure-parent", "admin-parent"},
|
||||
},
|
||||
},
|
||||
Middlewares: make(map[string]*dynamic.Middleware),
|
||||
Services: make(map[string]*dynamic.Service),
|
||||
Models: map[string]*dynamic.Model{
|
||||
"websecure@internal": {
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
"admin@internal": {
|
||||
Middlewares: []string{"admin-auth"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
||||
@@ -32,6 +32,9 @@ type Listener struct {
|
||||
// timeout defines how long to wait on an idle session,
|
||||
// before releasing its related resources.
|
||||
timeout time.Duration
|
||||
|
||||
// readBufferPool is a pool of byte slices for UDP packet reading.
|
||||
readBufferPool sync.Pool
|
||||
}
|
||||
|
||||
// ListenPacketConn creates a new listener from PacketConn.
|
||||
@@ -51,6 +54,11 @@ func ListenPacketConn(packetConn net.PacketConn, timeout time.Duration) (*Listen
|
||||
conns: make(map[string]*Conn),
|
||||
accepting: true,
|
||||
timeout: timeout,
|
||||
readBufferPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, maxDatagramSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
go l.readLoop()
|
||||
@@ -152,21 +160,26 @@ func (l *Listener) readLoop() {
|
||||
for {
|
||||
// Allocating a new buffer for every read avoids
|
||||
// overwriting data in c.msgs in case the next packet is received
|
||||
// before c.msgs is emptied via Read()
|
||||
buf := make([]byte, maxDatagramSize)
|
||||
// before c.msgs is emptied via Read().
|
||||
// Reuses buffers via the readBufferPool sync.Pool.
|
||||
buf := l.readBufferPool.Get().([]byte)
|
||||
|
||||
n, raddr, err := l.pConn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
l.readBufferPool.Put(buf)
|
||||
return
|
||||
}
|
||||
conn, err := l.getConn(raddr)
|
||||
if err != nil {
|
||||
l.readBufferPool.Put(buf)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
// Receiver must call releaseReadBuffer() when done reading the data.
|
||||
case conn.receiveCh <- buf[:n]:
|
||||
case <-conn.doneCh:
|
||||
l.readBufferPool.Put(buf)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -211,15 +224,15 @@ type Conn struct {
|
||||
listener *Listener
|
||||
rAddr net.Addr
|
||||
|
||||
receiveCh chan []byte // to receive the data from the listener's readLoop
|
||||
readCh chan []byte // to receive the buffer into which we should Read
|
||||
sizeCh chan int // to synchronize with the end of a Read
|
||||
msgs [][]byte // to store data from listener, to be consumed by Reads
|
||||
receiveCh chan []byte // to receive the data from the listener's readLoop.
|
||||
readCh chan []byte // to receive the buffer into which we should Read.
|
||||
sizeCh chan int // to synchronize with the end of a Read.
|
||||
msgs [][]byte // to store data from listener, to be consumed by Reads.
|
||||
|
||||
muActivity sync.RWMutex
|
||||
lastActivity time.Time // the last time the session saw either read or write activity
|
||||
lastActivity time.Time // the last time the session saw either read or write activity.
|
||||
|
||||
timeout time.Duration // for timeouts
|
||||
timeout time.Duration // for timeouts.
|
||||
doneOnce sync.Once
|
||||
doneCh chan struct{}
|
||||
}
|
||||
@@ -254,6 +267,8 @@ func (c *Conn) readLoop() {
|
||||
msg := c.msgs[0]
|
||||
c.msgs = c.msgs[1:]
|
||||
n := copy(cBuf, msg)
|
||||
// Return buffer to sync.Pool once done reading from it.
|
||||
c.listener.readBufferPool.Put(msg)
|
||||
c.sizeCh <- n
|
||||
case msg := <-c.receiveCh:
|
||||
c.msgs = append(c.msgs, msg)
|
||||
@@ -299,6 +314,11 @@ func (c *Conn) Write(p []byte) (n int, err error) {
|
||||
|
||||
func (c *Conn) close() {
|
||||
c.doneOnce.Do(func() {
|
||||
// Release any buffered data before closing.
|
||||
for _, msg := range c.msgs {
|
||||
c.listener.readBufferPool.Put(msg)
|
||||
}
|
||||
c.msgs = nil
|
||||
close(c.doneCh)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ RepositoryName = "traefik"
|
||||
OutputType = "file"
|
||||
FileName = "traefik_changelog.md"
|
||||
|
||||
# example new bugfix v3.6.1
|
||||
CurrentRef = "v3.6"
|
||||
PreviousRef = "v3.6.0"
|
||||
BaseBranch = "v3.6"
|
||||
FutureCurrentRefName = "v3.6.1"
|
||||
# example new bugfix v3.5.6
|
||||
CurrentRef = "v3.5"
|
||||
PreviousRef = "v3.5.5"
|
||||
BaseBranch = "v3.5"
|
||||
FutureCurrentRefName = "v3.5.6"
|
||||
|
||||
ThresholdPreviousRef = 10
|
||||
ThresholdCurrentRef = 10
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@traefiklabs/faency": "12.0.4",
|
||||
"@traefiklabs/faency": "11.1.4",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/react": "^18.2.0",
|
||||
|
||||
23
webui/public/traefiklabs-hub-button-app/main-v1.js
Normal file
23
webui/public/traefiklabs-hub-button-app/main-v1.js
Normal file
File diff suppressed because one or more lines are too long
1
webui/public/traefiklabs-hub-button-app/main-v1.js.map
Normal file
1
webui/public/traefiklabs-hub-button-app/main-v1.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,6 @@
|
||||
import { Box, Button, Flex, TextField, InputHandle } from '@traefiklabs/faency'
|
||||
import { Box, Button, Flex, TextField } from '@traefiklabs/faency'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { InputHandle } from '@traefiklabs/faency/dist/components/Input'
|
||||
import { isUndefined, omitBy } from 'lodash'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { FiSearch, FiXCircle } from 'react-icons/fi'
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import useHubUpgradeButton from './use-hub-upgrade-button'
|
||||
|
||||
import { VersionContext } from 'contexts/version'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('utils/workers/scriptVerification')
|
||||
|
||||
const mockVerifySignature = vi.mocked(verifySignature)
|
||||
|
||||
const createWrapper = (showHubButton: boolean) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<VersionContext.Provider value={{ showHubButton, version: '1.0.0' }}>{children}</VersionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useHubUpgradeButton Hook', () => {
|
||||
let originalCreateObjectURL: typeof URL.createObjectURL
|
||||
let originalRevokeObjectURL: typeof URL.revokeObjectURL
|
||||
const mockBlobUrl = 'blob:http://localhost:3000/mock-blob-url'
|
||||
|
||||
beforeEach(() => {
|
||||
originalCreateObjectURL = URL.createObjectURL
|
||||
originalRevokeObjectURL = URL.revokeObjectURL
|
||||
URL.createObjectURL = vi.fn(() => mockBlobUrl)
|
||||
URL.revokeObjectURL = vi.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
URL.createObjectURL = originalCreateObjectURL
|
||||
URL.revokeObjectURL = originalRevokeObjectURL
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should not verify script when showHubButton is false', async () => {
|
||||
renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(false),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should verify script and create blob URL when showHubButton is true and verification succeeds', async () => {
|
||||
const mockScriptContent = new ArrayBuffer(8)
|
||||
mockVerifySignature.mockResolvedValue({
|
||||
verified: true,
|
||||
scriptContent: mockScriptContent,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).toHaveBeenCalledWith(
|
||||
'https://traefik.github.io/traefiklabs-hub-button-app/main-v1.js',
|
||||
'https://traefik.github.io/traefiklabs-hub-button-app/main-v1.js.sig',
|
||||
'MCowBQYDK2VwAyEAY0OZFFE5kSuqYK6/UprTL5RmvQ+8dpPTGMCw1MiO/Gs=',
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.signatureVerified).toBe(true)
|
||||
})
|
||||
|
||||
expect(result.current.scriptBlobUrl).toBe(mockBlobUrl)
|
||||
expect(URL.createObjectURL).toHaveBeenCalledWith(expect.any(Blob))
|
||||
})
|
||||
|
||||
it('should set signatureVerified to false when verification fails', async () => {
|
||||
mockVerifySignature.mockResolvedValue({
|
||||
verified: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.signatureVerified).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.scriptBlobUrl).toBeNull()
|
||||
expect(URL.createObjectURL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle verification errors gracefully', async () => {
|
||||
mockVerifySignature.mockRejectedValue(new Error('Verification failed'))
|
||||
|
||||
const { result } = renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.signatureVerified).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.scriptBlobUrl).toBeNull()
|
||||
})
|
||||
|
||||
it('should create blob with correct MIME type', async () => {
|
||||
const mockScriptContent = new ArrayBuffer(8)
|
||||
mockVerifySignature.mockResolvedValue({
|
||||
verified: true,
|
||||
scriptContent: mockScriptContent,
|
||||
})
|
||||
|
||||
renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(URL.createObjectURL).toHaveBeenCalled()
|
||||
})
|
||||
const blobCall = vi.mocked(URL.createObjectURL).mock.calls[0][0] as Blob
|
||||
expect(blobCall).toBeInstanceOf(Blob)
|
||||
expect(blobCall.type).toBe('application/javascript')
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { VersionContext } from 'contexts/version'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
const HUB_BUTTON_URL = 'https://traefik.github.io/traefiklabs-hub-button-app/main-v1.js'
|
||||
const PUBLIC_KEY = 'MCowBQYDK2VwAyEAY0OZFFE5kSuqYK6/UprTL5RmvQ+8dpPTGMCw1MiO/Gs='
|
||||
|
||||
const useHubUpgradeButton = () => {
|
||||
const [signatureVerified, setSignatureVerified] = useState(false)
|
||||
const [scriptBlobUrl, setScriptBlobUrl] = useState<string | null>(null)
|
||||
const [isCustomElementDefined, setIsCustomElementDefined] = useState(false)
|
||||
|
||||
const { showHubButton } = useContext(VersionContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (showHubButton) {
|
||||
if (customElements.get('hub-button-app')) {
|
||||
setSignatureVerified(true)
|
||||
setIsCustomElementDefined(true)
|
||||
return
|
||||
}
|
||||
|
||||
const verifyAndLoadScript = async () => {
|
||||
try {
|
||||
const { verified, scriptContent: content } = await verifySignature(
|
||||
HUB_BUTTON_URL,
|
||||
`${HUB_BUTTON_URL}.sig`,
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
if (!verified || !content) {
|
||||
setSignatureVerified(false)
|
||||
} else {
|
||||
const blob = new Blob([content], { type: 'application/javascript' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
setScriptBlobUrl(blobUrl)
|
||||
setSignatureVerified(true)
|
||||
}
|
||||
} catch {
|
||||
setSignatureVerified(false)
|
||||
}
|
||||
}
|
||||
|
||||
verifyAndLoadScript()
|
||||
|
||||
return () => {
|
||||
setScriptBlobUrl((currentUrl) => {
|
||||
if (currentUrl) {
|
||||
URL.revokeObjectURL(currentUrl)
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [showHubButton])
|
||||
|
||||
return { signatureVerified, scriptBlobUrl, isCustomElementDefined }
|
||||
}
|
||||
|
||||
export default useHubUpgradeButton
|
||||
@@ -1,20 +1,9 @@
|
||||
import { AriaTd, Flex, Text } from '@traefiklabs/faency'
|
||||
import { Flex, Text } from '@traefiklabs/faency'
|
||||
import { FiAlertTriangle } from 'react-icons/fi'
|
||||
|
||||
type EmptyPlaceholderProps = {
|
||||
message?: string
|
||||
}
|
||||
export const EmptyPlaceholder = ({ message = 'No data available' }: EmptyPlaceholderProps) => (
|
||||
export const EmptyPlaceholder = ({ message = 'No data available' }: { message?: string }) => (
|
||||
<Flex align="center" justify="center" css={{ py: '$5', color: '$primary' }}>
|
||||
<FiAlertTriangle size={16} />
|
||||
<Text css={{ pl: '$2' }}>{message}</Text>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
export const EmptyPlaceholderTd = (props: EmptyPlaceholderProps) => {
|
||||
return (
|
||||
<AriaTd css={{ pointerEvents: 'none' }} fullColSpan>
|
||||
<EmptyPlaceholder {...props} />
|
||||
</AriaTd>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import { waitFor } from '@testing-library/react'
|
||||
|
||||
import { SideNav, TopNav } from './Navigation'
|
||||
|
||||
import useHubUpgradeButton from 'hooks/use-hub-upgrade-button'
|
||||
import { renderWithProviders } from 'utils/test'
|
||||
|
||||
vi.mock('hooks/use-hub-upgrade-button')
|
||||
|
||||
const mockUseHubUpgradeButton = vi.mocked(useHubUpgradeButton)
|
||||
|
||||
describe('Navigation', () => {
|
||||
beforeEach(() => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: false,
|
||||
scriptBlobUrl: null,
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render the side navigation bar', async () => {
|
||||
const { container } = renderWithProviders(<SideNav isExpanded={false} onSidePanelToggle={() => {}} />)
|
||||
|
||||
@@ -37,60 +18,4 @@ describe('Navigation', () => {
|
||||
expect(container.innerHTML).toContain('theme-switcher')
|
||||
expect(container.innerHTML).toContain('help-menu')
|
||||
})
|
||||
|
||||
describe('hub-button-app rendering', () => {
|
||||
it('should NOT render hub-button-app when signatureVerified is false', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: false,
|
||||
scriptBlobUrl: null,
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav />)
|
||||
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).toBeNull()
|
||||
})
|
||||
|
||||
it('should NOT render hub-button-app when scriptBlobUrl is null', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: true,
|
||||
scriptBlobUrl: null,
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav />)
|
||||
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).toBeNull()
|
||||
})
|
||||
|
||||
it('should render hub-button-app when signatureVerified is true and scriptBlobUrl exists', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: true,
|
||||
scriptBlobUrl: 'blob:http://localhost:3000/mock-blob-url',
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav />)
|
||||
|
||||
await waitFor(() => {
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT render hub-button-app when noHubButton prop is true', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: true,
|
||||
scriptBlobUrl: 'blob:http://localhost:3000/mock-blob-url',
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav noHubButton={true} />)
|
||||
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
VisuallyHidden,
|
||||
} from '@traefiklabs/faency'
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { BsChevronDoubleRight, BsChevronDoubleLeft } from 'react-icons/bs'
|
||||
import { FiBookOpen, FiGithub, FiHelpCircle } from 'react-icons/fi'
|
||||
import { matchPath, useHref } from 'react-router'
|
||||
@@ -37,7 +36,6 @@ import { PluginsIcon } from 'components/icons/PluginsIcon'
|
||||
import ThemeSwitcher from 'components/ThemeSwitcher'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import { VersionContext } from 'contexts/version'
|
||||
import useHubUpgradeButton from 'hooks/use-hub-upgrade-button'
|
||||
import useTotals from 'hooks/use-overview-totals'
|
||||
import { useIsDarkMode } from 'hooks/use-theme'
|
||||
import ApimDemoNavMenu from 'pages/hub-demo/HubDemoNav'
|
||||
@@ -295,7 +293,8 @@ export const SideNav = ({
|
||||
}
|
||||
|
||||
export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?: boolean }) => {
|
||||
const { version } = useContext(VersionContext)
|
||||
const [hasHubButtonComponent, setHasHubButtonComponent] = useState(false)
|
||||
const { showHubButton, version } = useContext(VersionContext)
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
const parsedVersion = useMemo(() => {
|
||||
@@ -309,73 +308,91 @@ export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?:
|
||||
return matches ? 'v' + matches[1] : 'master'
|
||||
}, [version])
|
||||
|
||||
const { signatureVerified, scriptBlobUrl, isCustomElementDefined } = useHubUpgradeButton()
|
||||
useEffect(() => {
|
||||
if (!showHubButton) {
|
||||
setHasHubButtonComponent(false)
|
||||
return
|
||||
}
|
||||
|
||||
const displayUpgradeToHubButton = useMemo(
|
||||
() => !noHubButton && signatureVerified && (!!scriptBlobUrl || isCustomElementDefined),
|
||||
[isCustomElementDefined, noHubButton, scriptBlobUrl, signatureVerified],
|
||||
)
|
||||
if (customElements.get('hub-button-app')) {
|
||||
setHasHubButtonComponent(true)
|
||||
return
|
||||
}
|
||||
|
||||
const scripts: HTMLScriptElement[] = []
|
||||
const createScript = (scriptSrc: string): HTMLScriptElement => {
|
||||
const script = document.createElement('script')
|
||||
script.src = scriptSrc
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
setHasHubButtonComponent(customElements.get('hub-button-app') !== undefined)
|
||||
}
|
||||
scripts.push(script)
|
||||
return script
|
||||
}
|
||||
|
||||
// Source: https://github.com/traefik/traefiklabs-hub-button-app
|
||||
document.head.appendChild(createScript('traefiklabs-hub-button-app/main-v1.js'))
|
||||
|
||||
return () => {
|
||||
// Remove the scripts on unmount.
|
||||
scripts.forEach((script) => {
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [showHubButton])
|
||||
|
||||
return (
|
||||
<>
|
||||
{displayUpgradeToHubButton && (
|
||||
<Helmet>
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="script-src 'self' blob: 'unsafe-inline'; object-src 'none'; base-uri 'self';"
|
||||
<Flex as="nav" role="navigation" justify="end" align="center" css={{ gap: '$2', mb: '$6', ...css }}>
|
||||
{!noHubButton && hasHubButtonComponent && (
|
||||
<Box css={{ fontFamily: '$rubik', fontWeight: '500 !important' }}>
|
||||
<hub-button-app
|
||||
key={`dark-mode-${isDarkMode}`}
|
||||
style={{ backgroundColor: isDarkMode ? DARK_PRIMARY_COLOR : LIGHT_PRIMARY_COLOR, fontWeight: 'inherit' }}
|
||||
/>
|
||||
<script src={scriptBlobUrl as string} type="module"></script>
|
||||
</Helmet>
|
||||
</Box>
|
||||
)}
|
||||
<Flex as="nav" role="navigation" justify="end" align="center" css={{ gap: '$2', mb: '$6', ...css }}>
|
||||
{displayUpgradeToHubButton && (
|
||||
<Box css={{ fontFamily: '$rubik', fontWeight: '500 !important' }}>
|
||||
<hub-button-app
|
||||
key={`dark-mode-${isDarkMode}`}
|
||||
style={{ backgroundColor: isDarkMode ? DARK_PRIMARY_COLOR : LIGHT_PRIMARY_COLOR, fontWeight: 'inherit' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<ThemeSwitcher />
|
||||
<ThemeSwitcher />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button ghost variant="secondary" css={{ px: '$2', boxShadow: 'none' }} data-testid="help-menu">
|
||||
<FiHelpCircle size={20} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" css={{ zIndex: 9999 }}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href={`https://doc.traefik.io/traefik/${parsedVersion}`}
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiBookOpen size={20} />
|
||||
<Text>Documentation</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href="https://github.com/traefik/traefik/"
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiGithub size={20} />
|
||||
<Text>Github Repository</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
</Flex>
|
||||
</>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button ghost variant="secondary" css={{ px: '$2', boxShadow: 'none' }} data-testid="help-menu">
|
||||
<FiHelpCircle size={20} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" css={{ zIndex: 9999 }}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href={`https://doc.traefik.io/traefik/${parsedVersion}`}
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiBookOpen size={20} />
|
||||
<Text>Documentation</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href="https://github.com/traefik/traefik/"
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiGithub size={20} />
|
||||
<Text>Github Repository</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
import { parseMiddlewareType } from 'libs/parsers'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
@@ -79,7 +79,9 @@ export const HttpMiddlewaresRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -16,7 +16,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
|
||||
export const makeRowRender = (protocol = 'http'): RenderRowType => {
|
||||
const HttpRoutersRenderRow = (row) => (
|
||||
@@ -100,7 +100,9 @@ export const HttpRoutersRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
const HttpServicesRenderRow = (row) => (
|
||||
@@ -78,7 +78,9 @@ export const HttpServicesRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { waitFor } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
import HubDashboard, { resetCache } from './HubDashboard'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
import { renderWithProviders } from 'utils/test'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('utils/workers/scriptVerification', () => ({
|
||||
vi.mock('./workers/scriptVerification', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
@@ -35,6 +34,7 @@ describe('HubDashboard demo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock URL.createObjectURL
|
||||
mockCreateObjectURL = vi.fn(() => 'blob:mock-url')
|
||||
globalThis.URL.createObjectURL = mockCreateObjectURL
|
||||
})
|
||||
@@ -45,6 +45,7 @@ describe('HubDashboard demo', () => {
|
||||
|
||||
describe('without cache', () => {
|
||||
beforeEach(() => {
|
||||
// Reset cache before each test suites
|
||||
resetCache()
|
||||
})
|
||||
|
||||
@@ -129,7 +130,6 @@ describe('HubDashboard demo', () => {
|
||||
expect(mockVerifyScriptSignature).toHaveBeenCalledWith(
|
||||
'https://assets.traefik.io/hub-ui-demo.js',
|
||||
'https://assets.traefik.io/hub-ui-demo.js.sig',
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,9 +3,7 @@ import { useMemo, useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import verifySignature from '../../utils/workers/scriptVerification'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
import { SpinnerLoader } from 'components/SpinnerLoader'
|
||||
import { useIsDarkMode } from 'hooks/use-theme'
|
||||
@@ -44,7 +42,7 @@ const HubDashboard = ({ path }: { path: string }) => {
|
||||
setVerificationInProgress(true)
|
||||
|
||||
try {
|
||||
const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`, PUBLIC_KEY)
|
||||
const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`)
|
||||
|
||||
if (!verified || !content) {
|
||||
setScriptError(true)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg='
|
||||
@@ -3,10 +3,9 @@ import { ReactNode } from 'react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { useHubDemo } from './use-hub-demo'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('utils/workers/scriptVerification', () => ({
|
||||
vi.mock('./workers/scriptVerification', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
|
||||
import HubDashboard from 'pages/hub-demo/HubDashboard'
|
||||
import { ApiIcon, DashboardIcon, GatewayIcon, PortalIcon } from 'pages/hub-demo/icons'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
import verifySignature from 'pages/hub-demo/workers/scriptVerification'
|
||||
|
||||
const ROUTES_MANIFEST_URL = 'https://traefik.github.io/hub-ui-demo-app/config/routes.json'
|
||||
|
||||
@@ -22,11 +20,7 @@ const useHubDemoRoutesManifest = (): HubDemo.Manifest | null => {
|
||||
useEffect(() => {
|
||||
const fetchManifest = async () => {
|
||||
try {
|
||||
const { verified, scriptContent } = await verifySignature(
|
||||
ROUTES_MANIFEST_URL,
|
||||
`${ROUTES_MANIFEST_URL}.sig`,
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
const { verified, scriptContent } = await verifySignature(ROUTES_MANIFEST_URL, `${ROUTES_MANIFEST_URL}.sig`)
|
||||
|
||||
if (!verified || !scriptContent) {
|
||||
setManifest(null)
|
||||
|
||||
@@ -2,8 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import verifySignature from './scriptVerification'
|
||||
|
||||
const SCRIPT_PATH = 'https://example.com/script.js'
|
||||
const MOCK_PUBLIC_KEY = 'MCowBQYDK2VwAyEAWH71OHphISjNK3mizCR/BawiDxc6IXT1vFHpBcxSIA0='
|
||||
class MockWorker {
|
||||
onmessage: ((event: MessageEvent) => void) | null = null
|
||||
onerror: ((error: ErrorEvent) => void) | null = null
|
||||
@@ -48,14 +46,17 @@ describe('verifySignature', () => {
|
||||
})
|
||||
|
||||
it('should return true when verification succeeds', async () => {
|
||||
const promise = verifySignature(SCRIPT_PATH, `${SCRIPT_PATH}.sig`, MOCK_PUBLIC_KEY)
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scriptUrl: SCRIPT_PATH,
|
||||
signatureUrl: `${SCRIPT_PATH}.sig`,
|
||||
scriptUrl: scriptPath,
|
||||
signatureUrl: signaturePath,
|
||||
requestId: expect.any(String),
|
||||
}),
|
||||
)
|
||||
@@ -75,9 +76,12 @@ describe('verifySignature', () => {
|
||||
})
|
||||
|
||||
it('should return false when verification fails', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const promise = verifySignature(SCRIPT_PATH, `${SCRIPT_PATH}.sig`, MOCK_PUBLIC_KEY)
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
@@ -97,12 +101,16 @@ describe('verifySignature', () => {
|
||||
})
|
||||
|
||||
it('should return false when worker throws an error', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const promise = verifySignature(SCRIPT_PATH, `${SCRIPT_PATH}.sig`, MOCK_PUBLIC_KEY)
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// Simulate worker onerror event
|
||||
const error = new Error('Worker crashed')
|
||||
mockWorkerInstance.simulateError(error)
|
||||
|
||||
@@ -3,10 +3,12 @@ export interface VerificationResult {
|
||||
scriptContent?: ArrayBuffer
|
||||
}
|
||||
|
||||
const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg='
|
||||
|
||||
async function verifySignature(
|
||||
contentPath: string,
|
||||
signaturePath: string,
|
||||
publicKey: string,
|
||||
publicKey: string = PUBLIC_KEY,
|
||||
): Promise<VerificationResult> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = Math.random().toString(36).substring(2)
|
||||
@@ -14,7 +14,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
import { parseMiddlewareType } from 'libs/parsers'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
@@ -79,7 +79,9 @@ export const TcpMiddlewaresRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -16,7 +16,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
const TcpRoutersRenderRow = (row) => (
|
||||
@@ -96,7 +96,9 @@ export const TcpRoutersRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
const TcpServicesRenderRow = (row) => (
|
||||
@@ -78,7 +78,9 @@ export const TcpServicesRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
const UdpRoutersRenderRow = (row) => (
|
||||
@@ -81,7 +81,9 @@ export const UdpRoutersRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,7 @@ import SortableTh from 'components/tables/SortableTh'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination'
|
||||
import { EmptyPlaceholderTd } from 'layout/EmptyPlaceholder'
|
||||
import { EmptyPlaceholder } from 'layout/EmptyPlaceholder'
|
||||
|
||||
export const makeRowRender = (): RenderRowType => {
|
||||
const UdpServicesRenderRow = (row) => (
|
||||
@@ -78,7 +78,9 @@ export const UdpServicesRender = ({
|
||||
{(isEmpty || !!error) && (
|
||||
<AriaTfoot>
|
||||
<AriaTr>
|
||||
<EmptyPlaceholderTd message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
<AriaTd fullColSpan>
|
||||
<EmptyPlaceholder message={error ? 'Failed to fetch data' : 'No data available'} />
|
||||
</AriaTd>
|
||||
</AriaTr>
|
||||
</AriaTfoot>
|
||||
)}
|
||||
|
||||
1445
webui/yarn.lock
1445
webui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user