Add first i18n stuff

This commit is contained in:
Lokowitz
2025-05-04 15:11:42 +00:00
parent 21f1326045
commit 7eb08474ff
35 changed files with 2629 additions and 759 deletions

View File

@@ -4,6 +4,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>[];
@@ -16,15 +17,18 @@ export function ResourcesDataTable<TData, TValue>({
data,
createResource
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
<DataTable
columns={columns}
data={data}
title="Resources"
searchPlaceholder="Search resources..."
searchPlaceholder={t('resourceSearch')}
searchColumn="name"
onAdd={createResource}
addButtonText="Add Resource"
addButtonText={t('resourceAdd')}
/>
);
}

View File

@@ -31,6 +31,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
import { Switch } from "@app/components/ui/switch";
import { AxiosResponse } from "axios";
import { UpdateResourceResponse } from "@server/routers/resource";
import { useTranslations } from 'next-intl';
export type ResourceRow = {
id: number;
@@ -53,6 +54,7 @@ type ResourcesTableProps = {
export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const router = useRouter();
const t = useTranslations();
const api = createApiClient(useEnvContext());
@@ -63,11 +65,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const deleteResource = (resourceId: number) => {
api.delete(`/resource/${resourceId}`)
.catch((e) => {
console.error("Error deleting resource", e);
console.error(t('resourceErrorDelte'), e);
toast({
variant: "destructive",
title: "Error deleting resource",
description: formatAxiosError(e, "Error deleting resource")
title: t('resourceErrorDelte'),
description: formatAxiosError(e, t('resourceErrorDelte'))
});
})
.then(() => {
@@ -108,7 +110,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -118,7 +120,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<DropdownMenuItem>
View settings
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
@@ -127,7 +129,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">Delete</span>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -144,7 +146,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Name
{t('name')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -160,7 +162,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Site
{t('site')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -219,7 +221,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Authentication
{t('authentication')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -231,12 +233,12 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
{resourceRow.authState === "protected" ? (
<span className="text-green-500 flex items-center space-x-2">
<ShieldCheck className="w-4 h-4" />
<span>Protected</span>
<span>{t('protected')}</span>
</span>
) : resourceRow.authState === "not_protected" ? (
<span className="text-yellow-500 flex items-center space-x-2">
<ShieldOff className="w-4 h-4" />
<span>Not Protected</span>
<span>{t('notProtected')}</span>
</span>
) : (
<span>-</span>
@@ -267,7 +269,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
Edit
{t('edit')}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -289,23 +291,15 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
dialog={
<div>
<p className="mb-2">
Are you sure you want to remove the resource{" "}
<b>
{selectedResource?.name ||
selectedResource?.id}
</b>{" "}
from the organization?
{t('resourceQuestionRemove', {selectedResource: selectedResource?.name || selectedResource?.id})}
</p>
<p className="mb-2">
Once removed, the resource will no longer be
accessible. All targets attached to the resource
will be removed.
{t('resourceMessageRemove')}
</p>
<p>
To confirm, please type the name of the resource
below.
{t('resourceMessageConfirm')}
</p>
</div>
}

View File

@@ -13,11 +13,13 @@ import {
} from "@app/components/InfoSection";
import Link from "next/link";
import { Switch } from "@app/components/ui/switch";
import { useTranslations } from 'next-intl';
type ResourceInfoBoxType = {};
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext();
const t = useTranslations();
let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
@@ -25,7 +27,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
Resource Information
{t('resourceInfo')}
</AlertTitle>
<AlertDescription className="mt-4">
<InfoSections cols={4}>
@@ -33,7 +35,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<>
<InfoSection>
<InfoSectionTitle>
Authentication
{t('authentication')}
</InfoSectionTitle>
<InfoSectionContent>
{authInfo.password ||
@@ -42,12 +44,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" />
<span>Protected</span>
<span>{t('protected')}</span>
</div>
) : (
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff className="w-4 h-4" />
<span>Not Protected</span>
<span>{t('notProtected')}</span>
</div>
)}
</InfoSectionContent>
@@ -62,7 +64,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>Site</InfoSectionTitle>
<InfoSectionTitle>{t('site')}</InfoSectionTitle>
<InfoSectionContent>
{resource.siteName}
</InfoSectionContent>
@@ -71,7 +73,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
) : (
<>
<InfoSection>
<InfoSectionTitle>Protocol</InfoSectionTitle>
<InfoSectionTitle>{t('protocol')}</InfoSectionTitle>
<InfoSectionContent>
<span>
{resource.protocol.toUpperCase()}
@@ -79,7 +81,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>Port</InfoSectionTitle>
<InfoSectionTitle>{t('port')}</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={resource.proxyPort!.toString()}
@@ -90,9 +92,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</>
)}
<InfoSection>
<InfoSectionTitle>Visibility</InfoSectionTitle>
<InfoSectionTitle>{t('visibility')}</InfoSectionTitle>
<InfoSectionContent>
<span>{resource.enabled ? "Enabled" : "Disabled"}</span>
<span>{resource.enabled ? t('enabled') : t('disabled')}</span>
</InfoSectionContent>
</InfoSection>
</InfoSections>

View File

@@ -22,6 +22,7 @@ import {
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { getTranslations } from 'next-intl/server';
interface ResourceLayoutProps {
children: React.ReactNode;
@@ -30,6 +31,7 @@ interface ResourceLayoutProps {
export default async function ResourceLayout(props: ResourceLayoutProps) {
const params = await props.params;
const t = await getTranslations();
const { children } = props;
@@ -82,22 +84,22 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
const navItems = [
{
title: "General",
title: t('general'),
href: `/{orgId}/settings/resources/{resourceId}/general`
},
{
title: "Proxy",
title: t('proxy'),
href: `/{orgId}/settings/resources/{resourceId}/proxy`
}
];
if (resource.http) {
navItems.push({
title: "Authentication",
title: t('authentication'),
href: `/{orgId}/settings/resources/{resourceId}/authentication`
});
navItems.push({
title: "Rules",
title: t('rules'),
href: `/{orgId}/settings/resources/{resourceId}/rules`
});
}
@@ -105,8 +107,8 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
return (
<>
<SettingsSectionTitle
title={`${resource?.name} Settings`}
description="Configure the settings on your resource"
title={t('resourceSetting', {resourceName: resource?.name})}
description={t('resourceSettingDescription')}
/>
<OrgProvider org={org}>

View File

@@ -62,6 +62,7 @@ import { cn } from "@app/lib/cn";
import { SquareArrowOutUpRight } from "lucide-react";
import CopyTextBox from "@app/components/CopyTextBox";
import Link from "next/link";
import { useTranslations } from 'next-intl';
const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255),
@@ -104,6 +105,7 @@ export default function Page() {
const api = createApiClient({ env });
const { orgId } = useParams();
const router = useRouter();
const t = useTranslations();
const [loadingPage, setLoadingPage] = useState(true);
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
@@ -117,15 +119,13 @@ export default function Page() {
const resourceTypes: ReadonlyArray<ResourceTypeOption> = [
{
id: "http",
title: "HTTPS Resource",
description:
"Proxy requests to your app over HTTPS using a subdomain or base domain."
title: t('resourceHTTP'),
description: t('resourceHTTPDescription')
},
{
id: "raw",
title: "Raw TCP/UDP Resource",
description:
"Proxy requests to your app over TCP/UDP using a port number.",
title: t('resourceRaw'),
description: t('resourceRawDescription'),
disabled: !env.flags.allowRawResources
}
];
@@ -300,8 +300,8 @@ export default function Page() {
<>
<div className="flex justify-between">
<HeaderTitle
title="Create Resource"
description="Follow the steps below to create a new resource"
title={t('resourceCreate')}
description={t('resourceCreateDescription')}
/>
<Button
variant="outline"
@@ -309,7 +309,7 @@ export default function Page() {
router.push(`/${orgId}/settings/resources`);
}}
>
See All Resources
{t('resourceSeeAll')}
</Button>
</div>
@@ -320,7 +320,7 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Resource Information
{t('resourceInfo')}
</SettingsSectionTitle>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -336,7 +336,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Name
{t('name')}
</FormLabel>
<FormControl>
<Input
@@ -345,9 +345,7 @@ export default function Page() {
</FormControl>
<FormMessage />
<FormDescription>
This is the
display name for
the resource.
{t('resourceNameDescription')}
</FormDescription>
</FormItem>
)}
@@ -359,7 +357,7 @@ export default function Page() {
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
Site
{t('site')}
</FormLabel>
<Popover>
<PopoverTrigger
@@ -384,19 +382,17 @@ export default function Page() {
field.value
)
?.name
: "Select site"}
: t('siteSelect')}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<Command>
<CommandInput placeholder="Search site" />
<CommandInput placeholder={t('siteSearch')} />
<CommandList>
<CommandEmpty>
No
site
found.
{t('siteNotFound')}
</CommandEmpty>
<CommandGroup>
{sites.map(
@@ -437,10 +433,7 @@ export default function Page() {
</Popover>
<FormMessage />
<FormDescription>
This site will
provide
connectivity to
the resource.
{t('siteSelectionDescription')}
</FormDescription>
</FormItem>
)}
@@ -454,11 +447,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Resource Type
{t('resourceType')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Determine how you want to access your
resource
{t('resourceTypeDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -480,11 +472,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
HTTPS Settings
{t('resourceHTTPSSettings')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how your resource will be
accessed over HTTPS
{t('resourceHTTPSSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -506,8 +497,7 @@ export default function Page() {
}) => (
<FormItem>
<FormLabel>
Domain
Type
{t('domainType')}
</FormLabel>
<Select
value={
@@ -531,11 +521,10 @@ export default function Page() {
</FormControl>
<SelectContent>
<SelectItem value="subdomain">
Subdomain
{t('subdomain')}
</SelectItem>
<SelectItem value="basedomain">
Base
Domain
{t('baseDomain')}
</SelectItem>
</SelectContent>
</Select>
@@ -550,7 +539,7 @@ export default function Page() {
) && (
<FormItem>
<FormLabel>
Subdomain
{t('subdomain')}
</FormLabel>
<div className="flex space-x-0">
<div className="w-1/2">
@@ -629,10 +618,7 @@ export default function Page() {
</div>
</div>
<FormDescription>
The subdomain
where your
resource will be
accessible.
{t('subdomnainDescription')}
</FormDescription>
</FormItem>
)}
@@ -650,8 +636,7 @@ export default function Page() {
}) => (
<FormItem>
<FormLabel>
Base
Domain
{t('baseDomain')}
</FormLabel>
<Select
onValueChange={
@@ -702,11 +687,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
TCP/UDP Settings
{t('resourceRawSettings')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how your resource will be
accessed over TCP/UDP
{t('resourceRawSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -724,7 +708,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Protocol
{t('protocol')}
</FormLabel>
<Select
onValueChange={
@@ -734,7 +718,7 @@ export default function Page() {
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a protocol" />
<SelectValue placeholder={t('protocolSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -759,7 +743,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
Port Number
{t('resourcePortNumber')}
</FormLabel>
<FormControl>
<Input
@@ -787,10 +771,7 @@ export default function Page() {
</FormControl>
<FormMessage />
<FormDescription>
The external
port number
to proxy
requests.
{t('resourcePortNumberDescription')}
</FormDescription>
</FormItem>
)}
@@ -810,7 +791,7 @@ export default function Page() {
router.push(`/${orgId}/settings/resources`)
}
>
Cancel
{t('cancle')}
</Button>
<Button
type="button"
@@ -827,7 +808,7 @@ export default function Page() {
}}
loading={createLoading}
>
Create Resource
{t('resourceCreate')}
</Button>
</div>
</SettingsContainer>
@@ -836,17 +817,17 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Configuration Snippets
{t('resourceConfig')}
</SettingsSectionTitle>
<SettingsSectionDescription>
Copy and paste these configuration snippets to set up your TCP/UDP resource
{t('resourceConfigDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold">
Traefik: Add Entrypoints
{t('resourceAddEntrypoints')}
</h3>
<CopyTextBox
text={`entryPoints:
@@ -858,7 +839,7 @@ export default function Page() {
<div className="space-y-4">
<h3 className="text-lg font-semibold">
Gerbil: Expose Ports in Docker Compose
{t('resourceExposePorts')}
</h3>
<CopyTextBox
text={`ports:
@@ -874,7 +855,7 @@ export default function Page() {
rel="noopener noreferrer"
>
<span>
Learn how to configure TCP/UDP resources
{t('resourceLearnRaw')}
</span>
<SquareArrowOutUpRight size={14} />
</Link>
@@ -890,7 +871,7 @@ export default function Page() {
router.push(`/${orgId}/settings/resources`)
}
>
Back to Resources
{t('resourceBack')}
</Button>
<Button
type="button"
@@ -900,7 +881,7 @@ export default function Page() {
)
}
>
Go to Resource
{t('resourceGoTo')}
</Button>
</div>
</SettingsContainer>

View File

@@ -9,6 +9,7 @@ import { cache } from "react";
import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider";
import ResourcesSplashCard from "./ResourcesSplashCard";
import { getTranslations } from 'next-intl/server';
type ResourcesPageProps = {
params: Promise<{ orgId: string }>;
@@ -68,13 +69,15 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
};
});
const t = await getTranslations();
return (
<>
{/* <ResourcesSplashCard /> */}
<SettingsSectionTitle
title="Manage Resources"
description="Create secure proxies to your private applications"
title={t('resourceTitle')}
description={t('resourceDescription')}
/>
<OrgProvider org={org}>

View File

@@ -4,6 +4,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>[];
@@ -16,15 +17,18 @@ export function ShareLinksDataTable<TData, TValue>({
data,
createShareLink
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
return (
<DataTable
columns={columns}
data={data}
title="Share Links"
searchPlaceholder="Search share links..."
searchPlaceholder={t('shareSearch')}
searchColumn="name"
onAdd={createShareLink}
addButtonText="Create Share Link"
addButtonText={t('shareCreate')}
/>
);
}

View File

@@ -33,6 +33,7 @@ import { ListAccessTokensResponse } from "@server/routers/accessToken";
import moment from "moment";
import CreateShareLinkForm from "./CreateShareLinkForm";
import { constructShareLink } from "@app/lib/shareLinks";
import { useTranslations } from 'next-intl';
export type ShareLinkRow = {
accessTokenId: string;
@@ -54,6 +55,7 @@ export default function ShareLinksTable({
orgId
}: ShareLinksTableProps) {
const router = useRouter();
const t = useTranslations();
const api = createApiClient(useEnvContext());
@@ -67,11 +69,8 @@ export default function ShareLinksTable({
async function deleteSharelink(id: string) {
await api.delete(`/access-token/${id}`).catch((e) => {
toast({
title: "Failed to delete link",
description: formatAxiosError(
e,
"An error occurred deleting link"
)
title: t('shareErrorDelete'),
description: formatAxiosError(e,t('shareErrorDeleteMessage'))
});
});
@@ -79,8 +78,8 @@ export default function ShareLinksTable({
setRows(newRows);
toast({
title: "Link deleted",
description: "The link has been deleted"
title: t('shareDeleted'),
description: t('shareDeletedDesciption')
});
}
@@ -102,7 +101,7 @@ export default function ShareLinksTable({
className="h-8 w-8 p-0"
>
<span className="sr-only">
Open menu
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
@@ -116,7 +115,7 @@ export default function ShareLinksTable({
}}
>
<button className="text-red-500">
Delete
{t('delete')}
</button>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -136,7 +135,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Resource
{t('resource')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -164,7 +163,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Title
{t('title')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -243,7 +242,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Created
{t('created')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -263,7 +262,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Expires
{t('expires')}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -273,7 +272,7 @@ export default function ShareLinksTable({
if (r.expiresAt) {
return moment(r.expiresAt).format("lll");
}
return "Never";
return t('never');
}
},
{
@@ -286,7 +285,7 @@ export default function ShareLinksTable({
deleteSharelink(row.original.accessTokenId)
}
>
Delete
{t('delete')}
</Button>
</div>
)

View File

@@ -9,6 +9,7 @@ import OrgProvider from "@app/providers/OrgProvider";
import { ListAccessTokensResponse } from "@server/routers/accessToken";
import ShareLinksTable, { ShareLinkRow } from "./ShareLinksTable";
import ShareableLinksSplash from "./ShareLinksSplash";
import { getTranslations } from 'next-intl/server';
type ShareLinksPageProps = {
params: Promise<{ orgId: string }>;
@@ -51,13 +52,15 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) {
(token) => ({ ...token }) as ShareLinkRow
);
const t = await getTranslations();
return (
<>
{/* <ShareableLinksSplash /> */}
<SettingsSectionTitle
title="Manage Share Links"
description="Create shareable links to grant temporary or permanent access to your resources"
title={t('shareTitle')}
description={t('shareDescription')}
/>
<OrgProvider org={org}>

View File

@@ -50,15 +50,18 @@ import {
CollapsibleTrigger
} from "@app/components/ui/collapsible";
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
import { useTranslations } from 'next-intl';
const t = useTranslations();
const createSiteFormSchema = z.object({
name: z
.string()
.min(2, {
message: "Name must be at least 2 characters."
message: {t('siteNameMin')}
})
.max(30, {
message: "Name must not be longer than 30 characters."
message: {t('siteNameMax')}
}),
method: z.enum(["wireguard", "newt", "local"])
});
@@ -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)
});
});
@@ -285,13 +288,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>
)}
@@ -301,7 +304,7 @@ PersistentKeepalive = 5`
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormLabel>{t('method')}</FormLabel>
<FormControl>
<Select
value={field.value}
@@ -331,7 +334,7 @@ PersistentKeepalive = 5`
</FormControl>
<FormMessage />
<FormDescription>
This is how you will expose connections.
{t('siteMethodDescription')}
</FormDescription>
</FormItem>
)}
@@ -345,7 +348,7 @@ PersistentKeepalive = 5`
rel="noopener noreferrer"
>
<span>
Learn how to install Newt on your system
{t('siteLearnNewt')}
</span>
<SquareArrowOutUpRight size={14} />
</Link>
@@ -356,13 +359,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">
@@ -378,8 +380,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>
@@ -389,13 +390,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>
@@ -403,7 +403,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
@@ -412,7 +412,7 @@ PersistentKeepalive = 5`
/>
</div>
<div className="space-y-2">
<b>Docker Run</b>
<b>{t('dockerRun')}</b>
<CopyTextBox
text={newtConfigDockerRun}
@@ -433,7 +433,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>
)}
@@ -450,7 +450,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

@@ -4,6 +4,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>[];
@@ -16,15 +17,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..."
searchPlaceholder={t('searchSites')}
searchColumn="name"
onAdd={createSite}
addButtonText="Add Site"
addButtonText={t('siteAdd')}
/>
);
}

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);
@@ -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>

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';
export type SiteRow = {
id: number;
@@ -52,15 +53,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(() => {
@@ -94,7 +96,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<DropdownMenuItem>
View settings
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
@@ -103,7 +105,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>
@@ -120,7 +122,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>
);
@@ -136,7 +138,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>
);
@@ -151,14 +153,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>
);
}
@@ -177,7 +179,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>
);
@@ -193,7 +195,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>
);
@@ -209,7 +211,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>
);
@@ -225,7 +227,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>
);
@@ -252,7 +254,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>
);
}
@@ -268,7 +270,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>
@@ -290,30 +292,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

@@ -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 }>;
@@ -49,13 +50,15 @@ export default async function SitesPage(props: SitesPageProps) {
};
});
const t = await getTranslations();
return (
<>
{/* <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} />