diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 1036538d..90bf88f5 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -227,6 +227,60 @@ export default function Page() { useState(null); const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("proxy-advanced-mode"); + return saved === "true"; + } + return false; + }); + + // Save advanced mode preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem( + "create-advanced-mode", + isAdvancedMode.toString() + ); + } + }, [isAdvancedMode]); + + function addNewTarget() { + const newTarget: LocalTarget = { + targetId: -Date.now(), // Use negative timestamp as temporary ID + ip: "", + method: baseForm.watch("http") ? "http" : null, + port: 0, + siteId: sites.length > 0 ? sites[0].siteId : 0, + path: null, + pathMatchType: null, + rewritePath: null, + rewritePathType: null, + priority: 100, + enabled: true, + resourceId: 0, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null, + siteType: sites.length > 0 ? sites[0].type : null, + new: true, + updated: false + }; + + setTargets((prev) => [...prev, newTarget]); + } + const resourceTypes: ReadonlyArray = [ { id: "http", @@ -644,23 +698,10 @@ export default function Page() { setHealthCheckDialogOpen(true); }; - const columns: ColumnDef[] = [ - { - accessorKey: "enabled", - header: t("enabled"), - cell: ({ row }) => ( - - updateTarget(row.original.targetId, { - ...row.original, - enabled: val - }) - } - /> - ) - }, - { + const getColumns = (): ColumnDef[] => { + const baseColumns: ColumnDef[] = []; + + const priorityColumn: ColumnDef = { id: "priority", header: () => (
@@ -671,7 +712,12 @@ export default function Page() { -

Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.

+

+ Higher priority routes are evaluated first. + Priority = 100 means automatic ordering + (system decides). Use another number to + enforce manual priority. +

@@ -679,13 +725,13 @@ export default function Page() { ), cell: ({ row }) => { return ( -
+
{ const value = parseInt(e.target.value, 10); if (value >= 1 && value <= 1000) { @@ -698,9 +744,13 @@ export default function Page() { />
); - } - }, - { + }, + size: 120, + minSize: 100, + maxSize: 150 + }; + + const healthCheckColumn: ColumnDef = { accessorKey: "healthCheck", header: t("healthCheck"), cell: ({ row }) => { @@ -744,89 +794,97 @@ export default function Page() { }; return ( - <> +
{row.original.siteType === "newt" ? ( - -
+ ) : ( - - {t("healthCheckNotAvailable")} - + - )} - +
); - } - }, - { + }, + size: 200, + minSize: 180, + maxSize: 250 + }; + + const matchPathColumn: ColumnDef = { accessorKey: "path", header: t("matchPath"), cell: ({ row }) => { - const hasPathMatch = !!(row.original.path || row.original.pathMatchType); + const hasPathMatch = !!( + row.original.path || row.original.pathMatchType + ); - return hasPathMatch ? ( -
- updateTarget(row.original.targetId, config)} - trigger={ - - } - /> - -
- ) : ( -
- updateTarget(row.original.targetId, config)} - trigger={ - - } - /> - + return ( +
+ {hasPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + )}
); - } - }, - { + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const addressColumn: ColumnDef = { accessorKey: "address", header: t("address"), cell: ({ row }) => { @@ -834,7 +892,10 @@ export default function Page() { (site) => site.siteId === row.original.siteId ); - const handleContainerSelectForTarget = (hostname: string, port?: number) => { + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { updateTarget(row.original.targetId, { ...row.original, ip: hostname @@ -848,40 +909,57 @@ export default function Page() { }; return ( -
- - + - {t("siteNotFound")} + + {t("siteNotFound")} + {sites.map((site) => ( - updateTarget(row.original.targetId, { siteId: site.siteId }) + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ) } > updateTarget(row.original.targetId, { ...row.original, - method: value, + method: value }) } > @@ -936,37 +1016,44 @@ export default function Page() { -
+
{"://"}
{ const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); + const hasProtocol = + /^(https?|h2c):\/\//.test(input); const hasPort = /:\d+(?:\/|$)/.test(input); if (hasProtocol || hasPort) { const parsed = parseHostTarget(input); if (parsed) { - updateTarget(row.original.targetId, { - ...row.original, - method: hasProtocol - ? parsed.protocol - : row.original.method, - ip: parsed.host, - port: hasPort - ? parsed.port - : row.original.port - }); + updateTarget( + row.original.targetId, + { + ...row.original, + method: hasProtocol + ? parsed.protocol + : row.original.method, + ip: parsed.host, + port: hasPort + ? parsed.port + : row.original.port + } + ); } else { - updateTarget(row.original.targetId, { - ...row.original, - ip: input - }); + updateTarget( + row.original.targetId, + { + ...row.original, + ip: input + } + ); } } else { updateTarget(row.original.targetId, { @@ -976,27 +1063,42 @@ export default function Page() { } }} /> -
+
{":"}
- updateTarget(row.original.targetId, { - ...row.original, - port: parseInt(e.target.value, 10) - }) + defaultValue={ + row.original.port === 0 + ? "" + : row.original.port } + className="w-[75px] pl-0 border-none placeholder-gray-400" + onBlur={(e) => { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value > 0) { + updateTarget(row.original.targetId, { + ...row.original, + port: value + }); + } else { + updateTarget(row.original.targetId, { + ...row.original, + port: 0 + }); + } + }} /> - - +
); - } - }, - { + }, + size: 400, + minSize: 350, + maxSize: 500 + }; + + const rewritePathColumn: ColumnDef = { accessorKey: "rewritePath", header: t("rewritePath"), cell: ({ row }) => { @@ -1006,70 +1108,125 @@ export default function Page() { const noPathMatch = !row.original.path && !row.original.pathMatchType; - return hasRewritePath && !noPathMatch ? ( -
- - updateTarget(row.original.targetId, config) - } - trigger={ - - } - /> + return ( +
+ {hasRewritePath && !noPathMatch ? ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + /> + ) : ( + + updateTarget(row.original.targetId, config) + } + trigger={ + + } + disabled={noPathMatch} + /> + )}
- ) : ( - - updateTarget(row.original.targetId, config) - } - trigger={ - - } - disabled={noPathMatch} - /> ); - } - }, - { + }, + size: 200, + minSize: 180, + maxSize: 200 + }; + + const enabledColumn: ColumnDef = { + accessorKey: "enabled", + header: t("enabled"), + cell: ({ row }) => ( +
+ + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + const actionsColumn: ColumnDef = { id: "actions", cell: ({ row }) => ( - <> -
- -
- - ) +
+ +
+ ), + size: 100, + minSize: 80, + maxSize: 120 + }; + + if (isAdvancedMode) { + return [ + matchPathColumn, + addressColumn, + rewritePathColumn, + priorityColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; + } else { + return [ + addressColumn, + healthCheckColumn, + enabledColumn, + actionsColumn + ]; } - ]; + }; + + const columns = getColumns(); const table = useReactTable({ data: targets, @@ -1385,421 +1542,110 @@ export default function Page() { -
-
- -
- ( - - - {t("site")} - -
- - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - addTargetForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - {field.value && - (() => { - const selectedSite = - sites.find( - ( - site - ) => - site.siteId === - field.value - ); - return selectedSite && - selectedSite.type === - "newt" ? (() => { - const dockerState = getDockerStateForSite(selectedSite.siteId); - return ( - refreshContainersForSite(selectedSite.siteId)} - /> - ); - })() : null; - })()} -
- -
- )} - /> - - {baseForm.watch("http") && ( - ( - - - {t( - "method" - )} - - - - - - - )} - /> - )} - - ( - - {t("targetAddr")} - - { - const input = e.target.value.trim(); - const hasProtocol = /^(https?|h2c):\/\//.test(input); - const hasPort = /:\d+(?:\/|$)/.test(input); - - if (hasProtocol || hasPort) { - const parsed = parseHostTarget(input); - if (parsed) { - if (hasProtocol || !addTargetForm.getValues("method")) { - addTargetForm.setValue("method", parsed.protocol); - } - addTargetForm.setValue("ip", parsed.host); - if (hasPort || !addTargetForm.getValues("port")) { - addTargetForm.setValue("port", parsed.port); - } - } - } else { - field.onBlur(); - } - }} - /> - - - - )} - /> - ( - - - {t( - "targetPort" - )} - - - - - - - )} - /> - -
-
- -
- {targets.length > 0 ? ( <> -
- {t("targetsList")} -
- -
- - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - - -
-
+
{table .getHeaderGroups() - .map( - ( - headerGroup - ) => ( - - {headerGroup.headers.map( - ( - header - ) => ( - - {header.isPlaceholder - ? null - : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} - - ) - )} - - ) - )} + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} - {table.getRowModel() - .rows?.length ? ( + {table.getRowModel().rows?.length ? ( table .getRowModel() - .rows.map( - (row) => ( - - {row - .getVisibleCells() - .map( - ( + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( cell - ) => ( - - {flexRender( - cell - .column - .columnDef - .cell, - cell.getContext() - )} - - ) - )} - - ) - ) + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) ) : ( - {t( - "targetNoOne" - )} + {t("targetNoOne")} )} + {/* */} + {/* {t('targetNoOneDescription')} */} + {/* */}
+
+
+ +
+ + +
+
+
) : ( -
-

+

+

{t("targetNoOne")}

+
)}