refactor: migrate client app queries/mutations to trpc
This commit is contained in:
parent
fa8f178433
commit
783c80714b
17 changed files with 594 additions and 437 deletions
|
@ -1,18 +1,24 @@
|
|||
import { AppCategoriesEnum } from '../generated/graphql';
|
||||
import { AppCategory } from './types';
|
||||
|
||||
export const APP_CATEGORIES = [
|
||||
{ name: 'Network', id: AppCategoriesEnum.Network, icon: 'FaNetworkWired' },
|
||||
{ name: 'Media', id: AppCategoriesEnum.Media, icon: 'FaVideo' },
|
||||
{ name: 'Development', id: AppCategoriesEnum.Development, icon: 'FaCode' },
|
||||
{ name: 'Automation', id: AppCategoriesEnum.Automation, icon: 'FaRobot' },
|
||||
{ name: 'Social', id: AppCategoriesEnum.Social, icon: 'FaUserFriends' },
|
||||
{ name: 'Utilities', id: AppCategoriesEnum.Utilities, icon: 'FaWrench' },
|
||||
{ name: 'Photography', id: AppCategoriesEnum.Photography, icon: 'FaCamera' },
|
||||
{ name: 'Security', id: AppCategoriesEnum.Security, icon: 'FaShieldAlt' },
|
||||
{ name: 'Featured', id: AppCategoriesEnum.Featured, icon: 'FaStar' },
|
||||
{ name: 'Books', id: AppCategoriesEnum.Books, icon: 'FaBook' },
|
||||
{ name: 'Data', id: AppCategoriesEnum.Data, icon: 'FaDatabase' },
|
||||
{ name: 'Music', id: AppCategoriesEnum.Music, icon: 'FaMusic' },
|
||||
{ name: 'Finance', id: AppCategoriesEnum.Finance, icon: 'FaMoneyBillAlt' },
|
||||
{ name: 'Gaming', id: AppCategoriesEnum.Gaming, icon: 'FaGamepad' },
|
||||
type AppCategoryEntry = {
|
||||
name: string;
|
||||
id: AppCategory;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
export const APP_CATEGORIES: AppCategoryEntry[] = [
|
||||
{ name: 'Network', id: 'network', icon: 'FaNetworkWired' },
|
||||
{ name: 'Media', id: 'media', icon: 'FaVideo' },
|
||||
{ name: 'Development', id: 'development', icon: 'FaCode' },
|
||||
{ name: 'Automation', id: 'automation', icon: 'FaRobot' },
|
||||
{ name: 'Social', id: 'social', icon: 'FaUserFriends' },
|
||||
{ name: 'Utilities', id: 'utilities', icon: 'FaWrench' },
|
||||
{ name: 'Photography', id: 'photography', icon: 'FaCamera' },
|
||||
{ name: 'Security', id: 'security', icon: 'FaShieldAlt' },
|
||||
{ name: 'Featured', id: 'featured', icon: 'FaStar' },
|
||||
{ name: 'Books', id: 'books', icon: 'FaBook' },
|
||||
{ name: 'Data', id: 'data', icon: 'FaDatabase' },
|
||||
{ name: 'Music', id: 'music', icon: 'FaMusic' },
|
||||
{ name: 'Finance', id: 'finance', icon: 'FaMoneyBillAlt' },
|
||||
{ name: 'Gaming', id: 'gaming', icon: 'FaGamepad' },
|
||||
];
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
import * as Router from '../../server/routers/_app';
|
||||
|
||||
export type RouterOutput = Router.RouterOutput;
|
||||
export type { FormField, AppInfo } from '../../server/services/apps/apps.helpers';
|
||||
export type { AppStatus, AppCategory } from '../../server/services/apps/apps.types';
|
||||
|
||||
export type App = Omit<Router.RouterOutput['app']['getApp'], 'info'>;
|
||||
export type AppWithInfo = Router.RouterOutput['app']['getApp'];
|
||||
|
||||
export interface IUser {
|
||||
name: string;
|
||||
email: string;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { App, AppCategoriesEnum, AppInfo, AppStatusEnum } from '../../generated/graphql';
|
||||
import { AppStatus, APP_CATEGORIES } from '../../../server/services/apps/apps.types';
|
||||
import { App, AppCategory, AppInfo, AppWithInfo } from '../../core/types';
|
||||
|
||||
const randomCategory = (): AppCategoriesEnum[] => {
|
||||
const categories = Object.values(AppCategoriesEnum);
|
||||
const randomCategory = (): AppCategory[] => {
|
||||
const categories = Object.values(APP_CATEGORIES);
|
||||
const randomIndex = faker.datatype.number({ min: 0, max: categories.length - 1 });
|
||||
return [categories[randomIndex] as AppCategoriesEnum];
|
||||
return [categories[randomIndex]!];
|
||||
};
|
||||
|
||||
export const createApp = (overrides?: Partial<AppInfo>): AppInfo => {
|
||||
|
@ -33,11 +34,11 @@ export const createApp = (overrides?: Partial<AppInfo>): AppInfo => {
|
|||
type CreateAppEntityParams = {
|
||||
overrides?: Omit<Partial<App>, 'info'>;
|
||||
overridesInfo?: Partial<AppInfo>;
|
||||
status?: AppStatusEnum;
|
||||
status?: AppStatus;
|
||||
};
|
||||
|
||||
export const createAppEntity = (params: CreateAppEntityParams) => {
|
||||
const { overrides, overridesInfo, status = AppStatusEnum.Running } = params;
|
||||
export const createAppEntity = (params: CreateAppEntityParams): AppWithInfo => {
|
||||
const { overrides, overridesInfo, status = 'running' } = params;
|
||||
|
||||
const id = faker.random.word().toLowerCase();
|
||||
const app = createApp({ id, ...overridesInfo });
|
||||
|
@ -47,9 +48,12 @@ export const createAppEntity = (params: CreateAppEntityParams) => {
|
|||
info: app,
|
||||
config: {},
|
||||
exposed: false,
|
||||
updateInfo: null,
|
||||
domain: null,
|
||||
version: 1,
|
||||
lastOpened: faker.date.past(),
|
||||
numOpened: 0,
|
||||
createdAt: faker.date.past(),
|
||||
updatedAt: faker.date.past(),
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -10,7 +10,6 @@ export type RpcSuccessResponse<Data> = {
|
|||
};
|
||||
|
||||
export type RpcErrorResponse = {
|
||||
id: null;
|
||||
error: {
|
||||
json: {
|
||||
message: string;
|
||||
|
@ -35,7 +34,6 @@ const jsonRpcSuccessResponse = (data: unknown): RpcSuccessResponse<any> => {
|
|||
};
|
||||
|
||||
const jsonRpcErrorResponse = (path: string, status: number, message: string): RpcErrorResponse => ({
|
||||
id: null,
|
||||
error: {
|
||||
json: {
|
||||
message,
|
||||
|
@ -49,31 +47,7 @@ const jsonRpcErrorResponse = (path: string, status: number, message: string): Rp
|
|||
},
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Mocks a TRPC endpoint and returns a msw handler for Storybook.
|
||||
* Only supports routes with two levels.
|
||||
* The path and response is fully typed and infers the type from your routes file.
|
||||
* @todo make it accept multiple endpoints
|
||||
* @param endpoint.path - path to the endpoint ex. ["post", "create"]
|
||||
* @param endpoint.response - response to return ex. {id: 1}
|
||||
* @param endpoint.type - specific type of the endpoint ex. "query" or "mutation" (defaults to "query")
|
||||
* @returns - msw endpoint
|
||||
* @example
|
||||
* Page.parameters = {
|
||||
msw: {
|
||||
handlers: [
|
||||
getTRPCMock({
|
||||
path: ["post", "getMany"],
|
||||
type: "query",
|
||||
response: [
|
||||
{ id: 0, title: "test" },
|
||||
{ id: 1, title: "test" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
*/
|
||||
|
||||
export const getTRPCMock = <
|
||||
K1 extends keyof RouterInput,
|
||||
K2 extends keyof RouterInput[K1], // object itself
|
||||
|
@ -88,7 +62,7 @@ export const getTRPCMock = <
|
|||
|
||||
const route = `http://localhost:3000/api/trpc/${endpoint.path[0]}.${endpoint.path[1] as string}`;
|
||||
|
||||
return fn(route, (req, res, ctx) => res(ctx.delay(endpoint.delay), ctx.json(jsonRpcSuccessResponse(endpoint.response))));
|
||||
return fn(route, (_, res, ctx) => res(ctx.delay(endpoint.delay), ctx.json(jsonRpcSuccessResponse(endpoint.response))));
|
||||
};
|
||||
|
||||
export const getTRPCMockError = <
|
||||
|
@ -104,7 +78,7 @@ export const getTRPCMockError = <
|
|||
|
||||
const route = `http://localhost:3000/api/trpc/${endpoint.path[0]}.${endpoint.path[1] as string}`;
|
||||
|
||||
return fn(route, (req, res, ctx) =>
|
||||
return fn(route, (_, res, ctx) =>
|
||||
res(ctx.delay(), ctx.json(jsonRpcErrorResponse(`${endpoint.path[0]}.${endpoint.path[1] as string}`, endpoint.status ?? 500, endpoint.message ?? 'Internal Server Error'))),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { createAppEntity } from './fixtures/app.fixtures';
|
||||
import { getTRPCMock } from './getTrpcMock';
|
||||
import appHandlers from './handlers/appHandlers';
|
||||
|
||||
const graphqlHandlers = [appHandlers.listApps, appHandlers.getApp, appHandlers.installedApps, appHandlers.installApp];
|
||||
import { createAppConfig } from '../../server/tests/apps.factory';
|
||||
|
||||
export const handlers = [
|
||||
getTRPCMock({
|
||||
|
@ -60,5 +59,20 @@ export const handlers = [
|
|||
type: 'query',
|
||||
response: true,
|
||||
}),
|
||||
...graphqlHandlers,
|
||||
// App
|
||||
getTRPCMock({
|
||||
path: ['app', 'getApp'],
|
||||
type: 'query',
|
||||
response: createAppEntity({ status: 'running' }),
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['app', 'installedApps'],
|
||||
type: 'query',
|
||||
response: [createAppEntity({ status: 'running' }), createAppEntity({ status: 'stopped' })],
|
||||
}),
|
||||
getTRPCMock({
|
||||
path: ['app', 'listApps'],
|
||||
type: 'query',
|
||||
response: { apps: [createAppConfig({}), createAppConfig({})], total: 2 },
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -1,173 +0,0 @@
|
|||
import { graphql } from 'msw';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createAppsRandomly } from '../fixtures/app.fixtures';
|
||||
import { AppInputType, AppStatusEnum, GetAppQuery, InstallAppMutation, InstalledAppsQuery, ListAppsQuery } from '../../generated/graphql';
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => value !== null && value !== undefined;
|
||||
|
||||
const removeDuplicates = <T extends { id: string }>(array: T[]) =>
|
||||
array.filter((a, i) => {
|
||||
const index = array.findIndex((_a) => _a.id === a.id);
|
||||
return index === i;
|
||||
});
|
||||
|
||||
export const mockedApps = removeDuplicates(createAppsRandomly(faker.datatype.number({ min: 20, max: 30 })));
|
||||
|
||||
export const mockInstalledAppIds = mockedApps.slice(0, faker.datatype.number({ min: 5, max: 8 })).map((a) => a.id);
|
||||
const stoppedAppsIds = mockInstalledAppIds.slice(0, faker.datatype.number({ min: 1, max: 3 }));
|
||||
|
||||
/**
|
||||
* GetApp handler
|
||||
*/
|
||||
const getApp = graphql.query('GetApp', (req, res, ctx) => {
|
||||
const { appId } = req.variables as { appId: string };
|
||||
|
||||
const app = mockedApps.find((a) => a.id === appId);
|
||||
|
||||
if (!app) {
|
||||
return res(ctx.errors([{ message: 'App not found' }]));
|
||||
}
|
||||
|
||||
const isInstalled = mockInstalledAppIds.includes(appId);
|
||||
|
||||
let status = AppStatusEnum.Missing;
|
||||
if (isInstalled) {
|
||||
status = AppStatusEnum.Running;
|
||||
}
|
||||
if (isInstalled && stoppedAppsIds.includes(appId)) {
|
||||
status = AppStatusEnum.Stopped;
|
||||
}
|
||||
|
||||
const result: GetAppQuery = {
|
||||
getApp: {
|
||||
id: app.id,
|
||||
status,
|
||||
info: app,
|
||||
__typename: 'App',
|
||||
config: {},
|
||||
exposed: false,
|
||||
updateInfo: null,
|
||||
domain: null,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const getAppError = graphql.query('GetApp', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
|
||||
|
||||
/**
|
||||
* ListApps handler
|
||||
*/
|
||||
const listApps = graphql.query('ListApps', async (req, res, ctx) => {
|
||||
const result: ListAppsQuery = {
|
||||
listAppsInfo: {
|
||||
apps: mockedApps,
|
||||
total: mockedApps.length,
|
||||
},
|
||||
};
|
||||
|
||||
await wait(100);
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const listAppsEmpty = graphql.query('ListApps', (req, res, ctx) => {
|
||||
const result: ListAppsQuery = {
|
||||
listAppsInfo: {
|
||||
apps: [],
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const listAppsError = graphql.query('ListApps', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
|
||||
|
||||
/**
|
||||
* InstalledApps handler
|
||||
*/
|
||||
const installedApps = graphql.query('InstalledApps', (req, res, ctx) => {
|
||||
const apps: InstalledAppsQuery['installedApps'] = mockInstalledAppIds
|
||||
.map((id) => {
|
||||
const app = mockedApps.find((a) => a.id === id);
|
||||
if (!app) return null;
|
||||
|
||||
let status = AppStatusEnum.Running;
|
||||
if (stoppedAppsIds.includes(id)) {
|
||||
status = AppStatusEnum.Stopped;
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'App' as const,
|
||||
id: app.id,
|
||||
status,
|
||||
config: {},
|
||||
info: app,
|
||||
version: 1,
|
||||
updateInfo: null,
|
||||
};
|
||||
})
|
||||
.filter(notEmpty);
|
||||
|
||||
const result: InstalledAppsQuery = {
|
||||
installedApps: apps,
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const installedAppsEmpty = graphql.query('InstalledApps', (req, res, ctx) => {
|
||||
const result: InstalledAppsQuery = {
|
||||
installedApps: [],
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const installedAppsError = graphql.query('InstalledApps', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
|
||||
|
||||
const installedAppsNoInfo = graphql.query('InstalledApps', (req, res, ctx) => {
|
||||
const result: InstalledAppsQuery = {
|
||||
installedApps: [
|
||||
{
|
||||
__typename: 'App' as const,
|
||||
id: 'app-id',
|
||||
status: AppStatusEnum.Running,
|
||||
config: {},
|
||||
info: null,
|
||||
version: 1,
|
||||
updateInfo: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
/**
|
||||
* Install app handler
|
||||
*/
|
||||
const installApp = graphql.mutation('InstallApp', (req, res, ctx) => {
|
||||
const { input } = req.variables as { input: AppInputType };
|
||||
|
||||
const app = mockedApps.find((a) => a.id === input.id);
|
||||
|
||||
if (!app) {
|
||||
return res(ctx.errors([{ message: 'App not found' }]));
|
||||
}
|
||||
|
||||
const result: InstallAppMutation = {
|
||||
installApp: {
|
||||
__typename: 'App' as const,
|
||||
id: app.id,
|
||||
status: AppStatusEnum.Running,
|
||||
},
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
export default { getApp, getAppError, listApps, listAppsEmpty, listAppsError, installedApps, installedAppsEmpty, installedAppsError, installedAppsNoInfo, installApp };
|
|
@ -1,18 +1,19 @@
|
|||
import React from 'react';
|
||||
import { render, screen, waitFor } from '../../../../../../tests/test-utils';
|
||||
import appHandlers from '../../../../mocks/handlers/appHandlers';
|
||||
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { AppStorePage } from './AppStorePage';
|
||||
|
||||
describe('Test: AppStorePage', () => {
|
||||
it('should render error state when error occurs', async () => {
|
||||
// Arrange
|
||||
server.use(appHandlers.listAppsError);
|
||||
server.use(getTRPCMockError({ path: ['app', 'listApps'], message: 'test error' }));
|
||||
render(<AppStorePage />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('An error occured')).toBeInTheDocument();
|
||||
expect(screen.getByText('test error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -46,7 +47,8 @@ describe('Test: AppStorePage', () => {
|
|||
|
||||
it('should render empty state when no apps are available', async () => {
|
||||
// Arrange
|
||||
server.use(appHandlers.listAppsEmpty);
|
||||
server.use(getTRPCMock({ path: ['app', 'listApps'], response: { apps: [], total: 0 } }));
|
||||
|
||||
render(<AppStorePage />);
|
||||
|
||||
// Assert
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import type { NextPage } from 'next';
|
||||
import clsx from 'clsx';
|
||||
import styles from './AppStorePage.module.scss';
|
||||
import { useListAppsQuery } from '../../../../generated/graphql';
|
||||
import { useAppStoreState } from '../../state/appStoreState';
|
||||
import { Input } from '../../../../components/ui/Input';
|
||||
import CategorySelector from '../../components/CategorySelector';
|
||||
|
@ -11,10 +10,10 @@ import { Layout } from '../../../../components/Layout';
|
|||
import { EmptyPage } from '../../../../components/ui/EmptyPage';
|
||||
import AppStoreContainer from '../../containers/AppStoreContainer';
|
||||
import { ErrorPage } from '../../../../components/ui/ErrorPage';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
|
||||
export const AppStorePage: NextPage = () => {
|
||||
const { loading, data, error } = useListAppsQuery();
|
||||
|
||||
const { data, isLoading, error } = trpc.app.listApps.useQuery();
|
||||
const { setCategory, setSearch, category, search, sort, sortDirection } = useAppStoreState();
|
||||
|
||||
const actions = (
|
||||
|
@ -24,15 +23,12 @@ export const AppStorePage: NextPage = () => {
|
|||
</div>
|
||||
);
|
||||
|
||||
const tableData = React.useMemo(
|
||||
() => sortTable({ data: data?.listAppsInfo.apps || [], col: sort, direction: sortDirection, category, search }),
|
||||
[data?.listAppsInfo.apps, sort, sortDirection, category, search],
|
||||
);
|
||||
const tableData = React.useMemo(() => sortTable({ data: data?.apps || [], col: sort, direction: sortDirection, category, search }), [data?.apps, sort, sortDirection, category, search]);
|
||||
|
||||
return (
|
||||
<Layout loading={loading && !data} title="App Store" actions={actions}>
|
||||
{(tableData.length > 0 || loading) && <AppStoreContainer loading={loading} apps={tableData} />}
|
||||
{tableData.length === 0 && <EmptyPage title="No app found" subtitle="Try to refine your search" />}
|
||||
<Layout title="App Store" actions={actions}>
|
||||
{(tableData.length > 0 || isLoading) && <AppStoreContainer loading={isLoading} apps={tableData} />}
|
||||
{tableData.length === 0 && !error && <EmptyPage title="No app found" subtitle="Try to refine your search" />}
|
||||
{error && <ErrorPage error={error.message} />}
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../../tests/test-utils';
|
||||
import { AppStatusEnum } from '../../../../generated/graphql';
|
||||
import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
|
||||
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { AppDetailsContainer } from './AppDetailsContainer';
|
||||
|
@ -12,7 +11,7 @@ describe('Test: AppDetailsContainer', () => {
|
|||
it('should render', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({});
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(app.info.short_desc)).toBeInTheDocument();
|
||||
|
@ -20,8 +19,8 @@ describe('Test: AppDetailsContainer', () => {
|
|||
|
||||
it('should display update button when update is available', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { updateInfo: { current: 2, latest: 3 } } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-update')).toBeInTheDocument();
|
||||
|
@ -29,9 +28,9 @@ describe('Test: AppDetailsContainer', () => {
|
|||
|
||||
it('should display install button when app is not installed', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
|
||||
const app = createAppEntity({ overrides: { status: 'missing' } });
|
||||
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-install')).toBeInTheDocument();
|
||||
|
@ -39,9 +38,9 @@ describe('Test: AppDetailsContainer', () => {
|
|||
|
||||
it('should display uninstall and start button when app is stopped', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Stopped } });
|
||||
const app = createAppEntity({ overrides: { status: 'stopped' } });
|
||||
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-remove')).toBeInTheDocument();
|
||||
|
@ -50,8 +49,8 @@ describe('Test: AppDetailsContainer', () => {
|
|||
|
||||
it('should display stop, open and settings buttons when app is running', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Running } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
const app = createAppEntity({ overrides: { status: 'running' } });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-stop')).toBeInTheDocument();
|
||||
|
@ -61,8 +60,8 @@ describe('Test: AppDetailsContainer', () => {
|
|||
|
||||
it('should not display update button when update is not available', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { updateInfo: { current: 3, latest: 3 } } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
const app = createAppEntity({ overrides: { version: 3 }, overridesInfo: { tipi_version: 3 } });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('action-button-update')).not.toBeInTheDocument();
|
||||
|
@ -71,7 +70,7 @@ describe('Test: AppDetailsContainer', () => {
|
|||
it('should not display open button when app has no_gui set to true', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overridesInfo: { no_gui: true } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('action-button-open')).not.toBeInTheDocument();
|
||||
|
@ -83,7 +82,7 @@ describe('Test: AppDetailsContainer', () => {
|
|||
// Arrange
|
||||
const app = createAppEntity({});
|
||||
const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const openButton = screen.getByTestId('action-button-open');
|
||||
|
@ -97,7 +96,7 @@ describe('Test: AppDetailsContainer', () => {
|
|||
// Arrange
|
||||
const app = createAppEntity({ overridesInfo: { https: true } });
|
||||
const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const openButton = screen.getByTestId('action-button-open');
|
||||
|
@ -109,35 +108,37 @@ describe('Test: AppDetailsContainer', () => {
|
|||
});
|
||||
|
||||
describe('Test: Install app', () => {
|
||||
const installFn = jest.fn();
|
||||
const fakeInstallHandler = graphql.mutation('InstallApp', (req, res, ctx) => {
|
||||
installFn(req.variables);
|
||||
return res(ctx.data({ installApp: { id: 'id', status: '', __typename: '' } }));
|
||||
});
|
||||
|
||||
it('should call install mutation when install form is submitted', async () => {
|
||||
it('should display toast success when install success', async () => {
|
||||
// Arrange
|
||||
server.use(fakeInstallHandler);
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
const app = createAppEntity({ overrides: { status: 'missing' } });
|
||||
server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app }));
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const installForm = screen.getByTestId('install-form');
|
||||
fireEvent.submit(installForm);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(installFn).toHaveBeenCalledWith({
|
||||
input: { id: app.id, form: {}, exposed: false, domain: '' },
|
||||
});
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].status).toEqual('success');
|
||||
expect(result.current.toasts[0].title).toEqual('App installed successfully');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when install mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(graphql.mutation('InstallApp', (req, res, ctx) => res(ctx.errors([{ message: 'my big error' }]))));
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
server.use(
|
||||
getTRPCMockError({
|
||||
path: ['app', 'installApp'],
|
||||
type: 'mutation',
|
||||
message: 'my big error',
|
||||
}),
|
||||
);
|
||||
|
||||
const app = createAppEntity({ overrides: { status: 'missing' } });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const installForm = screen.getByTestId('install-form');
|
||||
|
@ -149,5 +150,317 @@ describe('Test: AppDetailsContainer', () => {
|
|||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
|
||||
// Skipping because trpc.useContext is not working in tests
|
||||
it.skip('should put the app in installing state when install mutation is called', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const app = createAppEntity({ overrides: { status: 'missing' } });
|
||||
server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app, delay: 100 }));
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const installForm = screen.getByTestId('install-form');
|
||||
fireEvent.submit(installForm);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('installing')).toBeInTheDocument();
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Update app', () => {
|
||||
it('should display toast success when update success', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
||||
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const updateButton = screen.getByTestId('action-button-update');
|
||||
updateButton.click();
|
||||
const modalUpdateButton = screen.getByTestId('modal-update-button');
|
||||
modalUpdateButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].status).toEqual('success');
|
||||
expect(result.current.toasts[0].title).toEqual('App updated successfully');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when update mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
|
||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const updateButton = screen.getByTestId('action-button-update');
|
||||
updateButton.click();
|
||||
const modalUpdateButton = screen.getByTestId('modal-update-button');
|
||||
modalUpdateButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].description).toEqual('my big error');
|
||||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
|
||||
// Skipping because trpc.useContext is not working in tests
|
||||
it.skip('should put the app in updating state when update mutation is called', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
||||
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app, delay: 100 }));
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const updateButton = screen.getByTestId('action-button-update');
|
||||
updateButton.click();
|
||||
const modalUpdateButton = screen.getByTestId('modal-update-button');
|
||||
modalUpdateButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('updating')).toBeInTheDocument();
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Uninstall app', () => {
|
||||
it('should display toast success when uninstall success', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ status: 'stopped' });
|
||||
server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' } }));
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const uninstallButton = screen.getByTestId('action-button-remove');
|
||||
uninstallButton.click();
|
||||
const modalUninstallButton = screen.getByText('Uninstall');
|
||||
modalUninstallButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].status).toEqual('success');
|
||||
expect(result.current.toasts[0].title).toEqual('App uninstalled successfully');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when uninstall mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMockError({ path: ['app', 'uninstallApp'], type: 'mutation', message: 'my big error' }));
|
||||
const app = createAppEntity({ status: 'stopped' });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const uninstallButton = screen.getByTestId('action-button-remove');
|
||||
uninstallButton.click();
|
||||
const modalUninstallButton = screen.getByText('Uninstall');
|
||||
modalUninstallButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].description).toEqual('my big error');
|
||||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
|
||||
// Skipping because trpc.useContext is not working in tests
|
||||
it.skip('should put the app in uninstalling state when uninstall mutation is called', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const app = createAppEntity({ status: 'stopped' });
|
||||
server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' }, delay: 100 }));
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const uninstallButton = screen.getByTestId('action-button-remove');
|
||||
uninstallButton.click();
|
||||
const modalUninstallButton = screen.getByText('Uninstall');
|
||||
modalUninstallButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('uninstalling')).toBeInTheDocument();
|
||||
expect(screen.queryByText('installing')).not.toBeInTheDocument();
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Start app', () => {
|
||||
it('should display toast success when start success', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ status: 'stopped' });
|
||||
server.use(getTRPCMock({ path: ['app', 'startApp'], type: 'mutation', response: app }));
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const startButton = screen.getByTestId('action-button-start');
|
||||
startButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].status).toEqual('success');
|
||||
expect(result.current.toasts[0].title).toEqual('App started successfully');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when start mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMockError({ path: ['app', 'startApp'], type: 'mutation', message: 'my big error' }));
|
||||
const app = createAppEntity({ status: 'stopped' });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const startButton = screen.getByTestId('action-button-start');
|
||||
startButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].description).toEqual('my big error');
|
||||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
|
||||
// Skipping because trpc.useContext is not working in tests
|
||||
it.skip('should put the app in starting state when start mutation is called', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const app = createAppEntity({ status: 'stopped' });
|
||||
server.use(getTRPCMock({ path: ['app', 'startApp'], type: 'mutation', response: app, delay: 100 }));
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const startButton = screen.getByTestId('action-button-start');
|
||||
startButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('starting')).toBeInTheDocument();
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Stop app', () => {
|
||||
it('should display toast success when stop success', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ status: 'running' });
|
||||
server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const stopButton = screen.getByTestId('action-button-stop');
|
||||
stopButton.click();
|
||||
const modalStopButton = screen.getByTestId('modal-stop-button');
|
||||
modalStopButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].status).toEqual('success');
|
||||
expect(result.current.toasts[0].title).toEqual('App stopped successfully');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when stop mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMockError({ path: ['app', 'stopApp'], type: 'mutation', message: 'my big error' }));
|
||||
const app = createAppEntity({ status: 'running' });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const stopButton = screen.getByTestId('action-button-stop');
|
||||
stopButton.click();
|
||||
const modalStopButton = screen.getByTestId('modal-stop-button');
|
||||
modalStopButton.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].description).toEqual('my big error');
|
||||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
|
||||
// Skipping because trpc.useContext is not working in tests
|
||||
it.skip('should put the app in stopping state when stop mutation is called', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const app = createAppEntity({ status: 'running' });
|
||||
server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const stopButton = screen.getByTestId('action-button-stop');
|
||||
stopButton.click();
|
||||
const modalStopButton = screen.getByTestId('modal-stop-button');
|
||||
modalStopButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('stopping')).toBeInTheDocument();
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Update app config', () => {
|
||||
it('should display toast success when update config success', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
|
||||
server.use(getTRPCMock({ path: ['app', 'updateAppConfig'], type: 'mutation', response: app }));
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const configButton = screen.getByTestId('action-button-settings');
|
||||
configButton.click();
|
||||
const modalConfigButton = screen.getAllByText('Update');
|
||||
modalConfigButton[1]?.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].status).toEqual('success');
|
||||
expect(result.current.toasts[0].title).toEqual('App config updated successfully. Restart the app to apply the changes');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when update config mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(getTRPCMockError({ path: ['app', 'updateAppConfig'], type: 'mutation', message: 'my big error' }));
|
||||
const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
|
||||
render(<AppDetailsContainer app={app} />);
|
||||
|
||||
// Act
|
||||
const configButton = screen.getByTestId('action-button-settings');
|
||||
configButton.click();
|
||||
const modalConfigButton = screen.getAllByText('Update');
|
||||
modalConfigButton[1]?.click();
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].description).toEqual('my big error');
|
||||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,19 +3,6 @@ import { useDisclosure } from '../../../../hooks/useDisclosure';
|
|||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { AppLogo } from '../../../../components/AppLogo/AppLogo';
|
||||
import { AppStatus } from '../../../../components/AppStatus';
|
||||
import {
|
||||
App,
|
||||
AppInfo,
|
||||
AppStatusEnum,
|
||||
GetAppDocument,
|
||||
InstalledAppsDocument,
|
||||
useInstallAppMutation,
|
||||
useStartAppMutation,
|
||||
useStopAppMutation,
|
||||
useUninstallAppMutation,
|
||||
useUpdateAppConfigMutation,
|
||||
useUpdateAppMutation,
|
||||
} from '../../../../generated/graphql';
|
||||
import { AppActions } from '../../components/AppActions';
|
||||
import { AppDetailsTabs } from '../../components/AppDetailsTabs';
|
||||
import { InstallModal } from '../../components/InstallModal';
|
||||
|
@ -24,13 +11,15 @@ import { UninstallModal } from '../../components/UninstallModal';
|
|||
import { UpdateModal } from '../../components/UpdateModal';
|
||||
import { UpdateSettingsModal } from '../../components/UpdateSettingsModal';
|
||||
import { FormValues } from '../../components/InstallForm/InstallForm';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
import { AppRouterOutput } from '../../../../../server/routers/app/app.router';
|
||||
import { castAppConfig } from '../../helpers/castAppConfig';
|
||||
|
||||
interface IProps {
|
||||
app: Pick<App, 'id' | 'updateInfo' | 'config' | 'exposed' | 'domain' | 'status'>;
|
||||
info: AppInfo;
|
||||
app: AppRouterOutput['getApp'];
|
||||
}
|
||||
|
||||
export const AppDetailsContainer: React.FC<IProps> = ({ app, info }) => {
|
||||
export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
|
||||
const { addToast } = useToastStore();
|
||||
const installDisclosure = useDisclosure();
|
||||
const uninstallDisclosure = useDisclosure();
|
||||
|
@ -38,143 +27,154 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, info }) => {
|
|||
const updateDisclosure = useDisclosure();
|
||||
const updateSettingsDisclosure = useDisclosure();
|
||||
|
||||
// Mutations
|
||||
const [install] = useInstallAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }, { query: InstalledAppsDocument }] });
|
||||
const [update] = useUpdateAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const [uninstall] = useUninstallAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }, { query: InstalledAppsDocument }] });
|
||||
const [stop] = useStopAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const [start] = useStartAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const [updateConfig] = useUpdateAppConfigMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const updateAvailable = Number(app?.updateInfo?.current || 0) < Number(app?.updateInfo?.latest);
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
const invalidate = () => {
|
||||
utils.app.installedApps.invalidate();
|
||||
utils.app.getApp.invalidate({ id: app.id });
|
||||
};
|
||||
|
||||
const install = trpc.app.installApp.useMutation({
|
||||
onMutate: () => {
|
||||
utils.app.getApp.setData({ id: app.id }, { ...app, status: 'installing' });
|
||||
installDisclosure.close();
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
addToast({ title: 'App installed successfully', status: 'success' });
|
||||
},
|
||||
onError: (e) => {
|
||||
invalidate();
|
||||
addToast({ title: 'Install error', description: e.message, status: 'error' });
|
||||
},
|
||||
});
|
||||
|
||||
const uninstall = trpc.app.uninstallApp.useMutation({
|
||||
onMutate: () => {
|
||||
utils.app.getApp.setData({ id: app.id }, { ...app, status: 'uninstalling' });
|
||||
uninstallDisclosure.close();
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
addToast({ title: 'App uninstalled successfully', status: 'success' });
|
||||
},
|
||||
onError: (e) => addToast({ title: 'Uninstall error', description: e.message, status: 'error' }),
|
||||
});
|
||||
|
||||
const stop = trpc.app.stopApp.useMutation({
|
||||
onMutate: () => {
|
||||
utils.app.getApp.setData({ id: app.id }, { ...app, status: 'stopping' });
|
||||
stopDisclosure.close();
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
addToast({ title: 'App stopped successfully', status: 'success' });
|
||||
},
|
||||
onError: (e) => addToast({ title: 'Stop error', description: e.message, status: 'error' }),
|
||||
});
|
||||
|
||||
const update = trpc.app.updateApp.useMutation({
|
||||
onMutate: () => {
|
||||
utils.app.getApp.setData({ id: app.id }, { ...app, status: 'updating' });
|
||||
updateDisclosure.close();
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
addToast({ title: 'App updated successfully', status: 'success' });
|
||||
},
|
||||
onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
|
||||
});
|
||||
|
||||
const start = trpc.app.startApp.useMutation({
|
||||
onMutate: () => {
|
||||
utils.app.getApp.setData({ id: app.id }, { ...app, status: 'starting' });
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
addToast({ title: 'App started successfully', status: 'success' });
|
||||
},
|
||||
onError: (e) => addToast({ title: 'Start error', description: e.message, status: 'error' }),
|
||||
});
|
||||
|
||||
const updateConfig = trpc.app.updateAppConfig.useMutation({
|
||||
onMutate: () => updateSettingsDisclosure.close(),
|
||||
onSuccess: () => {
|
||||
invalidate();
|
||||
addToast({ title: 'App config updated successfully. Restart the app to apply the changes', status: 'success' });
|
||||
},
|
||||
onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
|
||||
});
|
||||
|
||||
const updateAvailable = Number(app?.version || 0) < Number(app?.info.tipi_version);
|
||||
|
||||
const handleInstallSubmit = async (values: FormValues) => {
|
||||
installDisclosure.close();
|
||||
const { exposed, domain, ...form } = values;
|
||||
|
||||
try {
|
||||
await install({
|
||||
variables: { input: { form, id: info.id, exposed: exposed || false, domain: domain || '' } },
|
||||
optimisticResponse: { installApp: { id: info.id, status: AppStatusEnum.Installing, __typename: 'App' } },
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
install.mutate({ id: app.id, form, exposed, domain });
|
||||
};
|
||||
|
||||
const handleUnistallSubmit = async () => {
|
||||
uninstallDisclosure.close();
|
||||
try {
|
||||
await uninstall({ variables: { id: info.id }, optimisticResponse: { uninstallApp: { id: info.id, status: AppStatusEnum.Uninstalling, __typename: 'App' } } });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
const handleUnistallSubmit = () => {
|
||||
uninstall.mutate({ id: app.id });
|
||||
};
|
||||
|
||||
const handleStopSubmit = async () => {
|
||||
stopDisclosure.close();
|
||||
try {
|
||||
await stop({ variables: { id: info.id }, optimisticResponse: { stopApp: { id: info.id, status: AppStatusEnum.Stopping, __typename: 'App' } } });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
const handleStopSubmit = () => {
|
||||
stop.mutate({ id: app.id });
|
||||
};
|
||||
|
||||
const handleStartSubmit = async () => {
|
||||
try {
|
||||
await start({ variables: { id: info.id }, optimisticResponse: { startApp: { id: info.id, status: AppStatusEnum.Starting, __typename: 'App' } } });
|
||||
} catch (e: unknown) {
|
||||
handleError(e);
|
||||
}
|
||||
start.mutate({ id: app.id });
|
||||
};
|
||||
|
||||
const handleUpdateSettingsSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
const { exposed, domain, ...form } = values;
|
||||
await updateConfig({ variables: { input: { form, id: info.id, exposed: exposed || false, domain: domain || '' } } });
|
||||
addToast({
|
||||
title: 'Success',
|
||||
description: 'App config updated successfully. Restart the app to apply the changes.',
|
||||
position: 'top',
|
||||
status: 'success',
|
||||
isClosable: true,
|
||||
});
|
||||
updateSettingsDisclosure.close();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
const { exposed, domain, ...form } = values;
|
||||
updateConfig.mutate({ id: app.id, form, exposed, domain });
|
||||
};
|
||||
|
||||
const handleUpdateSubmit = async () => {
|
||||
updateDisclosure.close();
|
||||
try {
|
||||
await update({ variables: { id: info.id }, optimisticResponse: { updateApp: { id: info.id, status: AppStatusEnum.Updating, __typename: 'App' } } });
|
||||
addToast({
|
||||
title: 'Success',
|
||||
description: 'App updated successfully',
|
||||
position: 'top',
|
||||
status: 'success',
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
update.mutate({ id: app.id });
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
const { https } = info;
|
||||
const { https } = app.info;
|
||||
const protocol = https ? 'https' : 'http';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Current domain
|
||||
const domain = window.location.hostname;
|
||||
window.open(`${protocol}://${domain}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
|
||||
window.open(`${protocol}://${domain}:${app.info.port}${app.info.url_suffix || ''}`, '_blank', 'noreferrer');
|
||||
}
|
||||
};
|
||||
|
||||
const newVersion = [app?.updateInfo?.dockerVersion ? `${app?.updateInfo?.dockerVersion}` : '', `(${String(app?.updateInfo?.latest)})`].join(' ');
|
||||
const newVersion = [app?.info.version ? `${app?.info.version}` : '', `(${String(app?.info.tipi_version)})`].join(' ');
|
||||
|
||||
return (
|
||||
<div className="card" data-testid="app-details">
|
||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} app={info} />
|
||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} app={info} />
|
||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} app={info} />
|
||||
<UpdateModal onConfirm={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} app={info} newVersion={newVersion} />
|
||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} info={app.info} />
|
||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} info={app.info} />
|
||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} info={app.info} />
|
||||
<UpdateModal onConfirm={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} info={app.info} newVersion={newVersion} />
|
||||
<UpdateSettingsModal
|
||||
onSubmit={handleUpdateSettingsSubmit}
|
||||
isOpen={updateSettingsDisclosure.isOpen}
|
||||
onClose={updateSettingsDisclosure.close}
|
||||
app={info}
|
||||
config={app?.config}
|
||||
info={app.info}
|
||||
config={castAppConfig(app?.config)}
|
||||
exposed={app?.exposed}
|
||||
domain={app?.domain || ''}
|
||||
/>
|
||||
<div className="card-header d-flex flex-column flex-md-row">
|
||||
<AppLogo id={info.id} size={130} alt={info.name} />
|
||||
<AppLogo id={app.id} size={130} alt={app.info.name} />
|
||||
<div className="w-100 d-flex flex-column ms-md-3 align-items-center align-items-md-start">
|
||||
<div>
|
||||
<span className="mt-1 me-1">Version: </span>
|
||||
<span className="badge bg-gray mt-2">{info?.version}</span>
|
||||
<span className="badge bg-gray mt-2">{app.info.version}</span>
|
||||
</div>
|
||||
{app.domain && (
|
||||
<a target="_blank" rel="noreferrer" className="mt-1" href={`https://${app.domain}`}>
|
||||
https://{app.domain}
|
||||
</a>
|
||||
)}
|
||||
<span className="mt-1 text-muted text-center mb-2">{info.short_desc}</span>
|
||||
<div className="mb-1">{app && app?.status !== AppStatusEnum.Missing && <AppStatus status={app.status} />}</div>
|
||||
<span className="mt-1 text-muted text-center mb-2">{app.info.short_desc}</span>
|
||||
<div className="mb-1">{app.status !== 'missing' && <AppStatus status={app.status} />}</div>
|
||||
<AppActions
|
||||
updateAvailable={updateAvailable}
|
||||
onUpdate={updateDisclosure.open}
|
||||
|
@ -185,12 +185,12 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, info }) => {
|
|||
onInstall={installDisclosure.open}
|
||||
onOpen={handleOpen}
|
||||
onStart={handleStartSubmit}
|
||||
app={info}
|
||||
status={app?.status}
|
||||
info={app.info}
|
||||
status={app.status}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AppDetailsTabs info={info} />
|
||||
<AppDetailsTabs info={app.info} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* This function takes an input of unknown type, checks if it is an object and not null,
|
||||
* and returns it as a record of unknown values, if it is not an object or is null, returns an empty object.
|
||||
*
|
||||
* @param {unknown} json - The input of unknown type.
|
||||
* @returns {Record<string, unknown>} - The input as a record of unknown values, or an empty object if the input is not an object or is null.
|
||||
*/
|
||||
export const castAppConfig = (json: unknown): Record<string, unknown> => {
|
||||
if (typeof json !== 'object' || json === null) {
|
||||
return {};
|
||||
}
|
||||
return json as Record<string, unknown>;
|
||||
};
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import { render, screen, waitFor } from '../../../../../../tests/test-utils';
|
||||
import { AppInfo } from '../../../../generated/graphql';
|
||||
import appHandlers, { mockedApps, mockInstalledAppIds } from '../../../../mocks/handlers/appHandlers';
|
||||
import { AppWithInfo } from '../../../../core/types';
|
||||
import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
|
||||
import { getTRPCMock } from '../../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { AppDetailsPage } from './AppDetailsPage';
|
||||
|
||||
describe('AppDetailsPage', () => {
|
||||
it('should render', async () => {
|
||||
// Arrange
|
||||
render(<AppDetailsPage appId={mockInstalledAppIds[0] as string} />);
|
||||
render(<AppDetailsPage appId="nothing" />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('app-details')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -16,10 +19,10 @@ describe('AppDetailsPage', () => {
|
|||
|
||||
it('should correctly pass the appId to the AppDetailsContainer', async () => {
|
||||
// Arrange
|
||||
const props = AppDetailsPage.getInitialProps?.({ query: { id: mockInstalledAppIds[0] } } as any);
|
||||
const props = AppDetailsPage.getInitialProps?.({ query: { id: 'random' } } as any);
|
||||
|
||||
// Assert
|
||||
expect(props).toHaveProperty('appId', mockInstalledAppIds[0]);
|
||||
expect(props).toHaveProperty('appId', 'random');
|
||||
});
|
||||
|
||||
it('should transform the appId to a string', async () => {
|
||||
|
@ -30,21 +33,15 @@ describe('AppDetailsPage', () => {
|
|||
expect(props).toHaveProperty('appId', '123');
|
||||
});
|
||||
|
||||
it('should render the error page when an error occurs', async () => {
|
||||
// Arrange
|
||||
server.use(appHandlers.getAppError);
|
||||
render(<AppDetailsPage appId={mockInstalledAppIds[0] as string} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('error-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('test-error')).toHaveTextContent('test-error');
|
||||
});
|
||||
|
||||
it('should set the breadcrumb prop of the Layout component to an array containing two elements with the correct name and href properties', async () => {
|
||||
// Arrange
|
||||
const app = mockedApps[0] as AppInfo;
|
||||
const app = createAppEntity({}) as AppWithInfo;
|
||||
server.use(
|
||||
getTRPCMock({
|
||||
path: ['app', 'getApp'],
|
||||
response: app,
|
||||
}),
|
||||
);
|
||||
render(<AppDetailsPage appId={app.id} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('app-details')).toBeInTheDocument();
|
||||
|
@ -58,7 +55,7 @@ describe('AppDetailsPage', () => {
|
|||
expect(breadcrumbs[0]).toHaveTextContent('Apps');
|
||||
expect(breadcrumbsLinks[0]).toHaveAttribute('href', '/apps');
|
||||
|
||||
expect(breadcrumbs[1]).toHaveTextContent(app.name);
|
||||
expect(breadcrumbs[1]).toHaveTextContent(app.info.name);
|
||||
expect(breadcrumbsLinks[1]).toHaveAttribute('href', `/apps/${app.id}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { NextPage } from 'next';
|
|||
import React from 'react';
|
||||
import { Layout } from '../../../../components/Layout';
|
||||
import { ErrorPage } from '../../../../components/ui/ErrorPage';
|
||||
import { useGetAppQuery } from '../../../../generated/graphql';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
import { AppDetailsContainer } from '../../containers/AppDetailsContainer/AppDetailsContainer';
|
||||
|
||||
interface IProps {
|
||||
|
@ -10,16 +10,17 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
|
||||
const { data, loading, error } = useGetAppQuery({ variables: { appId }, pollInterval: 3000 });
|
||||
const { data, error } = trpc.app.getApp.useQuery({ id: appId }, { refetchInterval: 3000 });
|
||||
|
||||
const breadcrumb = [
|
||||
{ name: 'Apps', href: '/apps' },
|
||||
{ name: data?.getApp.info?.name || '', href: `/apps/${appId}`, current: true },
|
||||
{ name: data?.info?.name || '', href: `/apps/${data?.id}`, current: true },
|
||||
];
|
||||
|
||||
// TODO: add loading state
|
||||
return (
|
||||
<Layout breadcrumbs={breadcrumb} loading={!data?.getApp && loading} title={data?.getApp.info?.name}>
|
||||
{data?.getApp.info && <AppDetailsContainer app={data?.getApp} info={data.getApp.info} />}
|
||||
<Layout title={data?.info.name} breadcrumbs={breadcrumb}>
|
||||
{data?.info && <AppDetailsContainer app={data} />}
|
||||
{error && <ErrorPage error={error.message} />}
|
||||
</Layout>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
|
||||
import appHandlers, { mockInstalledAppIds } from '../../../../mocks/handlers/appHandlers';
|
||||
import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
|
||||
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { AppsPage } from './AppsPage';
|
||||
|
||||
|
@ -20,6 +21,8 @@ jest.mock('next/router', () => {
|
|||
describe('AppsPage', () => {
|
||||
it('should render', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({});
|
||||
server.use(getTRPCMock({ path: ['app', 'installedApps'], response: [app] }));
|
||||
render(<AppsPage />);
|
||||
|
||||
// Assert
|
||||
|
@ -30,32 +33,36 @@ describe('AppsPage', () => {
|
|||
|
||||
it('should render all installed apps', async () => {
|
||||
// Arrange
|
||||
const app1 = createAppEntity({});
|
||||
const app2 = createAppEntity({});
|
||||
server.use(getTRPCMock({ path: ['app', 'installedApps'], response: [app1, app2] }));
|
||||
render(<AppsPage />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('apps-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Assert
|
||||
const displayedAppIds = screen.getAllByTestId(/app-tile-/);
|
||||
expect(displayedAppIds).toHaveLength(mockInstalledAppIds.length);
|
||||
expect(displayedAppIds).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Should not render app tile if app info is not available', async () => {
|
||||
it('Should not render app tile if app is not available', async () => {
|
||||
// Arrange
|
||||
server.use(appHandlers.installedAppsNoInfo);
|
||||
const app = createAppEntity({ overridesInfo: { available: false } });
|
||||
server.use(getTRPCMock({ path: ['app', 'installedApps'], response: [app] }));
|
||||
render(<AppsPage />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('apps-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId(/app-tile-/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppsPage - Empty', () => {
|
||||
beforeEach(() => {
|
||||
server.use(appHandlers.installedAppsEmpty);
|
||||
server.use(getTRPCMock({ path: ['app', 'installedApps'], response: [] }));
|
||||
});
|
||||
|
||||
it('should render empty page if no app is installed', async () => {
|
||||
|
@ -78,7 +85,7 @@ describe('AppsPage - Empty', () => {
|
|||
|
||||
// Act
|
||||
const actionButton = screen.getByTestId('empty-page-action');
|
||||
await fireEvent.click(actionButton);
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
// Assert
|
||||
expect(actionButton).toHaveTextContent('Go to app store');
|
||||
|
@ -87,17 +94,15 @@ describe('AppsPage - Empty', () => {
|
|||
});
|
||||
|
||||
describe('AppsPage - Error', () => {
|
||||
beforeEach(() => {
|
||||
server.use(appHandlers.installedAppsError);
|
||||
});
|
||||
|
||||
it('should render error page if an error occurs', async () => {
|
||||
// Arrange
|
||||
server.use(getTRPCMockError({ path: ['app', 'installedApps'], type: 'query', message: 'test-error' }));
|
||||
render(<AppsPage />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('error-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('test-error')).toHaveTextContent('test-error');
|
||||
expect(screen.queryByTestId('apps-list')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
@ -2,18 +2,19 @@ import React from 'react';
|
|||
import { useRouter } from 'next/router';
|
||||
import { NextPage } from 'next';
|
||||
import { AppTile } from '../../../../components/AppTile';
|
||||
import { InstalledAppsQuery, useInstalledAppsQuery } from '../../../../generated/graphql';
|
||||
import { Layout } from '../../../../components/Layout';
|
||||
import { EmptyPage } from '../../../../components/ui/EmptyPage';
|
||||
import { ErrorPage } from '../../../../components/ui/ErrorPage';
|
||||
import { trpc } from '../../../../utils/trpc';
|
||||
import { AppRouterOutput } from '../../../../../server/routers/app/app.router';
|
||||
|
||||
export const AppsPage: NextPage = () => {
|
||||
const { data, loading, error } = useInstalledAppsQuery({ pollInterval: 1000 });
|
||||
const { data, isLoading, error } = trpc.app.installedApps.useQuery();
|
||||
|
||||
const renderApp = (app: InstalledAppsQuery['installedApps'][0]) => {
|
||||
const updateAvailable = Number(app.updateInfo?.current) < Number(app.updateInfo?.latest);
|
||||
const renderApp = (app: AppRouterOutput['installedApps'][number]) => {
|
||||
const updateAvailable = Number(app.version) < Number(app.info.tipi_version);
|
||||
|
||||
if (app.info) return <AppTile key={app.id} app={app.info} status={app.status} updateAvailable={updateAvailable} />;
|
||||
if (app.info?.available) return <AppTile key={app.id} app={app.info} status={app.status} updateAvailable={updateAvailable} />;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -21,14 +22,14 @@ export const AppsPage: NextPage = () => {
|
|||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Layout loading={loading || !data?.installedApps} title="My Apps">
|
||||
<Layout title="My Apps">
|
||||
<div>
|
||||
{Boolean(data?.installedApps.length) && (
|
||||
{Boolean(data?.length) && (
|
||||
<div className="row row-cards" data-testid="apps-list">
|
||||
{data?.installedApps.map(renderApp)}
|
||||
{data?.map(renderApp)}
|
||||
</div>
|
||||
)}
|
||||
{!loading && data?.installedApps.length === 0 && (
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<EmptyPage title="No app installed" subtitle="Install an app from the app store to get started" onAction={() => router.push('/app-store')} actionLabel="Go to app store" />
|
||||
)}
|
||||
{error && <ErrorPage error={error.message} />}
|
||||
|
|
|
@ -17,9 +17,6 @@ let token: string | null = '';
|
|||
export const trpc = createTRPCNext<AppRouter>({
|
||||
config() {
|
||||
return {
|
||||
/**
|
||||
* @link https://trpc.io/docs/data-transformers
|
||||
*/
|
||||
transformer: superjson,
|
||||
links: [
|
||||
loggerLink({
|
||||
|
@ -40,8 +37,5 @@ export const trpc = createTRPCNext<AppRouter>({
|
|||
],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/ssr
|
||||
* */
|
||||
ssr: false,
|
||||
});
|
||||
|
|
|
@ -16,7 +16,8 @@ import { SystemStatus, useSystemStore } from '../client/state/systemStore';
|
|||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const { setDarkMode } = useUIStore();
|
||||
const { setStatus, setVersion } = useSystemStore();
|
||||
trpc.system.status.useQuery(undefined, { refetchInterval: 50000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
|
||||
|
||||
// trpc.system.status.useQuery(undefined, { refetchInterval: 1000, networkMode: 'online', onSuccess: (d) => setStatus(d.status || SystemStatus.RUNNING) });
|
||||
const version = trpc.system.getVersion.useQuery(undefined, { networkMode: 'online' });
|
||||
|
||||
useEffect(() => {
|
||||
|
|
Loading…
Add table
Reference in a new issue