feat: translate settings page
This commit is contained in:
parent
8ef069114d
commit
148391b9c0
11 changed files with 148 additions and 115 deletions
|
@ -7,33 +7,37 @@ import { z } from 'zod';
|
|||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/router';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1),
|
||||
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
newPasswordConfirm: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.newPassword !== data.newPasswordConfirm) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Passwords do not match',
|
||||
path: ['newPasswordConfirm'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { MessageKey } from '@/server/utils/errors';
|
||||
|
||||
export const ChangePasswordForm = () => {
|
||||
const globalT = useTranslations();
|
||||
const t = useTranslations('settings.security');
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1),
|
||||
newPassword: z.string().min(8, t('form.password-length')),
|
||||
newPasswordConfirm: z.string().min(8, t('form.password-length')),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.newPassword !== data.newPasswordConfirm) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: t('form.password-match'),
|
||||
path: ['newPasswordConfirm'],
|
||||
});
|
||||
}
|
||||
});
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
const router = useRouter();
|
||||
const changePassword = trpc.auth.changePassword.useMutation({
|
||||
onError: (e) => {
|
||||
toast.error(`Error changing password: ${e.message}`);
|
||||
toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Password successfully changed');
|
||||
toast.success(t('password-change-success'));
|
||||
router.push('/');
|
||||
},
|
||||
});
|
||||
|
@ -52,11 +56,18 @@ export const ChangePasswordForm = () => {
|
|||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-4 w-100 ">
|
||||
<Input disabled={changePassword.isLoading} {...register('currentPassword')} error={errors.currentPassword?.message} type="password" placeholder="Current password" />
|
||||
<Input disabled={changePassword.isLoading} {...register('newPassword')} error={errors.newPassword?.message} className="mt-2" type="password" placeholder="New password" />
|
||||
<Input disabled={changePassword.isLoading} {...register('newPasswordConfirm')} error={errors.newPasswordConfirm?.message} className="mt-2" type="password" placeholder="Confirm new password" />
|
||||
<Input disabled={changePassword.isLoading} {...register('currentPassword')} error={errors.currentPassword?.message} type="password" placeholder={t('form.current-password')} />
|
||||
<Input disabled={changePassword.isLoading} {...register('newPassword')} error={errors.newPassword?.message} className="mt-2" type="password" placeholder={t('form.new-password')} />
|
||||
<Input
|
||||
disabled={changePassword.isLoading}
|
||||
{...register('newPasswordConfirm')}
|
||||
error={errors.newPasswordConfirm?.message}
|
||||
className="mt-2"
|
||||
type="password"
|
||||
placeholder={t('form.confirm-password')}
|
||||
/>
|
||||
<Button disabled={changePassword.isLoading} className="mt-3" type="submit">
|
||||
Change password
|
||||
{t('form.change-password')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -8,8 +8,12 @@ import { QRCodeSVG } from 'qrcode.react';
|
|||
import { OtpInput } from '@/components/ui/OtpInput';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import type { MessageKey } from '@/server/utils/errors';
|
||||
|
||||
export const OtpForm = () => {
|
||||
const globalT = useTranslations();
|
||||
const t = useTranslations('settings.security');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [key, setKey] = React.useState('');
|
||||
const [uri, setUri] = React.useState('');
|
||||
|
@ -28,7 +32,7 @@ export const OtpForm = () => {
|
|||
},
|
||||
onError: (e) => {
|
||||
setPassword('');
|
||||
toast.error(`Error getting TOTP URI: ${e.message}`);
|
||||
toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setKey(data.key);
|
||||
|
@ -40,13 +44,13 @@ export const OtpForm = () => {
|
|||
onMutate: () => {},
|
||||
onError: (e) => {
|
||||
setTotpCode('');
|
||||
toast.error(`Error setting up TOTP: ${e.message}`);
|
||||
toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
setTotpCode('');
|
||||
setKey('');
|
||||
setUri('');
|
||||
toast.success('Two-factor authentication enabled');
|
||||
toast.success(t('2fa-enable-success'));
|
||||
ctx.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
@ -57,10 +61,10 @@ export const OtpForm = () => {
|
|||
},
|
||||
onError: (e) => {
|
||||
setPassword('');
|
||||
toast.error(`Error disabling TOTP: ${e.message}`);
|
||||
toast.error(globalT(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Two-factor authentication disabled');
|
||||
toast.success(t('2fa-disable-success'));
|
||||
ctx.auth.me.invalidate();
|
||||
},
|
||||
});
|
||||
|
@ -71,18 +75,18 @@ export const OtpForm = () => {
|
|||
return (
|
||||
<div className="mt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">Scan this QR code with your authenticator app.</p>
|
||||
<p className="text-muted">{t('scan-qr-code')}</p>
|
||||
<QRCodeSVG value={uri} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">Or enter this key manually.</p>
|
||||
<p className="text-muted">{t('enter-key-manually')}</p>
|
||||
<Input name="secret key" value={key} readOnly />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">Enter the code from your authenticator app.</p>
|
||||
<p className="text-muted">{t('enter-2fa-code')}</p>
|
||||
<OtpInput value={totpCode} valueLength={6} onChange={(e) => setTotpCode(e)} />
|
||||
<Button disabled={totpCode.trim().length < 6} onClick={() => setupTotp.mutate({ totpCode })} className="mt-3 btn-success">
|
||||
Enable 2FA
|
||||
{t('enable-2fa')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -99,7 +103,7 @@ export const OtpForm = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{!key && <Switch disabled={!me.isSuccess} onCheckedChange={handleTotp} checked={me.data?.totpEnabled} label="Enable two-factor authentication" />}
|
||||
{!key && <Switch disabled={!me.isSuccess} onCheckedChange={handleTotp} checked={me.data?.totpEnabled} label={t('enable-2fa')} />}
|
||||
{getTotpUri.isLoading && (
|
||||
<div className="progress w-50">
|
||||
<div className="progress-bar progress-bar-indeterminate bg-green" />
|
||||
|
@ -109,7 +113,7 @@ export const OtpForm = () => {
|
|||
<Dialog open={setupOtpDisclosure.isOpen} onOpenChange={(o: boolean) => setupOtpDisclosure.toggle(o)}>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Password needed</DialogTitle>
|
||||
<DialogTitle>{t('password-needed')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="d-flex flex-column">
|
||||
<form
|
||||
|
@ -118,10 +122,10 @@ export const OtpForm = () => {
|
|||
getTotpUri.mutate({ password });
|
||||
}}
|
||||
>
|
||||
<p className="text-muted">Your password is required to setup two-factor authentication.</p>
|
||||
<Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
||||
<p className="text-muted">{t('password-needed-hint')}</p>
|
||||
<Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder={t('form.password')} />
|
||||
<Button loading={getTotpUri.isLoading} type="submit" className="btn-success mt-3">
|
||||
Enable 2FA
|
||||
{t('enable-2fa')}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogDescription>
|
||||
|
@ -130,7 +134,7 @@ export const OtpForm = () => {
|
|||
<Dialog open={disableOtpDisclosure.isOpen} onOpenChange={(o: boolean) => disableOtpDisclosure.toggle(o)}>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Password needed</DialogTitle>
|
||||
<DialogTitle>{t('password-needed')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="d-flex flex-column">
|
||||
<form
|
||||
|
@ -139,10 +143,10 @@ export const OtpForm = () => {
|
|||
disableTotp.mutate({ password });
|
||||
}}
|
||||
>
|
||||
<p className="text-muted">Your password is required to disable two-factor authentication.</p>
|
||||
<Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
||||
<p className="text-muted">{t('password-needed-hint')}</p>
|
||||
<Input name="password" type="password" onChange={(e) => setPassword(e.target.value)} placeholder={t('form.password')} />
|
||||
<Button loading={disableTotp.isLoading} type="submit" className="btn-danger mt-3">
|
||||
Disable 2FA
|
||||
{t('disable-2fa')}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogDescription>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import validator from 'validator';
|
||||
|
@ -19,30 +20,31 @@ interface IProps {
|
|||
submitErrors?: Record<string, string>;
|
||||
}
|
||||
|
||||
const validateFields = (values: SettingsFormValues) => {
|
||||
const errors: { [K in keyof SettingsFormValues]?: string } = {};
|
||||
|
||||
if (values.dnsIp && !validator.isIP(values.dnsIp)) {
|
||||
errors.dnsIp = 'Invalid IP address';
|
||||
}
|
||||
|
||||
if (values.internalIp && values.internalIp !== 'localhost' && !validator.isIP(values.internalIp)) {
|
||||
errors.internalIp = 'Invalid IP address';
|
||||
}
|
||||
|
||||
if (values.appsRepoUrl && !validator.isURL(values.appsRepoUrl)) {
|
||||
errors.appsRepoUrl = 'Invalid URL';
|
||||
}
|
||||
|
||||
if (values.domain && !validator.isFQDN(values.domain)) {
|
||||
errors.domain = 'Invalid domain';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const SettingsForm = (props: IProps) => {
|
||||
const { onSubmit, initalValues, loading, submitErrors } = props;
|
||||
const t = useTranslations('settings.settings');
|
||||
|
||||
const validateFields = (values: SettingsFormValues) => {
|
||||
const errors: { [K in keyof SettingsFormValues]?: string } = {};
|
||||
|
||||
if (values.dnsIp && !validator.isIP(values.dnsIp)) {
|
||||
errors.dnsIp = t('invalid-ip');
|
||||
}
|
||||
|
||||
if (values.internalIp && values.internalIp !== 'localhost' && !validator.isIP(values.internalIp)) {
|
||||
errors.internalIp = t('invalid-ip');
|
||||
}
|
||||
|
||||
if (values.appsRepoUrl && !validator.isURL(values.appsRepoUrl)) {
|
||||
errors.appsRepoUrl = t('invalid-url');
|
||||
}
|
||||
|
||||
if (values.domain && !validator.isFQDN(values.domain)) {
|
||||
errors.domain = t('invalid-domain');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
|
@ -84,31 +86,29 @@ export const SettingsForm = (props: IProps) => {
|
|||
|
||||
return (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit(validate)}>
|
||||
<h2 className="text-2xl font-bold">General settings</h2>
|
||||
<p className="mb-4">This will update your settings.json file. Make sure you know what you are doing before updating these values.</p>
|
||||
<h2 className="text-2xl font-bold">{t('title')}</h2>
|
||||
<p className="mb-4">{t('subtitle')}</p>
|
||||
<div className="mb-3">
|
||||
<Input {...register('domain')} label="Domain name" error={errors.domain?.message} placeholder="tipi.localhost" />
|
||||
<span className="text-muted">
|
||||
Make sure this domain contains a <strong>A</strong> record pointing to your IP.
|
||||
</span>
|
||||
<Input {...register('domain')} label={t('domain-name')} error={errors.domain?.message} placeholder="tipi.localhost" />
|
||||
<span className="text-muted">{t('domain-name-hint')}</span>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input {...register('dnsIp')} label="DNS IP" error={errors.dnsIp?.message} placeholder="9.9.9.9" />
|
||||
<Input {...register('dnsIp')} label={t('dns-ip')} error={errors.dnsIp?.message} placeholder="9.9.9.9" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input {...register('internalIp')} label="Internal IP" error={errors.internalIp?.message} placeholder="192.168.1.100" />
|
||||
<span className="text-muted">IP address your server is listening on.</span>
|
||||
<Input {...register('internalIp')} label={t('internal-ip')} error={errors.internalIp?.message} placeholder="192.168.1.100" />
|
||||
<span className="text-muted">{t('internal-ip-hint')}</span>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input {...register('appsRepoUrl')} label="Apps repo URL" error={errors.appsRepoUrl?.message} placeholder="https://github.com/meienberger/runtipi-appstore" />
|
||||
<span className="text-muted">URL to the apps repository.</span>
|
||||
<Input {...register('appsRepoUrl')} label={t('apps-repo')} error={errors.appsRepoUrl?.message} placeholder="https://github.com/meienberger/runtipi-appstore" />
|
||||
<span className="text-muted">{t('apps-repo-hint')}</span>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input {...register('storagePath')} label="Storage path" error={errors.storagePath?.message} placeholder="Storage path" />
|
||||
<span className="text-muted">Path to the storage directory. Keep empty for default (runtipi/app-data). Make sure it is an absolute path and that it exists</span>
|
||||
<Input {...register('storagePath')} label={t('storage-path')} error={errors.storagePath?.message} placeholder="Storage path" />
|
||||
<span className="text-muted">{t('storage-path-hint')}</span>
|
||||
</div>
|
||||
<Button loading={loading} type="submit" className="btn-success">
|
||||
Save
|
||||
{t('submit')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,8 @@ import semver from 'semver';
|
|||
import { toast } from 'react-hot-toast';
|
||||
import Markdown from '@/components/Markdown/Markdown';
|
||||
import { IconStar } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { MessageKey } from '@/server/utils/errors';
|
||||
import { Button } from '../../../../components/ui/Button';
|
||||
import { useDisclosure } from '../../../../hooks/useDisclosure';
|
||||
import { RestartModal } from '../../components/RestartModal';
|
||||
|
@ -11,6 +13,7 @@ import { trpc } from '../../../../utils/trpc';
|
|||
import { useSystemStore } from '../../../../state/systemStore';
|
||||
|
||||
export const GeneralActions = () => {
|
||||
const t = useTranslations();
|
||||
const versionQuery = trpc.system.getVersion.useQuery(undefined, { staleTime: 0 });
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
@ -28,10 +31,10 @@ export const GeneralActions = () => {
|
|||
onSuccess: async () => {
|
||||
setPollStatus(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (e) => {
|
||||
updateDisclosure.close();
|
||||
setLoading(false);
|
||||
toast.error(`Error updating instance: ${error.message}`);
|
||||
toast.error(t(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -42,16 +45,16 @@ export const GeneralActions = () => {
|
|||
onSuccess: async () => {
|
||||
setPollStatus(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: (e) => {
|
||||
restartDisclosure.close();
|
||||
setLoading(false);
|
||||
toast.error(`Error restarting instance: ${error.message}`);
|
||||
toast.error(t(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
});
|
||||
|
||||
const renderUpdate = () => {
|
||||
if (isLatest) {
|
||||
return <Button disabled>Already up to date</Button>;
|
||||
return <Button disabled>{t('settings.actions.already-latest')}</Button>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -69,7 +72,7 @@ export const GeneralActions = () => {
|
|||
</div>
|
||||
)}
|
||||
<Button onClick={updateDisclosure.open} className="mt-3 mr-2 btn-success">
|
||||
Update to {versionQuery.data?.latest}
|
||||
{t('settings.actions.update', { version: versionQuery.data?.latest })}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
@ -78,14 +81,14 @@ export const GeneralActions = () => {
|
|||
return (
|
||||
<>
|
||||
<div className="card-body">
|
||||
<h2 className="mb-4">Actions</h2>
|
||||
<h3 className="card-title mt-4">Current version: {versionQuery.data?.current}</h3>
|
||||
<p className="card-subtitle">{isLatest ? 'Stay up to date with the latest version of Tipi' : `A new version (${versionQuery.data?.latest}) of Tipi is available`}</p>
|
||||
<h2 className="mb-4">{t('settings.actions.title')}</h2>
|
||||
<h3 className="card-title mt-4">{t('settings.actions.current-version', { version: versionQuery.data?.current })}</h3>
|
||||
<p className="card-subtitle">{isLatest ? t('settings.actions.stay-up-to-date') : t('settings.actions.new-version', { version: versionQuery.data?.latest })}</p>
|
||||
{renderUpdate()}
|
||||
<h3 className="card-title mt-4">Maintenance</h3>
|
||||
<p className="card-subtitle">Common actions to perform on your instance</p>
|
||||
<h3 className="card-title mt-4">{t('settings.actions.maintenance-title')}</h3>
|
||||
<p className="card-subtitle">{t('settings.actions.maintenance-subtitle')}</p>
|
||||
<div>
|
||||
<Button onClick={restartDisclosure.open}>Restart</Button>
|
||||
<Button onClick={restartDisclosure.open}>{t('settings.actions.restart')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<RestartModal isOpen={restartDisclosure.isOpen} onClose={restartDisclosure.close} onConfirm={() => restart.mutate()} loading={loading} />
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import React from 'react';
|
||||
import { IconLock, IconKey } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { OtpForm } from '../../components/OtpForm';
|
||||
import { ChangePasswordForm } from '../../components/ChangePasswordForm';
|
||||
|
||||
export const SecurityContainer = () => {
|
||||
const t = useTranslations('settings.security');
|
||||
return (
|
||||
<div className="card-body">
|
||||
<div className="d-flex">
|
||||
<IconKey className="me-2" />
|
||||
<h2>Change password</h2>
|
||||
<h2>{t('change-password-title')}</h2>
|
||||
</div>
|
||||
<p className="text-muted">Changing your password will log you out of all devices.</p>
|
||||
<p className="text-muted">{t('change-password-subtitle')}</p>
|
||||
<ChangePasswordForm />
|
||||
<div className="d-flex">
|
||||
<IconLock className="me-2" />
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
<h2>{t('2fa-title')}</h2>
|
||||
</div>
|
||||
<p className="text-muted">
|
||||
Two-factor authentication (2FA) adds an additional layer of security to your account.
|
||||
{t('2fa-subtitle')}
|
||||
<br />
|
||||
When enabled, you will be prompted to enter a code from your authenticator app when you log in.
|
||||
{t('2fa-subtitle-2')}
|
||||
</p>
|
||||
<OtpForm />
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
import React, { useState } from 'react';
|
||||
import { trpc } from '@/utils/trpc';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { MessageKey } from '@/server/utils/errors';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { SettingsForm, SettingsFormValues } from '../../components/SettingsForm';
|
||||
|
||||
export const SettingsContainer = () => {
|
||||
const t = useTranslations();
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const getSettings = trpc.system.getSettings.useQuery();
|
||||
const updateSettings = trpc.system.updateSettings.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Settings updated. Restart your instance to apply new settings.');
|
||||
toast.success(t('settings.settings.settings-updated'));
|
||||
},
|
||||
onError: (e) => {
|
||||
if (e.shape?.data.zodError) {
|
||||
setErrors(e.shape.data.zodError);
|
||||
}
|
||||
|
||||
toast.error(`Error saving settings: ${e.message}`);
|
||||
toast.error(t(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import React from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Layout } from '../../../../components/Layout';
|
||||
import { GeneralActions } from '../../containers/GeneralActions';
|
||||
import { SettingsContainer } from '../../containers/SettingsContainer';
|
||||
import { SecurityContainer } from '../../containers/SecurityContainer';
|
||||
|
||||
export const SettingsPage: NextPage = () => {
|
||||
const t = useTranslations('settings');
|
||||
return (
|
||||
<Layout title="Settings">
|
||||
<Layout title={t('title')}>
|
||||
<div className="card d-flex">
|
||||
<Tabs defaultValue="actions">
|
||||
<TabsList>
|
||||
<TabsTrigger value="actions">Actions</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
<TabsTrigger value="actions">{t('actions.tab-title')}</TabsTrigger>
|
||||
<TabsTrigger value="settings">{t('settings.tab-title')}</TabsTrigger>
|
||||
<TabsTrigger value="security">{t('security.tab-title')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="actions">
|
||||
<GeneralActions />
|
||||
|
|
|
@ -10,12 +10,16 @@ type UIStore = {
|
|||
translator: typeof defaultTranslator;
|
||||
setMenuItem: (menuItem: string) => void;
|
||||
setDarkMode: (darkMode: boolean) => void;
|
||||
setTranslator: (translator: typeof defaultTranslator) => void;
|
||||
};
|
||||
|
||||
export const useUIStore = create<UIStore>((set) => ({
|
||||
menuItem: 'dashboard',
|
||||
darkMode: false,
|
||||
translator: defaultTranslator,
|
||||
setTranslator: (translator: typeof defaultTranslator) => {
|
||||
set({ translator });
|
||||
},
|
||||
setDarkMode: (darkMode: boolean) => {
|
||||
if (darkMode) {
|
||||
localStorage.setItem('darkMode', darkMode.toString());
|
||||
|
|
|
@ -2,8 +2,6 @@ import nookies from 'nookies';
|
|||
import { GetServerSideProps } from 'next';
|
||||
import merge from 'lodash.merge';
|
||||
import { getLocaleFromString } from '@/shared/internationalization/locales';
|
||||
import { createTranslator } from 'next-intl';
|
||||
import { useUIStore } from '../state/uiStore';
|
||||
|
||||
export const getAuthedPageProps: GetServerSideProps = async (ctx) => {
|
||||
const { userId } = ctx.req.session;
|
||||
|
@ -42,9 +40,6 @@ export const getMessagesPageProps: GetServerSideProps = async (ctx) => {
|
|||
const messages = (await import(`../messages/${locale}.json`)).default;
|
||||
const mergedMessages = merge(englishMessages, messages);
|
||||
|
||||
const translator = createTranslator({ locale, messages: mergedMessages });
|
||||
useUIStore.setState({ translator });
|
||||
|
||||
return {
|
||||
props: {
|
||||
messages: mergedMessages,
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
|||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { NextIntlProvider } from 'next-intl';
|
||||
import { NextIntlProvider, createTranslator } from 'next-intl';
|
||||
import '../client/styles/global.css';
|
||||
import '../client/styles/global.scss';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
|
@ -20,7 +20,7 @@ import { SystemStatus, useSystemStore } from '../client/state/systemStore';
|
|||
* @returns {JSX.Element} - JSX element
|
||||
*/
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const { setDarkMode } = useUIStore();
|
||||
const { setDarkMode, setTranslator } = useUIStore();
|
||||
const { setStatus, setVersion, pollStatus } = useSystemStore();
|
||||
const { locale } = useLocale();
|
||||
|
||||
|
@ -47,6 +47,14 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
themeCheck();
|
||||
}, [setDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const translator = createTranslator({
|
||||
messages: pageProps.messages,
|
||||
locale,
|
||||
});
|
||||
setTranslator(translator);
|
||||
}, [pageProps.messages, locale, setTranslator]);
|
||||
|
||||
return (
|
||||
<main className="h-100">
|
||||
<NextIntlProvider locale={locale} messages={pageProps.messages}>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import semver from 'semver';
|
||||
import { z } from 'zod';
|
||||
import fetch from 'node-fetch-commonjs';
|
||||
import { TranslatedError } from '@/server/utils/errors';
|
||||
import { readJsonFile } from '../../common/fs.helpers';
|
||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||
import { Logger } from '../../core/Logger';
|
||||
|
@ -80,23 +81,23 @@ export class SystemServiceClass {
|
|||
const { current, latest } = await this.getVersion();
|
||||
|
||||
if (TipiConfig.getConfig().NODE_ENV === 'development') {
|
||||
throw new Error('Cannot update in development mode');
|
||||
throw new TranslatedError('server-messages.errors.not-allowed-in-dev');
|
||||
}
|
||||
|
||||
if (!latest) {
|
||||
throw new Error('Could not get latest version');
|
||||
throw new TranslatedError('server-messages.errors.could-not-get-latest-version');
|
||||
}
|
||||
|
||||
if (semver.gt(current, latest)) {
|
||||
throw new Error('Current version is newer than latest version');
|
||||
throw new TranslatedError('server-messages.errors.current-version-is-latest');
|
||||
}
|
||||
|
||||
if (semver.eq(current, latest)) {
|
||||
throw new Error('Current version is already up to date');
|
||||
throw new TranslatedError('server-messages.errors.current-version-is-latest');
|
||||
}
|
||||
|
||||
if (semver.major(current) !== semver.major(latest)) {
|
||||
throw new Error('The major version has changed. Please update manually (instructions on GitHub)');
|
||||
throw new TranslatedError('server-messages.errors.major-version-update');
|
||||
}
|
||||
|
||||
TipiConfig.setConfig('status', 'UPDATING');
|
||||
|
@ -108,11 +109,11 @@ export class SystemServiceClass {
|
|||
|
||||
public restart = async (): Promise<boolean> => {
|
||||
if (TipiConfig.getConfig().NODE_ENV === 'development') {
|
||||
throw new Error('Cannot restart in development mode');
|
||||
throw new TranslatedError('server-messages.errors.not-allowed-in-dev');
|
||||
}
|
||||
|
||||
if (TipiConfig.getConfig().demoMode) {
|
||||
throw new Error('Cannot restart in demo mode');
|
||||
throw new TranslatedError('server-messages.errors.not-allowed-in-demo');
|
||||
}
|
||||
|
||||
TipiConfig.setConfig('status', 'RESTARTING');
|
||||
|
|
Loading…
Add table
Reference in a new issue