Merge branch 'dev' into clients-pops

This commit is contained in:
Owen
2025-06-11 11:13:40 -04:00
149 changed files with 13888 additions and 5083 deletions

View File

@@ -50,25 +50,7 @@ import {
CollapsibleTrigger
} from "@app/components/ui/collapsible";
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
const createSiteFormSchema = z.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters."
})
.max(30, {
message: "Name must not be longer than 30 characters."
}),
method: z.enum(["wireguard", "newt", "local"])
});
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
const defaultValues: Partial<CreateSiteFormValues> = {
name: "",
method: "newt"
};
import { useTranslations } from "next-intl";
type CreateSiteFormProps = {
onCreate?: (site: SiteRow) => void;
@@ -96,6 +78,27 @@ export default function CreateSiteForm({
privateKey: string;
} | null>(null);
const t = useTranslations();
const createSiteFormSchema = z.object({
name: z
.string()
.min(2, {
message: t('nameMin', {len: 2})
})
.max(30, {
message: t('nameMax', {len: 30})
}),
method: z.enum(["wireguard", "newt", "local"])
});
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
const defaultValues: Partial<CreateSiteFormValues> = {
name: "",
method: "newt"
};
const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null);
@@ -169,8 +172,8 @@ export default function CreateSiteForm({
if (!keypair || !siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Key pair or site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateKeyPair')
});
setLoading?.(false);
setIsLoading(false);
@@ -188,8 +191,8 @@ export default function CreateSiteForm({
if (!siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateDefaults')
});
setLoading?.(false);
setIsLoading(false);
@@ -212,7 +215,7 @@ export default function CreateSiteForm({
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating site",
title: t('siteErrorCreate'),
description: formatAxiosError(e)
});
});
@@ -227,11 +230,11 @@ export default function CreateSiteForm({
address: data.address?.split("/")[0],
mbIn:
data.type == "wireguard" || data.type == "newt"
? "0 MB"
? t('megabytes', {count: 0})
: "-",
mbOut:
data.type == "wireguard" || data.type == "newt"
? "0 MB"
? t('megabytes', {count: 0})
: "-",
orgId: orgId as string,
type: data.type as any,
@@ -286,13 +289,13 @@ PersistentKeepalive = 5`
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input autoComplete="off" {...field} />
</FormControl>
<FormMessage />
<FormDescription>
This is the display name for the site.
{t('siteNameDescription')}
</FormDescription>
</FormItem>
)}
@@ -302,18 +305,18 @@ PersistentKeepalive = 5`
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormLabel>{t('method')}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Select method" />
<SelectValue placeholder={t('methodSelect')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="local">
Local
{t('local')}
</SelectItem>
<SelectItem
value="newt"
@@ -332,7 +335,7 @@ PersistentKeepalive = 5`
</FormControl>
<FormMessage />
<FormDescription>
This is how you will expose connections.
{t('siteMethodDescription')}
</FormDescription>
</FormItem>
)}
@@ -346,7 +349,7 @@ PersistentKeepalive = 5`
rel="noopener noreferrer"
>
<span>
Learn how to install Newt on your system
{t('siteLearnNewt')}
</span>
<SquareArrowOutUpRight size={14} />
</Link>
@@ -357,13 +360,12 @@ PersistentKeepalive = 5`
<>
<CopyTextBox text={wgConfig} />
<span className="text-sm text-muted-foreground mt-2">
You will only be able to see the
configuration once.
{t('siteSeeConfigOnce')}
</span>
</>
) : form.watch("method") === "wireguard" &&
isLoading ? (
<p>Loading WireGuard configuration...</p>
<p>{t('siteLoadWGConfig')}</p>
) : form.watch("method") === "newt" && siteDefaults ? (
<>
<div className="mb-2">
@@ -379,8 +381,7 @@ PersistentKeepalive = 5`
/>
</div>
<span className="text-sm text-muted-foreground">
You will only be able to see the
configuration once.
{t('siteSeeConfigOnce')}
</span>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
@@ -390,13 +391,12 @@ PersistentKeepalive = 5`
className="p-0 flex items-center justify-between w-full"
>
<h4 className="text-sm font-semibold">
Expand for Docker
Deployment Details
{t('siteDocker')}
</h4>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
{t('toggle')}
</span>
</div>
</Button>
@@ -404,7 +404,7 @@ PersistentKeepalive = 5`
</div>
<CollapsibleContent className="space-y-4">
<div className="space-y-2">
<b>Docker Compose</b>
<b>{t('dockerCompose')}</b>
<CopyTextBox
text={
newtConfigDockerCompose
@@ -413,7 +413,7 @@ PersistentKeepalive = 5`
/>
</div>
<div className="space-y-2">
<b>Docker Run</b>
<b>{t('dockerRun')}</b>
<CopyTextBox
text={newtConfigDockerRun}
@@ -434,7 +434,7 @@ PersistentKeepalive = 5`
target="_blank"
rel="noopener noreferrer"
>
<span> Local sites do not tunnel, learn more</span>
<span>{t('siteLearnLocal')}</span>
<SquareArrowOutUpRight size={14} />
</Link>
)}
@@ -451,7 +451,7 @@ PersistentKeepalive = 5`
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I have copied the config
{t('siteConfirmCopy')}
</label>
</div>
)}

View File

@@ -14,6 +14,7 @@ import {
} from "@app/components/Credenza";
import { SiteRow } from "./SitesTable";
import CreateSiteForm from "./CreateSiteForm";
import { useTranslations } from "next-intl";
type CreateSiteFormProps = {
open: boolean;
@@ -30,6 +31,7 @@ export default function CreateSiteFormModal({
}: CreateSiteFormProps) {
const [loading, setLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const t = useTranslations();
return (
<>
@@ -42,9 +44,9 @@ export default function CreateSiteFormModal({
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Create Site</CredenzaTitle>
<CredenzaTitle>{t('siteCreate')}</CredenzaTitle>
<CredenzaDescription>
Create a new site to start connecting your resources
{t('siteCreateDescription')}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
@@ -59,7 +61,7 @@ export default function CreateSiteFormModal({
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
<Button variant="outline">{t('close')}</Button>
</CredenzaClose>
<Button
type="submit"
@@ -70,7 +72,7 @@ export default function CreateSiteFormModal({
setOpen(false);
}}
>
Create Site
{t('siteCreate')}
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -2,6 +2,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
import { useTranslations } from "next-intl";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -14,15 +15,18 @@ export function SitesDataTable<TData, TValue>({
data,
createSite
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
<DataTable
columns={columns}
data={data}
title="Sites"
searchPlaceholder="Search sites..."
title={t('sites')}
searchPlaceholder={t('searchSitesProgress')}
searchColumn="name"
onAdd={createSite}
addButtonText="Add Site"
addButtonText={t('siteAdd')}
defaultSort={{
id: "name",
desc: false

View File

@@ -5,11 +5,13 @@ import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArrowRight, DockIcon as Docker, Globe, Server, X } from "lucide-react";
import Link from "next/link";
import { useTranslations } from 'next-intl';
export const SitesSplashCard = () => {
const [isDismissed, setIsDismissed] = useState(true);
const key = "sites-splash-card-dismissed";
const t = useTranslations();
useEffect(() => {
const dismissed = localStorage.getItem(key);
@@ -34,7 +36,7 @@ export const SitesSplashCard = () => {
<button
onClick={handleDismiss}
className="absolute top-2 right-2 p-2"
aria-label="Dismiss"
aria-label={t('dismiss')}
>
<X className="w-5 h-5" />
</button>
@@ -42,22 +44,19 @@ export const SitesSplashCard = () => {
<div className="space-y-4">
<h3 className="text-xl font-semibold flex items-center gap-2">
<Globe className="text-blue-500" />
Newt (Recommended)
Newt ({t('recommended')})
</h3>
<p className="text-sm">
For the best user experience, use Newt. It uses
WireGuard under the hood and allows you to address your
private resources by their LAN address on your private
network from within the Pangolin dashboard.
{t('siteNewtDescription')}
</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li className="flex items-center gap-2">
<Server className="text-green-500 w-4 h-4" />
Runs in Docker
{t('siteRunsInDocker')}
</li>
<li className="flex items-center gap-2">
<Server className="text-green-500 w-4 h-4" />
Runs in shell on macOS, Linux, and Windows
{t('siteRunsInShell')}
</li>
</ul>
@@ -71,7 +70,7 @@ export const SitesSplashCard = () => {
className="w-full flex items-center"
variant="secondary"
>
Install Newt{" "}
{t('siteInstallNewt')}{" "}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -79,20 +78,19 @@ export const SitesSplashCard = () => {
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold flex items-center gap-2">
Basic WireGuard
{t('siteWg')}
</h3>
<p className="text-sm">
Use any WireGuard client to connect. You will have to
address your internal resources using the peer IP.
{t('siteWgAnyClients')}
</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li className="flex items-center gap-2">
<Docker className="text-purple-500 w-4 h-4" />
Compatible with all WireGuard clients
{t('siteWgCompatibleAllClients')}
</li>
<li className="flex items-center gap-2">
<Server className="text-purple-500 w-4 h-4" />
Manual configuration required
{t('siteWgManualConfigurationRequired')}
</li>
</ul>
</div>

View File

@@ -27,6 +27,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal";
import { useTranslations } from "next-intl";
import { parseDataSize } from '@app/lib/dataSize';
export type SiteRow = {
@@ -54,15 +55,16 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const [rows, setRows] = useState<SiteRow[]>(sites);
const api = createApiClient(useEnvContext());
const t = useTranslations();
const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`)
.catch((e) => {
console.error("Error deleting site", e);
console.error(t('siteErrorDelete'), e);
toast({
variant: "destructive",
title: "Error deleting site",
description: formatAxiosError(e, "Error deleting site")
title: t('siteErrorDelete'),
description: formatAxiosError(e, t('siteErrorDelete'))
});
})
.then(() => {
@@ -96,7 +98,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<DropdownMenuItem>
View settings
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
@@ -105,7 +107,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">Delete</span>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -122,7 +124,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Name
{t('name')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -138,7 +140,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Online
{t('online')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -153,14 +155,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
return (
<span className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
<span>{t('online')}</span>
</span>
);
} else {
return (
<span className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
<span>{t('offline')}</span>
</span>
);
}
@@ -179,7 +181,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Site
{t('site')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -195,7 +197,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Data In
{t('dataIn')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -213,7 +215,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Data Out
{t('dataOut')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -231,7 +233,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Connection Type
{t('connectionType')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -258,7 +260,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
if (originalRow.type === "local") {
return (
<div className="flex items-center space-x-2">
<span>Local</span>
<span>{t('local')}</span>
</div>
);
}
@@ -290,7 +292,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
Edit
{t('edit')}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -312,30 +314,22 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
dialog={
<div className="space-y-4">
<p>
Are you sure you want to remove the site{" "}
<b>{selectedSite?.name || selectedSite?.id}</b>{" "}
from the organization?
{t('siteQuestionRemove', {selectedSite: selectedSite?.name || selectedSite?.id})}
</p>
<p>
{t('siteMessageRemove')}
</p>
<p>
Once removed, the site will no longer be
accessible.{" "}
<b>
All resources and targets associated with
the site will also be removed.
</b>
</p>
<p>
To confirm, please type the name of the site
below.
{t('siteMessageConfirm')}
</p>
</div>
}
buttonText="Confirm Delete Site"
buttonText={t('siteConfirmDelete')}
onConfirm={async () => deleteSite(selectedSite!.id)}
string={selectedSite.name}
title="Delete Site"
title={t('siteDelete')}
/>
)}

View File

@@ -9,11 +9,13 @@ import {
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import { useTranslations } from "next-intl";
type SiteInfoCardProps = {};
export default function SiteInfoCard({}: SiteInfoCardProps) {
const { site, updateSite } = useSiteContext();
const t = useTranslations();
const getConnectionTypeString = (type: string) => {
if (type === "newt") {
@@ -21,32 +23,32 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
} else if (type === "wireguard") {
return "WireGuard";
} else if (type === "local") {
return "Local";
return t('local');
} else {
return "Unknown";
return t('unknown');
}
};
return (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">Site Information</AlertTitle>
<AlertTitle className="font-semibold">{t('siteInfo')}</AlertTitle>
<AlertDescription className="mt-4">
<InfoSections cols={2}>
{(site.type == "newt" || site.type == "wireguard") && (
<>
<InfoSection>
<InfoSectionTitle>Status</InfoSectionTitle>
<InfoSectionTitle>{t('status')}</InfoSectionTitle>
<InfoSectionContent>
{site.online ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
<span>{t('online')}</span>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
<span>{t('offline')}</span>
</div>
)}
</InfoSectionContent>
@@ -54,7 +56,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
</>
)}
<InfoSection>
<InfoSectionTitle>Connection Type</InfoSectionTitle>
<InfoSectionTitle>{t('connectionType')}</InfoSectionTitle>
<InfoSectionContent>
{getConnectionTypeString(site.type)}
</InfoSectionContent>

View File

@@ -32,8 +32,8 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { ArrowRight, ExternalLink } from "lucide-react";
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"),
@@ -50,6 +50,7 @@ export default function GeneralPage() {
const [loading, setLoading] = useState(false);
const router = useRouter();
const t = useTranslations();
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
@@ -71,10 +72,10 @@ export default function GeneralPage() {
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to update site",
title: t("siteErrorUpdate"),
description: formatAxiosError(
e,
"An error occurred while updating the site."
t("siteErrorUpdateDescription")
)
});
});
@@ -85,8 +86,8 @@ export default function GeneralPage() {
});
toast({
title: "Site updated",
description: "The site has been updated."
title: t("siteUpdated"),
description: t("siteUpdatedDescription")
});
setLoading(false);
@@ -99,10 +100,10 @@ export default function GeneralPage() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
General Settings
{t("generalSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure the general settings for this site
{t("siteGeneralDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
@@ -119,14 +120,13 @@ export default function GeneralPage() {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
<FormDescription>
This is the display name of the
site.
{t("siteNameDescription")}
</FormDescription>
</FormItem>
)}
@@ -140,7 +140,9 @@ export default function GeneralPage() {
<FormControl>
<SwitchInput
id="docker-socket-enabled"
label="Enable Docker Socket"
label={t(
"enableDockerSocket"
)}
defaultChecked={
field.value
}
@@ -151,20 +153,21 @@ export default function GeneralPage() {
</FormControl>
<FormMessage />
<FormDescription>
Enable Docker Socket
discovery for populating
container information.
Socket path must be provided
to Newt.{" "}
<a
{t(
"enableDockerSocketDescription"
)}{" "}
<Link
href="https://docs.fossorial.io/Newt/overview#docker-socket-integration"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more
<ExternalLink className="ml-1 h-4 w-4" />
</a>
<span>
{t(
"enableDockerSocketLink"
)}
</span>
</Link>
</FormDescription>
</FormItem>
)}
@@ -182,7 +185,7 @@ export default function GeneralPage() {
loading={loading}
disabled={loading}
>
Save General Settings
{t("saveGeneralSettings")}
</Button>
</SettingsSectionFooter>
</SettingsSection>

View File

@@ -16,6 +16,7 @@ import {
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import SiteInfoCard from "./SiteInfoCard";
import { getTranslations } from "next-intl/server";
interface SettingsLayoutProps {
children: React.ReactNode;
@@ -38,9 +39,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
redirect(`/${params.orgId}/settings/sites`);
}
const t = await getTranslations();
const navItems = [
{
title: "General",
title: t('general'),
href: "/{orgId}/settings/sites/{niceId}/general"
}
];
@@ -48,8 +51,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return (
<>
<SettingsSectionTitle
title={`${site?.name} Settings`}
description="Configure the settings on your site"
title={t('siteSetting', {siteName: site?.name})}
description={t('siteSettingDescription')}
/>
<SiteProvider site={site}>

View File

@@ -65,32 +65,7 @@ import {
import Link from "next/link";
import { QRCodeCanvas } from "qrcode.react";
const createSiteFormSchema = z
.object({
name: z
.string()
.min(2, { message: "Name must be at least 2 characters." })
.max(30, {
message: "Name must not be longer than 30 characters."
}),
method: z.enum(["newt", "wireguard", "local"]),
copied: z.boolean(),
clientAddress: z.string().optional()
})
.refine(
(data) => {
if (data.method !== "local") {
return data.copied;
}
return true;
},
{
message: "Please confirm that you have copied the config.",
path: ["copied"]
}
);
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
import { useTranslations } from "next-intl";
type SiteType = "newt" | "wireguard" | "local";
@@ -125,28 +100,54 @@ export default function Page() {
const api = createApiClient({ env });
const { orgId } = useParams();
const router = useRouter();
const t = useTranslations();
const createSiteFormSchema = z
.object({
name: z
.string()
.min(2, { message: t('nameMin', {len: 2}) })
.max(30, {
message: t('nameMax', {len: 30})
}),
method: z.enum(["newt", "wireguard", "local"]),
copied: z.boolean(),
clientAddress: z.string().optional()
})
.refine(
(data) => {
if (data.method !== "local") {
return data.copied;
}
return true;
},
{
message: t('sitesConfirmCopy'),
path: ["copied"]
}
);
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
const [tunnelTypes, setTunnelTypes] = useState<
ReadonlyArray<TunnelTypeOption>
>([
{
id: "newt",
title: "Newt Tunnel (Recommended)",
description:
"Easiest way to create an entrypoint into your network. No extra setup.",
title: t('siteNewtTunnel'),
description: t('siteNewtTunnelDescription'),
disabled: true
},
{
id: "wireguard",
title: "Basic WireGuard",
description:
"Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
title: t('siteWg'),
description: t('siteWgDescription'),
disabled: true
},
{
id: "local",
title: "Local",
description: "Local resources only. No tunneling."
title: t('local'),
description: t('siteLocalDescription')
}
]);
@@ -325,7 +326,7 @@ WantedBy=default.target`
};
const getCommand = () => {
const placeholder = ["Unknown command"];
const placeholder = [t('unknownCommand')];
if (!commands) {
return placeholder;
}
@@ -387,8 +388,8 @@ WantedBy=default.target`
if (!siteDefaults || !wgConfig) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Key pair or site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateKeyPair')
});
setCreateLoading(false);
return;
@@ -405,8 +406,8 @@ WantedBy=default.target`
if (!siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Site defaults not found"
title: t('siteErrorCreate'),
description: t('siteErrorCreateDefaults')
});
setCreateLoading(false);
return;
@@ -429,7 +430,7 @@ WantedBy=default.target`
.catch((e) => {
toast({
variant: "destructive",
title: "Error creating site",
title: t('siteErrorCreate'),
description: formatAxiosError(e)
});
});
@@ -455,14 +456,14 @@ WantedBy=default.target`
);
if (!response.ok) {
throw new Error(
`Failed to fetch release info: ${response.statusText}`
t('newtErrorFetchReleases', {err: response.statusText})
);
}
const data = await response.json();
const latestVersion = data.tag_name;
newtVersion = latestVersion;
} catch (error) {
console.error("Error fetching latest release:", error);
console.error(t('newtErrorFetchLatest', {err: error instanceof Error ? error.message : String(error)}));
}
const generatedKeypair = generateKeypair();
@@ -529,8 +530,8 @@ WantedBy=default.target`
<>
<div className="flex justify-between">
<HeaderTitle
title="Create Site"
description="Follow the steps below to create and connect a new site"
title={t('siteCreate')}
description={t('siteCreateDescription2')}
/>
<Button
variant="outline"
@@ -538,7 +539,7 @@ WantedBy=default.target`
router.push(`/${orgId}/settings/sites`);
}}
>
See All Sites
{t('siteSeeAll')}
</Button>
</div>
@@ -548,7 +549,7 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Site Information
{t('siteInfo')}
</SettingsSectionTitle>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -564,7 +565,7 @@ WantedBy=default.target`
render={({ field }) => (
<FormItem>
<FormLabel>
Name
{t('name')}
</FormLabel>
<FormControl>
<Input
@@ -574,8 +575,7 @@ WantedBy=default.target`
</FormControl>
<FormMessage />
<FormDescription>
This is the display
name for the site.
{t('siteNameDescription')}
</FormDescription>
</FormItem>
)}
@@ -625,11 +625,10 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Tunnel Type
{t('tunnelType')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Determine how you want to connect to your
site
{t('siteTunnelDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -649,18 +648,17 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Newt Credentials
{t('siteNewtCredentials')}
</SettingsSectionTitle>
<SettingsSectionDescription>
This is how Newt will authenticate
with the server
{t('siteNewtCredentialsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
Newt Endpoint
{t('newtEndpoint')}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@@ -672,7 +670,7 @@ WantedBy=default.target`
</InfoSection>
<InfoSection>
<InfoSectionTitle>
Newt ID
{t('newtId')}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@@ -682,7 +680,7 @@ WantedBy=default.target`
</InfoSection>
<InfoSection>
<InfoSectionTitle>
Newt Secret Key
{t('newtSecretKey')}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
@@ -695,12 +693,10 @@ WantedBy=default.target`
<Alert variant="neutral" className="">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Save Your Credentials
{t('siteCredentialsSave')}
</AlertTitle>
<AlertDescription>
You will only be able to see
this once. Make sure to copy it
to a secure place.
{t('siteCredentialsSaveDescription')}
</AlertDescription>
</Alert>
@@ -735,9 +731,7 @@ WantedBy=default.target`
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I have
copied the
config
{t('siteConfirmCopy')}
</label>
</div>
<FormMessage />
@@ -751,16 +745,16 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Install Newt
{t('siteInstallNewt')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Get Newt running on your system
{t('siteInstallNewtDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div>
<p className="font-bold mb-3">
Operating System
{t('operatingSystem')}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{platforms.map((os) => (
@@ -788,8 +782,8 @@ WantedBy=default.target`
{["docker", "podman"].includes(
platform
)
? "Method"
: "Architecture"}
? t('method')
: t('architecture')}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures().map(
@@ -816,7 +810,7 @@ WantedBy=default.target`
</div>
<div className="pt-4">
<p className="font-bold mb-3">
Commands
{t('commands')}
</p>
<div className="mt-2">
<CopyTextBox
@@ -837,11 +831,10 @@ WantedBy=default.target`
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
WireGuard Configuration
{t('WgConfiguration')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Use the following configuration to
connect to your network
{t('WgConfigurationDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -862,12 +855,10 @@ WantedBy=default.target`
<Alert variant="neutral">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Save Your Credentials
{t('siteCredentialsSave')}
</AlertTitle>
<AlertDescription>
You will only be able to see this
once. Make sure to copy it to a
secure place.
{t('siteCredentialsSaveDescription')}
</AlertDescription>
</Alert>
@@ -902,8 +893,7 @@ WantedBy=default.target`
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I have copied
the config
{t('siteConfirmCopy')}
</label>
</div>
<FormMessage />
@@ -925,7 +915,7 @@ WantedBy=default.target`
router.push(`/${orgId}/settings/sites`);
}}
>
Cancel
{t('cancel')}
</Button>
<Button
type="button"
@@ -935,7 +925,7 @@ WantedBy=default.target`
form.handleSubmit(onSubmit)();
}}
>
Create Site
{t('siteCreate')}
</Button>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { AxiosResponse } from "axios";
import SitesTable, { SiteRow } from "./SitesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import SitesSplashCard from "./SitesSplashCard";
import { getTranslations } from "next-intl/server";
type SitesPageProps = {
params: Promise<{ orgId: string }>;
@@ -23,16 +24,18 @@ export default async function SitesPage(props: SitesPageProps) {
sites = res.data.data.sites;
} catch (e) {}
const t = await getTranslations();
function formatSize(mb: number, type: string): string {
if (type === "local") {
return "-"; // because we are not able to track the data use in a local site right now
}
if (mb >= 1024 * 1024) {
return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
return t('terabytes', {count: (mb / (1024 * 1024)).toFixed(2)});
} else if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)} GB`;
return t('gigabytes', {count: (mb / 1024).toFixed(2)});
} else {
return `${mb.toFixed(2)} MB`;
return t('megabytes', {count: mb.toFixed(2)});
}
}
@@ -55,8 +58,8 @@ export default async function SitesPage(props: SitesPageProps) {
{/* <SitesSplashCard /> */}
<SettingsSectionTitle
title="Manage Sites"
description="Allow connectivity to your network through secure tunnels"
title={t('siteManageSites')}
description={t('siteDescription')}
/>
<SitesTable sites={siteRows} orgId={params.orgId} />