瀏覽代碼

feat: move settings page to rsc

Nicolas Meienberger 1 年之前
父節點
當前提交
41e8270c33

+ 0 - 0
src/client/modules/Settings/containers/GeneralActions/GeneralActions.test.tsx → src/app/(dashboard)/settings/components/GeneralActions/GeneralActions.test.tsx


+ 29 - 24
src/client/modules/Settings/containers/GeneralActions/GeneralActions.tsx → src/app/(dashboard)/settings/components/GeneralActions/GeneralActions.tsx

@@ -1,38 +1,43 @@
+'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';
+
+type Props = { version: { current: string; latest: string; body?: string | null } };
 
-export const GeneralActions = () => {
+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: () => {
-      setLoading(true);
+  const restartMutation = useAction(restartAction, {
+    onSuccess: (data) => {
+      if (data.success) {
+        setPollStatus(true);
+      } else {
+        restartDisclosure.close();
+        setLoading(false);
+        toast.error(data.failure.reason);
+      }
     },
-    onSuccess: async () => {
-      setPollStatus(true);
-    },
-    onError: (e) => {
-      restartDisclosure.close();
-      setLoading(false);
-      toast.error(t(e.data?.tError.message as MessageKey, { ...e.data?.tError?.variables }));
+    onExecute: () => {
+      setLoading(true);
     },
   });
 
@@ -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} />
     </>
   );
 };

+ 0 - 0
src/client/modules/Settings/containers/GeneralActions/index.ts → src/app/(dashboard)/settings/components/GeneralActions/index.ts


+ 1 - 1
src/client/modules/Settings/components/RestartModal/RestartModal.tsx → src/app/(dashboard)/settings/components/RestartModal/RestartModal.tsx

@@ -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
src/client/modules/Settings/components/RestartModal/index.ts → src/app/(dashboard)/settings/components/RestartModal/index.ts


+ 29 - 0
src/app/(dashboard)/settings/components/SettingsTabTriggers/SettingsTabTriggers.tsx

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

+ 1 - 0
src/app/(dashboard)/settings/components/SettingsTabTriggers/index.ts

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

+ 34 - 0
src/app/(dashboard)/settings/page.tsx

@@ -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 - 0
src/app/actions/settings/restart.ts

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

+ 2 - 0
src/client/modules/Settings/containers/SettingsContainer/SettingsContainer.tsx

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

+ 0 - 11
src/client/modules/Settings/pages/SettingsPage/SettingsPage.test.tsx

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

+ 0 - 48
src/client/modules/Settings/pages/SettingsPage/SettingsPage.tsx

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

+ 0 - 1
src/client/modules/Settings/pages/SettingsPage/index.ts

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

+ 0 - 14
src/pages/settings.tsx

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

+ 2 - 1
src/server/services/system/system.service.ts

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