refactor: remove restart option from UI

This commit is contained in:
Nicolas Meienberger 2023-11-15 20:57:27 +01:00 committed by Nicolas Meienberger
parent bdfb019df2
commit 8e19c8b0e8
12 changed files with 7 additions and 227 deletions

View file

@ -1,6 +1,5 @@
name: Tipi CI
on:
push:
pull_request:
env:

View file

@ -2,16 +2,10 @@
import React from 'react';
import semver from 'semver';
import { toast } from 'react-hot-toast';
import { Markdown } from '@/components/Markdown';
import { IconStar } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
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 } };
@ -19,29 +13,9 @@ export const GeneralActions = (props: Props) => {
const t = useTranslations();
const { version } = props;
const [loading, setLoading] = React.useState(false);
const { setPollStatus, setStatus } = useSystemStore();
const restartDisclosure = useDisclosure();
const defaultVersion = '0.0.0';
const isLatest = semver.gte(version.current || defaultVersion, version.latest || defaultVersion);
const restartMutation = useAction(restartAction, {
onSuccess: (data) => {
if (data.success) {
setPollStatus(true);
setStatus('RESTARTING');
} else {
restartDisclosure.close();
setLoading(false);
toast.error(data.failure.reason);
}
},
onExecute: () => {
setLoading(true);
},
});
const renderUpdate = () => {
if (isLatest) {
return <Button disabled>{t('settings.actions.already-latest')}</Button>;
@ -66,19 +40,11 @@ export const GeneralActions = (props: Props) => {
};
return (
<>
<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: 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>
<div>
<Button onClick={restartDisclosure.open}>{t('settings.actions.restart')}</Button>
</div>
</div>
<RestartModal isOpen={restartDisclosure.isOpen} onClose={restartDisclosure.close} onConfirm={() => restartMutation.execute()} loading={loading} />
</>
<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: 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()}
</div>
);
};

View file

@ -1,28 +0,0 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Button } from '@/components/ui/Button';
interface IProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
loading: boolean;
}
export const RestartModal: React.FC<IProps> = ({ isOpen, onClose, onConfirm, loading }) => (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent type="danger" size="sm">
<DialogHeader>
<h5 className="modal-title">Restart Tipi</h5>
</DialogHeader>
<DialogDescription>
<div className="text-muted">Would you like to restart your Tipi server?</div>
</DialogDescription>
<DialogFooter>
<Button onClick={onConfirm} className="btn-danger" loading={loading}>
Restart
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

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

View file

@ -1,32 +0,0 @@
'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,15 +0,0 @@
import { TipiCache } from '@/server/core/TipiCache';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const cache = new TipiCache('getStatus');
const status = await cache.get('status');
await cache.close();
return Response.json({ success: true, status: status || 'RUNNING' });
} catch (error) {
return Response.json({ success: false, status: 'ERROR', error });
}
}

View file

@ -9,7 +9,6 @@ import { NextIntlClientProvider } from 'next-intl';
import './global.css';
import clsx from 'clsx';
import { Toaster } from 'react-hot-toast';
import { StatusProvider } from '@/components/hoc/StatusProvider';
import { getCurrentLocale } from '../utils/getCurrentLocale';
import { ClientProviders } from './components/ClientProviders';
@ -37,7 +36,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<NextIntlClientProvider locale={locale} messages={mergedMessages}>
<ClientProviders initialTheme={theme?.value} cookies={cookies().getAll()}>
<body data-bs-theme={theme?.value}>
<StatusProvider>{children}</StatusProvider>
{children}
<Toaster />
</body>
</ClientProviders>

View file

@ -1,52 +0,0 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { SystemStatus, useSystemStore } from '@/client/state/systemStore';
import { useRouter } from 'next/navigation';
import useSWR, { Fetcher } from 'swr';
import { StatusScreen } from '../../StatusScreen';
interface IProps {
children: React.ReactNode;
}
const fetcher: Fetcher<{ status?: SystemStatus; success?: boolean }> = () =>
fetch('/api/get-status', { cache: 'no-store', next: { revalidate: 0 } }).then((res) => res.json() as Promise<{ status: SystemStatus }>);
export const StatusProvider: React.FC<IProps> = ({ children }) => {
const { status, setStatus, pollStatus, setPollStatus } = useSystemStore();
const s = useRef(status);
const router = useRouter();
useSWR('/api/get-status', fetcher, {
refreshInterval: pollStatus ? 2000 : 0,
isPaused: () => !pollStatus,
onSuccess: (res) => {
if (res.success && res.status) {
setStatus(res.status);
}
},
});
useEffect(() => {
// If previous was not running and current is running, we need to refresh the page
if (status === 'RUNNING' && s.current !== 'RUNNING') {
setPollStatus(false);
router.push('/');
router.refresh();
}
if (status === 'RUNNING') {
s.current = 'RUNNING';
}
if (status === 'RESTARTING') {
s.current = 'RESTARTING';
}
}, [status, s, router, setPollStatus]);
if (status === 'RESTARTING') {
return <StatusScreen title="Your system is restarting..." subtitle="Please do not refresh this page" />;
}
return children;
};

View file

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

View file

@ -1,17 +1,8 @@
import { create } from 'zustand';
const SYSTEM_STATUS = {
RUNNING: 'RUNNING',
RESTARTING: 'RESTARTING',
LOADING: 'UPDATING',
} as const;
export type SystemStatus = (typeof SYSTEM_STATUS)[keyof typeof SYSTEM_STATUS];
type Store = {
status: SystemStatus;
pollStatus: boolean;
version: { current: string; latest?: string };
setStatus: (status: SystemStatus) => void;
setVersion: (version: { current: string; latest?: string }) => void;
setPollStatus: (pollStatus: boolean) => void;
};
@ -20,7 +11,6 @@ export const useSystemStore = create<Store>((set) => ({
status: 'RUNNING',
version: { current: '0.0.0', latest: '0.0.0' },
pollStatus: false,
setStatus: (status: SystemStatus) => set((state) => ({ ...state, status })),
setVersion: (version: { current: string; latest?: string }) => set((state) => ({ ...state, version })),
setPollStatus: (pollStatus: boolean) => set((state) => ({ ...state, pollStatus })),
}));

View file

@ -106,21 +106,3 @@ describe('Test: getVersion', () => {
expect(version2.current).toBeDefined();
});
});
describe('Test: restart', () => {
it('Should return true', async () => {
// Act
const restart = await SystemService.restart();
// Assert
expect(restart).toBeTruthy();
});
it('should throw an error in demo mode', async () => {
// Arrange
await setConfig('demoMode', true);
// Act & Assert
await expect(SystemService.restart()).rejects.toThrow('server-messages.errors.not-allowed-in-demo');
});
});

View file

@ -1,15 +1,10 @@
import { z } from 'zod';
import axios from 'redaxios';
import { TranslatedError } from '@/server/utils/errors';
import { EventDispatcher } from '@/server/core/EventDispatcher';
import { TipiCache } from '@/server/core/TipiCache';
import { readJsonFile } from '../../common/fs.helpers';
import { Logger } from '../../core/Logger';
import { getConfig } from '../../core/TipiConfig';
const SYSTEM_STATUS = ['UPDATING', 'RESTARTING', 'RUNNING'] as const;
type SystemStatus = (typeof SYSTEM_STATUS)[keyof typeof SYSTEM_STATUS];
const systemInfoSchema = z.object({
cpu: z.object({
load: z.number().default(0),
@ -74,26 +69,4 @@ export class SystemServiceClass {
return info.data;
};
public restart = async (): Promise<boolean> => {
if (getConfig().NODE_ENV === 'development') {
throw new TranslatedError('server-messages.errors.not-allowed-in-dev');
}
if (getConfig().demoMode) {
throw new TranslatedError('server-messages.errors.not-allowed-in-demo');
}
const cache = new TipiCache('restart');
await cache.set('status', 'RESTARTING', 360);
await cache.close();
const dispatcher = new EventDispatcher('restart');
await dispatcher.close();
throw new Error('Implement restart');
};
public static status = (): { status: SystemStatus } => ({
status: getConfig().status,
});
}