basic ui working

This commit is contained in:
miloschwartz
2026-04-09 22:24:39 -04:00
parent 510931e7d6
commit 79751c208d
12 changed files with 365 additions and 167 deletions

View File

@@ -46,7 +46,9 @@ export type InternalResourceRow = {
siteName: string;
siteAddress: string | null;
// mode: "host" | "cidr" | "port";
mode: "host" | "cidr" | "http" | "https";
mode: "host" | "cidr" | "http";
scheme: "http" | "https" | null;
ssl: boolean;
// protocol: string | null;
// proxyPort: number | null;
siteId: number;
@@ -64,30 +66,27 @@ export type InternalResourceRow = {
};
function resolveHttpHttpsDisplayPort(
mode: "http" | "https",
mode: "http",
httpHttpsPort: number | null
): number {
if (httpHttpsPort != null) {
return httpHttpsPort;
}
return mode === "https" ? 443 : 80;
return 80;
}
function formatDestinationDisplay(row: InternalResourceRow): string {
const { mode, destination, httpHttpsPort } = row;
if (mode !== "http" && mode !== "https") {
const { mode, destination, httpHttpsPort, scheme } = row;
if (mode !== "http") {
return destination;
}
const port = resolveHttpHttpsDisplayPort(mode, httpHttpsPort);
const downstreamScheme = scheme ?? "http";
const hostPart =
destination.includes(":") && !destination.startsWith("[")
? `[${destination}]`
: destination;
return `${hostPart}:${port}`;
}
function formatHttpHttpsAliasUrl(mode: "http" | "https", alias: string): string {
return `${mode}://${alias}`;
return `${downstreamScheme}://${hostPart}:${port}`;
}
function isSafeUrlForLink(href: string): boolean {
@@ -255,10 +254,6 @@ export default function ClientResourcesTable({
{
value: "http",
label: t("editInternalResourceDialogModeHttp")
},
{
value: "https",
label: t("editInternalResourceDialogModeHttps")
}
]}
selectedValue={searchParams.get("mode") ?? undefined}
@@ -272,14 +267,13 @@ export default function ClientResourcesTable({
cell: ({ row }) => {
const resourceRow = row.original;
const modeLabels: Record<
"host" | "cidr" | "port" | "http" | "https",
"host" | "cidr" | "port" | "http",
string
> = {
host: t("editInternalResourceDialogModeHost"),
cidr: t("editInternalResourceDialogModeCidr"),
port: t("editInternalResourceDialogModePort"),
http: t("editInternalResourceDialogModeHttp"),
https: t("editInternalResourceDialogModeHttps")
http: t("editInternalResourceDialogModeHttp")
};
return <span>{modeLabels[resourceRow.mode]}</span>;
}
@@ -319,15 +313,8 @@ export default function ClientResourcesTable({
/>
);
}
if (
(resourceRow.mode === "http" ||
resourceRow.mode === "https") &&
resourceRow.alias
) {
const url = formatHttpHttpsAliasUrl(
resourceRow.mode,
resourceRow.alias
);
if (resourceRow.mode === "http" && resourceRow.alias) {
const url = `${resourceRow.ssl ? "https" : "http"}://${resourceRow.alias}`;
return (
<CopyToClipboard
text={url}

View File

@@ -51,9 +51,7 @@ export default function CreateInternalResourceDialog({
try {
let data = { ...values };
if (
(data.mode === "host" ||
data.mode === "http" ||
data.mode === "https") &&
(data.mode === "host" || data.mode === "http") &&
isHostname(data.destination)
) {
const currentAlias = data.alias?.trim() || "";
@@ -74,21 +72,42 @@ export default function CreateInternalResourceDialog({
mode: data.mode,
destination: data.destination,
enabled: true,
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined,
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
destinationPort: data.httpHttpsPort ?? undefined
}),
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: undefined,
tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false,
...(data.authDaemonMode != null && { authDaemonMode: data.authDaemonMode }),
...(data.authDaemonMode === "remote" && data.authDaemonPort != null && { authDaemonPort: data.authDaemonPort }),
roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [],
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" &&
data.authDaemonPort != null && {
authDaemonPort: data.authDaemonPort
}),
roleIds: data.roles
? data.roles.map((r) => parseInt(r.id))
: [],
userIds: data.users ? data.users.map((u) => u.id) : [],
clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : []
clientIds: data.clients
? data.clients.map((c) => parseInt(c.id))
: []
}
);
toast({
title: t("createInternalResourceDialogSuccess"),
description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"),
description: t(
"createInternalResourceDialogInternalResourceCreatedSuccessfully"
),
variant: "default"
});
setOpen(false);
@@ -98,7 +117,9 @@ export default function CreateInternalResourceDialog({
title: t("createInternalResourceDialogError"),
description: formatAxiosError(
error,
t("createInternalResourceDialogFailedToCreateInternalResource")
t(
"createInternalResourceDialogFailedToCreateInternalResource"
)
),
variant: "destructive"
});
@@ -111,9 +132,13 @@ export default function CreateInternalResourceDialog({
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent className="max-w-3xl">
<CredenzaHeader>
<CredenzaTitle>{t("createInternalResourceDialogCreateClientResource")}</CredenzaTitle>
<CredenzaTitle>
{t("createInternalResourceDialogCreateClientResource")}
</CredenzaTitle>
<CredenzaDescription>
{t("createInternalResourceDialogCreateClientResourceDescription")}
{t(
"createInternalResourceDialogCreateClientResourceDescription"
)}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -128,7 +153,11 @@ export default function CreateInternalResourceDialog({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSubmitting}>
<Button
variant="outline"
onClick={() => setOpen(false)}
disabled={isSubmitting}
>
{t("createInternalResourceDialogCancel")}
</Button>
</CredenzaClose>

View File

@@ -55,9 +55,7 @@ export default function EditInternalResourceDialog({
try {
let data = { ...values };
if (
(data.mode === "host" ||
data.mode === "http" ||
data.mode === "https") &&
(data.mode === "host" || data.mode === "http") &&
isHostname(data.destination)
) {
const currentAlias = data.alias?.trim() || "";
@@ -76,6 +74,11 @@ export default function EditInternalResourceDialog({
mode: data.mode,
niceId: data.niceId,
destination: data.destination,
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
destinationPort: data.httpHttpsPort ?? null
}),
alias:
data.alias &&
typeof data.alias === "string" &&

View File

@@ -46,6 +46,7 @@ import { SitesSelector, type Selectedsite } from "./site-selector";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { MachinesSelector } from "./machines-selector";
import DomainPicker from "@app/components/DomainPicker";
import { SwitchInput } from "@app/components/SwitchInput";
// --- Helpers (shared) ---
@@ -121,7 +122,7 @@ export const cleanForFQDN = (name: string): string =>
type Site = ListSitesResponse["sites"][0];
export type InternalResourceMode = "host" | "cidr" | "http" | "https";
export type InternalResourceMode = "host" | "cidr" | "http";
export type InternalResourceData = {
id: number;
@@ -139,6 +140,8 @@ export type InternalResourceData = {
authDaemonMode?: "site" | "remote" | null;
authDaemonPort?: number | null;
httpHttpsPort?: number | null;
scheme?: "http" | "https" | null;
ssl?: boolean;
httpConfigSubdomain?: string | null;
httpConfigDomainId?: string | null;
httpConfigFullDomain?: string | null;
@@ -159,6 +162,8 @@ export type InternalResourceFormValues = {
authDaemonMode?: "site" | "remote" | null;
authDaemonPort?: number | null;
httpHttpsPort?: number | null;
scheme?: "http" | "https";
ssl?: boolean;
httpConfigSubdomain?: string | null;
httpConfigDomainId?: string | null;
httpConfigFullDomain?: string | null;
@@ -226,10 +231,18 @@ export function InternalResourceForm({
variant === "create"
? "createInternalResourceDialogModeHttp"
: "editInternalResourceDialogModeHttp";
const modeHttpsKey =
const schemeLabelKey =
variant === "create"
? "createInternalResourceDialogModeHttps"
: "editInternalResourceDialogModeHttps";
? "createInternalResourceDialogScheme"
: "editInternalResourceDialogScheme";
const enableSslLabelKey =
variant === "create"
? "createInternalResourceDialogEnableSsl"
: "editInternalResourceDialogEnableSsl";
const enableSslDescriptionKey =
variant === "create"
? "createInternalResourceDialogEnableSslDescription"
: "editInternalResourceDialogEnableSslDescription";
const destinationLabelKey =
variant === "create"
? "createInternalResourceDialogDestination"
@@ -255,48 +268,78 @@ export function InternalResourceForm({
? "createInternalResourceDialogHttpConfigurationDescription"
: "editInternalResourceDialogHttpConfigurationDescription";
const formSchema = z.object({
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
siteId: z
.number()
.int()
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
mode: z.enum(["host", "cidr", "http", "https"]),
destination: z
.string()
.min(
1,
destinationRequiredKey
? { message: t(destinationRequiredKey) }
: undefined
),
alias: z.string().nullish(),
httpHttpsPort: z.number().int().min(1).max(65535).optional().nullable(),
httpConfigSubdomain: z.string().nullish(),
httpConfigDomainId: z.string().nullish(),
httpConfigFullDomain: z.string().nullish(),
niceId: z
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/)
.optional(),
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional(),
authDaemonMode: z.enum(["site", "remote"]).optional().nullable(),
authDaemonPort: z.number().int().positive().optional().nullable(),
roles: z.array(tagSchema).optional(),
users: z.array(tagSchema).optional(),
clients: z
.array(
z.object({
clientId: z.number(),
name: z.string()
})
)
.optional()
});
const formSchema = z
.object({
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
siteId: z
.number()
.int()
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
mode: z.enum(["host", "cidr", "http"]),
destination: z
.string()
.min(
1,
destinationRequiredKey
? { message: t(destinationRequiredKey) }
: undefined
),
alias: z.string().nullish(),
httpHttpsPort: z
.number()
.int()
.min(1)
.max(65535)
.optional()
.nullable(),
scheme: z.enum(["http", "https"]).optional(),
ssl: z.boolean().optional(),
httpConfigSubdomain: z.string().nullish(),
httpConfigDomainId: z.string().nullish(),
httpConfigFullDomain: z.string().nullish(),
niceId: z
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/)
.optional(),
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional(),
authDaemonMode: z.enum(["site", "remote"]).optional().nullable(),
authDaemonPort: z.number().int().positive().optional().nullable(),
roles: z.array(tagSchema).optional(),
users: z.array(tagSchema).optional(),
clients: z
.array(
z.object({
clientId: z.number(),
name: z.string()
})
)
.optional()
})
.superRefine((data, ctx) => {
if (data.mode !== "http") return;
if (!data.scheme) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceDownstreamSchemeRequired"),
path: ["scheme"]
});
}
if (
data.httpHttpsPort == null ||
!Number.isFinite(data.httpHttpsPort) ||
data.httpHttpsPort < 1
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
path: ["httpHttpsPort"]
});
}
});
type FormData = z.infer<typeof formSchema>;
@@ -430,6 +473,8 @@ export function InternalResourceForm({
authDaemonMode: resource.authDaemonMode ?? "site",
authDaemonPort: resource.authDaemonPort ?? null,
httpHttpsPort: resource.httpHttpsPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
httpConfigDomainId: resource.httpConfigDomainId ?? null,
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
@@ -445,6 +490,8 @@ export function InternalResourceForm({
destination: "",
alias: null,
httpHttpsPort: null,
scheme: "http",
ssl: false,
httpConfigSubdomain: null,
httpConfigDomainId: null,
httpConfigFullDomain: null,
@@ -471,7 +518,7 @@ export function InternalResourceForm({
const httpConfigSubdomain = form.watch("httpConfigSubdomain");
const httpConfigDomainId = form.watch("httpConfigDomainId");
const httpConfigFullDomain = form.watch("httpConfigFullDomain");
const isHttpOrHttps = mode === "http" || mode === "https";
const isHttpMode = mode === "http";
const authDaemonMode = form.watch("authDaemonMode") ?? "site";
const hasInitialized = useRef(false);
const previousResourceId = useRef<number | null>(null);
@@ -496,6 +543,8 @@ export function InternalResourceForm({
destination: "",
alias: null,
httpHttpsPort: null,
scheme: "http",
ssl: false,
httpConfigSubdomain: null,
httpConfigDomainId: null,
httpConfigFullDomain: null,
@@ -527,6 +576,8 @@ export function InternalResourceForm({
destination: resource.destination ?? "",
alias: resource.alias ?? null,
httpHttpsPort: resource.httpHttpsPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.httpConfigSubdomain ?? null,
httpConfigDomainId: resource.httpConfigDomainId ?? null,
httpConfigFullDomain: resource.httpConfigFullDomain ?? null,
@@ -681,6 +732,37 @@ export function InternalResourceForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="mode"
render={({ field }) => (
<FormItem>
<FormLabel>{t(modeLabelKey)}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="host">
{t(modeHostKey)}
</SelectItem>
<SelectItem value="cidr">
{t(modeCidrKey)}
</SelectItem>
<SelectItem value="http">
{t(modeHttpKey)}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<HorizontalTabs
@@ -718,63 +800,56 @@ export function InternalResourceForm({
<div
className={cn(
"grid gap-4 items-start",
mode === "cidr"
? "grid-cols-4"
: "grid-cols-12"
mode === "cidr" && "grid-cols-1",
mode === "http" && "grid-cols-3",
mode === "host" && "grid-cols-2"
)}
>
{mode === "http" && (
<div className="min-w-0">
<FormField
control={form.control}
name="scheme"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(schemeLabelKey)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
value={
field.value ??
"http"
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
<div
className={
mode === "cidr"
? "col-span-1"
: "col-span-3"
}
>
<FormField
control={form.control}
name="mode"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(modeLabelKey)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="host">
{t(modeHostKey)}
</SelectItem>
<SelectItem value="cidr">
{t(modeCidrKey)}
</SelectItem>
<SelectItem value="http">
{t(modeHttpKey)}
</SelectItem>
<SelectItem value="https">
{t(modeHttpsKey)}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div
className={
mode === "cidr"
? "col-span-3"
: "col-span-5"
}
className={cn(
mode === "cidr" && "col-span-1",
(mode === "http" || mode === "host") &&
"min-w-0"
)}
>
<FormField
control={form.control}
@@ -785,7 +860,7 @@ export function InternalResourceForm({
{t(destinationLabelKey)}
</FormLabel>
<FormControl>
<Input {...field} />
<Input {...field} className="w-full" />
</FormControl>
<FormMessage />
</FormItem>
@@ -793,7 +868,7 @@ export function InternalResourceForm({
/>
</div>
{mode === "host" && (
<div className="col-span-4">
<div className="min-w-0">
<FormField
control={form.control}
name="alias"
@@ -805,6 +880,7 @@ export function InternalResourceForm({
<FormControl>
<Input
{...field}
className="w-full"
value={
field.value ??
""
@@ -817,8 +893,8 @@ export function InternalResourceForm({
/>
</div>
)}
{(mode === "http" || mode === "https") && (
<div className="col-span-4">
{mode === "http" && (
<div className="min-w-0">
<FormField
control={form.control}
name="httpHttpsPort"
@@ -831,6 +907,7 @@ export function InternalResourceForm({
</FormLabel>
<FormControl>
<Input
className="w-full"
type="number"
min={1}
max={65535}
@@ -842,16 +919,16 @@ export function InternalResourceForm({
const raw =
e.target
.value;
if (
raw === ""
) {
if (raw === "") {
field.onChange(
null
);
return;
}
const n =
Number(raw);
Number(
raw
);
field.onChange(
Number.isFinite(
n
@@ -871,7 +948,7 @@ export function InternalResourceForm({
</div>
</div>
{isHttpOrHttps ? (
{isHttpMode ? (
<div className="space-y-4">
<div className="my-8">
<label className="font-medium block">
@@ -881,6 +958,29 @@ export function InternalResourceForm({
{t(httpConfigurationDescriptionKey)}
</div>
</div>
<FormField
control={form.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="internal-resource-ssl"
label={t(
enableSslLabelKey
)}
description={t(
enableSslDescriptionKey
)}
checked={!!field.value}
onCheckedChange={
field.onChange
}
/>
</FormControl>
</FormItem>
)}
/>
<DomainPicker
key={
variant === "edit" && siteResourceId
@@ -913,6 +1013,7 @@ export function InternalResourceForm({
"httpConfigFullDomain",
null
);
form.setValue("alias", null);
return;
}
form.setValue(
@@ -927,6 +1028,7 @@ export function InternalResourceForm({
"httpConfigFullDomain",
res.fullDomain
);
form.setValue("alias", res.fullDomain);
}}
/>
</div>