refactor: remove restart option from UI
This commit is contained in:
parent
bdfb019df2
commit
8e19c8b0e8
12 changed files with 7 additions and 227 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -1,6 +1,5 @@
|
|||
name: Tipi CI
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
export { RestartModal } from './RestartModal';
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { StatusProvider } from './StatusProvider';
|
|
@ -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 })),
|
||||
}));
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue