Browse Source

WIP - Common package

Nicolas Meienberger 3 years ago
parent
commit
123aaee235
41 changed files with 560 additions and 554 deletions
  1. 4 0
      .dockerignore
  2. 1 1
      apps/adguard/config.json
  3. 1 0
      docker-compose.dev.yml
  4. 4 0
      packages/common/.eslintignore
  5. 18 0
      packages/common/.eslintrc.js
  6. 2 0
      packages/common/.gitignore
  7. 1 0
      packages/common/.npmignore
  8. 6 0
      packages/common/.prettierrc.cjs
  9. 23 0
      packages/common/package.json
  10. 13 0
      packages/common/src/constants/app.constants.ts
  11. 1 0
      packages/common/src/constants/index.ts
  12. 2 0
      packages/common/src/index.ts
  13. 60 0
      packages/common/src/types/app.types.ts
  14. 1 0
      packages/common/src/types/index.ts
  15. 8 0
      packages/common/tsconfig.build.json
  16. 22 0
      packages/common/tsconfig.json
  17. 1 1
      packages/dashboard/.dockerignore
  18. 1 1
      packages/dashboard/Dockerfile.dev
  19. 7 0
      packages/dashboard/next.config.js
  20. 3 2
      packages/dashboard/package.json
  21. 3 3
      packages/dashboard/src/components/AppTile/AppStatus.tsx
  22. 1 1
      packages/dashboard/src/components/AppTile/index.tsx
  23. 1 1
      packages/dashboard/src/components/Form/validators.ts
  24. 19 104
      packages/dashboard/src/constants/apps.ts
  25. 0 48
      packages/dashboard/src/core/types.ts
  26. 4 4
      packages/dashboard/src/modules/Apps/components/AppActions.tsx
  27. 1 1
      packages/dashboard/src/modules/Apps/components/InstallForm.tsx
  28. 1 1
      packages/dashboard/src/modules/Apps/components/InstallModal.tsx
  29. 1 1
      packages/dashboard/src/modules/Apps/components/StopModal.tsx
  30. 1 1
      packages/dashboard/src/modules/Apps/components/UninstallModal.tsx
  31. 1 1
      packages/dashboard/src/modules/Apps/components/UpdateModal.tsx
  32. 2 2
      packages/dashboard/src/modules/Apps/containers/AppDetails.tsx
  33. 6 10
      packages/dashboard/src/pages/apps/index.tsx
  34. 11 20
      packages/dashboard/src/state/appsStore.ts
  35. 2 1
      packages/system-api/package.json
  36. 0 36
      packages/system-api/src/config/types.ts
  37. 1 1
      packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
  38. 1 1
      packages/system-api/src/modules/apps/apps.controller.ts
  39. 1 1
      packages/system-api/src/modules/apps/apps.helpers.ts
  40. 5 4
      packages/system-api/src/modules/apps/apps.service.ts
  41. 319 307
      pnpm-lock.yaml

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
+**/node_modules/
+**/.next/
+/node_modules/
+/.next/

+ 1 - 1
apps/adguard/config.json

@@ -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]
   },

+ 1 - 0
docker-compose.dev.yml

@@ -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 - 0
packages/common/.eslintignore

@@ -0,0 +1,4 @@
+node_modules/
+dist/
+*.cjs
+dist/

+ 18 - 0
packages/common/.eslintrc.js

@@ -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 - 0
packages/common/.gitignore

@@ -0,0 +1,2 @@
+node_modules/
+dist/

+ 1 - 0
packages/common/.npmignore

@@ -0,0 +1 @@
+

+ 6 - 0
packages/common/.prettierrc.cjs

@@ -0,0 +1,6 @@
+module.exports = {
+  singleQuote: true,
+  semi: true,
+  trailingComma: 'all',
+  printWidth: 200,
+};

+ 23 - 0
packages/common/package.json

@@ -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 - 0
packages/common/src/constants/app.constants.ts

@@ -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 - 0
packages/common/src/constants/index.ts

@@ -0,0 +1 @@
+export * from './app.constants';

+ 2 - 0
packages/common/src/index.ts

@@ -0,0 +1,2 @@
+export * from './types';
+export * from './constants';

+ 60 - 0
packages/common/src/types/app.types.ts

@@ -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 - 0
packages/common/src/types/index.ts

@@ -0,0 +1 @@
+export * from './app.types';

+ 8 - 0
packages/common/tsconfig.build.json

@@ -0,0 +1,8 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "./dist"
+  },
+  "include": ["**/*.ts", "**/*.tsx"]
+}

+ 22 - 0
packages/common/tsconfig.json

@@ -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 - 1
packages/dashboard/.dockerignore

@@ -1,2 +1,2 @@
 node_modules/
-.next/
+.next/

+ 1 - 1
packages/dashboard/Dockerfile.dev

@@ -1,4 +1,4 @@
-FROM node:18-buster-slim 
+FROM node:18
 
 WORKDIR /app
 

+ 7 - 0
packages/dashboard/next.config.js

@@ -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,

+ 3 - 2
packages/dashboard/package.json

@@ -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"

+ 3 - 3
packages/dashboard/src/components/AppTile/AppStatus.tsx

@@ -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} />

+ 1 - 1
packages/dashboard/src/components/AppTile/index.tsx

@@ -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 - 1
packages/dashboard/src/components/Form/validators.ts

@@ -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) {

+ 19 - 104
packages/dashboard/src/constants/apps.ts

@@ -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 {};

+ 0 - 48
packages/dashboard/src/core/types.ts

@@ -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;

+ 4 - 4
packages/dashboard/src/modules/Apps/components/AppActions.tsx

@@ -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">

+ 1 - 1
packages/dashboard/src/modules/Apps/components/InstallForm.tsx

@@ -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 - 1
packages/dashboard/src/modules/Apps/components/InstallModal.tsx

@@ -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 - 1
packages/dashboard/src/modules/Apps/components/StopModal.tsx

@@ -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 - 1
packages/dashboard/src/modules/Apps/components/UninstallModal.tsx

@@ -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 - 1
packages/dashboard/src/modules/Apps/components/UpdateModal.tsx

@@ -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 {

+ 2 - 2
packages/dashboard/src/modules/Apps/containers/AppDetails.tsx

@@ -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 (

+ 6 - 10
packages/dashboard/src/pages/apps/index.tsx

@@ -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>

+ 11 - 20
packages/dashboard/src/state/appsStore.ts

@@ -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}`,

+ 2 - 1
packages/system-api/package.json

@@ -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",

+ 0 - 36
packages/system-api/src/config/types.ts

@@ -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 - 1
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -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 - 1
packages/system-api/src/modules/apps/apps.controller.ts

@@ -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 - 1
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -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';

+ 5 - 4
packages/system-api/src/modules/apps/apps.service.ts

@@ -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;
 };

File diff suppressed because it is too large
+ 319 - 307
pnpm-lock.yaml


Some files were not shown because too many files changed in this diff