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",
|
||||
"redirectsSearch": "Search redirects...",
|
||||
"redirectAdd": "Add Redirect",
|
||||
"redirectSourceDomain": "Source Domain",
|
||||
"redirectSource": "Source",
|
||||
"redirectDestination": "Destination",
|
||||
"redirectAttachedTo": "Attached To",
|
||||
"redirectType": "Type",
|
||||
"redirectTypePermanent": "Permanent (308)",
|
||||
@@ -4385,6 +4386,8 @@
|
||||
"redirectDestinationDomain": "Destination Domain",
|
||||
"redirectDestinationDomainDescription": "The domain requests are sent to, such as example.com",
|
||||
"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",
|
||||
"redirectMatchPathDescription": "Which incoming paths this redirect applies to",
|
||||
"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 { useTranslations } from "next-intl";
|
||||
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 { z } from "zod";
|
||||
import { ResourceSelector, type SelectedResource } from "./resource-selector";
|
||||
@@ -95,6 +95,16 @@ export default function RedirectForm({
|
||||
const [selectedResource, setSelectedResource] =
|
||||
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(
|
||||
() =>
|
||||
z
|
||||
@@ -178,6 +188,29 @@ export default function RedirectForm({
|
||||
});
|
||||
|
||||
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 rewritePath = form.watch("rewritePath");
|
||||
const rewritePathType = form.watch("rewritePathType");
|
||||
@@ -430,6 +463,10 @@ export default function RedirectForm({
|
||||
res?.subdomain ||
|
||||
null
|
||||
);
|
||||
setDomainFullDomain(
|
||||
res?.fullDomain ??
|
||||
null
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormMessage />
|
||||
@@ -547,6 +584,22 @@ export default function RedirectForm({
|
||||
<Form {...form}>
|
||||
<form action={formAction}>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<SwitchInput
|
||||
id="redirect-same-domain"
|
||||
label={t(
|
||||
"redirectSameDomainAsSource"
|
||||
)}
|
||||
description={t(
|
||||
"redirectSameDomainAsSourceDescription"
|
||||
)}
|
||||
checked={sameDomainAsSource}
|
||||
onCheckedChange={
|
||||
setSameDomainAsSource
|
||||
}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -562,6 +615,9 @@ export default function RedirectForm({
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="example.com"
|
||||
readOnly={
|
||||
sameDomainAsSource
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -110,24 +110,6 @@ export default function RedirectsTable({
|
||||
filter({ searchParams });
|
||||
}, 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) {
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
@@ -264,10 +246,10 @@ export default function RedirectsTable({
|
||||
},
|
||||
|
||||
{
|
||||
id: "sourceDomain",
|
||||
friendlyName: t("redirectSourceDomain"),
|
||||
id: "source",
|
||||
friendlyName: t("redirectSource"),
|
||||
header: () => (
|
||||
<span className="p-3">{t("redirectSourceDomain")}</span>
|
||||
<span className="p-3">{t("redirectSource")}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const redirect = row.original;
|
||||
@@ -280,71 +262,34 @@ export default function RedirectsTable({
|
||||
: null;
|
||||
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 (
|
||||
<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}
|
||||
</code>
|
||||
</div>
|
||||
</span>
|
||||
</code>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "destination",
|
||||
accessorKey: "destinationDomain",
|
||||
friendlyName: t("redirectDestinationDomain"),
|
||||
friendlyName: t("redirectDestination"),
|
||||
header: () => (
|
||||
<span className="p-3">
|
||||
{t("redirectDestinationDomain")}
|
||||
</span>
|
||||
<span className="p-3">{t("redirectDestination")}</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 }) => {
|
||||
const redirect = row.original;
|
||||
const hasRewrite =
|
||||
Boolean(redirect.rewritePath) ||
|
||||
redirect.rewritePathType === "stripPrefix";
|
||||
|
||||
if (!hasRewrite) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="shrink-0">
|
||||
{rewriteTypeLabel(redirect.rewritePathType)}
|
||||
</Badge>
|
||||
<code className="text-sm truncate">
|
||||
{redirect.rewritePath ?? ""}
|
||||
</code>
|
||||
</div>
|
||||
<code className="text-sm truncate">
|
||||
{redirect.destinationDomain}
|
||||
{redirect.rewritePath && (
|
||||
<span className="text-muted-foreground">
|
||||
{redirect.rewritePath}
|
||||
</span>
|
||||
)}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -468,7 +413,8 @@ export default function RedirectsTable({
|
||||
rowCount={rowCount}
|
||||
columnVisibility={{
|
||||
attachedTo: false,
|
||||
niceId: false
|
||||
niceId: false,
|
||||
permanent: false
|
||||
}}
|
||||
enableColumnVisibility
|
||||
stickyLeftColumn="name"
|
||||
|
||||
@@ -44,6 +44,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
data-slot="input"
|
||||
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",
|
||||
"read-only:opacity-50",
|
||||
"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",
|
||||
className
|
||||
|
||||
Reference in New Issue
Block a user