mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-12 05:51:30 +02:00
💄 Some UI changes
This commit is contained in:
+4
-1
@@ -4369,7 +4369,8 @@
|
|||||||
"redirectsDescription": "Forward requests from a path on your domains or resources to another URL",
|
"redirectsDescription": "Forward requests from a path on your domains or resources to another URL",
|
||||||
"redirectsSearch": "Search redirects...",
|
"redirectsSearch": "Search redirects...",
|
||||||
"redirectAdd": "Add Redirect",
|
"redirectAdd": "Add Redirect",
|
||||||
"redirectSourceDomain": "Source Domain",
|
"redirectSource": "Source",
|
||||||
|
"redirectDestination": "Destination",
|
||||||
"redirectAttachedTo": "Attached To",
|
"redirectAttachedTo": "Attached To",
|
||||||
"redirectType": "Type",
|
"redirectType": "Type",
|
||||||
"redirectTypePermanent": "Permanent (308)",
|
"redirectTypePermanent": "Permanent (308)",
|
||||||
@@ -4385,6 +4386,8 @@
|
|||||||
"redirectDestinationDomain": "Destination Domain",
|
"redirectDestinationDomain": "Destination Domain",
|
||||||
"redirectDestinationDomainDescription": "The domain requests are sent to, such as example.com",
|
"redirectDestinationDomainDescription": "The domain requests are sent to, such as example.com",
|
||||||
"redirectDestinationDomainRequired": "Enter a destination domain",
|
"redirectDestinationDomainRequired": "Enter a destination domain",
|
||||||
|
"redirectSameDomainAsSource": "Same domain as source",
|
||||||
|
"redirectSameDomainAsSourceDescription": "Keep the destination on the source domain and only change the path",
|
||||||
"redirectDestinationDomainInvalid": "Enter a valid domain, such as example.com",
|
"redirectDestinationDomainInvalid": "Enter a valid domain, such as example.com",
|
||||||
"redirectMatchPathDescription": "Which incoming paths this redirect applies to",
|
"redirectMatchPathDescription": "Which incoming paths this redirect applies to",
|
||||||
"redirectRewritePathDescription": "Optionally change the path before redirecting. Leave unset to keep the original path.",
|
"redirectRewritePathDescription": "Optionally change the path before redirecting. Leave unset to keep the original path.",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import type {
|
|||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useActionState, useMemo, useState } from "react";
|
import { useActionState, useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ResourceSelector, type SelectedResource } from "./resource-selector";
|
import { ResourceSelector, type SelectedResource } from "./resource-selector";
|
||||||
@@ -95,6 +95,16 @@ export default function RedirectForm({
|
|||||||
const [selectedResource, setSelectedResource] =
|
const [selectedResource, setSelectedResource] =
|
||||||
useState<SelectedResource | null>(initialResource);
|
useState<SelectedResource | null>(initialResource);
|
||||||
|
|
||||||
|
// DomainPicker only hands back the composed host through its callback, so
|
||||||
|
// keep it locally; seed from the saved redirect for the edit case.
|
||||||
|
const [domainFullDomain, setDomainFullDomain] = useState<string | null>(
|
||||||
|
redirect?.baseDomain
|
||||||
|
? [redirect.subdomain, redirect.baseDomain]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(".")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const formSchema = useMemo(
|
const formSchema = useMemo(
|
||||||
() =>
|
() =>
|
||||||
z
|
z
|
||||||
@@ -178,6 +188,29 @@ export default function RedirectForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const attachTo = form.watch("attachTo");
|
const attachTo = form.watch("attachTo");
|
||||||
|
|
||||||
|
const sourceFullDomain =
|
||||||
|
attachTo === "domain"
|
||||||
|
? domainFullDomain
|
||||||
|
: (selectedResource?.fullDomain ?? null);
|
||||||
|
|
||||||
|
// Mirror is UI-only state; on edit, infer it from whether the saved
|
||||||
|
// destination already equals the source host.
|
||||||
|
const [sameDomainAsSource, setSameDomainAsSource] = useState(
|
||||||
|
Boolean(
|
||||||
|
redirect &&
|
||||||
|
sourceFullDomain &&
|
||||||
|
redirect.destinationDomain === sourceFullDomain
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sameDomainAsSource && sourceFullDomain) {
|
||||||
|
form.setValue("destinationDomain", sourceFullDomain, {
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [sameDomainAsSource, sourceFullDomain, form]);
|
||||||
const pathMatchType = form.watch("pathMatchType");
|
const pathMatchType = form.watch("pathMatchType");
|
||||||
const rewritePath = form.watch("rewritePath");
|
const rewritePath = form.watch("rewritePath");
|
||||||
const rewritePathType = form.watch("rewritePathType");
|
const rewritePathType = form.watch("rewritePathType");
|
||||||
@@ -430,6 +463,10 @@ export default function RedirectForm({
|
|||||||
res?.subdomain ||
|
res?.subdomain ||
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
setDomainFullDomain(
|
||||||
|
res?.fullDomain ??
|
||||||
|
null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -547,6 +584,22 @@ export default function RedirectForm({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form action={formAction}>
|
<form action={formAction}>
|
||||||
<SettingsFormGrid>
|
<SettingsFormGrid>
|
||||||
|
<SettingsFormCell span="full">
|
||||||
|
<SwitchInput
|
||||||
|
id="redirect-same-domain"
|
||||||
|
label={t(
|
||||||
|
"redirectSameDomainAsSource"
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"redirectSameDomainAsSourceDescription"
|
||||||
|
)}
|
||||||
|
checked={sameDomainAsSource}
|
||||||
|
onCheckedChange={
|
||||||
|
setSameDomainAsSource
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsFormCell>
|
||||||
|
|
||||||
<SettingsFormCell span="full">
|
<SettingsFormCell span="full">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -562,6 +615,9 @@ export default function RedirectForm({
|
|||||||
<Input
|
<Input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
|
readOnly={
|
||||||
|
sameDomainAsSource
|
||||||
|
}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -110,24 +110,6 @@ export default function RedirectsTable({
|
|||||||
filter({ searchParams });
|
filter({ searchParams });
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
function matchTypeLabel(type: RedirectRow["pathMatchType"]) {
|
|
||||||
return {
|
|
||||||
prefix: t("pathMatchPrefix"),
|
|
||||||
exact: t("pathMatchExact"),
|
|
||||||
regex: t("pathMatchRegex")
|
|
||||||
}[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
function rewriteTypeLabel(type: RedirectRow["rewritePathType"]) {
|
|
||||||
if (!type) return "";
|
|
||||||
return {
|
|
||||||
prefix: t("pathRewritePrefix"),
|
|
||||||
exact: t("pathRewriteExact"),
|
|
||||||
regex: t("pathRewriteRegex"),
|
|
||||||
stripPrefix: t("pathRewriteStrip")
|
|
||||||
}[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleEnabled(row: RedirectRow, enabled: boolean) {
|
async function toggleEnabled(row: RedirectRow, enabled: boolean) {
|
||||||
setRows((prev) =>
|
setRows((prev) =>
|
||||||
prev.map((r) =>
|
prev.map((r) =>
|
||||||
@@ -264,10 +246,10 @@ export default function RedirectsTable({
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "sourceDomain",
|
id: "source",
|
||||||
friendlyName: t("redirectSourceDomain"),
|
friendlyName: t("redirectSource"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">{t("redirectSourceDomain")}</span>
|
<span className="p-3">{t("redirectSource")}</span>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const redirect = row.original;
|
const redirect = row.original;
|
||||||
@@ -280,71 +262,34 @@ export default function RedirectsTable({
|
|||||||
: null;
|
: null;
|
||||||
const host = redirect.resourceFullDomain ?? domainHost;
|
const host = redirect.resourceFullDomain ?? domainHost;
|
||||||
|
|
||||||
return host ? (
|
|
||||||
<code className="text-sm">{host}</code>
|
|
||||||
) : (
|
|
||||||
<span>-</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "matchPath",
|
|
||||||
accessorKey: "matchPath",
|
|
||||||
friendlyName: t("matchPath"),
|
|
||||||
header: () => <span className="p-3">{t("matchPath")}</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const redirect = row.original;
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="secondary" className="shrink-0">
|
|
||||||
{matchTypeLabel(redirect.pathMatchType)}
|
|
||||||
</Badge>
|
|
||||||
<code className="text-sm truncate">
|
<code className="text-sm truncate">
|
||||||
|
{host ?? ""}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
{redirect.matchPath}
|
{redirect.matchPath}
|
||||||
|
</span>
|
||||||
</code>
|
</code>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
id: "destination",
|
||||||
accessorKey: "destinationDomain",
|
accessorKey: "destinationDomain",
|
||||||
friendlyName: t("redirectDestinationDomain"),
|
friendlyName: t("redirectDestination"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">
|
<span className="p-3">{t("redirectDestination")}</span>
|
||||||
{t("redirectDestinationDomain")}
|
|
||||||
</span>
|
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
|
||||||
<code className="text-sm">
|
|
||||||
{row.original.destinationDomain}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "rewritePath",
|
|
||||||
accessorKey: "rewritePath",
|
|
||||||
friendlyName: t("rewritePath"),
|
|
||||||
header: () => <span className="p-3">{t("rewritePath")}</span>,
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const redirect = row.original;
|
const redirect = row.original;
|
||||||
const hasRewrite =
|
|
||||||
Boolean(redirect.rewritePath) ||
|
|
||||||
redirect.rewritePathType === "stripPrefix";
|
|
||||||
|
|
||||||
if (!hasRewrite) {
|
|
||||||
return <span>-</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="secondary" className="shrink-0">
|
|
||||||
{rewriteTypeLabel(redirect.rewritePathType)}
|
|
||||||
</Badge>
|
|
||||||
<code className="text-sm truncate">
|
<code className="text-sm truncate">
|
||||||
{redirect.rewritePath ?? ""}
|
{redirect.destinationDomain}
|
||||||
|
{redirect.rewritePath && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{redirect.rewritePath}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -468,7 +413,8 @@ export default function RedirectsTable({
|
|||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
attachedTo: false,
|
attachedTo: false,
|
||||||
niceId: false
|
niceId: false,
|
||||||
|
permanent: false
|
||||||
}}
|
}}
|
||||||
enableColumnVisibility
|
enableColumnVisibility
|
||||||
stickyLeftColumn="name"
|
stickyLeftColumn="name"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"read-only:opacity-50",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0",
|
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0",
|
||||||
className
|
className
|
||||||
|
|||||||
Reference in New Issue
Block a user