WIP - Common package
This commit is contained in:
parent
31c9295a76
commit
123aaee235
41 changed files with 593 additions and 584 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
**/node_modules/
|
||||
**/.next/
|
||||
/node_modules/
|
||||
/.next/
|
|
@ -3,12 +3,12 @@
|
|||
"available": true,
|
||||
"port": 8104,
|
||||
"id": "adguard",
|
||||
"categories": ["network", "security"],
|
||||
"description": "Adguard is the best way to get rid of annoying ads and online tracking and protect your computer from malware. Make your web surfing fast, safe and ad-free.",
|
||||
"short_desc": "World's most advanced adblocker!",
|
||||
"author": "ArneNaessens",
|
||||
"source": "https://github.com/AdguardTeam",
|
||||
"image": "https://avatars.githubusercontent.com/u/8361145?s=200&v=4",
|
||||
"cagegories": ["network", "security"],
|
||||
"requirements": {
|
||||
"ports": [53]
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@ services:
|
|||
volumes:
|
||||
- ${PWD}/packages/dashboard:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/") # Host(`tipi.local`) &&
|
||||
|
|
4
packages/common/.eslintignore
Normal file
4
packages/common/.eslintignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
dist/
|
||||
*.cjs
|
||||
dist/
|
18
packages/common/.eslintrc.js
Normal file
18
packages/common/.eslintrc.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
env: { node: true, jest: true },
|
||||
extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import', 'react'],
|
||||
rules: {
|
||||
'arrow-body-style': 0,
|
||||
'no-restricted-exports': 0,
|
||||
'max-len': [1, { code: 200 }],
|
||||
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
|
||||
},
|
||||
};
|
2
packages/common/.gitignore
vendored
Normal file
2
packages/common/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
1
packages/common/.npmignore
Normal file
1
packages/common/.npmignore
Normal file
|
@ -0,0 +1 @@
|
|||
|
6
packages/common/.prettierrc.cjs
Normal file
6
packages/common/.prettierrc.cjs
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
printWidth: 200,
|
||||
};
|
23
packages/common/package.json
Normal file
23
packages/common/package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "@runtipi/common",
|
||||
"version": "0.2.7",
|
||||
"main": "./dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.14.38",
|
||||
"typescript": "4.6.4"
|
||||
},
|
||||
"dependencies": {},
|
||||
"description": "",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
13
packages/common/src/constants/app.constants.ts
Normal file
13
packages/common/src/constants/app.constants.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { AppCategoriesEnum } from '../types';
|
||||
|
||||
// Icons should come from FontAwesome https://react-icons.github.io/react-icons/icons?name=fa
|
||||
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' },
|
||||
];
|
1
packages/common/src/constants/index.ts
Normal file
1
packages/common/src/constants/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './app.constants';
|
2
packages/common/src/index.ts
Normal file
2
packages/common/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './types';
|
||||
export * from './constants';
|
60
packages/common/src/types/app.types.ts
Normal file
60
packages/common/src/types/app.types.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
export enum AppCategoriesEnum {
|
||||
NETWORK = 'network',
|
||||
MEDIA = 'media',
|
||||
DEVELOPMENT = 'development',
|
||||
AUTOMATION = 'automation',
|
||||
SOCIAL = 'social',
|
||||
UTILITIES = 'utilities',
|
||||
PHOTOGRAPHY = 'photography',
|
||||
SECURITY = 'security',
|
||||
}
|
||||
|
||||
export enum FieldTypes {
|
||||
text = 'text',
|
||||
password = 'password',
|
||||
email = 'email',
|
||||
number = 'number',
|
||||
fqdn = 'fqdn',
|
||||
ip = 'ip',
|
||||
fqdnip = 'fqdnip',
|
||||
url = 'url',
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
type: FieldTypes;
|
||||
label: string;
|
||||
max?: number;
|
||||
min?: number;
|
||||
hint?: string;
|
||||
required?: boolean;
|
||||
env_variable: string;
|
||||
}
|
||||
|
||||
export enum AppStatusEnum {
|
||||
RUNNING = 'running',
|
||||
STOPPED = 'stopped',
|
||||
INSTALLING = 'installing',
|
||||
UNINSTALLING = 'uninstalling',
|
||||
STOPPING = 'stopping',
|
||||
STARTING = 'starting',
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
id: string;
|
||||
available: boolean;
|
||||
port: number;
|
||||
name: string;
|
||||
requirements?: {
|
||||
ports?: number[];
|
||||
};
|
||||
description: string;
|
||||
version: string;
|
||||
image: string;
|
||||
form_fields: Record<string, FormField>;
|
||||
short_desc: string;
|
||||
author: string;
|
||||
source: string;
|
||||
installed: boolean;
|
||||
categories: AppCategoriesEnum[];
|
||||
status: AppStatusEnum;
|
||||
}
|
1
packages/common/src/types/index.ts
Normal file
1
packages/common/src/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './app.types';
|
8
packages/common/tsconfig.build.json
Normal file
8
packages/common/tsconfig.build.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
22
packages/common/tsconfig.json
Normal file
22
packages/common/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": false,
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": false,
|
||||
"jsx": "preserve",
|
||||
"incremental": false,
|
||||
"declaration": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "jest.config.cjs"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
node_modules/
|
||||
.next/
|
||||
.next/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:18-buster-slim
|
||||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
@ -2,6 +2,13 @@
|
|||
const { NODE_ENV, INTERNAL_IP } = process.env;
|
||||
|
||||
const nextConfig = {
|
||||
webpackDevMiddleware: (config) => {
|
||||
config.watchOptions = {
|
||||
poll: 1000,
|
||||
aggregateTimeout: 300,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
reactStrictMode: true,
|
||||
env: {
|
||||
INTERNAL_IP: INTERNAL_IP,
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.0.2",
|
||||
"@chakra-ui/react": "^2.1.2",
|
||||
"@emotion/react": "^11",
|
||||
"@emotion/styled": "^11",
|
||||
"@fontsource/open-sans": "^4.5.8",
|
||||
"@runtipi/common": "^0.2.7",
|
||||
"axios": "^0.26.1",
|
||||
"clsx": "^1.1.1",
|
||||
"final-form": "^4.20.6",
|
||||
|
@ -38,11 +39,11 @@
|
|||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"eslint": "8.12.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "12.1.4",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"postcss": "^8.4.12",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"typescript": "4.6.4"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { FiPauseCircle, FiPlayCircle } from 'react-icons/fi';
|
||||
import { AppStatus as TAppStatus } from '../../core/types';
|
||||
import { AppStatusEnum } from '@runtipi/common';
|
||||
|
||||
const AppStatus: React.FC<{ status: TAppStatus }> = ({ status }) => {
|
||||
if (status === 'running') {
|
||||
const AppStatus: React.FC<{ status: AppStatusEnum }> = ({ status }) => {
|
||||
if (status === AppStatusEnum.RUNNING) {
|
||||
return (
|
||||
<>
|
||||
<FiPlayCircle className="text-green-500 mr-1" size={20} />
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Box, SlideFade, Image, useColorModeValue } from '@chakra-ui/react';
|
|||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { FiChevronRight } from 'react-icons/fi';
|
||||
import { AppConfig } from '../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import AppStatus from './AppStatus';
|
||||
|
||||
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import validator from 'validator';
|
||||
import { AppConfig, FieldTypes } from '../../core/types';
|
||||
import { AppConfig, FieldTypes } from '@runtipi/common';
|
||||
|
||||
const validateField = (field: AppConfig['form_fields'][0], value: string): string | undefined => {
|
||||
if (field.required && !value) {
|
||||
|
|
|
@ -1,107 +1,22 @@
|
|||
import validator from 'validator';
|
||||
// import validator from 'validator';
|
||||
|
||||
interface IFormField {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
validate?: (value: string) => boolean;
|
||||
}
|
||||
// interface IFormField {
|
||||
// name: string;
|
||||
// type: string;
|
||||
// required: boolean;
|
||||
// description?: string;
|
||||
// placeholder?: string;
|
||||
// validate?: (value: string) => boolean;
|
||||
// }
|
||||
|
||||
interface IAppConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
logo: string;
|
||||
url: string;
|
||||
color: string;
|
||||
install_form: { fields: IFormField[] };
|
||||
}
|
||||
// interface IAppConfig {
|
||||
// id: string;
|
||||
// name: string;
|
||||
// description: string;
|
||||
// logo: string;
|
||||
// url: string;
|
||||
// color: string;
|
||||
// install_form: { fields: IFormField[] };
|
||||
// }
|
||||
|
||||
const APP_ANONADDY: IAppConfig = {
|
||||
id: 'anonaddy',
|
||||
name: 'Anonaddy',
|
||||
description: 'Create Unlimited Email Aliases For Free',
|
||||
url: 'https://anonaddy.com/',
|
||||
color: '#00a8ff',
|
||||
logo: 'https://anonaddy.com/favicon.ico',
|
||||
install_form: {
|
||||
fields: [
|
||||
{
|
||||
name: 'API Key',
|
||||
type: 'text',
|
||||
placeholder: 'API Key',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isBase64(value),
|
||||
},
|
||||
{
|
||||
name: 'Return Path',
|
||||
type: 'text',
|
||||
description: 'The email address that bounces will be sent to',
|
||||
placeholder: 'Return Path',
|
||||
required: false,
|
||||
validate: (value: string) => validator.isEmail(value),
|
||||
},
|
||||
{
|
||||
name: 'Admin Username',
|
||||
type: 'text',
|
||||
description: 'The username of the admin user',
|
||||
placeholder: 'Admin Username',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'Enable Registration',
|
||||
type: 'boolean',
|
||||
description: 'Allow users to register',
|
||||
placeholder: 'Enable Registration',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'Domain',
|
||||
type: 'text',
|
||||
description: 'The domain that will be used for the email address',
|
||||
placeholder: 'Domain',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isFQDN(value),
|
||||
},
|
||||
{
|
||||
name: 'Hostname',
|
||||
type: 'text',
|
||||
description: 'The hostname that will be used for the email address',
|
||||
placeholder: 'Hostname',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isFQDN(value),
|
||||
},
|
||||
{
|
||||
name: 'Secret',
|
||||
type: 'text',
|
||||
description: 'The secret that will be used for the email address',
|
||||
placeholder: 'Secret',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'From Name',
|
||||
type: 'text',
|
||||
description: 'The name that will be used for the email address',
|
||||
placeholder: 'From Name',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isLength(value, { min: 1, max: 64 }),
|
||||
},
|
||||
{
|
||||
name: 'From Address',
|
||||
type: 'text',
|
||||
description: 'The email address that will be used for the email address',
|
||||
placeholder: 'From Address',
|
||||
required: true,
|
||||
validate: (value: string) => validator.isEmail(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const APPS_CONFIG = {
|
||||
available: [APP_ANONADDY],
|
||||
};
|
||||
|
||||
export default APPS_CONFIG;
|
||||
export {};
|
||||
|
|
|
@ -1,57 +1,9 @@
|
|||
export enum FieldTypes {
|
||||
text = 'text',
|
||||
password = 'password',
|
||||
email = 'email',
|
||||
number = 'number',
|
||||
fqdn = 'fqdn',
|
||||
ip = 'ip',
|
||||
fqdnip = 'fqdnip',
|
||||
url = 'url',
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
type: FieldTypes;
|
||||
label: string;
|
||||
max?: number;
|
||||
min?: number;
|
||||
hint?: string;
|
||||
required?: boolean;
|
||||
env_variable: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
id: string;
|
||||
port: number;
|
||||
requirements?: {
|
||||
ports?: number[];
|
||||
};
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
image: string;
|
||||
form_fields: Record<string, FormField>;
|
||||
short_desc: string;
|
||||
author: string;
|
||||
source: string;
|
||||
installed: boolean;
|
||||
status: AppStatus;
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
SUCCESS = 'SUCCESS',
|
||||
ERROR = 'ERROR',
|
||||
LOADING = 'LOADING',
|
||||
}
|
||||
|
||||
export enum AppStatus {
|
||||
RUNNING = 'running',
|
||||
STOPPED = 'stopped',
|
||||
INSTALLING = 'installing',
|
||||
UNINSTALLING = 'uninstalling',
|
||||
STOPPING = 'stopping',
|
||||
STARTING = 'starting',
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
name: string;
|
||||
email: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
|
||||
import { AppConfig, AppStatus } from '../../../core/types';
|
||||
import { AppConfig, AppStatusEnum } from '@runtipi/common';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
|
@ -16,7 +16,7 @@ interface IProps {
|
|||
const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
|
||||
const hasSettings = Object.keys(app.form_fields).length > 0;
|
||||
|
||||
if (app?.installed && app.status === AppStatus.STOPPED) {
|
||||
if (app?.installed && app.status === AppStatusEnum.STOPPED) {
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center">
|
||||
<Button onClick={onStart} width={160} colorScheme="green" className="mt-3 mr-2">
|
||||
|
@ -35,7 +35,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (app?.installed && app.status === AppStatus.RUNNING) {
|
||||
} else if (app?.installed && app.status === AppStatusEnum.RUNNING) {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={onOpen} width={160} colorScheme="gray" className="mt-3 mr-2">
|
||||
|
@ -48,7 +48,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
|
|||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else if (app.status === AppStatus.INSTALLING || app.status === AppStatus.UNINSTALLING || app.status === AppStatus.STARTING || app.status === AppStatus.STOPPING) {
|
||||
} else if (app.status === AppStatusEnum.INSTALLING || app.status === AppStatusEnum.UNINSTALLING || app.status === AppStatusEnum.STARTING || app.status === AppStatusEnum.STOPPING) {
|
||||
return (
|
||||
<div className="flex items-center flex-col md:flex-row">
|
||||
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import { Form, Field } from 'react-final-form';
|
||||
import FormInput from '../../../components/Form/FormInput';
|
||||
import { validateAppConfig } from '../../../components/Form/validators';
|
||||
import { AppConfig } from '../../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { objectKeys } from '../../../utils/typescript';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { AppConfig } from '../../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import InstallForm from './InstallForm';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { AppConfig } from '../../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { AppConfig } from '../../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve
|
|||
import React, { useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import fetcher from '../../../core/fetcher';
|
||||
import { AppConfig } from '../../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import InstallForm from './InstallForm';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SlideFade, Image, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
import { AppConfig } from '../../../core/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { useAppsStore } from '../../../state/appsStore';
|
||||
import { useSytemStore } from '../../../state/systemStore';
|
||||
import AppActions from '../components/AppActions';
|
||||
|
@ -88,7 +88,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
window.open(`http://${internalIp}:${app.port}`, '_blank');
|
||||
window.open(`http://${internalIp}:${app.port}`, '_blank', 'noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,27 +7,23 @@ import { useAppsStore } from '../../state/appsStore';
|
|||
import AppTile from '../../components/AppTile';
|
||||
|
||||
const Apps: NextPage = () => {
|
||||
const { available, installed, fetch, status } = useAppsStore((state) => state);
|
||||
const { fetch, status, apps } = useAppsStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, [fetch]);
|
||||
|
||||
const installedCount: number = installed().length || 0;
|
||||
const loading = status === RequestStatus.LOADING && !installed && !available;
|
||||
const installed = apps.filter((app) => app.installed);
|
||||
|
||||
const installedCount: number = installed.length || 0;
|
||||
const loading = status === RequestStatus.LOADING && installedCount === 0;
|
||||
|
||||
return (
|
||||
<Layout loading={loading}>
|
||||
<Flex className="flex-col">
|
||||
{installedCount > 0 && <h1 className="font-bold text-3xl mb-5">Your Apps ({installedCount})</h1>}
|
||||
<SimpleGrid minChildWidth="400px" spacing="20px">
|
||||
{installed().map((app) => (
|
||||
<AppTile key={app.name} app={app} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{available().length && <h1 className="font-bold text-3xl mb-5 mt-5">Available Apps</h1>}
|
||||
<SimpleGrid minChildWidth="400px" spacing="20px">
|
||||
{available().map((app) => (
|
||||
{installed.map((app) => (
|
||||
<AppTile key={app.name} app={app} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import produce from 'immer';
|
||||
import create, { GetState, SetState } from 'zustand';
|
||||
import create, { SetState } from 'zustand';
|
||||
import api from '../core/api';
|
||||
import { AppConfig, AppStatus, RequestStatus } from '../core/types';
|
||||
import { AppConfig, AppStatusEnum } from '@runtipi/common';
|
||||
import { RequestStatus } from '../core/types';
|
||||
|
||||
type AppsStore = {
|
||||
apps: AppConfig[];
|
||||
status: RequestStatus;
|
||||
installed: () => AppConfig[];
|
||||
available: () => AppConfig[];
|
||||
fetch: () => void;
|
||||
getApp: (id: string) => AppConfig | undefined;
|
||||
fetchApp: (id: string) => void;
|
||||
|
@ -19,11 +18,10 @@ type AppsStore = {
|
|||
};
|
||||
|
||||
type Set = SetState<AppsStore>;
|
||||
type Get = GetState<AppsStore>;
|
||||
|
||||
const sortApps = (apps: AppConfig[]) => apps.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const setAppStatus = (appId: string, status: AppStatus, set: Set) => {
|
||||
const setAppStatus = (appId: string, status: AppStatusEnum, set: Set) => {
|
||||
set((state) => {
|
||||
return produce(state, (draft) => {
|
||||
const app = draft.apps.find((a) => a.id === appId);
|
||||
|
@ -32,11 +30,6 @@ const setAppStatus = (appId: string, status: AppStatus, set: Set) => {
|
|||
});
|
||||
};
|
||||
|
||||
const installed = (get: Get) => {
|
||||
const i = get().apps.filter((app) => app.installed);
|
||||
return i;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch one app and add it to the list of apps.
|
||||
* @param appId
|
||||
|
@ -59,10 +52,6 @@ const fetchApp = async (appId: string, set: Set) => {
|
|||
export const useAppsStore = create<AppsStore>((set, get) => ({
|
||||
apps: [],
|
||||
status: RequestStatus.LOADING,
|
||||
installed: () => installed(get),
|
||||
available: () => {
|
||||
return get().apps.filter((app) => !app.installed);
|
||||
},
|
||||
fetchApp: async (appId: string) => fetchApp(appId, set),
|
||||
fetch: async () => {
|
||||
set({ status: RequestStatus.LOADING });
|
||||
|
@ -72,13 +61,15 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
|
|||
method: 'get',
|
||||
});
|
||||
|
||||
set({ apps: sortApps(response), status: RequestStatus.SUCCESS });
|
||||
const apps = sortApps(response);
|
||||
|
||||
set({ apps, status: RequestStatus.SUCCESS });
|
||||
},
|
||||
getApp: (appId: string) => {
|
||||
return get().apps.find((app) => app.id === appId);
|
||||
},
|
||||
install: async (appId: string, form?: Record<string, string>) => {
|
||||
setAppStatus(appId, AppStatus.INSTALLING, set);
|
||||
setAppStatus(appId, AppStatusEnum.INSTALLING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/install/${appId}`,
|
||||
|
@ -98,7 +89,7 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
|
|||
await get().fetchApp(appId);
|
||||
},
|
||||
uninstall: async (appId: string) => {
|
||||
setAppStatus(appId, AppStatus.UNINSTALLING, set);
|
||||
setAppStatus(appId, AppStatusEnum.UNINSTALLING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/uninstall/${appId}`,
|
||||
|
@ -107,7 +98,7 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
|
|||
await get().fetchApp(appId);
|
||||
},
|
||||
stop: async (appId: string) => {
|
||||
setAppStatus(appId, AppStatus.STOPPING, set);
|
||||
setAppStatus(appId, AppStatusEnum.STOPPING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/stop/${appId}`,
|
||||
|
@ -116,7 +107,7 @@ export const useAppsStore = create<AppsStore>((set, get) => ({
|
|||
await get().fetchApp(appId);
|
||||
},
|
||||
start: async (appId: string) => {
|
||||
setAppStatus(appId, AppStatus.STARTING, set);
|
||||
setAppStatus(appId, AppStatusEnum.STARTING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/start/${appId}`,
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@runtipi/common": "^0.2.7",
|
||||
"argon2": "^0.28.5",
|
||||
"axios": "^0.26.1",
|
||||
"compression": "^1.7.4",
|
||||
|
@ -57,7 +58,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.22.0",
|
||||
"concurrently": "^7.1.0",
|
||||
"esbuild": "^0.14.32",
|
||||
"esbuild": "^0.14.38",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
|
|
|
@ -1,41 +1,5 @@
|
|||
export enum FieldTypes {
|
||||
text = 'text',
|
||||
password = 'password',
|
||||
email = 'email',
|
||||
number = 'number',
|
||||
fqdn = 'fqdn',
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
type: FieldTypes;
|
||||
label: string;
|
||||
max?: number;
|
||||
min?: number;
|
||||
required?: boolean;
|
||||
env_variable: string;
|
||||
}
|
||||
|
||||
export type Maybe<T> = T | null | undefined;
|
||||
|
||||
export interface AppConfig {
|
||||
id: string;
|
||||
available: boolean;
|
||||
port: number;
|
||||
name: string;
|
||||
requirements?: {
|
||||
ports?: number[];
|
||||
};
|
||||
description: string;
|
||||
version: string;
|
||||
image: string;
|
||||
form_fields: Record<string, FormField>;
|
||||
short_desc: string;
|
||||
author: string;
|
||||
source: string;
|
||||
installed: boolean;
|
||||
status: 'running' | 'stopped';
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
email: string;
|
||||
name: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AppsService from '../apps.service';
|
||||
import fs from 'fs';
|
||||
import config from '../../../config';
|
||||
import { AppConfig, FieldTypes } from '../../../config/types';
|
||||
import { AppConfig, FieldTypes } from '@runtipi/common';
|
||||
import childProcess from 'child_process';
|
||||
|
||||
jest.mock('fs');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import AppsService from './apps.service';
|
||||
import { AppConfig } from '../../config/types';
|
||||
import { getInitalFormValues } from './apps.helpers';
|
||||
|
||||
const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import portUsed from 'tcp-port-used';
|
||||
import p from 'p-iteration';
|
||||
import { AppConfig } from '../../config/types';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { fileExists, readdirSync, readFile, readJsonFile, runScript, writeFile } from '../fs/fs.helpers';
|
||||
import InternalIp from 'internal-ip';
|
||||
import config from '../../config';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import si from 'systeminformation';
|
||||
import { AppConfig } from '../../config/types';
|
||||
import { createFolder, fileExists, readJsonFile } from '../fs/fs.helpers';
|
||||
import { AppConfig, AppStatusEnum } from '@runtipi/common';
|
||||
import { createFolder, fileExists, readFile, readJsonFile } from '../fs/fs.helpers';
|
||||
import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
|
||||
|
||||
const startApp = async (appName: string): Promise<void> => {
|
||||
|
@ -61,7 +61,8 @@ const listApps = async (): Promise<AppConfig[]> => {
|
|||
|
||||
apps.forEach((app) => {
|
||||
app.installed = installed.includes(app.id);
|
||||
app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as 'running') || 'stopped';
|
||||
app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
|
||||
app.description = readFile(`/apps/${app.id}/description.md`);
|
||||
});
|
||||
|
||||
return apps;
|
||||
|
@ -74,7 +75,7 @@ const getAppInfo = async (id: string): Promise<AppConfig> => {
|
|||
const state = getStateFile();
|
||||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
configFile.installed = installed.includes(id);
|
||||
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as 'running') || 'stopped';
|
||||
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
|
||||
|
||||
return configFile;
|
||||
};
|
||||
|
|
689
pnpm-lock.yaml
689
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue