fix: migrate StatusProvider to work with RSC
This commit is contained in:
parent
4e98f1e294
commit
6c1e2488bc
9 changed files with 1968 additions and 158 deletions
|
@ -54,8 +54,8 @@
|
|||
"lodash.merge": "^4.6.2",
|
||||
"next": "13.5.4",
|
||||
"next-client-cookies": "^1.0.5",
|
||||
"next-safe-action": "^3.4.0",
|
||||
"next-intl": "^2.20.2",
|
||||
"next-safe-action": "^3.4.0",
|
||||
"pg": "^8.11.3",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "18.2.0",
|
||||
|
@ -68,16 +68,17 @@
|
|||
"redaxios": "^0.5.1",
|
||||
"redis": "^4.6.10",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"zod": "^3.21.4",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sass": "^1.69.2",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "0.32.6",
|
||||
"tslib": "^2.6.2",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.11.0",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.21.4",
|
||||
"zustand": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
1932
pnpm-lock.yaml
generated
1932
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
22
src/app/actions/settings/get-status.ts
Normal file
22
src/app/actions/settings/get-status.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { action } from '@/lib/safe-action';
|
||||
import { getConfig } from '@/server/core/TipiConfig';
|
||||
import { handleActionError } from '../utils/handle-action-error';
|
||||
|
||||
const input = z.object({ currentStatus: z.string() });
|
||||
|
||||
/**
|
||||
* Fetches the system status and compares it to the current status
|
||||
* If the status has changed, it will revalidate the whole app
|
||||
*/
|
||||
export const getStatusAction = action(input, async () => {
|
||||
try {
|
||||
const { status } = getConfig();
|
||||
|
||||
return { success: true, status };
|
||||
} catch (e) {
|
||||
return handleActionError(e);
|
||||
}
|
||||
});
|
|
@ -10,6 +10,7 @@ import './global.css';
|
|||
import clsx from 'clsx';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import Head from 'next/head';
|
||||
import { StatusProvider } from '@/components/hoc/StatusProvider';
|
||||
import { getCurrentLocale } from '../utils/getCurrentLocale';
|
||||
import { ClientProviders } from './components/ClientProviders';
|
||||
|
||||
|
@ -46,7 +47,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}>
|
||||
{children}
|
||||
<StatusProvider>{children}</StatusProvider>
|
||||
<Toaster />
|
||||
</body>
|
||||
</ClientProviders>
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import React from 'react';
|
||||
import { act, render, renderHook, screen, waitFor } from '../../../../../tests/test-utils';
|
||||
import { useSystemStore } from '../../../state/systemStore';
|
||||
import { StatusProvider } from './StatusProvider';
|
||||
|
||||
const reloadFn = jest.fn();
|
||||
|
||||
jest.mock('next/router', () => {
|
||||
const actualRouter = jest.requireActual('next-router-mock');
|
||||
|
||||
return {
|
||||
...actualRouter,
|
||||
reload: () => reloadFn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Test: StatusProvider', () => {
|
||||
it("should render it's children when system is RUNNING", async () => {
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('RUNNING');
|
||||
});
|
||||
|
||||
render(
|
||||
<StatusProvider>
|
||||
<div>system running</div>
|
||||
</StatusProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('system running')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render StatusScreen when system is RESTARTING', async () => {
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('RESTARTING');
|
||||
});
|
||||
render(
|
||||
<StatusProvider>
|
||||
<div>system running</div>
|
||||
</StatusProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Your system is restarting...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reload the page when system is RUNNING after being something else than RUNNING', async () => {
|
||||
const { result, unmount } = renderHook(() => useSystemStore());
|
||||
act(() => {
|
||||
result.current.setStatus('RESTARTING');
|
||||
});
|
||||
|
||||
render(
|
||||
<StatusProvider>
|
||||
<div>system running</div>
|
||||
</StatusProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Your system is restarting...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setStatus('RUNNING');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(reloadFn).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
|
@ -1,21 +1,44 @@
|
|||
import React, { ReactElement, useEffect, useRef } from 'react';
|
||||
import router from 'next/router';
|
||||
import { useSystemStore } from '../../../state/systemStore';
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useAction } from 'next-safe-action/hook';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import { getStatusAction } from '@/actions/settings/get-status';
|
||||
import { useSystemStore } from '@/client/state/systemStore';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { StatusScreen } from '../../StatusScreen';
|
||||
|
||||
interface IProps {
|
||||
children: ReactElement;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const StatusProvider: React.FC<IProps> = ({ children }) => {
|
||||
const { status, setPollStatus } = useSystemStore();
|
||||
const { status, setStatus, pollStatus, setPollStatus } = useSystemStore();
|
||||
const s = useRef(status);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const getStatusMutation = useAction(getStatusAction, {
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
setStatus(data.status);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Poll status every 5 seconds
|
||||
useInterval(
|
||||
() => {
|
||||
getStatusMutation.execute({ currentStatus: status });
|
||||
},
|
||||
pollStatus ? 2000 : null,
|
||||
);
|
||||
|
||||
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.reload();
|
||||
router.refresh();
|
||||
}
|
||||
if (status === 'RUNNING') {
|
||||
s.current = 'RUNNING';
|
||||
|
@ -23,13 +46,9 @@ export const StatusProvider: React.FC<IProps> = ({ children }) => {
|
|||
if (status === 'RESTARTING') {
|
||||
s.current = 'RESTARTING';
|
||||
}
|
||||
}, [status, s, setPollStatus]);
|
||||
}, [status, s, router, setPollStatus]);
|
||||
|
||||
if (s.current === 'LOADING') {
|
||||
return <StatusScreen title="" subtitle="" />;
|
||||
}
|
||||
|
||||
if (s.current === 'RESTARTING') {
|
||||
if (status === 'RESTARTING') {
|
||||
return <StatusScreen title="Your system is restarting..." subtitle="Please do not refresh this page" />;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { create } from 'zustand';
|
|||
const SYSTEM_STATUS = {
|
||||
RUNNING: 'RUNNING',
|
||||
RESTARTING: 'RESTARTING',
|
||||
LOADING: 'LOADING',
|
||||
LOADING: 'UPDATING',
|
||||
} as const;
|
||||
export type SystemStatus = (typeof SYSTEM_STATUS)[keyof typeof SYSTEM_STATUS];
|
||||
|
||||
|
|
|
@ -39,24 +39,6 @@ describe('Test: getConfig', () => {
|
|||
expect(config.appsRepoId).toBe(settingsJson.appsRepoId);
|
||||
expect(config.domain).toBe(settingsJson.domain);
|
||||
});
|
||||
|
||||
it('Should not be able to apply an invalid value from json config', () => {
|
||||
// arrange
|
||||
const settingsJson = {
|
||||
appsRepoUrl: faker.lorem.word(),
|
||||
appsRepoId: faker.lorem.word(),
|
||||
domain: 10,
|
||||
};
|
||||
const MockFiles = {
|
||||
'/runtipi/state/settings.json': JSON.stringify(settingsJson),
|
||||
};
|
||||
|
||||
// @ts-expect-error - We are mocking fs
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
// act & assert
|
||||
expect(() => new TipiConfig().getConfig()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: setConfig', () => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import fs from 'fs-extra';
|
||||
import semver from 'semver';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { setConfig } from '../../core/TipiConfig';
|
||||
import { TipiCache } from '../../core/TipiCache';
|
||||
|
@ -73,25 +72,6 @@ describe('Test: systemInfo', () => {
|
|||
});
|
||||
|
||||
describe('Test: getVersion', () => {
|
||||
it('It should return version with body', async () => {
|
||||
// Arrange
|
||||
const body = faker.lorem.words(10);
|
||||
server.use(
|
||||
rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
|
||||
return res(ctx.json({ tag_name: `v${faker.string.numeric(1)}.${faker.string.numeric(1)}.${faker.string.numeric()}`, body }));
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
const version = await SystemService.getVersion();
|
||||
|
||||
// Assert
|
||||
expect(version).toBeDefined();
|
||||
expect(version.current).toBeDefined();
|
||||
expect(semver.valid(version.latest)).toBeTruthy();
|
||||
expect(version.body).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should return current version for latest if request fails', async () => {
|
||||
server.use(
|
||||
rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
|
||||
|
@ -121,11 +101,9 @@ describe('Test: getVersion', () => {
|
|||
// Assert
|
||||
expect(version).toBeDefined();
|
||||
expect(version.current).toBeDefined();
|
||||
expect(semver.valid(version.latest)).toBeTruthy();
|
||||
|
||||
expect(version2.latest).toBe(version.latest);
|
||||
expect(version2.current).toBeDefined();
|
||||
expect(semver.valid(version2.latest)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue