Compare commits

...

3 commits

Author SHA1 Message Date
Nicolas Meienberger
1d8f608936 refactor(app-actions): correctly revalidatePath after each action 2023-11-12 12:30:32 +01:00
Nicolas Meienberger
900c31ec19 refactor(app.queries): automatically inject db dependency 2023-11-12 12:29:29 +01:00
Nicolas Meienberger
c736fff601 refactor(AppDetails): move handlers to onExecute 2023-11-12 12:29:06 +01:00
10 changed files with 101 additions and 104 deletions

View file

@ -1,4 +1,4 @@
APPS_REPO_ID=7a92c8307e0a8074763c80be1fcfa4f87da6641daea9211aea6743b0116aba3b APPS_REPO_ID=29ca930bfdaffa1dfabf5726336380ede7066bc53297e3c0c868b27c97282903
APPS_REPO_URL=https://github.com/runtipi/runtipi-appstore APPS_REPO_URL=https://github.com/runtipi/runtipi-appstore
TZ=Etc/UTC TZ=Etc/UTC
INTERNAL_IP=localhost INTERNAL_IP=localhost

View file

@ -23,7 +23,6 @@ import { UpdateModal } from '../UpdateModal';
import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal'; import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal';
import { AppActions } from '../AppActions'; import { AppActions } from '../AppActions';
import { AppDetailsTabs } from '../AppDetailsTabs'; import { AppDetailsTabs } from '../AppDetailsTabs';
import { FormValues } from '../InstallForm';
interface IProps { interface IProps {
app: Awaited<ReturnType<AppService['getApp']>>; app: Awaited<ReturnType<AppService['getApp']>>;
@ -42,6 +41,10 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
const updateSettingsDisclosure = useDisclosure(); const updateSettingsDisclosure = useDisclosure();
const installMutation = useAction(installAppAction, { const installMutation = useAction(installAppAction, {
onExecute: () => {
setCustomStatus('installing');
installDisclosure.close();
},
onSuccess: (data) => { onSuccess: (data) => {
if (!data.success) { if (!data.success) {
setCustomStatus(app.status); setCustomStatus(app.status);
@ -54,6 +57,10 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
}); });
const uninstallMutation = useAction(uninstallAppAction, { const uninstallMutation = useAction(uninstallAppAction, {
onExecute: () => {
setCustomStatus('uninstalling');
uninstallDisclosure.close();
},
onSuccess: (data) => { onSuccess: (data) => {
if (!data.success) { if (!data.success) {
setCustomStatus(app.status); setCustomStatus(app.status);
@ -66,6 +73,10 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
}); });
const stopMutation = useAction(stopAppAction, { const stopMutation = useAction(stopAppAction, {
onExecute: () => {
setCustomStatus('stopping');
stopDisclosure.close();
},
onSuccess: (data) => { onSuccess: (data) => {
if (!data.success) { if (!data.success) {
setCustomStatus(app.status); setCustomStatus(app.status);
@ -78,6 +89,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
}); });
const startMutation = useAction(startAppAction, { const startMutation = useAction(startAppAction, {
onExecute: () => {
setCustomStatus('starting');
},
onSuccess: (data) => { onSuccess: (data) => {
if (!data.success) { if (!data.success) {
setCustomStatus(app.status); setCustomStatus(app.status);
@ -90,6 +104,10 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
}); });
const updateMutation = useAction(updateAppAction, { const updateMutation = useAction(updateAppAction, {
onExecute: () => {
setCustomStatus('updating');
updateDisclosure.close();
},
onSuccess: (data) => { onSuccess: (data) => {
if (!data.success) { if (!data.success) {
setCustomStatus(app.status); setCustomStatus(app.status);
@ -102,6 +120,9 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
}); });
const updateConfigMutation = useAction(updateAppConfigAction, { const updateConfigMutation = useAction(updateAppConfigAction, {
onExecute: () => {
updateSettingsDisclosure.close();
},
onSuccess: (data) => { onSuccess: (data) => {
if (!data.success) { if (!data.success) {
toast.error(data.failure.reason); toast.error(data.failure.reason);
@ -113,40 +134,6 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0); const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);
const handleInstallSubmit = async (values: FormValues) => {
setCustomStatus('installing');
installDisclosure.close();
installMutation.execute({ id: app.id, form: values });
};
const handleUnistallSubmit = () => {
setCustomStatus('uninstalling');
uninstallDisclosure.close();
uninstallMutation.execute({ id: app.id });
};
const handleStopSubmit = () => {
setCustomStatus('stopping');
stopDisclosure.close();
stopMutation.execute({ id: app.id });
};
const handleStartSubmit = async () => {
setCustomStatus('starting');
startMutation.execute({ id: app.id });
};
const handleUpdateSettingsSubmit = async (values: FormValues) => {
updateSettingsDisclosure.close();
updateConfigMutation.execute({ id: app.id, form: values });
};
const handleUpdateSubmit = async () => {
setCustomStatus('updating');
updateDisclosure.close();
updateMutation.execute({ id: app.id });
};
const handleOpen = (type: OpenType) => { const handleOpen = (type: OpenType) => {
let url = ''; let url = '';
const { https } = app.info; const { https } = app.info;
@ -173,12 +160,12 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
return ( return (
<div className="card" data-testid="app-details"> <div className="card" data-testid="app-details">
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} info={app.info} /> <InstallModal onSubmit={(form) => installMutation.execute({ id: app.id, form })} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} info={app.info} />
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} info={app.info} /> <StopModal onConfirm={() => stopMutation.execute({ id: app.id })} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} info={app.info} />
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} info={app.info} /> <UninstallModal onConfirm={() => uninstallMutation.execute({ id: app.id })} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} info={app.info} />
<UpdateModal onConfirm={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} info={app.info} newVersion={newVersion} /> <UpdateModal onConfirm={() => updateMutation.execute({ id: app.id })} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} info={app.info} newVersion={newVersion} />
<UpdateSettingsModal <UpdateSettingsModal
onSubmit={handleUpdateSettingsSubmit} onSubmit={(form) => updateConfigMutation.execute({ id: app.id, form })}
isOpen={updateSettingsDisclosure.isOpen} isOpen={updateSettingsDisclosure.isOpen}
onClose={updateSettingsDisclosure.close} onClose={updateSettingsDisclosure.close}
info={app.info} info={app.info}
@ -203,7 +190,7 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
onUninstall={uninstallDisclosure.open} onUninstall={uninstallDisclosure.open}
onInstall={installDisclosure.open} onInstall={installDisclosure.open}
onOpen={handleOpen} onOpen={handleOpen}
onStart={handleStartSubmit} onStart={() => startMutation.execute({ id: app.id })}
app={app} app={app}
status={customStatus} status={customStatus}
/> />

View file

@ -1,7 +1,6 @@
'use server'; 'use server';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/server/db';
import { action } from '@/lib/safe-action'; import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { AppServiceClass } from '@/server/services/apps/apps.service'; import { AppServiceClass } from '@/server/services/apps/apps.service';
@ -19,16 +18,15 @@ const input = z.object({
*/ */
export const installAppAction = action(input, async ({ id, form }) => { export const installAppAction = action(input, async ({ id, form }) => {
try { try {
const appsService = new AppServiceClass(db); const appsService = new AppServiceClass();
await appsService.installApp(id, form); await appsService.installApp(id, form);
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
return { success: true }; return { success: true };
} catch (e) { } catch (e) {
return handleActionError(e); return await handleActionError(e);
} finally {
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
} }
}); });

View file

@ -1,7 +1,6 @@
'use server'; 'use server';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/server/db';
import { action } from '@/lib/safe-action'; import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { AppServiceClass } from '@/server/services/apps/apps.service'; import { AppServiceClass } from '@/server/services/apps/apps.service';
@ -14,16 +13,15 @@ const input = z.object({ id: z.string() });
*/ */
export const startAppAction = action(input, async ({ id }) => { export const startAppAction = action(input, async ({ id }) => {
try { try {
const appsService = new AppServiceClass(db); const appsService = new AppServiceClass();
await appsService.startApp(id); await appsService.startApp(id);
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
return { success: true }; return { success: true };
} catch (e) { } catch (e) {
return handleActionError(e); return await handleActionError(e);
} finally {
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
} }
}); });

View file

@ -1,7 +1,6 @@
'use server'; 'use server';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/server/db';
import { action } from '@/lib/safe-action'; import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { AppServiceClass } from '@/server/services/apps/apps.service'; import { AppServiceClass } from '@/server/services/apps/apps.service';
@ -14,16 +13,15 @@ const input = z.object({ id: z.string() });
*/ */
export const stopAppAction = action(input, async ({ id }) => { export const stopAppAction = action(input, async ({ id }) => {
try { try {
const appsService = new AppServiceClass(db); const appsService = new AppServiceClass();
await appsService.stopApp(id); await appsService.stopApp(id);
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
return { success: true }; return { success: true };
} catch (e) { } catch (e) {
return handleActionError(e); return await handleActionError(e);
} finally {
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
} }
}); });

View file

@ -1,7 +1,6 @@
'use server'; 'use server';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/server/db';
import { action } from '@/lib/safe-action'; import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { AppServiceClass } from '@/server/services/apps/apps.service'; import { AppServiceClass } from '@/server/services/apps/apps.service';
@ -14,16 +13,15 @@ const input = z.object({ id: z.string() });
*/ */
export const uninstallAppAction = action(input, async ({ id }) => { export const uninstallAppAction = action(input, async ({ id }) => {
try { try {
const appsService = new AppServiceClass(db); const appsService = new AppServiceClass();
await appsService.uninstallApp(id); await appsService.uninstallApp(id);
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
return { success: true }; return { success: true };
} catch (e) { } catch (e) {
return handleActionError(e); return await handleActionError(e);
} finally {
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
} }
}); });

View file

@ -1,7 +1,6 @@
'use server'; 'use server';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/server/db';
import { action } from '@/lib/safe-action'; import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { AppServiceClass } from '@/server/services/apps/apps.service'; import { AppServiceClass } from '@/server/services/apps/apps.service';
@ -14,16 +13,15 @@ const input = z.object({ id: z.string() });
*/ */
export const updateAppAction = action(input, async ({ id }) => { export const updateAppAction = action(input, async ({ id }) => {
try { try {
const appsService = new AppServiceClass(db); const appsService = new AppServiceClass();
await appsService.updateApp(id); await appsService.updateApp(id);
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
return { success: true }; return { success: true };
} catch (e) { } catch (e) {
return handleActionError(e); return await handleActionError(e);
} finally {
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
} }
}); });

View file

@ -1,7 +1,6 @@
'use server'; 'use server';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '@/server/db';
import { action } from '@/lib/safe-action'; import { action } from '@/lib/safe-action';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { AppServiceClass } from '@/server/services/apps/apps.service'; import { AppServiceClass } from '@/server/services/apps/apps.service';
@ -21,16 +20,15 @@ const input = z.object({
*/ */
export const updateAppConfigAction = action(input, async ({ id, form }) => { export const updateAppConfigAction = action(input, async ({ id, form }) => {
try { try {
const appsService = new AppServiceClass(db); const appsService = new AppServiceClass();
await appsService.updateAppConfig(id, form); await appsService.updateAppConfig(id, form);
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
return { success: true }; return { success: true };
} catch (e) { } catch (e) {
return handleActionError(e); return await handleActionError(e);
} finally {
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
} }
}); });

View file

@ -1,11 +1,11 @@
import { and, asc, eq, ne, notInArray } from 'drizzle-orm'; import { and, asc, eq, ne, notInArray } from 'drizzle-orm';
import { Database } from '@/server/db'; import { Database, db } from '@/server/db';
import { appTable, NewApp, AppStatus } from '../../db/schema'; import { appTable, NewApp, AppStatus } from '../../db/schema';
export class AppQueries { export class AppQueries {
private db; private db;
constructor(p: Database) { constructor(p: Database = db) {
this.db = p; this.db = p;
} }

View file

@ -6,6 +6,7 @@ import { Database } from '@/server/db';
import { AppInfo } from '@runtipi/shared'; import { AppInfo } from '@runtipi/shared';
import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher'; import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher';
import { castAppConfig } from '@/lib/helpers/castAppConfig'; import { castAppConfig } from '@/lib/helpers/castAppConfig';
import { revalidatePath } from 'next/cache';
import { checkAppRequirements, getAvailableApps, getAppInfo, getUpdateInfo } from './apps.helpers'; import { checkAppRequirements, getAvailableApps, getAppInfo, getUpdateInfo } from './apps.helpers';
import { getConfig } from '../../core/TipiConfig'; import { getConfig } from '../../core/TipiConfig';
import { Logger } from '../../core/Logger'; import { Logger } from '../../core/Logger';
@ -32,7 +33,7 @@ const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(fi
export class AppServiceClass { export class AppServiceClass {
private queries; private queries;
constructor(p: Database) { constructor(p?: Database) {
this.queries = new AppQueries(p); this.queries = new AppQueries(p);
} }
@ -76,29 +77,34 @@ export class AppServiceClass {
* This function starts an app specified by its appName, regenerates its environment file and checks for missing requirements. * This function starts an app specified by its appName, regenerates its environment file and checks for missing requirements.
* It updates the app's status in the database to 'starting' and 'running' if the start process is successful, otherwise it updates the status to 'stopped'. * It updates the app's status in the database to 'starting' and 'running' if the start process is successful, otherwise it updates the status to 'stopped'.
* *
* @param {string} appName - The name of the app to start * @param {string} id - The name of the app to start
* @throws {Error} - If the app is not found or the start process fails. * @throws {Error} - If the app is not found or the start process fails.
*/ */
public startApp = async (appName: string) => { public startApp = async (id: string) => {
const app = await this.queries.getApp(appName); const app = await this.queries.getApp(id);
if (!app) { if (!app) {
throw new TranslatedError('server-messages.errors.app-not-found', { id: appName }); throw new TranslatedError('server-messages.errors.app-not-found', { id });
} }
await this.queries.updateApp(appName, { status: 'starting' }); await this.queries.updateApp(id, { status: 'starting' });
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
const eventDispatcher = new EventDispatcher('startApp'); const eventDispatcher = new EventDispatcher('startApp');
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: appName, form: castAppConfig(app.config) }); const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: id, form: castAppConfig(app.config) });
await eventDispatcher.close(); await eventDispatcher.close();
if (success) { if (success) {
await this.queries.updateApp(appName, { status: 'running' }); await this.queries.updateApp(id, { status: 'running' });
} else { } else {
await this.queries.updateApp(appName, { status: 'stopped' }); await this.queries.updateApp(id, { status: 'stopped' });
Logger.error(`Failed to start app ${appName}: ${stdout}`); Logger.error(`Failed to start app ${id}: ${stdout}`);
throw new TranslatedError('server-messages.errors.app-failed-to-start', { id: appName }); throw new TranslatedError('server-messages.errors.app-failed-to-start', { id });
} }
const updatedApp = await this.queries.getApp(appName); const updatedApp = await this.queries.getApp(id);
return updatedApp; return updatedApp;
}; };
@ -164,6 +170,10 @@ export class AppServiceClass {
isVisibleOnGuestDashboard, isVisibleOnGuestDashboard,
}); });
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
// Run script // Run script
const eventDispatcher = new EventDispatcher('installApp'); const eventDispatcher = new EventDispatcher('installApp');
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form }); const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form });
@ -263,6 +273,10 @@ export class AppServiceClass {
// Run script // Run script
await this.queries.updateApp(id, { status: 'stopping' }); await this.queries.updateApp(id, { status: 'stopping' });
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
const eventDispatcher = new EventDispatcher('stopApp'); const eventDispatcher = new EventDispatcher('stopApp');
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) }); const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) });
await eventDispatcher.close(); await eventDispatcher.close();
@ -297,6 +311,10 @@ export class AppServiceClass {
await this.queries.updateApp(id, { status: 'uninstalling' }); await this.queries.updateApp(id, { status: 'uninstalling' });
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
const eventDispatcher = new EventDispatcher('uninstallApp'); const eventDispatcher = new EventDispatcher('uninstallApp');
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) }); const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) });
await eventDispatcher.close(); await eventDispatcher.close();
@ -348,6 +366,10 @@ export class AppServiceClass {
await this.queries.updateApp(id, { status: 'updating' }); await this.queries.updateApp(id, { status: 'updating' });
revalidatePath('/apps');
revalidatePath(`/app/${id}`);
revalidatePath(`/app-store/${id}`);
const eventDispatcher = new EventDispatcher('updateApp'); const eventDispatcher = new EventDispatcher('updateApp');
const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'update', appid: id, form: castAppConfig(app.config) }); const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'update', appid: id, form: castAppConfig(app.config) });
await eventDispatcher.close(); await eventDispatcher.close();