feat: move settings page to rsc
This commit is contained in:
parent
42edd1c4a4
commit
41e8270c33
15 changed files with 130 additions and 100 deletions
|
@ -1,39 +1,44 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
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';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
import { useSystemStore } from '../../../../state/systemStore';
|
||||
import { useDisclosure } from '@/client/hooks/useDisclosure';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useSystemStore } from '@/client/state/systemStore';
|
||||
import { useAction } from 'next-safe-action/hook';
|
||||
import { restartAction } from '@/actions/settings/restart';
|
||||
import { RestartModal } from '../RestartModal';
|
||||
|
||||
export const GeneralActions = () => {
|
||||
type Props = { version: { current: string; latest: string; body?: string | null } };
|
||||
|
||||
export const GeneralActions = (props: Props) => {
|
||||
const t = useTranslations();
|
||||
const versionQuery = trpc.system.getVersion.useQuery(undefined, { staleTime: 0 });
|
||||
const { version } = props;
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { setPollStatus } = useSystemStore();
|
||||
const restartDisclosure = useDisclosure();
|
||||
|
||||
const defaultVersion = '0.0.0';
|
||||
const isLatest = semver.gte(versionQuery.data?.current || defaultVersion, versionQuery.data?.latest || defaultVersion);
|
||||
const isLatest = semver.gte(version.current || defaultVersion, version.latest || defaultVersion);
|
||||
|
||||
const restart = trpc.system.restart.useMutation({
|
||||
onMutate: () => {
|
||||
const restartMutation = useAction(restartAction, {
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
setPollStatus(true);
|
||||
} else {
|
||||
restartDisclosure.close();
|
||||
setLoading(false);
|
||||
toast.error(data.failure.reason);
|
||||
}
|
||||
},
|
||||
onExecute: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setPollStatus(true);
|
||||
},
|
||||
onError: (e) => {
|
||||
restartDisclosure.close();
|
||||
setLoading(false);
|
||||
toast.error(t(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
|
||||
},
|
||||
});
|
||||
|
||||
const renderUpdate = () => {
|
||||
|
@ -43,7 +48,7 @@ export const GeneralActions = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{versionQuery.data?.body && (
|
||||
{version.body && (
|
||||
<div className="mt-3 card col-12 col-md-8">
|
||||
<div className="card-stamp">
|
||||
<div className="card-stamp-icon bg-yellow">
|
||||
|
@ -51,7 +56,7 @@ export const GeneralActions = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<Markdown className="">{versionQuery.data.body}</Markdown>
|
||||
<Markdown className="">{version.body}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -63,8 +68,8 @@ export const GeneralActions = () => {
|
|||
<>
|
||||
<div className="card-body">
|
||||
<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>
|
||||
<h3 className="card-title mt-4">{t('settings.actions.current-version', { version: version.current })}</h3>
|
||||
<p className="card-subtitle">{isLatest ? t('settings.actions.stay-up-to-date') : t('settings.actions.new-version', { version: version.latest })}</p>
|
||||
{renderUpdate()}
|
||||
<h3 className="card-title mt-4">{t('settings.actions.maintenance-title')}</h3>
|
||||
<p className="card-subtitle">{t('settings.actions.maintenance-subtitle')}</p>
|
||||
|
@ -72,7 +77,7 @@ export const GeneralActions = () => {
|
|||
<Button onClick={restartDisclosure.open}>{t('settings.actions.restart')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<RestartModal isOpen={restartDisclosure.isOpen} onClose={restartDisclosure.close} onConfirm={() => restart.mutate()} loading={loading} />
|
||||
<RestartModal isOpen={restartDisclosure.isOpen} onClose={restartDisclosure.close} onConfirm={() => restartMutation.execute()} loading={loading} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { Button } from '../../../../components/ui/Button';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
interface IProps {
|
||||
isOpen: boolean;
|
|
@ -0,0 +1,29 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export const SettingsTabTriggers = () => {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
|
||||
const handleTabChange = (newTab: string) => {
|
||||
router.push(`/settings?tab=${newTab}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsList>
|
||||
<TabsTrigger onClick={() => handleTabChange('actions')} value="actions">
|
||||
{t('settings.actions.tab-title')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => handleTabChange('settings')} value="settings">
|
||||
{t('settings.settings.tab-title')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => handleTabChange('security')} value="security">
|
||||
{t('settings.security.tab-title')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { SettingsTabTriggers } from './SettingsTabTriggers';
|
34
src/app/(dashboard)/settings/page.tsx
Normal file
34
src/app/(dashboard)/settings/page.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Tabs, TabsContent } from '@/components/ui/tabs';
|
||||
import { getTranslatorFromCookie } from '@/lib/get-translator';
|
||||
import { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
import { SystemServiceClass } from '@/server/services/system';
|
||||
import { SettingsTabTriggers } from './components/SettingsTabTriggers';
|
||||
import { GeneralActions } from './components/GeneralActions';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const translator = await getTranslatorFromCookie();
|
||||
|
||||
return {
|
||||
title: `${translator('settings.title')} - Tipi`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function SettingsPage({ searchParams }: { searchParams: { tab: string } }) {
|
||||
const { tab } = searchParams;
|
||||
const systemService = new SystemServiceClass();
|
||||
const version = await systemService.getVersion();
|
||||
|
||||
return (
|
||||
<div className="card d-flex">
|
||||
<Tabs defaultValue={(tab as string) || 'actions'}>
|
||||
<SettingsTabTriggers />
|
||||
<TabsContent value="actions">
|
||||
<GeneralActions version={version} />
|
||||
</TabsContent>
|
||||
<TabsContent value="settings">{/* <SettingsContainer /> */}</TabsContent>
|
||||
<TabsContent value="security">{/* <SecurityContainer /> */}</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
32
src/app/actions/settings/restart.ts
Normal file
32
src/app/actions/settings/restart.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { action } from '@/lib/safe-action';
|
||||
import { getUserFromCookie } from '@/server/common/session.helpers';
|
||||
import { SystemServiceClass } from '@/server/services/system';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { handleActionError } from '../utils/handle-action-error';
|
||||
|
||||
const input = z.void();
|
||||
|
||||
/**
|
||||
* Restarts the system
|
||||
*/
|
||||
export const restartAction = action(input, async () => {
|
||||
try {
|
||||
const user = await getUserFromCookie();
|
||||
|
||||
if (!user?.operator) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
|
||||
const systemService = new SystemServiceClass();
|
||||
await systemService.restart();
|
||||
|
||||
revalidatePath('/');
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return handleActionError(e);
|
||||
}
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { trpc } from '@/utils/trpc';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '../../../../../../tests/test-utils';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
|
||||
describe('Test: SettingsPage', () => {
|
||||
it('should render', async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await screen.findByTestId('settings-layout');
|
||||
});
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
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');
|
||||
const router = useRouter();
|
||||
const { tab } = router.query;
|
||||
|
||||
const handleTabChange = (newTab: string) => {
|
||||
router.push(`/settings?tab=${newTab}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout title={t('title')}>
|
||||
<div className="card d-flex">
|
||||
<Tabs defaultValue={(tab as string) || 'actions'}>
|
||||
<TabsList>
|
||||
<TabsTrigger onClick={() => handleTabChange('actions')} value="actions">
|
||||
{t('actions.tab-title')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => handleTabChange('settings')} value="settings">
|
||||
{t('settings.tab-title')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => handleTabChange('security')} value="security">
|
||||
{t('security.tab-title')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="actions">
|
||||
<GeneralActions />
|
||||
</TabsContent>
|
||||
<TabsContent value="settings">
|
||||
<SettingsContainer />
|
||||
</TabsContent>
|
||||
<TabsContent value="security">
|
||||
<SecurityContainer />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { SettingsPage } from './SettingsPage';
|
|
@ -1,14 +0,0 @@
|
|||
import { getAuthedPageProps, getMessagesPageProps } from '@/utils/page-helpers';
|
||||
import merge from 'lodash.merge';
|
||||
import { GetServerSideProps } from 'next';
|
||||
|
||||
export { SettingsPage as default } from '../client/modules/Settings/pages/SettingsPage';
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const authedProps = await getAuthedPageProps(ctx);
|
||||
const messagesProps = await getMessagesPageProps(ctx);
|
||||
|
||||
return merge(authedProps, messagesProps, {
|
||||
props: {},
|
||||
});
|
||||
};
|
|
@ -36,11 +36,12 @@ export class SystemServiceClass {
|
|||
const cache = new TipiCache('getVersion');
|
||||
try {
|
||||
const { seePreReleaseVersions } = TipiConfig.getConfig();
|
||||
const currentVersion = TipiConfig.getConfig().version;
|
||||
|
||||
if (seePreReleaseVersions) {
|
||||
const { data } = await axios.get<{ tag_name: string; body: string }[]>('https://api.github.com/repos/meienberger/runtipi/releases');
|
||||
|
||||
return { current: TipiConfig.getConfig().version, latest: data[0]?.tag_name, body: data[0]?.body };
|
||||
return { current: currentVersion, latest: data[0]?.tag_name ?? currentVersion, body: data[0]?.body };
|
||||
}
|
||||
|
||||
let version = await cache.get('latestVersion');
|
||||
|
|
Loading…
Add table
Reference in a new issue