♻️ handle priority field

This commit is contained in:
Fred KISSIE
2026-09-17 21:44:56 +02:00
parent ec36317057
commit 85adae063b
9 changed files with 90 additions and 5 deletions
+1
View File
@@ -4393,6 +4393,7 @@
"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.",
"redirectRewritePathRequired": "Enter a rewrite path, or choose Strip Prefix", "redirectRewritePathRequired": "Enter a rewrite path, or choose Strip Prefix",
"redirectMatchPathInvalidRegex": "Match path must be a valid regular expression", "redirectMatchPathInvalidRegex": "Match path must be a valid regular expression",
"redirectPriorityInvalid": "Enter a whole number between 1 and 1000",
"redirectCreate": "Create Redirect", "redirectCreate": "Create Redirect",
"redirectCreateDescription": "Forward requests matching a path to another URL", "redirectCreateDescription": "Forward requests matching a path to another URL",
"redirectEditDescription": "Update how this redirect forwards incoming requests", "redirectEditDescription": "Update how this redirect forwards incoming requests",
@@ -15,6 +15,7 @@ import {
redirectPathMatchTypeSchema, redirectPathMatchTypeSchema,
redirectRewritePathSchema, redirectRewritePathSchema,
isValidMatchPath, isValidMatchPath,
redirectPrioritySchema,
redirectRewritePathTypeSchema redirectRewritePathTypeSchema
} from "@server/routers/redirect/validation"; } from "@server/routers/redirect/validation";
import { getUniqueRedirectName } from "@server/db/names"; import { getUniqueRedirectName } from "@server/db/names";
@@ -39,6 +40,7 @@ const bodySchema = z
matchPath: redirectMatchPathSchema.optional().nullable(), matchPath: redirectMatchPathSchema.optional().nullable(),
rewritePath: redirectRewritePathSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(),
rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), rewritePathType: redirectRewritePathTypeSchema.optional().nullable(),
priority: redirectPrioritySchema.optional().nullable(),
permanent: z.boolean().optional(), permanent: z.boolean().optional(),
enabled: z.boolean().optional() enabled: z.boolean().optional()
}) })
@@ -123,6 +125,7 @@ export async function createRedirect(
matchPath, matchPath,
rewritePath, rewritePath,
rewritePathType, rewritePathType,
priority,
permanent, permanent,
enabled enabled
} = parsedBody.data; } = parsedBody.data;
@@ -195,6 +198,7 @@ export async function createRedirect(
matchPath: matchPath ?? null, matchPath: matchPath ?? null,
rewritePath: rewritePath ?? null, rewritePath: rewritePath ?? null,
rewritePathType: rewritePathType ?? null, rewritePathType: rewritePathType ?? null,
priority: priority ?? 100,
permanent: permanent ?? false, permanent: permanent ?? false,
enabled: enabled ?? true enabled: enabled ?? true
}) })
+2
View File
@@ -22,6 +22,7 @@ export type GetRedirectResponse = {
matchPath: string | null; matchPath: string | null;
rewritePath: string | null; rewritePath: string | null;
rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null; rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null;
priority: number | null;
permanent: boolean; permanent: boolean;
enabled: boolean; enabled: boolean;
resourceId: number | null; resourceId: number | null;
@@ -46,6 +47,7 @@ const redirectColumns = {
matchPath: redirects.matchPath, matchPath: redirects.matchPath,
rewritePath: redirects.rewritePath, rewritePath: redirects.rewritePath,
rewritePathType: redirects.rewritePathType, rewritePathType: redirects.rewritePathType,
priority: redirects.priority,
permanent: redirects.permanent, permanent: redirects.permanent,
enabled: redirects.enabled, enabled: redirects.enabled,
resourceId: redirects.resourceId, resourceId: redirects.resourceId,
+3 -1
View File
@@ -22,6 +22,7 @@ export type ListRedirectsResponse = PaginatedResponse<{
matchPath: string | null; matchPath: string | null;
rewritePath: string | null; rewritePath: string | null;
rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null; rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null;
priority: number | null;
permanent: boolean; permanent: boolean;
enabled: boolean; enabled: boolean;
resourceId: number | null; resourceId: number | null;
@@ -145,6 +146,7 @@ export async function listRedirects(
matchPath: redirects.matchPath, matchPath: redirects.matchPath,
rewritePath: redirects.rewritePath, rewritePath: redirects.rewritePath,
rewritePathType: redirects.rewritePathType, rewritePathType: redirects.rewritePathType,
priority: redirects.priority,
permanent: redirects.permanent, permanent: redirects.permanent,
enabled: redirects.enabled, enabled: redirects.enabled,
resourceId: redirects.resourceId, resourceId: redirects.resourceId,
@@ -173,7 +175,7 @@ export async function listRedirects(
baseQuery baseQuery
.limit(pageSize) .limit(pageSize)
.offset(pageSize * (page - 1)) .offset(pageSize * (page - 1))
.orderBy(desc(redirects.redirectId)) .orderBy(desc(redirects.priority), desc(redirects.redirectId))
]); ]);
return response<ListRedirectsResponse>(res, { return response<ListRedirectsResponse>(res, {
@@ -16,6 +16,7 @@ import {
redirectPathMatchTypeSchema, redirectPathMatchTypeSchema,
redirectRewritePathSchema, redirectRewritePathSchema,
redirectRewritePathTypeSchema, redirectRewritePathTypeSchema,
redirectPrioritySchema,
isValidMatchPath isValidMatchPath
} from "@server/routers/redirect/validation"; } from "@server/routers/redirect/validation";
import { createCertificate } from "../certificates"; import { createCertificate } from "../certificates";
@@ -40,6 +41,7 @@ const bodySchema = z.strictObject({
matchPath: redirectMatchPathSchema.optional().nullable(), matchPath: redirectMatchPathSchema.optional().nullable(),
rewritePath: redirectRewritePathSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(),
rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), rewritePathType: redirectRewritePathTypeSchema.optional().nullable(),
priority: redirectPrioritySchema.optional(),
permanent: z.boolean().optional(), permanent: z.boolean().optional(),
enabled: z.boolean().optional() enabled: z.boolean().optional()
}); });
@@ -255,6 +257,9 @@ export async function updateRedirect(
if (body.rewritePathType !== undefined) { if (body.rewritePathType !== undefined) {
updateData.rewritePathType = body.rewritePathType; updateData.rewritePathType = body.rewritePathType;
} }
if (body.priority !== undefined) {
updateData.priority = body.priority;
}
if (body.permanent !== undefined) { if (body.permanent !== undefined) {
updateData.permanent = body.permanent; updateData.permanent = body.permanent;
} }
+3
View File
@@ -45,6 +45,9 @@ export function isValidMatchPath(
export const redirectRewritePathSchema = z.string().nonempty(); export const redirectRewritePathSchema = z.string().nonempty();
// Same range as target priorities; 100 means "let the system order it".
export const redirectPrioritySchema = z.int().min(1).max(1000);
export const redirectDestinationDomainSchema = z export const redirectDestinationDomainSchema = z
.string() .string()
.nonempty() .nonempty()
@@ -54,6 +54,7 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
matchPath: redirect.matchPath, matchPath: redirect.matchPath,
rewritePath: redirect.rewritePath, rewritePath: redirect.rewritePath,
rewritePathType: redirect.rewritePathType, rewritePathType: redirect.rewritePathType,
priority: redirect.priority,
permanent: redirect.permanent, permanent: redirect.permanent,
enabled: redirect.enabled, enabled: redirect.enabled,
resourceId: redirect.resourceId, resourceId: redirect.resourceId,
+42
View File
@@ -67,6 +67,7 @@ import DomainPicker from "@app/components/DomainPicker";
import Link from "next/link"; import Link from "next/link";
const DEFAULT_PATH_MATCH_TYPE = "regex" as const; const DEFAULT_PATH_MATCH_TYPE = "regex" as const;
const DEFAULT_PRIORITY = 100;
export type ExistingRedirect = GetRedirectResponse["redirect"]; export type ExistingRedirect = GetRedirectResponse["redirect"];
@@ -132,6 +133,11 @@ export default function RedirectForm({
rewritePathType: z rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"]) .enum(["exact", "prefix", "regex", "stripPrefix"])
.nullable(), .nullable(),
priority: z
.number()
.int()
.min(1, { message: t("redirectPriorityInvalid") })
.max(1000, { message: t("redirectPriorityInvalid") }),
permanent: z.boolean(), permanent: z.boolean(),
enabled: z.boolean() enabled: z.boolean()
}) })
@@ -193,6 +199,7 @@ export default function RedirectForm({
matchPath: redirect?.matchPath ?? null, matchPath: redirect?.matchPath ?? null,
rewritePath: redirect?.rewritePath ?? null, rewritePath: redirect?.rewritePath ?? null,
rewritePathType: redirect?.rewritePathType ?? null, rewritePathType: redirect?.rewritePathType ?? null,
priority: redirect?.priority ?? DEFAULT_PRIORITY,
permanent: redirect?.permanent ?? false, permanent: redirect?.permanent ?? false,
enabled: redirect?.enabled ?? true enabled: redirect?.enabled ?? true
} }
@@ -248,6 +255,7 @@ export default function RedirectForm({
matchPath: values.matchPath?.trim() || null, matchPath: values.matchPath?.trim() || null,
rewritePath: values.rewritePath?.trim() || null, rewritePath: values.rewritePath?.trim() || null,
rewritePathType: values.rewritePathType, rewritePathType: values.rewritePathType,
priority: values.priority,
permanent: values.permanent, permanent: values.permanent,
enabled: values.enabled enabled: values.enabled
}; };
@@ -792,6 +800,40 @@ export default function RedirectForm({
/> />
</SettingsFormCell> </SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("priority")}
</FormLabel>
<FormControl>
<Input
type="number"
min={1}
max={1000}
{...field}
onChange={(e) =>
field.onChange(
e.target
.valueAsNumber
)
}
/>
</FormControl>
<FormDescription>
{t(
"priorityDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="full"> <SettingsFormCell span="full">
<FormField <FormField
control={form.control} control={form.control}
+29 -4
View File
@@ -22,9 +22,12 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import type { GetBatchedCertificateResponse } from "@server/routers/certificates/types"; import type { GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import type { PaginationState } from "@tanstack/react-table"; import type { PaginationState } from "@tanstack/react-table";
import { import {
ArrowDown,
ArrowRight, ArrowRight,
ArrowUp,
ArrowUpRight, ArrowUpRight,
GlobeIcon, GlobeIcon,
MinusIcon,
MoreHorizontal, MoreHorizontal,
WaypointsIcon WaypointsIcon
} from "lucide-react"; } from "lucide-react";
@@ -52,6 +55,7 @@ export type RedirectRow = {
matchPath: string | null; matchPath: string | null;
rewritePath: string | null; rewritePath: string | null;
rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null; rewritePathType: "exact" | "prefix" | "regex" | "stripPrefix" | null;
priority: number | null;
permanent: boolean; permanent: boolean;
enabled: boolean; enabled: boolean;
resourceId: number | null; resourceId: number | null;
@@ -286,9 +290,7 @@ export default function RedirectsTable({
{redirect.matchPath && ( {redirect.matchPath && (
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{redirect.pathMatchType === "prefix" {redirect.pathMatchType === "prefix"
? withPrefixGlob( ? withPrefixGlob(redirect.matchPath)
redirect.matchPath
)
: redirect.matchPath} : redirect.matchPath}
</span> </span>
)} )}
@@ -320,6 +322,28 @@ export default function RedirectsTable({
); );
} }
}, },
{
accessorKey: "priority",
friendlyName: t("priority"),
header: () => <span className="p-3">{t("priority")}</span>,
cell: ({ row }) => {
// 100 is the automatic default; anything else was set
// deliberately, so flag which way it deviates.
const priority = row.original.priority ?? 100;
return (
<span className="inline-flex items-center gap-1">
{priority}
{priority > 100 ? (
<ArrowUp className="size-3 text-green-500" />
) : priority < 100 ? (
<ArrowDown className="size-3 text-red-500" />
) : (
<MinusIcon className="size-3 text-muted-foreground" />
)}
</span>
);
}
},
{ {
accessorKey: "permanent", accessorKey: "permanent",
friendlyName: t("redirectType"), friendlyName: t("redirectType"),
@@ -439,7 +463,8 @@ export default function RedirectsTable({
columnVisibility={{ columnVisibility={{
attachedTo: false, attachedTo: false,
niceId: false, niceId: false,
permanent: false permanent: false,
priority: false
}} }}
enableColumnVisibility enableColumnVisibility
stickyLeftColumn="name" stickyLeftColumn="name"