feat: move settings page to rsc

This commit is contained in:
Nicolas Meienberger 2023-10-01 15:02:02 +02:00
parent 42edd1c4a4
commit 41e8270c33
15 changed files with 130 additions and 100 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
export { SettingsTabTriggers } from './SettingsTabTriggers';

View 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>
);
}

View 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);
}
});

View file

@ -1,3 +1,5 @@
'use client';
import React, { useState } from 'react';
import { trpc } from '@/utils/trpc';
import { toast } from 'react-hot-toast';

View file

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

View file

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

View file

@ -1 +0,0 @@
export { SettingsPage } from './SettingsPage';

View file

@ -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: {},
});
};

View file

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