✨ finish redirect traefik config

This commit is contained in:
Fred KISSIE
2026-09-17 21:32:55 +02:00
parent c51bcbb578
commit ec36317057
13 changed files with 243 additions and 80 deletions
+39 -18
View File
@@ -41,6 +41,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { isValidDomain } from "@server/lib/validators";
import { isValidRegex } from "@server/routers/redirect/validation";
import { cn } from "@app/lib/cn";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -65,7 +66,6 @@ import { Plus } from "lucide-react";
import DomainPicker from "@app/components/DomainPicker";
import Link from "next/link";
const DEFAULT_MATCH_PATH = ".*";
const DEFAULT_PATH_MATCH_TYPE = "regex" as const;
export type ExistingRedirect = GetRedirectResponse["redirect"];
@@ -127,7 +127,7 @@ export default function RedirectForm({
message: t("redirectDestinationDomainInvalid")
}),
pathMatchType: z.enum(["exact", "prefix", "regex"]),
matchPath: z.string().trim().min(1),
matchPath: z.string().trim().nullable(),
rewritePath: z.string().nullable(),
rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"])
@@ -150,6 +150,17 @@ export default function RedirectForm({
path: ["resourceId"]
});
}
if (
data.pathMatchType === "regex" &&
data.matchPath &&
!isValidRegex(data.matchPath)
) {
ctx.addIssue({
code: "custom",
message: t("redirectMatchPathInvalidRegex"),
path: ["matchPath"]
});
}
// stripPrefix drops the matched prefix outright, so it is
// the one rewrite type that needs no replacement value.
if (
@@ -179,7 +190,7 @@ export default function RedirectForm({
resourceId: redirect?.resourceId ?? null,
destinationDomain: redirect?.destinationDomain ?? "",
pathMatchType: redirect?.pathMatchType ?? DEFAULT_PATH_MATCH_TYPE,
matchPath: redirect?.matchPath ?? DEFAULT_MATCH_PATH,
matchPath: redirect?.matchPath ?? null,
rewritePath: redirect?.rewritePath ?? null,
rewritePathType: redirect?.rewritePathType ?? null,
permanent: redirect?.permanent ?? false,
@@ -234,7 +245,7 @@ export default function RedirectForm({
values.attachTo === "resource" ? values.resourceId : null,
destinationDomain: values.destinationDomain.trim(),
pathMatchType: values.pathMatchType,
matchPath: values.matchPath.trim(),
matchPath: values.matchPath?.trim() || null,
rewritePath: values.rewritePath?.trim() || null,
rewritePathType: values.rewritePathType,
permanent: values.permanent,
@@ -650,15 +661,16 @@ export default function RedirectForm({
onChange={(
config
) => {
// matchPath and
// pathMatchType are
// NOT NULL, so a
// clear falls back
// to the defaults
// rather than null.
// No match path
// means the
// redirect applies
// to every path;
// pathMatchType is
// NOT NULL so it
// keeps a default.
field.onChange(
config.path ||
DEFAULT_MATCH_PATH
null
);
form.setValue(
"pathMatchType",
@@ -675,13 +687,22 @@ export default function RedirectForm({
variant="outline"
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer"
>
<PathMatchDisplay
value={{
path: field.value,
pathMatchType:
pathMatchType
}}
/>
{field.value ? (
<PathMatchDisplay
value={{
path: field.value,
pathMatchType:
pathMatchType
}}
/>
) : (
<>
<Plus className="h-4 w-4" />
{t(
"matchPath"
)}
</>
)}
</Button>
}
/>
+57 -27
View File
@@ -31,7 +31,15 @@ import {
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react";
import {
useEffect,
useMemo,
useOptimistic,
useRef,
useState,
useTransition,
type ComponentRef
} from "react";
import { useDebouncedCallback } from "use-debounce";
export type RedirectRow = {
@@ -41,7 +49,7 @@ export type RedirectRow = {
subdomain: string | null;
destinationDomain: string;
pathMatchType: "exact" | "prefix" | "regex";
matchPath: string;
matchPath: string | null;
rewritePath: string | null;
rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null;
permanent: boolean;
@@ -121,15 +129,9 @@ export default function RedirectsTable({
return path.endsWith("/") ? `${path}*` : `${path}/*`;
}
async function toggleEnabled(row: RedirectRow, enabled: boolean) {
setRows((prev) =>
prev.map((r) =>
r.redirectId === row.redirectId ? { ...r, enabled } : r
)
);
async function toggleEnabled(enabled: boolean, redirectId: number) {
try {
await api.post(`/org/${orgId}/redirects/${row.redirectId}`, {
await api.post(`/org/${orgId}/redirects/${redirectId}`, {
enabled
});
toast({
@@ -138,13 +140,6 @@ export default function RedirectsTable({
});
router.refresh();
} catch (e) {
setRows((prev) =>
prev.map((r) =>
r.redirectId === row.redirectId
? { ...r, enabled: row.enabled }
: r
)
);
toast({
variant: "destructive",
title: t("redirectErrorUpdate"),
@@ -288,11 +283,15 @@ export default function RedirectsTable({
) : null}
<code className="text-sm truncate">
{host ?? ""}
<span className="text-muted-foreground">
{redirect.pathMatchType === "prefix"
? withPrefixGlob(redirect.matchPath)
: redirect.matchPath}
</span>
{redirect.matchPath && (
<span className="text-muted-foreground">
{redirect.pathMatchType === "prefix"
? withPrefixGlob(
redirect.matchPath
)
: redirect.matchPath}
</span>
)}
</code>
</div>
);
@@ -338,11 +337,9 @@ export default function RedirectsTable({
friendlyName: t("enabled"),
header: () => <span className="p-3">{t("enabled")}</span>,
cell: ({ row }) => (
<Switch
checked={row.original.enabled}
onCheckedChange={(checked) =>
toggleEnabled(row.original, checked)
}
<RedirectEnabledForm
redirect={row.original}
onToggleEnabled={toggleEnabled}
/>
)
},
@@ -451,3 +448,36 @@ export default function RedirectsTable({
</>
);
}
type RedirectEnabledFormProps = {
redirect: RedirectRow;
onToggleEnabled: (val: boolean, redirectId: number) => Promise<void>;
};
function RedirectEnabledForm({
redirect,
onToggleEnabled
}: RedirectEnabledFormProps) {
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(
redirect.enabled
);
const formRef = useRef<ComponentRef<"form">>(null);
async function submitAction(formData: FormData) {
const newEnabled = !(formData.get("enabled") === "on");
setOptimisticEnabled(newEnabled);
await onToggleEnabled(newEnabled, redirect.redirectId);
}
return (
<form action={submitAction} ref={formRef}>
<Switch
checked={optimisticEnabled}
disabled={optimisticEnabled !== redirect.enabled}
name="enabled"
onCheckedChange={() => formRef.current?.requestSubmit()}
/>
</form>
);
}