mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 09:56:36 +00:00
complete share link i18n
This commit is contained in:
@@ -225,7 +225,7 @@ export default function GeneralPage() {
|
||||
loading={loadingSave}
|
||||
disabled={loadingSave}
|
||||
>
|
||||
{t('orgGeneralSave')}
|
||||
{t('saveGeneralSettings')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function ResourcesDataTable<TData, TValue>({
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Resources"
|
||||
searchPlaceholder={t('resourceSearch')}
|
||||
searchPlaceholder={t('resourcesSearch')}
|
||||
searchColumn="name"
|
||||
onAdd={createResource}
|
||||
addButtonText={t('resourceAdd')}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface AccessTokenSectionProps {
|
||||
token: string;
|
||||
@@ -37,37 +38,37 @@ export default function AccessTokenSection({
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start space-x-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your access token can be passed in two ways: as a query
|
||||
parameter or in the request headers. These must be passed
|
||||
from the client on every request for authenticated access.
|
||||
{t('shareTokenDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="token" className="w-full mt-4">
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="token">Access Token</TabsTrigger>
|
||||
<TabsTrigger value="usage">Usage Examples</TabsTrigger>
|
||||
<TabsTrigger value="token">{t('accessToken')}</TabsTrigger>
|
||||
<TabsTrigger value="usage">{t('usageExamples')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="token" className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold">Token ID</div>
|
||||
<div className="font-bold">{t('tokenId')}</div>
|
||||
<CopyToClipboard text={tokenId} isLink={false} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold">Token</div>
|
||||
<div className="font-bold">{t('token')}</div>
|
||||
<CopyToClipboard text={token} isLink={false} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="usage" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Request Headers</h3>
|
||||
<h3 className="text-sm font-medium">{t('requestHeades')}</h3>
|
||||
<CopyTextBox
|
||||
text={`${env.server.resourceAccessTokenHeadersId}: ${tokenId}
|
||||
${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
@@ -75,7 +76,7 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Query Parameter</h3>
|
||||
<h3 className="text-sm font-medium">{t('queryParameter')}</h3>
|
||||
<CopyTextBox
|
||||
text={`${resourceUrl}?${env.server.resourceAccessTokenParam}=${tokenId}.${token}`}
|
||||
/>
|
||||
@@ -84,21 +85,17 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Important Note
|
||||
{t('importantNote')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
For security reasons, using headers is recommended
|
||||
over query parameters when possible, as query
|
||||
parameters may be logged in server logs or browser
|
||||
history.
|
||||
{t('shareImportantDescription')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="text-sm text-muted-foreground mt-4">
|
||||
Keep your access token secure. Do not share it in publicly
|
||||
accessible areas or client-side code.
|
||||
{t('shareTokenSecurety')}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import AccessTokenSection from "./AccessTokenUsage";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type FormProps = {
|
||||
open: boolean;
|
||||
@@ -91,6 +92,7 @@ export default function CreateShareLinkForm({
|
||||
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const [accessTokenId, setAccessTokenId] = useState<string | null>(null);
|
||||
@@ -110,12 +112,12 @@ export default function CreateShareLinkForm({
|
||||
>([]);
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: "minutes", name: "Minutes" },
|
||||
{ unit: "hours", name: "Hours" },
|
||||
{ unit: "days", name: "Days" },
|
||||
{ unit: "weeks", name: "Weeks" },
|
||||
{ unit: "months", name: "Months" },
|
||||
{ unit: "years", name: "Years" }
|
||||
{ unit: "minutes", name: t('minutes') },
|
||||
{ unit: "hours", name: t('hours') },
|
||||
{ unit: "days", name: t('days') },
|
||||
{ unit: "weeks", name: t('weeks') },
|
||||
{ unit: "months", name: t('months') },
|
||||
{ unit: "years", name: t('years') }
|
||||
];
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -141,11 +143,8 @@ export default function CreateShareLinkForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch resources",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the resources"
|
||||
)
|
||||
title: t('shareErrorFetchResource'),
|
||||
description: formatAxiosError(e, t('shareErrorFetchResourceDescription'))
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,11 +207,8 @@ export default function CreateShareLinkForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create share link",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the share link"
|
||||
)
|
||||
title: t('shareErrorCreate'),
|
||||
description: formatAxiosError(e, t('shareErrorCreateDescription'))
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,9 +256,9 @@ export default function CreateShareLinkForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Create Shareable Link</CredenzaTitle>
|
||||
<CredenzaTitle>{t('shareCreate')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Anyone with this link can access the resource
|
||||
{t('shareCreateDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -280,7 +276,7 @@ export default function CreateShareLinkForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>
|
||||
Resource
|
||||
{t('resource')}
|
||||
</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -305,12 +301,10 @@ export default function CreateShareLinkForm({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search resources" />
|
||||
<CommandInput placeholder={t('resourceSearch')} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
No
|
||||
resources
|
||||
found
|
||||
{t('resourceNotFound')}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{resources.map(
|
||||
@@ -366,7 +360,7 @@ export default function CreateShareLinkForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Title (optional)
|
||||
{t('shareTitleOptional')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
@@ -378,7 +372,7 @@ export default function CreateShareLinkForm({
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Expire In</FormLabel>
|
||||
<FormLabel>{t('expireIn')}</FormLabel>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -455,18 +449,12 @@ export default function CreateShareLinkForm({
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Never expire
|
||||
{t('neverExpire')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Expiration time is how long the
|
||||
link will be usable and provide
|
||||
access to the resource. After
|
||||
this time, the link will no
|
||||
longer work, and users who used
|
||||
this link will lose access to
|
||||
the resource.
|
||||
{t('shareExpireDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
@@ -475,12 +463,10 @@ export default function CreateShareLinkForm({
|
||||
{link && (
|
||||
<div className="max-w-md space-y-4">
|
||||
<p>
|
||||
You will only be able to see this link
|
||||
once. Make sure to copy it.
|
||||
{t('shareSeeOnce')}
|
||||
</p>
|
||||
<p>
|
||||
Anyone with this link can access the
|
||||
resource. Share it with care.
|
||||
{t('shareAccessHint')}
|
||||
</p>
|
||||
|
||||
<div className="h-[250px] w-full mx-auto flex items-center justify-center">
|
||||
@@ -506,7 +492,7 @@ export default function CreateShareLinkForm({
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
See Access Token Usage
|
||||
{t('shareTokenUsage')}
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
@@ -549,7 +535,7 @@ export default function CreateShareLinkForm({
|
||||
loading={loading}
|
||||
disabled={link !== null || loading}
|
||||
>
|
||||
Create Link
|
||||
{t('createLink')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -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,7 +23,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
} else if (type === "wireguard") {
|
||||
return "WireGuard";
|
||||
} else if (type === "local") {
|
||||
return "Local";
|
||||
return t('local');
|
||||
} else {
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -30,23 +32,23 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
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>
|
||||
|
||||
@@ -31,6 +31,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required")
|
||||
@@ -46,6 +47,7 @@ export default function GeneralPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
const form = useForm<GeneralFormValues>({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
@@ -65,19 +67,16 @@ export default function GeneralPage() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to update site",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while updating the site."
|
||||
)
|
||||
title: t('siteErrorUpdate'),
|
||||
description: formatAxiosError(e,t('siteErrorUpdateDescription'))
|
||||
});
|
||||
});
|
||||
|
||||
updateSite({ name: data.name });
|
||||
|
||||
toast({
|
||||
title: "Site updated",
|
||||
description: "The site has been updated."
|
||||
title: t('siteUpdated'),
|
||||
description: t('siteUpdatedDescription')
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
@@ -90,10 +89,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>
|
||||
|
||||
@@ -110,14 +109,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>
|
||||
)}
|
||||
@@ -134,7 +132,7 @@ export default function GeneralPage() {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Save General Settings
|
||||
{t('saveGeneralSettings')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user