Fix priority display in dashboard and ACME bypass redirect

This commit is contained in:
Michael
2026-03-06 10:04:05 +01:00
committed by GitHub
parent d5745c3807
commit fc32e6dc0b
35 changed files with 311 additions and 18 deletions
@@ -87,12 +87,13 @@ additionalArguments:
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------|:---------|
| <a id="opt-address" href="#opt-address" title="#opt-address">`address`</a> | Define the port, and optionally the hostname, on which to listen for incoming connections and packets.<br /> It also defines the protocol to use (TCP or UDP).<br /> If no protocol is specified, the default is TCP. The format is:`[host]:port[/tcp\|/udp] | - | Yes |
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| <a id="opt-allowACMEByPass" href="#opt-allowACMEByPass" title="#opt-allowACMEByPass">`allowACMEByPass`</a> | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No |
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| <a id="opt-http-redirections-entryPoint-to" href="#opt-http-redirections-entryPoint-to" title="#opt-http-redirections-entryPoint-to">`http.redirections.`<br />`entryPoint.to`</a> | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
| <a id="opt-http-redirections-entryPoint-scheme" href="#opt-http-redirections-entryPoint-scheme" title="#opt-http-redirections-entryPoint-scheme">`http.redirections.`<br />`entryPoint.scheme`</a> | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
| <a id="opt-http-redirections-entryPoint-permanent" href="#opt-http-redirections-entryPoint-permanent" title="#opt-http-redirections-entryPoint-permanent">`http.redirections.`<br />`entryPoint.permanent`</a> | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
| <a id="opt-http-redirections-entryPoint-priority" href="#opt-http-redirections-entryPoint-priority" title="#opt-http-redirections-entryPoint-priority">`http.redirections.`<br />`entryPoint.priority`</a> | Default priority applied to the routers attached to the `entryPoint`. | MaxInt32-1 (2147483646) | No |
| <a id="opt-http-redirections-entryPoint-priority" href="#opt-http-redirections-entryPoint-priority" title="#opt-http-redirections-entryPoint-priority">`http.redirections.`<br />`entryPoint.priority`</a> | Default priority applied to the routers attached to the `entryPoint`. | MaxInt-1 (`2147483646` on 32-bit, `9223372036854775806` on 64-bit) | No |
| <a id="opt-http-encodedCharacters" href="#opt-http-encodedCharacters" title="#opt-http-encodedCharacters">`http.encodedCharacters`</a> | Defines which encoded characters are allowed in the request path. More information [here](#encoded-characters). | false | No |
| <a id="opt-http-encodedCharacters-allowEncodedSlash" href="#opt-http-encodedCharacters-allowEncodedSlash" title="#opt-http-encodedCharacters-allowEncodedSlash">`http.encodedCharacters.`<br />`allowEncodedSlash`</a> | Defines whether requests with encoded slash characters in the path are allowed. | true | No |
| <a id="opt-http-encodedCharacters-allowEncodedBackSlash" href="#opt-http-encodedCharacters-allowEncodedBackSlash" title="#opt-http-encodedCharacters-allowEncodedBackSlash">`http.encodedCharacters.`<br />`allowEncodedBackSlash`</a> | Defines whether requests with encoded back slash characters in the path are allowed. | true | No |
@@ -144,6 +145,53 @@ The `asDefault` option has no effect on UDP entryPoints.
When a UDP router does not define the entryPoints option, it is attached to all
available UDP entryPoints.
### allowACMEByPass
By default, Traefik creates an internal router with the highest possible priority (`MaxInt`) to handle
ACME HTTP and TLS challenges. This ensures that certificate challenges always succeed,
but it also prevents any user-defined router from intercepting challenge requests on the same entrypoint.
When `allowACMEByPass` is set to `true` on an entrypoint:
- The internal ACME HTTP challenge router is created **without** an explicit high priority,
allowing user-defined routers to handle challenge requests instead.
- The TLS-ALPN challenge passthrough is enabled on the entrypoint,
allowing user-defined TLS routers to handle TLS challenges.
This is useful when you need custom handling of ACME challenges,
for example when using a dedicated service to solve HTTP-01 or TLS-ALPN-01 challenges.
!!! note
When no TLS challenge resolver is configured, `allowACMEByPass` is implicitly enabled
for TLS passthrough on all entrypoints.
!!! note
When `allowACMEByPass` is enabled and the entrypoint has an HTTP redirect configured
(via `http.redirections.entryPoint`), the redirect router automatically excludes
the ACME challenge path (`/.well-known/acme-challenge/`).
This allows user-defined ACME challenge routers to handle challenge requests
without being overridden by the redirect.
```yaml tab="File (YAML)"
entryPoints:
web:
address: ":80"
allowACMEByPass: true
```
```toml tab="File (TOML)"
[entryPoints.web]
address = ":80"
allowACMEByPass = true
```
```bash tab="CLI"
--entryPoints.web.address=:80
--entryPoints.web.allowACMEByPass=true
```
### http.middlewares
- You can attach a list of [middlewares](../../middlewares/http/overview.md)
@@ -229,8 +229,8 @@ Negative priority values are supported.
Traefik reserves a range of priorities for its internal routers, the maximum user-defined router priority value is:
- `(MaxInt32 - 1000)` for 32-bit platforms,
- `(MaxInt64 - 1000)` for 64-bit platforms.
- `(MaxInt32 - 1000)` = `2147482647` for 32-bit platforms,
- `(MaxInt64 - 1000)` = `9223372036854774807` for 64-bit platforms.
### Example
@@ -0,0 +1,38 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
noColor = true
[entryPoints]
[entryPoints.web]
address = ":8888"
allowACMEByPass = true
[entryPoints.web.http.redirections.entryPoint]
to = ":8443"
scheme = "https"
[entryPoints.websecure]
address = ":8443"
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.routers]
[http.routers.acme-challenge]
entryPoints = ["web"]
rule = "PathPrefix(`/.well-known/acme-challenge/`)"
service = "acme-solver"
[http.services]
[http.services.acme-solver]
[http.services.acme-solver.loadBalancer]
[[http.services.acme-solver.loadBalancer.servers]]
url = "{{ .AcmeSolverURL }}"
+37
View File
@@ -2227,3 +2227,40 @@ func waitForWritePartial(t *testing.T, conn net.Conn) {
t.Fatalf("timeout waiting for connection timeout")
}
}
func (s *SimpleSuite) TestAllowACMEByPassRedirect() {
// Start a local server that simulates an ACME challenge solver.
acmeSolver := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte("acme-challenge-token"))
}))
defer acmeSolver.Close()
file := s.adaptFile("fixtures/simple_acme_bypass_redirect.toml", struct {
AcmeSolverURL string
}{
AcmeSolverURL: acmeSolver.URL,
})
s.traefikCmd(withConfigFile(file))
// Wait for Traefik to be ready with the user-defined ACME challenge router.
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 5*time.Second, try.BodyContains("acme-challenge@file"))
require.NoError(s.T(), err)
noRedirectClient := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// ACME challenge path should NOT be redirected — it should reach the solver.
resp, err := noRedirectClient.Get("http://127.0.0.1:8888/.well-known/acme-challenge/test-token")
require.NoError(s.T(), err)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode)
// Normal path should be redirected to HTTPS.
resp, err = noRedirectClient.Get("http://127.0.0.1:8888/other-path")
require.NoError(s.T(), err)
assert.Equal(s.T(), http.StatusMovedPermanently, resp.StatusCode)
}
+7 -5
View File
@@ -17,8 +17,9 @@ import (
type routerRepresentation struct {
*runtime.RouterInfo
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
PriorityStr string `json:"priorityStr,omitempty"`
}
func newRouterRepresentation(name string, rt *runtime.RouterInfo) routerRepresentation {
@@ -27,9 +28,10 @@ func newRouterRepresentation(name string, rt *runtime.RouterInfo) routerRepresen
}
return routerRepresentation{
RouterInfo: rt,
Name: name,
Provider: getProviderName(name),
RouterInfo: rt,
Name: name,
Provider: getProviderName(name),
PriorityStr: strconv.FormatInt(int64(rt.Priority), 10),
}
}
+4 -2
View File
@@ -16,8 +16,9 @@ import (
type tcpRouterRepresentation struct {
*runtime.TCPRouterInfo
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
Name string `json:"name,omitempty"`
Provider string `json:"provider,omitempty"`
PriorityStr string `json:"priorityStr,omitempty"`
}
func newTCPRouterRepresentation(name string, rt *runtime.TCPRouterInfo) tcpRouterRepresentation {
@@ -25,6 +26,7 @@ func newTCPRouterRepresentation(name string, rt *runtime.TCPRouterInfo) tcpRoute
TCPRouterInfo: rt,
Name: name,
Provider: getProviderName(name),
PriorityStr: strconv.FormatInt(int64(rt.Priority), 10),
}
}
+1
View File
@@ -8,6 +8,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
+1
View File
@@ -8,6 +8,7 @@
],
"name": "baz@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.baz`)",
"service": "foo-service@myprovider",
"tls": {
+1
View File
@@ -8,6 +8,7 @@
],
"name": "baz@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.baz`)",
"service": "foo-service@myprovider",
"tls": {
+1
View File
@@ -8,6 +8,7 @@
],
"name": "foo / bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
+2
View File
@@ -9,6 +9,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "disabled",
@@ -26,6 +27,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`fii.bar.other`)",
"service": "fii-service@myprovider",
"status": "enabled",
+1
View File
@@ -9,6 +9,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`fii.bar.other`)",
"service": "fii-service@myprovider",
"status": "enabled",
+2
View File
@@ -5,6 +5,7 @@
],
"name": "foo@otherprovider",
"provider": "otherprovider",
"priorityStr": "0",
"rule": "Host(`fii.foo.other`)",
"service": "fii-service",
"status": "enabled",
@@ -22,6 +23,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`fii.bar.other`)",
"service": "fii-service@myprovider",
"status": "enabled",
+1
View File
@@ -9,6 +9,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
+5
View File
@@ -5,6 +5,7 @@
],
"name": "bar14@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar14`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -18,6 +19,7 @@
],
"name": "bar15@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar15`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -31,6 +33,7 @@
],
"name": "bar16@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar16`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -44,6 +47,7 @@
],
"name": "bar17@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar17`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -57,6 +61,7 @@
],
"name": "bar18@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar18`)",
"service": "foo-service@myprovider",
"status": "enabled",
+1
View File
@@ -5,6 +5,7 @@
],
"name": "baz@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`toto.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
+2
View File
@@ -9,6 +9,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -26,6 +27,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
+1
View File
@@ -4,6 +4,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
+1
View File
@@ -4,6 +4,7 @@
],
"name": "foo / bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -9,6 +9,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service",
"status": "warning",
@@ -26,6 +27,7 @@
],
"name": "foo@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "bar-service@myprovider",
"status": "disabled",
+1
View File
@@ -5,6 +5,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "warning",
+2
View File
@@ -5,6 +5,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service",
"status": "warning",
@@ -18,6 +19,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
+1
View File
@@ -5,6 +5,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
+1
View File
@@ -5,6 +5,7 @@
],
"name": "baz@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`toto.bar`)",
"service": "foo-service@myprovider",
"status": "enabled",
+3
View File
@@ -5,6 +5,7 @@
],
"name": "bar@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "warning",
@@ -18,6 +19,7 @@
],
"name": "foo@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar`)",
"service": "foo-service@myprovider",
"status": "disabled",
@@ -31,6 +33,7 @@
],
"name": "test@myprovider",
"provider": "myprovider",
"priorityStr": "0",
"rule": "Host(`foo.bar.other`)",
"service": "foo-service@myprovider",
"status": "enabled",
@@ -0,0 +1,31 @@
{
"http": {
"routers": {
"web-to-websecure": {
"entryPoints": [
"web"
],
"middlewares": [
"redirect-web-to-websecure"
],
"service": "noop@internal",
"rule": "HostRegexp(`^.+$`) \u0026\u0026 !PathPrefix(`/.well-known/acme-challenge/`)",
"ruleSyntax": "default"
}
},
"services": {
"noop": {}
},
"middlewares": {
"redirect-web-to-websecure": {
"redirectScheme": {
"scheme": "https",
"port": "443",
"permanent": true
}
}
}
},
"tcp": {},
"tls": {}
}
@@ -0,0 +1,41 @@
{
"http": {
"routers": {
"acme-http": {
"entryPoints": [
"web"
],
"service": "acme-http@internal",
"rule": "PathPrefix(`/.well-known/acme-challenge/`)",
"ruleSyntax": "default",
"priority": 9223372036854775807
},
"web-to-websecure": {
"entryPoints": [
"web"
],
"middlewares": [
"redirect-web-to-websecure"
],
"service": "noop@internal",
"rule": "HostRegexp(`^.+$`)",
"ruleSyntax": "default"
}
},
"services": {
"acme-http": {},
"noop": {}
},
"middlewares": {
"redirect-web-to-websecure": {
"redirectScheme": {
"scheme": "https",
"port": "443",
"permanent": true
}
}
}
},
"tcp": {},
"tls": {}
}
+6 -1
View File
@@ -161,11 +161,16 @@ func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration)
continue
}
rule := "HostRegexp(`^.+$`)"
if ep.AllowACMEByPass {
rule = "HostRegexp(`^.+$`) && !PathPrefix(`/.well-known/acme-challenge/`)"
}
rtName := provider.Normalize(name + "-to-" + def.EntryPoint.To)
mdName := "redirect-" + rtName
rt := &dynamic.Router{
Rule: "HostRegexp(`^.+$`)",
Rule: rule,
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
RuleSyntax: "default",
EntryPoints: []string{name},
+55
View File
@@ -12,6 +12,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/static"
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
"github.com/traefik/traefik/v3/pkg/ping"
acmeprovider "github.com/traefik/traefik/v3/pkg/provider/acme"
"github.com/traefik/traefik/v3/pkg/provider/rest"
"github.com/traefik/traefik/v3/pkg/types"
)
@@ -261,6 +262,60 @@ func Test_createConfiguration(t *testing.T) {
},
},
},
{
desc: "redirection_with_acme_bypass.json",
staticCfg: static.Configuration{
EntryPoints: map[string]*static.EntryPoint{
"web": {
Address: ":80",
AllowACMEByPass: true,
HTTP: static.HTTPConfig{
Redirections: &static.Redirections{
EntryPoint: &static.RedirectEntryPoint{
To: "websecure",
Scheme: "https",
Permanent: true,
},
},
},
},
"websecure": {
Address: ":443",
},
},
},
},
{
desc: "redirection_without_acme_bypass.json",
staticCfg: static.Configuration{
EntryPoints: map[string]*static.EntryPoint{
"web": {
Address: ":80",
HTTP: static.HTTPConfig{
Redirections: &static.Redirections{
EntryPoint: &static.RedirectEntryPoint{
To: "websecure",
Scheme: "https",
Permanent: true,
},
},
},
},
"websecure": {
Address: ":443",
},
},
CertificatesResolvers: map[string]static.CertificateResolver{
"default": {
ACME: &acmeprovider.Configuration{
HTTPChallenge: &acmeprovider.HTTPChallenge{
EntryPoint: "web",
},
},
},
},
},
},
}
for _, test := range testCases {
@@ -27,10 +27,10 @@ const RouterPanel = ({ data }: Props) => (
<ProviderName css={{ ml: '$2' }}>{data.provider}</ProviderName>
</ItemBlock>
)}
{data.priority && (
{(data.priorityStr || data.priority) && (
<ItemBlock title="Priority">
<Tooltip label={data.priority.toString()} action="copy">
<Text css={{ overflowWrap: 'break-word' }}>{data.priority.toString()}</Text>
<Tooltip label={data.priorityStr ?? data.priority?.toString() ?? ''} action="copy">
<Text css={{ overflowWrap: 'break-word' }}>{data.priorityStr ?? data.priority?.toString()}</Text>
</Tooltip>
</ItemBlock>
)}
+3
View File
@@ -36,6 +36,7 @@ type Router = {
status: 'enabled' | 'disabled' | 'warning'
rule?: string
priority?: number
priorityStr?: string
provider: string
tls?: {
options: string
@@ -141,6 +142,8 @@ export const useResourceDetail = (name: string, resource: string, protocol = 'ht
status: routeDetail.status,
provider: routeDetail.provider,
rule: routeDetail.rule,
priority: routeDetail.priority,
priorityStr: routeDetail.priorityStr,
tls: routeDetail.tls,
error: routeDetail.error,
middlewares: validMiddlewares,
+2 -1
View File
@@ -108,7 +108,8 @@
"using": [
"web-redirect"
],
"priority": 9223372036854776000,
"priority": 9223372036854775806,
"priorityStr": "9223372036854775806",
"provider": "docker"
},
{
+1 -1
View File
@@ -59,7 +59,7 @@ export const makeRowRender = (protocol = 'http'): RenderRowType => {
</Tooltip>
</AriaTd>
<AriaTd>
<TooltipText text={row.priority} isTruncated />
<TooltipText text={row.priorityStr ?? row.priority} isTruncated />
</AriaTd>
</ClickableRow>
)
+1 -1
View File
@@ -55,7 +55,7 @@ export const makeRowRender = (): RenderRowType => {
</Tooltip>
</AriaTd>
<AriaTd>
<TooltipText text={row.priority} isTruncated />
<TooltipText text={row.priorityStr ?? row.priority} isTruncated />
</AriaTd>
</ClickableRow>
)
+1 -1
View File
@@ -42,7 +42,7 @@ export const makeRowRender = (): RenderRowType => {
</Tooltip>
</AriaTd>
<AriaTd>
<TooltipText text={row.priority} isTruncated />
<TooltipText text={row.priorityStr ?? row.priority} isTruncated />
</AriaTd>
</ClickableRow>
)