From 32949127d28fc0aa2604a9f6c033d80ab2990733 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Wed, 29 Oct 2025 00:12:41 +0530 Subject: [PATCH] Make site niceId editable --- server/routers/site/updateSite.ts | 19 +++++ src/components/SiteInfoCard.tsx | 124 +++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts index e3724f36..de0c3f9c 100644 --- a/server/routers/site/updateSite.ts +++ b/server/routers/site/updateSite.ts @@ -20,6 +20,7 @@ const updateSiteParamsSchema = z const updateSiteBodySchema = z .object({ name: z.string().min(1).max(255).optional(), + niceId: z.string().min(1).max(255).optional(), dockerSocketEnabled: z.boolean().optional(), remoteSubnets: z .string() @@ -89,6 +90,24 @@ export async function updateSite( const { siteId } = parsedParams.data; const updateData = parsedBody.data; + // if niceId is provided, check if it's already in use by another site + if (updateData.niceId) { + const existingSite = await db + .select() + .from(sites) + .where(eq(sites.niceId, updateData.niceId)) + .limit(1); + + if (existingSite.length > 0 && existingSite[0].siteId !== siteId) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A site with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs if (updateData.remoteSubnets) { const subnets = updateData.remoteSubnets.split(",").map((s) => s.trim()); diff --git a/src/components/SiteInfoCard.tsx b/src/components/SiteInfoCard.tsx index 6d2ded82..5e3e1194 100644 --- a/src/components/SiteInfoCard.tsx +++ b/src/components/SiteInfoCard.tsx @@ -1,7 +1,7 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon } from "lucide-react"; +import { Check, InfoIcon, Pencil, X } from "lucide-react"; import { useSiteContext } from "@app/hooks/useSiteContext"; import { InfoSection, @@ -11,6 +11,11 @@ import { } from "@app/components/InfoSection"; import { useTranslations } from "next-intl"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { useState } from "react"; +import { useToast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; type SiteInfoCardProps = {}; @@ -18,6 +23,13 @@ export default function SiteInfoCard({ }: SiteInfoCardProps) { const { site, updateSite } = useSiteContext(); const t = useTranslations(); const { env } = useEnvContext(); + const api = createApiClient(useEnvContext()); + + const [isEditing, setIsEditing] = useState(false); + const [niceId, setNiceId] = useState(site.niceId); + const [tempNiceId, setTempNiceId] = useState(site.niceId); + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); const getConnectionTypeString = (type: string) => { if (type === "newt") { @@ -31,6 +43,71 @@ export default function SiteInfoCard({ }: SiteInfoCardProps) { } }; + 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 response = await api.post(`/site/${site.siteId}`, { + niceId: tempNiceId.trim() + }); + + setNiceId(tempNiceId.trim()); + setIsEditing(false); + + updateSite({ + 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 ( @@ -40,7 +117,50 @@ export default function SiteInfoCard({ }: SiteInfoCardProps) { {t("niceId")} - {site.niceId} +
+ {isEditing ? ( + <> + setTempNiceId(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + className="flex-1" + autoFocus + /> + + + + ) : ( + <> + {niceId} + + + )} +
{(site.type == "newt" || site.type == "wireguard") && (