refactor(app actions): remove duplicate code & improve mobile ui
This commit is contained in:
parent
417efce959
commit
2ff55d0de3
8 changed files with 108 additions and 101 deletions
|
@ -8,3 +8,5 @@ fi
|
|||
|
||||
pnpm -r test
|
||||
pnpm -r lint:fix
|
||||
|
||||
docker stop test-db
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Button, Tooltip } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { IconType } from 'react-icons';
|
||||
import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
|
||||
import { MdSystemUpdateAlt } from 'react-icons/md';
|
||||
import { TiCancel } from 'react-icons/ti';
|
||||
|
@ -19,6 +20,26 @@ interface IProps {
|
|||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface BtnProps {
|
||||
Icon?: IconType;
|
||||
onClick: () => void;
|
||||
width?: number | null;
|
||||
title?: string;
|
||||
color?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const ActionButton: React.FC<BtnProps> = (props) => {
|
||||
const { Icon, onClick, title, loading, width = 150, color = 'gray' } = props;
|
||||
|
||||
return (
|
||||
<Button isLoading={loading} onClick={onClick} width={width || undefined} colorScheme={color} className="mt-3 mr-2">
|
||||
{title}
|
||||
{Icon && <Icon className="ml-1" />}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const AppActions: React.FC<IProps> = ({ app, status, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate, onCancel, updateAvailable, onUpdateSettings }) => {
|
||||
const hasSettings = Object.keys(app.form_fields).length > 0;
|
||||
|
||||
|
@ -30,110 +51,64 @@ const AppActions: React.FC<IProps> = ({ app, status, onInstall, onUninstall, onS
|
|||
}
|
||||
};
|
||||
|
||||
const StartButton = <ActionButton Icon={FiPlay} onClick={onStart} title="Start" color="green" />;
|
||||
const RemoveButton = <ActionButton Icon={FiTrash2} onClick={onUninstall} title="Remove" />;
|
||||
const SettingsButton = <ActionButton Icon={FiSettings} width={null} onClick={onUpdateSettings} />;
|
||||
const StopButton = <ActionButton Icon={FiPause} onClick={onStop} title="Stop" color="red" />;
|
||||
const OpenButton = <ActionButton Icon={FiExternalLink} onClick={onOpen} title="Open" />;
|
||||
const LoadingButtion = <ActionButton loading onClick={() => null} color="green" />;
|
||||
const CancelButton = <ActionButton Icon={TiCancel} onClick={onCancel} title="Cancel" />;
|
||||
const InstallButton = <ActionButton onClick={onInstall} title="Install" color="green" />;
|
||||
const UpdateButton = (
|
||||
<Tooltip label="Download update">
|
||||
<ActionButton Icon={MdSystemUpdateAlt} onClick={onUpdate} width={null} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
switch (status) {
|
||||
case AppStatusEnum.Stopped:
|
||||
buttons.push(
|
||||
<Button onClick={onStart} width={150} colorScheme="green" className="mt-3 mr-2">
|
||||
Start
|
||||
<FiPlay className="ml-1" />
|
||||
</Button>,
|
||||
<Button onClick={onUninstall} width={150} colorScheme="gray" className="mt-3 mr-2">
|
||||
Remove
|
||||
<FiTrash2 className="ml-1" />
|
||||
</Button>,
|
||||
);
|
||||
buttons.push(StartButton, RemoveButton);
|
||||
if (hasSettings) {
|
||||
buttons.push(
|
||||
<Tooltip label="Update settings">
|
||||
<Button onClick={onUpdateSettings} colorScheme="gray" className="mt-3 mr-2">
|
||||
<FiSettings className="ml-1" />
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
);
|
||||
buttons.push(SettingsButton);
|
||||
}
|
||||
if (updateAvailable) {
|
||||
buttons.push(
|
||||
<Tooltip label="Download update">
|
||||
<Button onClick={onUpdate} colorScheme="gray" className="mt-3 mr-2">
|
||||
<MdSystemUpdateAlt className="ml-1" />
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
);
|
||||
buttons.push(UpdateButton);
|
||||
}
|
||||
break;
|
||||
case AppStatusEnum.Running:
|
||||
buttons.push(
|
||||
<Button onClick={onStop} width={150} colorScheme="red" className="mt-3 mr-2">
|
||||
Stop
|
||||
<FiPause className="ml-1" />
|
||||
</Button>,
|
||||
<Button onClick={onOpen} width={150} colorScheme="gray" className="mt-3 mr-2">
|
||||
Open
|
||||
<FiExternalLink className="ml-1" />
|
||||
</Button>,
|
||||
);
|
||||
buttons.push(StopButton, OpenButton);
|
||||
if (hasSettings) {
|
||||
buttons.push(
|
||||
<Tooltip label="Update settings">
|
||||
<Button onClick={onUpdateSettings} colorScheme="gray" className="mt-3 mr-2">
|
||||
<FiSettings className="ml-1" />
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
);
|
||||
buttons.push(SettingsButton);
|
||||
}
|
||||
if (updateAvailable) {
|
||||
buttons.push(
|
||||
<Tooltip label="Download update">
|
||||
<Button onClick={onUpdate} colorScheme="gray" className="mt-3 mr-2">
|
||||
<MdSystemUpdateAlt className="ml-1" />
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
);
|
||||
buttons.push(UpdateButton);
|
||||
}
|
||||
break;
|
||||
case AppStatusEnum.Installing:
|
||||
case AppStatusEnum.Uninstalling:
|
||||
case AppStatusEnum.Starting:
|
||||
case AppStatusEnum.Stopping:
|
||||
buttons.push(
|
||||
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
|
||||
Install
|
||||
<FiPlay className="ml-1" />
|
||||
</Button>,
|
||||
<Button onClick={onCancel} colorScheme="gray" className="mt-3 mr-2 ml-2">
|
||||
<TiCancel />
|
||||
</Button>,
|
||||
);
|
||||
buttons.push(LoadingButtion, CancelButton);
|
||||
break;
|
||||
|
||||
case AppStatusEnum.Updating:
|
||||
buttons.push(
|
||||
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
|
||||
Updating
|
||||
<FiPlay className="ml-1" />
|
||||
</Button>,
|
||||
<Button onClick={onCancel} colorScheme="gray" className="mt-3 mr-2 ml-2">
|
||||
<TiCancel />
|
||||
</Button>,
|
||||
);
|
||||
buttons.push(LoadingButtion, CancelButton);
|
||||
break;
|
||||
case AppStatusEnum.Missing:
|
||||
buttons.push(
|
||||
<Button onClick={onInstall} width={160} colorScheme="green" className="mt-3">
|
||||
Install
|
||||
</Button>,
|
||||
);
|
||||
buttons.push(InstallButton);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center sm:items-start flex-col md:flex-row">
|
||||
{buttons.map((button) => {
|
||||
return button;
|
||||
})}
|
||||
{renderStatus()}
|
||||
<div className="flex flex-1 flex-col justify-start">
|
||||
<div className="flex flex-1 justify-center md:justify-start flex-wrap">
|
||||
{buttons.map((button) => {
|
||||
return button;
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 flex justify-center md:justify-start">{renderStatus()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SlideFade, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { SlideFade, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
import { useSytemStore } from '../../../state/systemStore';
|
||||
|
@ -133,17 +133,19 @@ const AppDetails: React.FC<IProps> = ({ app, info }) => {
|
|||
window.open(`http://${internalIp}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
|
||||
};
|
||||
|
||||
const version = [info?.version || 'unknown', app?.version ? `(${app.version})` : ''].join(' ');
|
||||
|
||||
return (
|
||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||
<div className="flex flex-1 p-4 mt-3 rounded-lg flex-col">
|
||||
<div className="flex flex-1 p-4 mt-3 rounded-lg flex-col">
|
||||
<Flex className="flex-col md:flex-row">
|
||||
<AppLogo id={info.id} size={180} className="self-center sm:self-auto" alt={info.name} />
|
||||
<VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
|
||||
<div className="mt-3 items-center self-center flex flex-col sm:items-start sm:self-start md:mt-0">
|
||||
<AppLogo id={info.id} size={180} className="self-center md:self-auto" alt={info.name} />
|
||||
<div className="flex flex-col justify-between flex-1 ml-0 md:ml-4">
|
||||
<div className="mt-3 items-center self-center flex flex-col md:items-start md:self-start md:mt-0">
|
||||
<h1 className="font-bold text-2xl">{info.name}</h1>
|
||||
<h2 className="text-center md:text-left">{info.short_desc}</h2>
|
||||
<h3 className="text-center md:text-left text-sm">
|
||||
version: <b>{info.version}</b> ({app?.version})
|
||||
version: <b>{version}</b>
|
||||
</h3>
|
||||
{info.source && (
|
||||
<a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.source}>
|
||||
|
@ -155,7 +157,7 @@ const AppDetails: React.FC<IProps> = ({ app, info }) => {
|
|||
)}
|
||||
<p className="text-xs text-gray-600">By {info.author}</p>
|
||||
</div>
|
||||
<div className="flex justify-center xs:absolute md:static top-0 right-5 self-center sm:self-auto">
|
||||
<div className="flex flex-1">
|
||||
<AppActions
|
||||
updateAvailable={updateAvailable}
|
||||
onUpdate={updateDisclosure.onOpen}
|
||||
|
@ -170,7 +172,7 @@ const AppDetails: React.FC<IProps> = ({ app, info }) => {
|
|||
status={app?.status}
|
||||
/>
|
||||
</div>
|
||||
</VStack>
|
||||
</div>
|
||||
</Flex>
|
||||
<Divider className="mt-5" />
|
||||
<Markdown className="mt-3">{info.description}</Markdown>
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = {
|
|||
testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
setupFiles: ['<rootDir>/src/test/dotenv-config.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test/jest-setup.ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/migrations/**/*.{ts,tsx}', '!**/config/**/*.{ts,tsx}'],
|
||||
passWithNoTests: true,
|
||||
|
|
|
@ -27,11 +27,6 @@ const createApp = async (props: IProps) => {
|
|||
env_variable: 'TEST_FIELD',
|
||||
},
|
||||
],
|
||||
requirements: requiredPort
|
||||
? {
|
||||
ports: [requiredPort],
|
||||
}
|
||||
: undefined,
|
||||
name: faker.random.word(),
|
||||
description: faker.random.words(),
|
||||
tipi_version: faker.datatype.number({ min: 1, max: 10 }),
|
||||
|
@ -41,6 +36,20 @@ const createApp = async (props: IProps) => {
|
|||
categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
|
||||
};
|
||||
|
||||
if (randomField) {
|
||||
appInfo.form_fields?.push({
|
||||
type: FieldTypes.random,
|
||||
label: faker.random.word(),
|
||||
env_variable: 'RANDOM_FIELD',
|
||||
});
|
||||
}
|
||||
|
||||
if (requiredPort) {
|
||||
appInfo.requirements = {
|
||||
ports: [requiredPort],
|
||||
};
|
||||
}
|
||||
|
||||
let MockFiles: any = {};
|
||||
MockFiles[`${config.ROOT_FOLDER}/.env`] = 'TEST=test';
|
||||
MockFiles[`${config.ROOT_FOLDER}/repos/repo-id`] = '';
|
||||
|
|
|
@ -7,6 +7,7 @@ import App from '../app.entity';
|
|||
import { createApp } from './apps.factory';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { getEnvMap } from '../apps.helpers';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('child_process');
|
||||
|
@ -98,10 +99,15 @@ describe('Install app', () => {
|
|||
});
|
||||
|
||||
it('Correctly generates a random value if the field has a "random" type', async () => {
|
||||
// const { appInfo } = await createApp({ randomField: true });
|
||||
// await AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' });
|
||||
// const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`).toString();
|
||||
// expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appInfo.port}\nTEST_FIELD=${appInfo.randomValue}`);
|
||||
const { appInfo, MockFiles } = await createApp({ randomField: true });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
await AppsService.installApp(appInfo.id, { TEST_FIELD: 'yolo' });
|
||||
const envMap = getEnvMap(appInfo.id);
|
||||
|
||||
expect(envMap.get('RANDOM_FIELD')).toBeDefined();
|
||||
expect(envMap.get('RANDOM_FIELD')).toHaveLength(32);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -258,6 +264,21 @@ describe('Update app config', () => {
|
|||
it('Should throw if app is not installed', async () => {
|
||||
await expect(AppsService.updateAppConfig('test-app-2', { test: 'test' })).rejects.toThrowError('App test-app-2 not found');
|
||||
});
|
||||
|
||||
it('Should not recreate random field if already present in .env', async () => {
|
||||
const { appInfo, MockFiles } = await createApp({ randomField: true, installed: true });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
const envFile = fs.readFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`).toString();
|
||||
fs.writeFileSync(`${config.ROOT_FOLDER}/app-data/${appInfo.id}/app.env`, `${envFile}\nRANDOM_FIELD=test`);
|
||||
|
||||
await AppsService.updateAppConfig(appInfo.id, { TEST_FIELD: 'test' });
|
||||
|
||||
const envMap = getEnvMap(appInfo.id);
|
||||
|
||||
expect(envMap.get('RANDOM_FIELD')).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get app config', () => {
|
||||
|
|
|
@ -55,14 +55,6 @@ export const checkEnvFile = (appName: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const checkAppExists = (appName: string) => {
|
||||
const appExists = fileExists(`/app-data/${appName}`);
|
||||
|
||||
if (!appExists) {
|
||||
throw new Error(`App ${appName} not installed`);
|
||||
}
|
||||
};
|
||||
|
||||
export const runAppScript = async (params: string[]): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
runScript('/scripts/app.sh', [...params, config.ROOT_FOLDER_HOST, config.APPS_REPO_ID], (err: string) => {
|
||||
|
|
5
packages/system-api/src/test/jest-setup.ts
Normal file
5
packages/system-api/src/test/jest-setup.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
jest.mock('../config/logger/logger', () => ({
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
}));
|
Loading…
Add table
Reference in a new issue