diff --git a/messages/en-US.json b/messages/en-US.json index fa5d229c..936ef98c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2096,5 +2096,10 @@ "enableSelected": "Enable Selected", "disableSelected": "Disable Selected", "checkSelectedStatus": "Check Status of Selected", - "niceId": "Nice ID" + "niceId": "Nice ID", + "niceIdUpdated": "Nice ID Updated", + "niceIdUpdatedSuccessfully": "Nice ID Updated Successfully", + "niceIdUpdateError": "Error updating Nice ID", + "niceIdUpdateErrorDescription": "An error occurred while updating the Nice ID.", + "niceIdCannotBeEmpty": "Nice ID cannot be empty" } diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 13c5220d..bfe6b2cf 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -37,6 +37,7 @@ const updateResourceParamsSchema = z const updateHttpResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), + niceId: z.string().min(1).max(255).optional(), subdomain: subdomainSchema.nullable().optional(), ssl: z.boolean().optional(), sso: z.boolean().optional(), @@ -97,6 +98,7 @@ export type UpdateResourceResponse = Resource; const updateRawResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), + niceId: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), enabled: z.boolean().optional(), @@ -236,6 +238,25 @@ async function updateHttpResource( const updateData = parsedBody.data; + if (updateData.niceId) { + const [existingResource] = await db + .select() + .from(resources) + .where(eq(resources.niceId, updateData.niceId)); + + if ( + existingResource && + existingResource.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A resource with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + if (updateData.domainId) { const domainId = updateData.domainId; @@ -362,6 +383,25 @@ async function updateRawResource( const updateData = parsedBody.data; + if (updateData.niceId) { + const [existingResource] = await db + .select() + .from(resources) + .where(eq(resources.niceId, updateData.niceId)); + + if ( + existingResource && + existingResource.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A resource with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + const updatedResource = await db .update(resources) .set(updateData) diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx index e7cf2c82..7997ccc7 100644 --- a/src/components/ResourceInfoBox.tsx +++ b/src/components/ResourceInfoBox.tsx @@ -1,7 +1,7 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; +import { Check, InfoIcon, Pencil, ShieldCheck, ShieldOff, X } from "lucide-react"; import { useResourceContext } from "@app/hooks/useResourceContext"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { @@ -14,30 +14,158 @@ import { useTranslations } from "next-intl"; import CertificateStatus from "@app/components/private/CertificateStatus"; import { toUnicode } from "punycode"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useState } from "react"; +import { useToast } from "@app/hooks/useToast"; +import { UpdateResourceResponse } from "@server/routers/resource"; +import { AxiosResponse } from "axios"; type ResourceInfoBoxType = {}; export default function ResourceInfoBox({ }: ResourceInfoBoxType) { - const { resource, authInfo } = useResourceContext(); + const { resource, authInfo, updateResource } = useResourceContext(); const { env } = useEnvContext(); + const api = createApiClient(useEnvContext()); + + const [isEditing, setIsEditing] = useState(false); + const [niceId, setNiceId] = useState(resource.niceId); + const [tempNiceId, setTempNiceId] = useState(resource.niceId); + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); const t = useTranslations(); const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`; + + const handleEdit = () => { + setTempNiceId(niceId); + setIsEditing(true); + }; + + const handleCancel = () => { + setTempNiceId(niceId); + setIsEditing(false); + }; + + const handleSave = async () => { + if (tempNiceId.trim() === "") { + toast({ + variant: "destructive", + title: t("error"), + description: t("niceIdCannotBeEmpty") + }); + return; + } + + if (tempNiceId === niceId) { + setIsEditing(false); + return; + } + + setIsLoading(true); + + try { + const res = await api + .post>( + `resource/${resource?.resourceId}`, + { + niceId: tempNiceId.trim() + } + ) + + setNiceId(tempNiceId.trim()); + setIsEditing(false); + + updateResource({ + niceId: tempNiceId.trim() + }); + + toast({ + title: t("niceIdUpdated"), + description: t("niceIdUpdatedSuccessfully") + }); + } catch (e: any) { + toast({ + variant: "destructive", + title: t("niceIdUpdateError"), + description: formatAxiosError( + e, + t("niceIdUpdateErrorDescription") + ) + }); + } finally { + setIsLoading(false); + } + }; + + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSave(); + } else if (e.key === "Escape") { + handleCancel(); + } + }; + return ( {/* 4 cols because of the certs */} {t("niceId")} - {resource.niceId} +
+ {isEditing ? ( + <> + setTempNiceId(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + className="flex-1" + autoFocus + /> + + + + ) : ( + <> + {niceId} + + + )} +
{resource.http ? (