mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-30 07:29:04 +01:00
✨ finish redirect traefik config
This commit is contained in:
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user