fix(ui): improve listbox hover state and Traefik redirects

Add an opaque hover background for listbox triggers and preserve single-escaped
Traefik redirect capture groups when generating application labels.
This commit is contained in:
Andras Bacsai
2026-08-17 21:26:57 +02:00
parent 43cd1f4c0d
commit 9d64b30612
4 changed files with 31 additions and 3 deletions
+8 -3
View File
@@ -563,7 +563,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
return $labels->sort();
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -646,14 +646,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$to_www_name = "{$loop}-{$uuid}-to-www";
$to_non_www_name = "{$loop}-{$uuid}-to-non-www";
$redirect_capture_prefix = $escape_redirect_replacement_for_compose ? '$$' : '$';
$redirect_to_non_www = [
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.regex=^(http|https)://www\.(.+)",
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement=\$\${1}://\$\${2}",
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://{$redirect_capture_prefix}{2}",
"traefik.http.middlewares.{$to_non_www_name}.redirectregex.permanent=false",
];
$redirect_to_www = [
"traefik.http.middlewares.{$to_www_name}.redirectregex.regex=^(http|https)://(?:www\.)?(.+)",
"traefik.http.middlewares.{$to_www_name}.redirectregex.replacement=\$\${1}://www.\$\${2}",
"traefik.http.middlewares.{$to_www_name}.redirectregex.replacement={$redirect_capture_prefix}{1}://www.{$redirect_capture_prefix}{2}",
"traefik.http.middlewares.{$to_www_name}.redirectregex.permanent=false",
];
if ($schema === 'https') {
@@ -876,6 +877,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
));
break;
}
@@ -892,6 +894,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -932,6 +935,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
));
break;
case ProxyTypes::CADDY->value:
@@ -962,6 +966,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
+4
View File
@@ -1903,6 +1903,10 @@ html[data-theme="custom"] textarea:disabled {
color: var(--color-fg);
}
.listbox-trigger:hover {
background: var(--coollabs-fill);
}
.listbox-trigger:disabled {
cursor: not-allowed;
opacity: 0.5;
@@ -22,6 +22,13 @@ test('listbox trigger height matches shared inputs', function () {
->toMatch('/\.application-settings-workspace \.listbox-trigger[^}]*height: 2rem;/s');
});
test('listbox trigger uses an opaque background on hover', function () {
$css = file_get_contents(resource_path('css/app.css'));
expect($css)
->toMatch('/\.listbox-trigger:hover \{[^}]*background: var\(--coollabs-fill\);/s');
});
test('listbox component uses shared trigger label truncation', function () {
$html = Blade::render(<<<'BLADE'
<x-forms.listbox id="longOption" label="Example"
@@ -95,3 +95,15 @@ test('fqdnLabelsForTraefik hyphenated services also receive a hash suffix', func
expect($routerLine)->toContain("https-0-{$uuid}-another-service-{$hash}.rule=");
});
test('application labels keep redirect capture groups single escaped before compose generation', function () {
$labels = fqdnLabelsForTraefik(
uuid: 'application-uuid',
domains: collect(['https://example.com']),
redirect_direction: 'www',
escape_redirect_replacement_for_compose: false,
);
expect($labels)
->toContain('traefik.http.middlewares.0-application-uuid-to-www.redirectregex.replacement=${1}://www.${2}');
});