浏览代码

Release/1.0.0 (#316)

* fix: create default media folder structure on install

* feat: add link to open exposed app to domain

* [ImgBot] Optimize images

*Total -- 2,048.42kb -> 1,263.43kb (38.32%)

/screenshots/darkmode.png -- 998.43kb -> 609.77kb (38.93%)
/screenshots/appstore.png -- 1,006.73kb -> 620.12kb (38.4%)
/packages/dashboard/public/error.png -- 42.38kb -> 32.70kb (22.84%)
/packages/dashboard/public/empty.svg -- 0.87kb -> 0.85kb (2.35%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

* chore: bump version 0.8.1

* refactor: move all dashboard's files into a client folder

* feat: setup trpc and create system router

* test: split jest config for client and server

* refactor: replace grapqhl queries with trpc in the frontend

* refactor: remove now un-used system queries/mutations/resolvers from both client and server

* chore: bump dependencies

* feat: setup prisma and configure it for tests and development

* feat: create trpc router for auth service

* refactor: migrate client auth queries to trpc procedures

* refactor: cleanup now un-used graphql resolvers and services

* feat: create sql migrations by replicating typeorm ones in an idempotent manner

* feat: create server-preload script to run migrations upon server start

* chore: remove legacy migrations steps

* feat: add redis_host as an env variable

* refactor: remove prisma from context and use client directly in service

* feat: create trpc router & service for apps

* refactor: migrate client app queries/mutations to trpc

* refactor: removal and replace usage of old graphql generated types

* refactor: move from node --require to custom next server

* test: fix tests and bump various dependencies

* chore: cleanup system-api from now un-used files

* refactor(dashboard): remove code related to apollo

* refactor: serve static files through next's server instead of system-api

* refactor(server): move auth and system services to class

* refactor(client): remove layoutv2 abstraction

* fix: return correct update info

* chore: remove legacy system-api folder

* refactor: remove system-api from docker files

* feat: create scheduler to run cron jobs and setup periodic repo update

* fix: failing build caused by remark-mdx

* refactor: move migrations to server folder

* feat: compile server using esbuild

* refactor: ts issue mis-used file from client in server

* ci: make pipeline pass by cd into dashboard before each step (temp)

* chore: drop armv7 support

* refactor: move dashboard files in root folder

* feat(db): create migration to add operator field on user

* feat(user): create routes and services for password reset

* feat(auth): add reset password page, container & form

* refactor(dashboard): change layout and page of auth to be url based instead of state based

* feat(script): add reset-password script

* fix(dashboard): only check status if restart or update has been requested

* test: increase coverage for get-server-auth-session

* fix(start.sh): prompt for network interface only if there is not an internal ip set

* feat(script): support user docker-compose.yml and app.env

* chore: bump version

* fix: add missing postgres variables to start script

* fix: check for 32 bits before installing/starting

* fix: create default media folder structure on install

* Updated demo instance link

Changed demo.runtipi.com to https://demo.runtipi.com

* feat: adding config for codespaces

* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: Freddie Sackur <github@dustyfox.uk>
Co-authored-by: Kieran Klukas <92754843+kcoderhtml@users.noreply.github.com>
Co-authored-by: alwerner <alexander.werner@bonprix.net>
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Nicolas Meienberger 2 年之前
父节点
当前提交
3925cfa7bb
共有 100 个文件被更改,包括 586 次插入3073 次删除
  1. 15 0
      .dockerignore
  2. 18 0
      .env.example
  3. 6 0
      .env.test
  4. 0 0
      .eslintignore
  5. 6 2
      .eslintrc.js
  6. 12 8
      .github/workflows/ci.yml
  7. 6 6
      .github/workflows/release-candidate.yml
  8. 7 8
      .github/workflows/release.yml
  9. 46 26
      .gitignore
  10. 0 0
      .prettierrc.js
  11. 38 32
      Dockerfile
  12. 21 12
      Dockerfile.dev
  13. 118 0
      __mocks__/fs-extra.ts
  14. 16 0
      __mocks__/redis.ts
  15. 0 0
      app-data/.gitkeep
  16. 0 0
      apps/.gitkeep
  17. 0 3
      commitlint.config.js
  18. 14 39
      docker-compose.dev.yml
  19. 15 45
      docker-compose.rc.yml
  20. 18 67
      docker-compose.test.yml
  21. 15 46
      docker-compose.yml
  22. 40 0
      esbuild.js
  23. 0 7
      jest.config.js
  24. 39 0
      jest.config.ts
  25. 0 0
      media/.gitkeep
  26. 0 0
      media/data/.gitkeep
  27. 0 0
      media/data/books/.gitkeep
  28. 0 0
      media/data/books/ebooks/.gitkeep
  29. 二进制
      media/data/books/metadata.db
  30. 0 0
      media/data/books/spoken/.gitkeep
  31. 0 0
      media/data/images/.gitkeep
  32. 0 0
      media/data/movies/.gitkeep
  33. 0 0
      media/data/music/.gitkeep
  34. 0 0
      media/data/podcasts/.gitkeep
  35. 0 0
      media/data/tv/.gitkeep
  36. 0 0
      media/torrents/.gitkeep
  37. 0 0
      media/torrents/complete/.gitkeep
  38. 0 0
      media/torrents/incomplete/.gitkeep
  39. 0 0
      media/torrents/watch/.gitkeep
  40. 0 0
      next-env.d.ts
  41. 23 0
      next.config.mjs
  42. 113 18
      package.json
  43. 0 5
      packages/dashboard/.dockerignore
  44. 0 35
      packages/dashboard/.gitignore
  45. 0 34
      packages/dashboard/README.md
  46. 0 9
      packages/dashboard/codegen.yml
  47. 0 18
      packages/dashboard/jest.config.js
  48. 0 8
      packages/dashboard/next.config.js
  49. 0 85
      packages/dashboard/package.json
  50. 0 47
      packages/dashboard/src/components/hoc/StatusProvider/StatusProvider.tsx
  51. 0 11
      packages/dashboard/src/core/apollo/client.ts
  52. 0 14
      packages/dashboard/src/core/apollo/links/authLink.ts
  53. 0 14
      packages/dashboard/src/core/apollo/links/errorLink.ts
  54. 0 7
      packages/dashboard/src/core/apollo/links/httpLink.ts
  55. 0 11
      packages/dashboard/src/core/apollo/links/index.ts
  56. 0 18
      packages/dashboard/src/core/constants.ts
  57. 0 4
      packages/dashboard/src/core/types.ts
  58. 0 1131
      packages/dashboard/src/generated/graphql.tsx
  59. 0 7
      packages/dashboard/src/graphql/mutations/installApp.graphql
  60. 0 5
      packages/dashboard/src/graphql/mutations/login.graphql
  61. 0 3
      packages/dashboard/src/graphql/mutations/logout.graphql
  62. 0 5
      packages/dashboard/src/graphql/mutations/register.graphql
  63. 0 3
      packages/dashboard/src/graphql/mutations/restart.graphql
  64. 0 7
      packages/dashboard/src/graphql/mutations/startApp.graphql
  65. 0 7
      packages/dashboard/src/graphql/mutations/stopApp.graphql
  66. 0 7
      packages/dashboard/src/graphql/mutations/unintallApp.graphql
  67. 0 3
      packages/dashboard/src/graphql/mutations/update.graphql
  68. 0 7
      packages/dashboard/src/graphql/mutations/updateApp.graphql
  69. 0 7
      packages/dashboard/src/graphql/mutations/updateAppConfig.graphql
  70. 0 42
      packages/dashboard/src/graphql/queries/getApp.graphql
  71. 0 21
      packages/dashboard/src/graphql/queries/installedApps.graphql
  72. 0 3
      packages/dashboard/src/graphql/queries/isConfigured.graphql
  73. 0 18
      packages/dashboard/src/graphql/queries/listApps.graphql
  74. 0 5
      packages/dashboard/src/graphql/queries/me.graphql
  75. 0 5
      packages/dashboard/src/graphql/queries/refreshToken.graphql
  76. 0 17
      packages/dashboard/src/graphql/queries/systemInfo.graphql
  77. 0 6
      packages/dashboard/src/graphql/queries/version.graphql
  78. 0 32
      packages/dashboard/src/hooks/useCachedRessources.ts
  79. 0 133
      packages/dashboard/src/mocks/handlers.ts
  80. 0 173
      packages/dashboard/src/mocks/handlers/appHandlers.ts
  81. 0 22
      packages/dashboard/src/modules/Apps/components/InstallModal/InstallModal.tsx
  82. 0 25
      packages/dashboard/src/modules/Apps/components/UpdateSettingsModal.tsx
  83. 0 153
      packages/dashboard/src/modules/Apps/containers/AppDetailsContainer/AppDetailsContainer.test.tsx
  84. 0 196
      packages/dashboard/src/modules/Apps/containers/AppDetailsContainer/AppDetailsContainer.tsx
  85. 0 51
      packages/dashboard/src/modules/Auth/containers/LoginContainer/LoginContainer.tsx
  86. 0 48
      packages/dashboard/src/modules/Auth/containers/RegisterContainer/RegisterContainer.tsx
  87. 0 14
      packages/dashboard/src/modules/Dashboard/pages/DashboardPage/DashboardPage.tsx
  88. 0 122
      packages/dashboard/src/modules/Settings/containers/SettingsContainer/SettingsContainer.test.tsx
  89. 0 21
      packages/dashboard/src/modules/Settings/pages/SettingsPage/SettingsPage.test.tsx
  90. 0 54
      packages/dashboard/src/pages/_app.tsx
  91. 0 1
      packages/dashboard/src/pages/app-store/[id].tsx
  92. 0 1
      packages/dashboard/src/pages/app-store/index.tsx
  93. 0 1
      packages/dashboard/src/pages/apps/[id].tsx
  94. 0 1
      packages/dashboard/src/pages/apps/index.tsx
  95. 0 1
      packages/dashboard/src/pages/index.tsx
  96. 0 1
      packages/dashboard/src/pages/settings.tsx
  97. 0 17
      packages/dashboard/src/state/systemStore.ts
  98. 0 31
      packages/dashboard/tests/test-utils.tsx
  99. 0 23
      packages/dashboard/tsconfig.json
  100. 0 4
      packages/system-api/.dockerignore

+ 15 - 0
.dockerignore

@@ -10,3 +10,18 @@ dist/
 docker-compose*.yml
 Dockerfile*
 .dockerignore
+
+# Tipi folder
+logs/
+tests/
+state/
+templates/
+scripts/
+screenshots/
+repos/
+media/
+data/
+apps/
+app-data/
+.github/
+__mocks__/

+ 18 - 0
.env.example

@@ -0,0 +1,18 @@
+# Only edit this file if you know what you are doing!
+# It will be overwritten on update.
+
+APPS_REPO_ID=7a92c8307e0a8074763c80be1fcfa4f87da6641daea9211aea6743b0116aba3b
+APPS_REPO_URL=https://github.com/meienberger/runtipi-appstore
+TZ=UTC
+INTERNAL_IP=localhost
+DNS_IP=9.9.9.9
+ARCHITECTURE=arm64
+TIPI_VERSION=0.8.0
+JWT_SECRET=secret
+ROOT_FOLDER_HOST=/Users/nicolas/Projects/runtipi
+NGINX_PORT=3000
+NGINX_PORT_SSL=443
+POSTGRES_PASSWORD=postgres
+DOMAIN=tipi.localhost
+STORAGE_PATH=/Users/nicolas/Projects/runtipi
+REDIS_HOST=tipi-redis

+ 6 - 0
.env.test

@@ -0,0 +1,6 @@
+POSTGRES_HOST=localhost
+POSTGRES_DBNAME=postgres
+POSTGRES_USERNAME=postgres
+POSTGRES_PASSWORD=postgres
+POSTGRES_PORT=5433
+APPS_REPO_ID=repo-id

+ 0 - 0
packages/dashboard/.eslintignore → .eslintignore


+ 6 - 2
packages/dashboard/.eslintrc.js → .eslintrc.js

@@ -1,5 +1,5 @@
 module.exports = {
-  plugins: ['@typescript-eslint', 'import', 'react', 'jest'],
+  plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc'],
   extends: [
     'plugin:@typescript-eslint/recommended',
     'next/core-web-vitals',
@@ -10,6 +10,7 @@ module.exports = {
     'plugin:import/typescript',
     'prettier',
     'plugin:react/recommended',
+    'plugin:jsdoc/recommended',
   ],
   parser: '@typescript-eslint/parser',
   parserOptions: {
@@ -28,10 +29,13 @@ module.exports = {
     'react/jsx-props-no-spreading': 0,
     'react/no-unused-prop-types': 0,
     'react/button-has-type': 0,
-    'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
+    'import/no-extraneous-dependencies': ['error', { devDependencies: ['esbuild.js', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/*.factory.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
+    'no-underscore-dangle': 0,
+    'arrow-body-style': 0,
   },
   globals: {
     JSX: true,
+    NodeJS: true,
   },
   env: {
     'jest/globals': true,

+ 12 - 8
.github/workflows/ci.yml

@@ -7,8 +7,12 @@ env:
   JWT_SECRET: "secret"
   ROOT_FOLDER_HOST: /tipi
   APPS_REPO_ID: repo-id
-  INTERNAL_IP: 192.168.1.10
-    
+  INTERNAL_IP: localhost
+  REDIS_HOST: redis
+  APPS_REPO_URL: https://repo.github.com/
+  DOMAIN: localhost
+  TIPI_VERSION: 0.0.1
+
 jobs:
   ci:
     runs-on: ubuntu-latest
@@ -57,15 +61,15 @@ jobs:
       - name: Install dependencies
         run: pnpm install
 
-      - name: Build packages
-        run: pnpm -r build
+      - name: Build client
+        run: pnpm build:next
 
       - name: Run linter
-        run: pnpm -r lint
-      
+        run: pnpm lint
+
       - name: Run tests
-        run: pnpm -r test
+        run: pnpm test
 
       - uses: codecov/codecov-action@v3
         with:
-          token: ${{ secrets.CODECOV_TOKEN }}
+          token: ${{ secrets.CODECOV_TOKEN }}

+ 6 - 6
.github/workflows/release-candidate.yml

@@ -15,29 +15,29 @@ jobs:
 
       - name: Set up QEMU
         uses: docker/setup-qemu-action@v2
-      
+
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
-      
+
       - name: Login to DockerHub
         uses: docker/login-action@v2
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
-      
+
       - name: Get tag from VERSION file
         id: meta
         run: |
           VERSION=$(npm run version --silent)
           TAG=${VERSION}
           echo "::set-output name=tag::${TAG}"
-      
+
       - name: Build and push images
         uses: docker/build-push-action@v3
         with:
           context: .
-          platforms: linux/amd64,linux/arm64,linux/arm/v7
+          platforms: linux/amd64,linux/arm64
           push: true
           tags: meienberger/runtipi:rc-${{ steps.meta.outputs.TAG }}
           cache-from: type=registry,ref=meienberger/runtipi:buildcache
-          cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
+          cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max

+ 7 - 8
.github/workflows/release.yml

@@ -1,10 +1,9 @@
-
 name: Publish release
 on:
   push:
-    branches:    
+    branches:
       - master
-      
+
 jobs:
   release:
     if: github.repository == 'meienberger/runtipi'
@@ -18,25 +17,25 @@ jobs:
 
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
-      
+
       - name: Login to DockerHub
         uses: docker/login-action@v2
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
-      
+
       - name: Get tag from VERSION file
         id: meta
         run: |
           VERSION=$(npm run version --silent)
           TAG=${VERSION}
           echo "::set-output name=tag::${TAG}"
-      
+
       - name: Build and push images
         uses: docker/build-push-action@v3
         with:
           context: .
-          platforms: linux/amd64,linux/arm64,linux/arm/v7
+          platforms: linux/amd64,linux/arm64
           push: true
           tags: meienberger/runtipi:latest,meienberger/runtipi:${{ steps.meta.outputs.TAG }}
           cache-from: type=registry,ref=meienberger/runtipi:buildcache
@@ -48,7 +47,7 @@ jobs:
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           package-path: ./package.json
-    
+
       - name: Create Release
         id: create_release
         uses: actions/create-release@latest

+ 46 - 26
.gitignore

@@ -4,38 +4,58 @@
 .DS_Store
 .vscode
 
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+/dist
+server-preload.js
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+
 logs
 .pnpm-debug.log
 .env*
+!.env.example
+!.env.test
 github.secrets
 node_modules/
-app-data/*
-data/postgres
-data/redis
-!app-data/.gitkeep
-repos/*
-!repos/.gitkeep
-apps/*
-!apps/.gitkeep
+/app-data/
+/data/
+/repos/
+/apps/
 traefik/shared
 
 # media folder
 media
-!media/.gitkeep
-!media/data/.gitkeep
-!media/data/books/.gitkeep
-!media/data/books/ebooks/.gitkeep
-!media/data/books/spoken/.gitkeep
-!media/data/movies/.gitkeep
-!media/data/music/.gitkeep
-!media/data/podcasts/.gitkeep
-!media/data/tv/.gitkeep
-!media/data/images/.gitkeep
-!media/torrents/.gitkeep
-!media/torrents/complete/.gitkeep
-!media/torrents/incomplete/.gitkeep
-!media/torrents/watch/.gitkeep
-
-# state folder
-state/*
-!state/.gitkeep
+
+/state/

+ 0 - 0
packages/dashboard/.prettierrc.js → .prettierrc.js


+ 38 - 32
Dockerfile

@@ -1,43 +1,49 @@
-FROM node:18-alpine3.16 AS builder
+ARG NODE_VERSION="18.12.1"
+ARG ALPINE_VERSION="3.16"
 
-# Required for argon2
-RUN apk --no-cache add g++
-RUN apk --no-cache add make
-RUN apk --no-cache add python3
+FROM node:${NODE_VERSION}-buster-slim AS node_base
+RUN apt update
+RUN apt install -y openssl
+
+FROM node_base AS builder_base
 
-# Required for sharp
-RUN apk --no-cache add vips-dev=8.12.2-r5
 RUN npm install node-gyp -g
+RUN npm install pnpm -g
 
-WORKDIR /api
-COPY ./packages/system-api/package.json /api/package.json
-RUN npm i
-# ---
-WORKDIR /dashboard
-COPY ./packages/dashboard/package.json /dashboard/package.json
-RUN npm i
+# BUILDER
+FROM builder_base AS builder
+
+WORKDIR /app
+
+COPY ./pnpm-lock.yaml ./
+RUN pnpm fetch
+
+COPY ./package*.json ./
+COPY ./prisma/schema.prisma ./prisma/
+
+RUN pnpm install -r --prefer-offline 
+COPY ./src ./src
+COPY ./esbuild.js ./esbuild.js
+COPY ./tsconfig.json ./tsconfig.json
+COPY ./next.config.mjs ./next.config.mjs
+COPY ./public ./public
 
-WORKDIR /api
-COPY ./packages/system-api /api
-RUN npm run build
-# ---
-WORKDIR /dashboard
-COPY ./packages/dashboard /dashboard
 RUN npm run build
 
-FROM node:18-alpine3.16 as app
+# APP
+FROM node_base AS app
+
+# USER node
 
-WORKDIR /
+WORKDIR /app
 
-WORKDIR /api
-COPY ./packages/system-api/package.json /api/
-COPY --from=builder /api/dist /api/dist
+COPY --from=builder /app/dist ./
+COPY --from=builder /app/next.config.mjs ./
+COPY --from=builder /app/public ./public
+COPY --from=builder /app/package.json ./package.json
+COPY --from=builder --chown=node:node /app/.next/standalone ./
+COPY --from=builder --chown=node:node /app/.next/static ./.next/static
 
-WORKDIR /dashboard
-COPY --from=builder /dashboard/next.config.js ./
-COPY --from=builder /dashboard/public ./public
-COPY --from=builder /dashboard/package.json ./package.json
-COPY --from=builder --chown=node:node /dashboard/.next/standalone ./
-COPY --from=builder --chown=node:node /dashboard/.next/static ./.next/static
+EXPOSE 3000
 
-WORKDIR /
+CMD ["npm", "run", "start"]

+ 21 - 12
Dockerfile.dev

@@ -1,19 +1,28 @@
-FROM node:18-alpine3.16
+ARG NODE_VERSION="18.12.1"
+ARG ALPINE_VERSION="3.16"
 
-WORKDIR /
+FROM node:${NODE_VERSION}-buster-slim 
 
-RUN apk --no-cache add g++ make
+RUN apt update
+RUN apt install -y openssl
+
+RUN npm install pnpm -g
 RUN npm install node-gyp -g
 
-WORKDIR /api
-COPY ./packages/system-api/package*.json /api/
-RUN npm install
+WORKDIR /app
+
+COPY ./pnpm-lock.yaml ./
+RUN pnpm fetch
+
+COPY ./package*.json ./
+COPY ./prisma/schema.prisma ./prisma/
 
-WORKDIR /dashboard
-COPY ./packages/dashboard/package*.json /dashboard/
-RUN npm install
+RUN pnpm install -r --prefer-offline 
 
-COPY ./packages/system-api /api
-COPY ./packages/dashboard /dashboard
+COPY ./src ./src
+COPY ./esbuild.js ./esbuild.js
+COPY ./tsconfig.json ./tsconfig.json
+COPY ./next.config.mjs ./next.config.mjs
+COPY ./public ./public
 
-WORKDIR /
+CMD ["npm", "run", "dev"]

+ 118 - 0
__mocks__/fs-extra.ts

@@ -0,0 +1,118 @@
+import path from 'path';
+
+class FsMock {
+  private static instance: FsMock;
+
+  private mockFiles = Object.create(null);
+
+  // private constructor() {}
+
+  static getInstance(): FsMock {
+    if (!FsMock.instance) {
+      FsMock.instance = new FsMock();
+    }
+    return FsMock.instance;
+  }
+
+  __createMockFiles = (newMockFiles: Record<string, string>) => {
+    this.mockFiles = Object.create(null);
+
+    // Create folder tree
+    Object.keys(newMockFiles).forEach((file) => {
+      const dir = path.dirname(file);
+
+      if (!this.mockFiles[dir]) {
+        this.mockFiles[dir] = [];
+      }
+
+      this.mockFiles[dir].push(path.basename(file));
+      this.mockFiles[file] = newMockFiles[file];
+    });
+  };
+
+  __resetAllMocks = () => {
+    this.mockFiles = Object.create(null);
+  };
+
+  readFileSync = (p: string) => this.mockFiles[p];
+
+  existsSync = (p: string) => this.mockFiles[p] !== undefined;
+
+  writeFileSync = (p: string, data: string | string[]) => {
+    this.mockFiles[p] = data;
+  };
+
+  mkdirSync = (p: string) => {
+    this.mockFiles[p] = Object.create(null);
+  };
+
+  rmSync = (p: string) => {
+    if (this.mockFiles[p] instanceof Array) {
+      this.mockFiles[p].forEach((file: string) => {
+        delete this.mockFiles[path.join(p, file)];
+      });
+    }
+
+    delete this.mockFiles[p];
+  };
+
+  readdirSync = (p: string) => {
+    const files: string[] = [];
+
+    const depth = p.split('/').length;
+
+    Object.keys(this.mockFiles).forEach((file) => {
+      if (file.startsWith(p)) {
+        const fileDepth = file.split('/').length;
+
+        if (fileDepth === depth + 1) {
+          files.push(file.split('/').pop() || '');
+        }
+      }
+    });
+
+    return files;
+  };
+
+  copyFileSync = (source: string, destination: string) => {
+    this.mockFiles[destination] = this.mockFiles[source];
+  };
+
+  copySync = (source: string, destination: string) => {
+    this.mockFiles[destination] = this.mockFiles[source];
+
+    if (this.mockFiles[source] instanceof Array) {
+      this.mockFiles[source].forEach((file: string) => {
+        this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
+      });
+    }
+  };
+
+  createFileSync = (p: string) => {
+    this.mockFiles[p] = '';
+  };
+
+  unlinkSync = (p: string) => {
+    if (this.mockFiles[p] instanceof Array) {
+      this.mockFiles[p].forEach((file: string) => {
+        delete this.mockFiles[path.join(p, file)];
+      });
+    }
+    delete this.mockFiles[p];
+  };
+
+  getMockFiles = () => this.mockFiles;
+
+  promises = {
+    unlink: (p: string) => {
+      if (this.mockFiles[p] instanceof Array) {
+        this.mockFiles[p].forEach((file: string) => {
+          delete this.mockFiles[path.join(p, file)];
+        });
+      }
+      delete this.mockFiles[p];
+    },
+  };
+}
+
+export default FsMock.getInstance();

+ 16 - 0
__mocks__/redis.ts

@@ -0,0 +1,16 @@
+export const createClient = jest.fn(() => {
+  const values = new Map();
+  const expirations = new Map();
+  return {
+    isOpen: true,
+    connect: jest.fn(),
+    set: (key: string, value: string, exp: number) => {
+      values.set(key, value);
+      expirations.set(key, exp);
+    },
+    get: (key: string) => values.get(key),
+    quit: jest.fn(),
+    del: (key: string) => values.delete(key),
+    ttl: (key: string) => expirations.get(key),
+  };
+});

+ 0 - 0
app-data/.gitkeep


+ 0 - 0
apps/.gitkeep


+ 0 - 3
commitlint.config.js

@@ -1,3 +0,0 @@
-module.exports = {
-  extends: ["@commitlint/config-conventional"],
-};

+ 14 - 39
docker-compose.dev.yml

@@ -50,65 +50,40 @@ services:
     networks:
       - tipi_main_network
 
-  api:
+  dashboard:
     build:
       context: .
       dockerfile: Dockerfile.dev
-    command: /bin/sh -c "cd /api && npm run build && npm run dev"
+    container_name: dashboard
     depends_on:
       tipi-db:
         condition: service_healthy
-    container_name: api
-    volumes:
-      - ${PWD}/repos:/runtipi/repos:ro
-      - ${PWD}/apps:/runtipi/apps
-      - ${PWD}/state:/runtipi/state
-      - ${PWD}/packages/system-api/src:/api/src
-      - ${STORAGE_PATH}:/app/storage
-      - ${PWD}/logs:/app/logs
-      - ${PWD}/.env.dev:/runtipi/.env
-      # - /api/node_modules
     environment:
+      NODE_ENV: development
       INTERNAL_IP: ${INTERNAL_IP}
       TIPI_VERSION: ${TIPI_VERSION}
       JWT_SECRET: ${JWT_SECRET}
       NGINX_PORT: ${NGINX_PORT}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
-      POSTGRES_USERNAME: tipi
-      POSTGRES_DBNAME: tipi
-      POSTGRES_HOST: tipi-db
+      POSTGRES_USERNAME: ${POSTGRES_USERNAME}
+      POSTGRES_DBNAME: ${POSTGRES_DBNAME}
+      POSTGRES_HOST: ${POSTGRES_HOST}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       ARCHITECTURE: ${ARCHITECTURE}
-    networks:
-      - tipi_main_network
-    labels:
-      traefik.enable: true
-      # Web
-      traefik.http.routers.api.rule: PathPrefix(`/api`)
-      traefik.http.routers.api.service: api
-      traefik.http.routers.api.entrypoints: web
-      traefik.http.routers.api.middlewares: api-stripprefix
-      traefik.http.services.api.loadbalancer.server.port: 3001
-      # Middlewares
-      traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
-
-  dashboard:
-    build:
-      context: .
-      dockerfile: Dockerfile.dev
-    command: /bin/sh -c "cd /dashboard && npm run dev"
-    container_name: dashboard
-    depends_on:
-      api:
-        condition: service_started
+      REDIS_HOST: ${REDIS_HOST}
     networks:
       - tipi_main_network
     volumes:
-      - ${PWD}/packages/dashboard/src:/dashboard/src
+      - ${PWD}/src:/app/src
       # - /dashboard/node_modules
       # - /dashboard/.next
+      - ${PWD}/state:/runtipi/state
+      - ${PWD}/repos:/runtipi/repos:ro
+      - ${PWD}/apps:/runtipi/apps
+      - ${PWD}/logs:/app/logs
+      - ${STORAGE_PATH}:/app/storage
     labels:
       traefik.enable: true
       # Web
@@ -126,4 +101,4 @@ networks:
         - subnet: 10.21.21.0/24
 
 volumes:
-  pgdata:
+  pgdata:

+ 15 - 45
docker-compose.rc.yml

@@ -45,65 +45,35 @@ services:
     networks:
       - tipi_main_network
 
-  api:
+  dashboard:
     image: meienberger/runtipi:rc-${TIPI_VERSION}
-    command: /bin/sh -c "cd /api && npm run start"
-    container_name: api
+    container_name: dashboard
+    networks:
+      - tipi_main_network
     depends_on:
       tipi-db:
         condition: service_healthy
-    volumes:
-      - ${PWD}/repos:/runtipi/repos:ro
-      - ${PWD}/apps:/runtipi/apps
-      - ${PWD}/state:/runtipi/state
-      - ${PWD}/logs:/app/logs
-      - ${STORAGE_PATH}:/app/storage
-      - ${PWD}/.env:/runtipi/.env:ro
     environment:
+      NODE_ENV: production
       INTERNAL_IP: ${INTERNAL_IP}
       TIPI_VERSION: ${TIPI_VERSION}
       JWT_SECRET: ${JWT_SECRET}
       NGINX_PORT: ${NGINX_PORT}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
-      POSTGRES_USERNAME: tipi
-      POSTGRES_DBNAME: tipi
-      POSTGRES_HOST: tipi-db
-      NODE_ENV: production
+      POSTGRES_USERNAME: ${POSTGRES_USERNAME}
+      POSTGRES_DBNAME: ${POSTGRES_DBNAME}
+      POSTGRES_HOST: ${POSTGRES_HOST}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       ARCHITECTURE: ${ARCHITECTURE}
-    networks:
-      - tipi_main_network
-    labels:
-      traefik.enable: true
-      # Web
-      traefik.http.routers.api.rule: PathPrefix(`/api`)
-      traefik.http.routers.api.service: api
-      traefik.http.routers.api.entrypoints: web
-      traefik.http.routers.api.middlewares: api-stripprefix
-      traefik.http.services.api.loadbalancer.server.port: 3001
-      # Websecure
-      traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
-      traefik.http.routers.api-secure.entrypoints: websecure
-      traefik.http.routers.api-secure.service: api-secure
-      traefik.http.routers.api-secure.tls.certresolver: myresolver
-      traefik.http.routers.api-secure.middlewares: api-stripprefix
-      traefik.http.services.api-secure.loadbalancer.server.port: 3001
-      # Middlewares
-      traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
-
-  dashboard:
-    image: meienberger/runtipi:rc-${TIPI_VERSION}
-    command: /bin/sh -c "cd /dashboard && node server.js"
-    container_name: dashboard
-    networks:
-      - tipi_main_network
-    depends_on:
-      api:
-        condition: service_started
-    environment:
-      NODE_ENV: production
+      REDIS_HOST: ${REDIS_HOST}
+    volumes:
+      - ${PWD}/state:/runtipi/state
+      - ${PWD}/repos:/runtipi/repos:ro
+      - ${PWD}/apps:/runtipi/apps
+      - ${PWD}/logs:/app/logs
+      - ${STORAGE_PATH}:/app/storage
     labels:
       traefik.enable: true
       # Web

+ 18 - 67
docker-compose.test.yml

@@ -44,100 +44,51 @@ services:
     networks:
       - tipi_main_network
 
-  api:
+  dashboard:
     build:
       context: .
       dockerfile: Dockerfile
-    command: /bin/sh -c "cd /api && npm run start"
     restart: unless-stopped
-    container_name: api
+    container_name: dashboard
+    networks:
+      - tipi_main_network
     depends_on:
       tipi-db:
         condition: service_healthy
-    volumes:
-      - ${PWD}/repos:/runtipi/repos:ro
-      - ${PWD}/apps:/runtipi/apps
-      - ${PWD}/state:/runtipi/state
-      - ${PWD}/logs:/app/logs
-      - ${STORAGE_PATH}:/app/storage
-      - ${PWD}/.env:/runtipi/.env:ro
     environment:
+      NODE_ENV: production
       INTERNAL_IP: ${INTERNAL_IP}
       TIPI_VERSION: ${TIPI_VERSION}
       JWT_SECRET: ${JWT_SECRET}
       NGINX_PORT: ${NGINX_PORT}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
-      POSTGRES_USERNAME: tipi
-      POSTGRES_DBNAME: tipi
-      POSTGRES_HOST: tipi-db
-      NODE_ENV: production
+      POSTGRES_USERNAME: ${POSTGRES_USERNAME}
+      POSTGRES_DBNAME: ${POSTGRES_DBNAME}
+      POSTGRES_HOST: ${POSTGRES_HOST}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       ARCHITECTURE: ${ARCHITECTURE}
-    networks:
-      - tipi_main_network
-    labels:
-      traefik.enable: true
-      # Web
-      traefik.http.routers.api.rule: PathPrefix(`/api`)
-      traefik.http.routers.api.service: api
-      traefik.http.routers.api.entrypoints: web
-      traefik.http.routers.api.middlewares: api-stripprefix
-      traefik.http.services.api.loadbalancer.server.port: 3001
-      # Websecure
-      traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
-      traefik.http.routers.api-secure.entrypoints: websecure
-      traefik.http.routers.api-secure.service: api-secure
-      traefik.http.routers.api-secure.tls.certresolver: myresolver
-      traefik.http.routers.api-secure.middlewares: api-stripprefix
-      traefik.http.services.api-secure.loadbalancer.server.port: 3001
-      # Middlewares
-      traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
-
-  dashboard:
-    build:
-      context: .
-      dockerfile: Dockerfile
-    command: /bin/sh -c "cd /dashboard && node server.js"
-    restart: unless-stopped
-    container_name: dashboard
-    networks:
-      - tipi_main_network
-    depends_on:
-      api:
-        condition: service_started
-    environment:
-      NODE_ENV: production
+      REDIS_HOST: ${REDIS_HOST}
+    volumes:
+      - ${PWD}/state:/runtipi/state
+      - ${PWD}/repos:/runtipi/repos:ro
+      - ${PWD}/apps:/runtipi/apps
+      - ${PWD}/logs:/app/logs
+      - ${STORAGE_PATH}:/app/storage
     labels:
       traefik.enable: true
-      traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")
-      traefik.http.routers.dashboard-redirect.entrypoints: web
-      traefik.http.routers.dashboard-redirect.middlewares: redirect-middleware
-      traefik.http.routers.dashboard-redirect.service: dashboard
-      traefik.http.services.dashboard-redirect.loadbalancer.server.port: 3000
-
-      traefik.http.routers.dashboard-redirect-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
-      traefik.http.routers.dashboard-redirect-secure.entrypoints: websecure
-      traefik.http.routers.dashboard-redirect-secure.middlewares: redirect-middleware
-      traefik.http.routers.dashboard-redirect-secure.service: dashboard
-      traefik.http.routers.dashboard-redirect-secure.tls.certresolver: myresolver
-      traefik.http.services.dashboard-redirect-secure.loadbalancer.server.port: 3000
-
       # Web
-      traefik.http.routers.dashboard.rule: PathPrefix("/dashboard")
+      traefik.http.routers.dashboard.rule: PathPrefix("/")
       traefik.http.routers.dashboard.service: dashboard
       traefik.http.routers.dashboard.entrypoints: web
       traefik.http.services.dashboard.loadbalancer.server.port: 3000
       # Websecure
-      traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/dashboard`)
+      traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
       traefik.http.routers.dashboard-secure.service: dashboard-secure
       traefik.http.routers.dashboard-secure.entrypoints: websecure
       traefik.http.routers.dashboard-secure.tls.certresolver: myresolver
       traefik.http.services.dashboard-secure.loadbalancer.server.port: 3000
-      # Middlewares
-      traefik.http.middlewares.redirect-middleware.redirectregex.regex: .*
-      traefik.http.middlewares.redirect-middleware.redirectregex.replacement: /dashboard
 
 networks:
   tipi_main_network:
@@ -148,4 +99,4 @@ networks:
         - subnet: 10.21.21.0/24
 
 volumes:
-  pgdata:
+  pgdata:

+ 15 - 46
docker-compose.yml

@@ -44,67 +44,36 @@ services:
     networks:
       - tipi_main_network
 
-  api:
+  dashboard:
     image: meienberger/runtipi:${TIPI_VERSION}
-    command: /bin/sh -c "cd /api && npm run start"
     restart: unless-stopped
-    container_name: api
+    container_name: dashboard
+    networks:
+      - tipi_main_network
     depends_on:
       tipi-db:
         condition: service_healthy
-    volumes:
-      - ${PWD}/repos:/runtipi/repos:ro
-      - ${PWD}/apps:/runtipi/apps
-      - ${PWD}/state:/runtipi/state
-      - ${PWD}/logs:/app/logs
-      - ${STORAGE_PATH}:/app/storage
-      - ${PWD}/.env:/runtipi/.env:ro
     environment:
+      NODE_ENV: production
       INTERNAL_IP: ${INTERNAL_IP}
       TIPI_VERSION: ${TIPI_VERSION}
       JWT_SECRET: ${JWT_SECRET}
       NGINX_PORT: ${NGINX_PORT}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
-      POSTGRES_USERNAME: tipi
-      POSTGRES_DBNAME: tipi
-      POSTGRES_HOST: tipi-db
-      NODE_ENV: production
+      POSTGRES_USERNAME: ${POSTGRES_USERNAME}
+      POSTGRES_DBNAME: ${POSTGRES_DBNAME}
+      POSTGRES_HOST: ${POSTGRES_HOST}
       APPS_REPO_ID: ${APPS_REPO_ID}
       APPS_REPO_URL: ${APPS_REPO_URL}
       DOMAIN: ${DOMAIN}
       ARCHITECTURE: ${ARCHITECTURE}
-    networks:
-      - tipi_main_network
-    labels:
-      traefik.enable: true
-      # Web
-      traefik.http.routers.api.rule: PathPrefix(`/api`)
-      traefik.http.routers.api.service: api
-      traefik.http.routers.api.entrypoints: web
-      traefik.http.routers.api.middlewares: api-stripprefix
-      traefik.http.services.api.loadbalancer.server.port: 3001
-      # Websecure
-      traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
-      traefik.http.routers.api-secure.entrypoints: websecure
-      traefik.http.routers.api-secure.service: api-secure
-      traefik.http.routers.api-secure.tls.certresolver: myresolver
-      traefik.http.routers.api-secure.middlewares: api-stripprefix
-      traefik.http.services.api-secure.loadbalancer.server.port: 3001
-      # Middlewares
-      traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
-
-  dashboard:
-    image: meienberger/runtipi:${TIPI_VERSION}
-    command: /bin/sh -c "cd /dashboard && node server.js"
-    restart: unless-stopped
-    container_name: dashboard
-    networks:
-      - tipi_main_network
-    depends_on:
-      api:
-        condition: service_started
-    environment:
-      NODE_ENV: production
+      REDIS_HOST: ${REDIS_HOST}
+    volumes:
+      - ${PWD}/state:/runtipi/state
+      - ${PWD}/repos:/runtipi/repos:ro
+      - ${PWD}/apps:/runtipi/apps
+      - ${PWD}/logs:/app/logs
+      - ${STORAGE_PATH}:/app/storage
     labels:
       traefik.enable: true
       # Web

+ 40 - 0
esbuild.js

@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+/* eslint-disable @typescript-eslint/no-var-requires */
+const esbuild = require('esbuild');
+const { spawn } = require('child_process');
+const pkg = require('./package.json');
+
+const isDev = process.argv[2] !== 'build';
+
+process.env.NODE_ENV = isDev ? 'development' : 'production';
+
+let server;
+const onRebuild = () => {
+  if (isDev) {
+    if (server) server.kill('SIGINT');
+    server = spawn('node', ['dist/index.js'], { stdio: [0, 1, 2] });
+  } else {
+    spawn('pnpm', ['next', 'build'], { stdio: [0, 1, 2] });
+  }
+};
+
+const included = ['express', 'pg', '@runtipi/postgres-migrations'];
+const excluded = ['pg-native', '*required-server-files.json'];
+const external = Object.keys(pkg.dependencies || {}).filter((dep) => !included.includes(dep));
+external.push(...excluded);
+
+esbuild
+  .build({
+    entryPoints: ['src/server/index.ts'],
+    external,
+    define: { 'process.env.NODE_ENV': `"${process.env.NODE_ENV}"` },
+    platform: 'node',
+    target: 'node14',
+    outfile: 'dist/index.js',
+    tsconfig: 'tsconfig.json',
+    bundle: true,
+    minify: isDev,
+    sourcemap: isDev,
+    watch: isDev,
+  })
+  .finally(onRebuild);

+ 0 - 7
jest.config.js

@@ -1,7 +0,0 @@
-/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
-module.exports = {
-  preset: "ts-jest",
-  testEnvironment: "node",
-  testMatch: ["**/__tests__/**/*.test.ts"],
-  testPathIgnorePatterns: ["/node_modules/", "/packages/"],
-};

+ 39 - 0
jest.config.ts

@@ -0,0 +1,39 @@
+import nextJest from 'next/jest';
+
+const createJestConfig = nextJest({
+  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
+  dir: './',
+});
+
+const customClientConfig = {
+  testEnvironment: 'jest-environment-jsdom',
+  setupFilesAfterEnv: ['<rootDir>/tests/client/jest.setup.tsx'],
+  testMatch: ['<rootDir>/src/client/**/*.{spec,test}.{ts,tsx}', '!<rootDir>/src/server/**/*.{spec,test}.{ts,tsx}'],
+};
+
+const customServerConfig = {
+  testEnvironment: 'node',
+  testMatch: ['<rootDir>/src/server/**/*.test.ts'],
+  setupFilesAfterEnv: ['<rootDir>/tests/server/jest.setup.ts'],
+};
+
+export default async () => {
+  const clientConfig = await createJestConfig(customClientConfig)();
+  const serverConfig = await createJestConfig(customServerConfig)();
+
+  return {
+    verbose: true,
+    collectCoverage: true,
+    collectCoverageFrom: ['src/server/**/*.{ts,tsx}', 'src/client/**/*.{ts,tsx}', '!src/**/mocks/**/*.{ts,tsx}', '!**/*.{spec,test}.{ts,tsx}', '!**/index.{ts,tsx}'],
+    projects: [
+      {
+        displayName: 'client',
+        ...clientConfig,
+      },
+      {
+        displayName: 'server',
+        ...serverConfig,
+      },
+    ],
+  };
+};

+ 0 - 0
media/.gitkeep


+ 0 - 0
media/data/.gitkeep


+ 0 - 0
media/data/books/.gitkeep


+ 0 - 0
media/data/books/ebooks/.gitkeep


二进制
media/data/books/metadata.db


+ 0 - 0
media/data/books/spoken/.gitkeep


+ 0 - 0
media/data/images/.gitkeep


+ 0 - 0
media/data/movies/.gitkeep


+ 0 - 0
media/data/music/.gitkeep


+ 0 - 0
media/data/podcasts/.gitkeep


+ 0 - 0
media/data/tv/.gitkeep


+ 0 - 0
media/torrents/.gitkeep


+ 0 - 0
media/torrents/complete/.gitkeep


+ 0 - 0
media/torrents/incomplete/.gitkeep


+ 0 - 0
media/torrents/watch/.gitkeep


+ 0 - 0
packages/dashboard/next-env.d.ts → next-env.d.ts


+ 23 - 0
next.config.mjs

@@ -0,0 +1,23 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  swcMinify: true,
+  output: 'standalone',
+  reactStrictMode: true,
+  serverRuntimeConfig: {
+    INTERNAL_IP: process.env.INTERNAL_IP,
+    TIPI_VERSION: process.env.TIPI_VERSION,
+    JWT_SECRET: process.env.JWT_SECRET,
+    POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
+    POSTGRES_USERNAME: process.env.POSTGRES_USERNAME,
+    POSTGRES_DBNAME: process.env.POSTGRES_DBNAME,
+    POSTGRES_HOST: process.env.POSTGRES_HOST,
+    APPS_REPO_ID: process.env.APPS_REPO_ID,
+    APPS_REPO_URL: process.env.APPS_REPO_URL,
+    DOMAIN: process.env.DOMAIN,
+    ARCHITECTURE: process.env.ARCHITECTURE,
+    NODE_ENV: process.env.NODE_ENV,
+    REDIS_HOST: process.env.REDIS_HOST,
+  },
+};
+
+export default nextConfig;

+ 113 - 18
package.json

@@ -1,26 +1,126 @@
 {
   "name": "runtipi",
-  "version": "0.8.1",
+  "version": "1.0.0",
   "description": "A homeserver for everyone",
   "scripts": {
-    "commit": "git-cz",
-    "act:test-install": "act --container-architecture linux/amd64 -j test-install",
-    "act:docker": "act --container-architecture linux/amd64 --secret-file github.secrets -j build-images",
+    "copy:migrations": "mkdir -p dist/migrations && cp -r ./src/server/migrations dist",
+    "prisma:pull": "prisma db pull",
+    "test": "dotenv -e .env.test -- jest --colors",
+    "test:client": "jest --colors --selectProjects client --",
+    "test:server": "jest --colors --selectProjects server --",
+    "postinstall": "prisma generate",
+    "dev": "npm run copy:migrations && node ./esbuild.js dev",
+    "start": "NODE_ENV=production node index.js",
+    "lint": "next lint",
+    "lint:fix": "next lint --fix",
+    "build": "npm run copy:migrations && node ./esbuild.js build",
+    "build:server": "node ./esbuild.js build",
+    "build:next": "next build",
     "start:dev": "./scripts/start-dev.sh",
     "start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
     "start:rc": "docker-compose -f docker-compose.rc.yml --env-file .env up --build",
     "start:prod": "docker-compose -f docker-compose.test.yml --env-file .env up --build",
-    "start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres",
+    "start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
     "version": "echo $npm_package_version",
     "release:rc": "./scripts/deploy/release-rc.sh",
-    "test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test ."
+    "test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test .",
+    "test:build:arm64": "docker buildx build --platform linux/arm64 -t meienberger/runtipi:test .",
+    "test:build:arm7": "docker buildx build --platform linux/arm/v7 -t meienberger/runtipi:test .",
+    "test:build:amd64": "docker buildx build --platform linux/amd64 -t meienberger/runtipi:test ."
+  },
+  "dependencies": {
+    "@hookform/resolvers": "^2.9.10",
+    "@prisma/client": "^4.8.0",
+    "@runtipi/postgres-migrations": "^5.3.0",
+    "@tabler/core": "1.0.0-beta16",
+    "@tabler/icons": "^1.109.0",
+    "@tanstack/react-query": "^4.24.4",
+    "@trpc/client": "^10.11.1",
+    "@trpc/next": "^10.11.1",
+    "@trpc/react-query": "^10.11.1",
+    "@trpc/server": "^10.11.1",
+    "argon2": "^0.29.1",
+    "clsx": "^1.1.1",
+    "express": "^4.17.3",
+    "fs-extra": "^10.1.0",
+    "graphql": "^15.8.0",
+    "graphql-tag": "^2.12.6",
+    "isomorphic-fetch": "^3.0.0",
+    "jsonwebtoken": "^9.0.0",
+    "next": "13.1.6",
+    "node-cron": "^3.0.1",
+    "node-fetch-commonjs": "^3.2.4",
+    "pg": "^8.7.3",
+    "react": "18.2.0",
+    "react-dom": "18.2.0",
+    "react-hook-form": "^7.38.0",
+    "react-markdown": "^8.0.3",
+    "react-select": "^5.6.1",
+    "react-tooltip": "^4.4.3",
+    "redis": "^4.3.1",
+    "remark-breaks": "^3.0.2",
+    "remark-gfm": "^3.0.1",
+    "sass": "^1.55.0",
+    "semver": "^7.3.7",
+    "sharp": "0.30.7",
+    "superjson": "^1.12.0",
+    "tslib": "^2.4.0",
+    "uuid": "^9.0.0",
+    "validator": "^13.7.0",
+    "winston": "^3.7.2",
+    "zod": "^3.19.1",
+    "zustand": "^3.7.2"
   },
   "devDependencies": {
-    "@commitlint/cli": "^17.0.3",
-    "@commitlint/config-conventional": "^17.0.3",
-    "@commitlint/cz-commitlint": "^17.0.3",
-    "commitizen": "^4.2.4",
-    "inquirer": "8.2.4"
+    "@babel/core": "^7.0.0",
+    "@faker-js/faker": "^7.6.0",
+    "@testing-library/dom": "^8.19.0",
+    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/user-event": "^14.4.3",
+    "@types/express": "^4.17.13",
+    "@types/fs-extra": "^9.0.13",
+    "@types/isomorphic-fetch": "^0.0.36",
+    "@types/jest": "^29.2.4",
+    "@types/jsonwebtoken": "^9.0.0",
+    "@types/node": "18.11.18",
+    "@types/node-cron": "^3.0.2",
+    "@types/pg": "^8.6.5",
+    "@types/react": "18.0.8",
+    "@types/react-dom": "18.0.3",
+    "@types/semver": "^7.3.12",
+    "@types/testing-library__jest-dom": "^5.14.5",
+    "@types/uuid": "^8.3.4",
+    "@types/validator": "^13.7.2",
+    "@typescript-eslint/eslint-plugin": "^5.47.1",
+    "@typescript-eslint/parser": "^5.47.1",
+    "dotenv-cli": "^6.0.0",
+    "esbuild": "^0.16.17",
+    "eslint": "8.30.0",
+    "eslint-config-airbnb": "^19.0.4",
+    "eslint-config-airbnb-typescript": "^17.0.0",
+    "eslint-config-next": "13.1.1",
+    "eslint-config-prettier": "^8.6.0",
+    "eslint-plugin-import": "^2.25.3",
+    "eslint-plugin-jest": "^27.1.7",
+    "eslint-plugin-jsdoc": "^39.6.9",
+    "eslint-plugin-jsx-a11y": "^6.6.1",
+    "eslint-plugin-react": "^7.31.10",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "jest": "^29.3.1",
+    "jest-environment-jsdom": "^29.3.1",
+    "msw": "^1.0.0",
+    "next-router-mock": "^0.8.0",
+    "prettier": "^2.8.4",
+    "prisma": "^4.10.1",
+    "ts-jest": "^29.0.3",
+    "ts-node": "^10.9.1",
+    "typescript": "4.9.4",
+    "wait-for-expect": "^3.0.2",
+    "whatwg-fetch": "^3.6.2"
+  },
+  "msw": {
+    "workerDirectory": "public"
   },
   "repository": {
     "type": "git",
@@ -31,10 +131,5 @@
   "bugs": {
     "url": "https://github.com/meienberger/runtipi/issues"
   },
-  "homepage": "https://github.com/meienberger/runtipi#readme",
-  "config": {
-    "commitizen": {
-      "path": "@commitlint/cz-commitlint"
-    }
-  }
-}
+  "homepage": "https://github.com/meienberger/runtipi#readme"
+}

+ 0 - 5
packages/dashboard/.dockerignore

@@ -1,5 +0,0 @@
-node_modules/
-.next/
-dist/
-sessions/
-logs/

+ 0 - 35
packages/dashboard/.gitignore

@@ -1,35 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo

+ 0 - 34
packages/dashboard/README.md

@@ -1,34 +0,0 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
-
-[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
-
-The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

+ 0 - 9
packages/dashboard/codegen.yml

@@ -1,9 +0,0 @@
-overwrite: true
-schema: "http://localhost:3000/api/graphql"
-documents: "src/graphql/**/*.graphql"
-generates:
-  src/generated/graphql.tsx:
-    plugins:
-      - "typescript"
-      - "typescript-operations"
-      - "typescript-react-apollo"

+ 0 - 18
packages/dashboard/jest.config.js

@@ -1,18 +0,0 @@
-const nextJest = require('next/jest');
-
-const createJestConfig = nextJest({
-  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
-  dir: './',
-});
-
-// Add any custom config to be passed to Jest
-const customJestConfig = {
-  setupFilesAfterEnv: ['<rootDir>/tests/jest.setup.tsx'],
-  testEnvironment: 'jest-environment-jsdom',
-  collectCoverage: true,
-  collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/index.ts', '!**/src/pages/**/*.{ts,tsx}', '!**/src/mocks/**', '!**/src/core/apollo/**'],
-  testMatch: ['<rootDir>/src/**/*.{spec,test}.{ts,tsx}'],
-};
-
-// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
-module.exports = createJestConfig(customJestConfig);

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

@@ -1,8 +0,0 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {
-  output: 'standalone',
-  reactStrictMode: true,
-  swcMinify: true,
-};
-
-module.exports = nextConfig;

+ 0 - 85
packages/dashboard/package.json

@@ -1,85 +0,0 @@
-{
-  "name": "dashboard",
-  "version": "0.8.1",
-  "private": true,
-  "scripts": {
-    "test": "jest --colors",
-    "dev": "next dev",
-    "dev:msw": "NEXT_PUBLIC_API_MOCKING=enabled next dev",
-    "build": "next build",
-    "start": "next start",
-    "lint": "next lint",
-    "lint:fix": "next lint --fix",
-    "gen": "graphql-codegen --config codegen.yml"
-  },
-  "dependencies": {
-    "@apollo/client": "^3.6.8",
-    "@hookform/resolvers": "^2.9.10",
-    "@tabler/core": "1.0.0-beta16",
-    "@tabler/icons": "^1.109.0",
-    "clsx": "^1.1.1",
-    "graphql": "^15.8.0",
-    "graphql-tag": "^2.12.6",
-    "isomorphic-fetch": "^3.0.0",
-    "next": "13.0.3",
-    "react": "18.2.0",
-    "react-dom": "18.2.0",
-    "react-hook-form": "^7.38.0",
-    "react-markdown": "^8.0.3",
-    "react-select": "^5.6.1",
-    "react-tooltip": "^4.4.3",
-    "remark-breaks": "^3.0.2",
-    "remark-gfm": "^3.0.1",
-    "remark-mdx": "^2.1.1",
-    "sass": "^1.55.0",
-    "semver": "^7.3.7",
-    "sharp": "0.30.7",
-    "swr": "^1.3.0",
-    "tslib": "^2.4.0",
-    "validator": "^13.7.0",
-    "zod": "^3.19.1",
-    "zustand": "^3.7.2"
-  },
-  "devDependencies": {
-    "@babel/core": "^7.0.0",
-    "@faker-js/faker": "^7.3.0",
-    "@graphql-codegen/cli": "^2.6.2",
-    "@graphql-codegen/typescript": "^2.5.1",
-    "@graphql-codegen/typescript-operations": "^2.4.2",
-    "@graphql-codegen/typescript-react-apollo": "^3.2.16",
-    "@testing-library/dom": "^8.19.0",
-    "@testing-library/jest-dom": "^5.16.5",
-    "@testing-library/react": "^13.4.0",
-    "@testing-library/user-event": "^14.4.3",
-    "@types/isomorphic-fetch": "^0.0.36",
-    "@types/jest": "^27.5.0",
-    "@types/node": "17.0.31",
-    "@types/react": "18.0.8",
-    "@types/react-dom": "18.0.3",
-    "@types/semver": "^7.3.12",
-    "@types/testing-library__jest-dom": "^5.14.5",
-    "@types/validator": "^13.7.2",
-    "@typescript-eslint/eslint-plugin": "^5.18.0",
-    "@typescript-eslint/parser": "^5.0.0",
-    "concurrently": "^7.1.0",
-    "eslint": "8.12.0",
-    "eslint-config-airbnb": "^19.0.4",
-    "eslint-config-airbnb-typescript": "^17.0.0",
-    "eslint-config-next": "12.1.4",
-    "eslint-plugin-import": "^2.25.3",
-    "eslint-plugin-jest": "^27.1.6",
-    "eslint-plugin-jsx-a11y": "^6.6.1",
-    "eslint-plugin-react": "^7.31.10",
-    "eslint-plugin-react-hooks": "^4.6.0",
-    "jest": "^28.1.0",
-    "jest-environment-jsdom": "^29.3.1",
-    "msw": "^0.49.1",
-    "next-router-mock": "^0.8.0",
-    "ts-jest": "^28.0.2",
-    "typescript": "4.6.4",
-    "whatwg-fetch": "^3.6.2"
-  },
-  "msw": {
-    "workerDirectory": "public"
-  }
-}

+ 0 - 47
packages/dashboard/src/components/hoc/StatusProvider/StatusProvider.tsx

@@ -1,47 +0,0 @@
-import React, { ReactElement, useEffect, useState } from 'react';
-import useSWR from 'swr';
-import router from 'next/router';
-import { SystemStatus } from '../../../state/systemStore';
-import { StatusScreen } from '../../StatusScreen';
-
-interface IProps {
-  children: ReactElement;
-}
-
-const fetcher = (url: string) => fetch(url).then((res) => res.json());
-
-export const StatusProvider: React.FC<IProps> = ({ children }) => {
-  const [s, setS] = useState<SystemStatus>(SystemStatus.RUNNING);
-  const { data, isValidating } = useSWR<{ status: SystemStatus }>('/api/status', fetcher, { refreshInterval: 1000 });
-
-  useEffect(() => {
-    // If previous was not running and current is running, we need to refresh the page
-    if (data?.status === SystemStatus.RUNNING && s !== SystemStatus.RUNNING) {
-      router.reload();
-    }
-
-    if (data?.status === SystemStatus.RUNNING) {
-      setS(SystemStatus.RUNNING);
-    }
-    if (data?.status === SystemStatus.RESTARTING) {
-      setS(SystemStatus.RESTARTING);
-    }
-    if (data?.status === SystemStatus.UPDATING) {
-      setS(SystemStatus.UPDATING);
-    }
-  }, [data?.status, s]);
-
-  if (isValidating && !data?.status) {
-    return <StatusScreen title="" subtitle="" />;
-  }
-
-  if (s === SystemStatus.RESTARTING) {
-    return <StatusScreen title="Your system is restarting..." subtitle="Please do not refresh this page" />;
-  }
-
-  if (s === SystemStatus.UPDATING) {
-    return <StatusScreen title="Your system is updating..." subtitle="Please do not refresh this page" />;
-  }
-
-  return children;
-};

+ 0 - 11
packages/dashboard/src/core/apollo/client.ts

@@ -1,11 +0,0 @@
-import { ApolloClient, from, InMemoryCache } from '@apollo/client';
-import links from './links';
-
-export const createApolloClient = (): ApolloClient<unknown> => {
-  const additiveLink = from([links.errorLink, links.authLink, links.httpLink]);
-
-  return new ApolloClient({
-    link: additiveLink,
-    cache: new InMemoryCache(),
-  });
-};

+ 0 - 14
packages/dashboard/src/core/apollo/links/authLink.ts

@@ -1,14 +0,0 @@
-import { setContext } from '@apollo/client/link/context';
-
-const authLink = setContext((_, { headers }) => {
-  const token = localStorage.getItem('token');
-
-  return {
-    headers: {
-      ...headers,
-      authorization: token ? `Bearer ${token}` : '',
-    },
-  };
-});
-
-export default authLink;

+ 0 - 14
packages/dashboard/src/core/apollo/links/errorLink.ts

@@ -1,14 +0,0 @@
-import { onError } from '@apollo/client/link/error';
-
-const errorLink = onError(({ graphQLErrors, networkError }) => {
-  if (graphQLErrors)
-    graphQLErrors.forEach(({ message, locations, path }) => {
-      console.warn(`Error link [GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
-    });
-
-  if (networkError) {
-    console.warn(`Error link [Network error]: ${networkError}`);
-  }
-});
-
-export default errorLink;

+ 0 - 7
packages/dashboard/src/core/apollo/links/httpLink.ts

@@ -1,7 +0,0 @@
-import { HttpLink } from '@apollo/client';
-
-const httpLink = new HttpLink({
-  uri: '/api/graphql',
-});
-
-export default httpLink;

+ 0 - 11
packages/dashboard/src/core/apollo/links/index.ts

@@ -1,11 +0,0 @@
-import errorLink from './errorLink';
-import httpLink from './httpLink';
-import authLink from './authLink';
-
-const links = {
-  errorLink,
-  httpLink,
-  authLink,
-};
-
-export default links;

+ 0 - 18
packages/dashboard/src/core/constants.ts

@@ -1,18 +0,0 @@
-import { AppCategoriesEnum } from '../generated/graphql';
-
-export const APP_CATEGORIES = [
-  { name: 'Network', id: AppCategoriesEnum.Network, icon: 'FaNetworkWired' },
-  { name: 'Media', id: AppCategoriesEnum.Media, icon: 'FaVideo' },
-  { name: 'Development', id: AppCategoriesEnum.Development, icon: 'FaCode' },
-  { name: 'Automation', id: AppCategoriesEnum.Automation, icon: 'FaRobot' },
-  { name: 'Social', id: AppCategoriesEnum.Social, icon: 'FaUserFriends' },
-  { name: 'Utilities', id: AppCategoriesEnum.Utilities, icon: 'FaWrench' },
-  { name: 'Photography', id: AppCategoriesEnum.Photography, icon: 'FaCamera' },
-  { name: 'Security', id: AppCategoriesEnum.Security, icon: 'FaShieldAlt' },
-  { name: 'Featured', id: AppCategoriesEnum.Featured, icon: 'FaStar' },
-  { name: 'Books', id: AppCategoriesEnum.Books, icon: 'FaBook' },
-  { name: 'Data', id: AppCategoriesEnum.Data, icon: 'FaDatabase' },
-  { name: 'Music', id: AppCategoriesEnum.Music, icon: 'FaMusic' },
-  { name: 'Finance', id: AppCategoriesEnum.Finance, icon: 'FaMoneyBillAlt' },
-  { name: 'Gaming', id: AppCategoriesEnum.Gaming, icon: 'FaGamepad' },
-];

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

@@ -1,4 +0,0 @@
-export interface IUser {
-  name: string;
-  email: string;
-}

+ 0 - 1131
packages/dashboard/src/generated/graphql.tsx

@@ -1,1131 +0,0 @@
-import { gql } from '@apollo/client';
-import * as Apollo from '@apollo/client';
-
-export type Maybe<T> = T | null;
-export type InputMaybe<T> = Maybe<T>;
-export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
-export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
-export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
-const defaultOptions = {} as const;
-/** All built-in and custom scalars, mapped to their actual values */
-export type Scalars = {
-  ID: string;
-  String: string;
-  Boolean: boolean;
-  Int: number;
-  Float: number;
-  /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
-  DateTime: any;
-  /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
-  JSONObject: any;
-};
-
-export type App = {
-  __typename?: 'App';
-  config: Scalars['JSONObject'];
-  createdAt: Scalars['DateTime'];
-  domain?: Maybe<Scalars['String']>;
-  exposed: Scalars['Boolean'];
-  id: Scalars['String'];
-  info?: Maybe<AppInfo>;
-  lastOpened: Scalars['DateTime'];
-  numOpened: Scalars['Float'];
-  status: AppStatusEnum;
-  updateInfo?: Maybe<UpdateInfo>;
-  updatedAt: Scalars['DateTime'];
-  version?: Maybe<Scalars['Float']>;
-};
-
-export enum AppCategoriesEnum {
-  Automation = 'AUTOMATION',
-  Books = 'BOOKS',
-  Data = 'DATA',
-  Development = 'DEVELOPMENT',
-  Featured = 'FEATURED',
-  Finance = 'FINANCE',
-  Gaming = 'GAMING',
-  Media = 'MEDIA',
-  Music = 'MUSIC',
-  Network = 'NETWORK',
-  Photography = 'PHOTOGRAPHY',
-  Security = 'SECURITY',
-  Social = 'SOCIAL',
-  Utilities = 'UTILITIES',
-}
-
-export type AppInfo = {
-  __typename?: 'AppInfo';
-  author: Scalars['String'];
-  available: Scalars['Boolean'];
-  categories: Array<AppCategoriesEnum>;
-  description: Scalars['String'];
-  exposable?: Maybe<Scalars['Boolean']>;
-  form_fields: Array<FormField>;
-  https?: Maybe<Scalars['Boolean']>;
-  id: Scalars['String'];
-  name: Scalars['String'];
-  no_gui?: Maybe<Scalars['Boolean']>;
-  port: Scalars['Float'];
-  short_desc: Scalars['String'];
-  source: Scalars['String'];
-  supported_architectures?: Maybe<Array<AppSupportedArchitecturesEnum>>;
-  tipi_version: Scalars['Float'];
-  url_suffix?: Maybe<Scalars['String']>;
-  version?: Maybe<Scalars['String']>;
-};
-
-export type AppInputType = {
-  domain: Scalars['String'];
-  exposed: Scalars['Boolean'];
-  form: Scalars['JSONObject'];
-  id: Scalars['String'];
-};
-
-export enum AppStatusEnum {
-  Installing = 'INSTALLING',
-  Missing = 'MISSING',
-  Running = 'RUNNING',
-  Starting = 'STARTING',
-  Stopped = 'STOPPED',
-  Stopping = 'STOPPING',
-  Uninstalling = 'UNINSTALLING',
-  Updating = 'UPDATING',
-}
-
-export enum AppSupportedArchitecturesEnum {
-  Amd64 = 'AMD64',
-  Arm = 'ARM',
-  Arm64 = 'ARM64',
-}
-
-export type Cpu = {
-  __typename?: 'Cpu';
-  load: Scalars['Float'];
-};
-
-export type DiskMemory = {
-  __typename?: 'DiskMemory';
-  available: Scalars['Float'];
-  total: Scalars['Float'];
-  used: Scalars['Float'];
-};
-
-export enum FieldTypesEnum {
-  Email = 'email',
-  Fqdn = 'fqdn',
-  Fqdnip = 'fqdnip',
-  Ip = 'ip',
-  Number = 'number',
-  Password = 'password',
-  Random = 'random',
-  Text = 'text',
-  Url = 'url',
-}
-
-export type FormField = {
-  __typename?: 'FormField';
-  env_variable: Scalars['String'];
-  hint?: Maybe<Scalars['String']>;
-  label: Scalars['String'];
-  max?: Maybe<Scalars['Float']>;
-  min?: Maybe<Scalars['Float']>;
-  placeholder?: Maybe<Scalars['String']>;
-  required?: Maybe<Scalars['Boolean']>;
-  type: FieldTypesEnum;
-};
-
-export type ListAppsResonse = {
-  __typename?: 'ListAppsResonse';
-  apps: Array<AppInfo>;
-  total: Scalars['Float'];
-};
-
-export type Mutation = {
-  __typename?: 'Mutation';
-  installApp: App;
-  login: TokenResponse;
-  logout: Scalars['Boolean'];
-  register: TokenResponse;
-  restart: Scalars['Boolean'];
-  startApp: App;
-  stopApp: App;
-  uninstallApp: App;
-  update: Scalars['Boolean'];
-  updateApp: App;
-  updateAppConfig: App;
-};
-
-export type MutationInstallAppArgs = {
-  input: AppInputType;
-};
-
-export type MutationLoginArgs = {
-  input: UsernamePasswordInput;
-};
-
-export type MutationRegisterArgs = {
-  input: UsernamePasswordInput;
-};
-
-export type MutationStartAppArgs = {
-  id: Scalars['String'];
-};
-
-export type MutationStopAppArgs = {
-  id: Scalars['String'];
-};
-
-export type MutationUninstallAppArgs = {
-  id: Scalars['String'];
-};
-
-export type MutationUpdateAppArgs = {
-  id: Scalars['String'];
-};
-
-export type MutationUpdateAppConfigArgs = {
-  input: AppInputType;
-};
-
-export type Query = {
-  __typename?: 'Query';
-  getApp: App;
-  installedApps: Array<App>;
-  isConfigured: Scalars['Boolean'];
-  listAppsInfo: ListAppsResonse;
-  me?: Maybe<User>;
-  refreshToken?: Maybe<TokenResponse>;
-  systemInfo?: Maybe<SystemInfoResponse>;
-  version: VersionResponse;
-};
-
-export type QueryGetAppArgs = {
-  id: Scalars['String'];
-};
-
-export type SystemInfoResponse = {
-  __typename?: 'SystemInfoResponse';
-  cpu: Cpu;
-  disk: DiskMemory;
-  memory: DiskMemory;
-};
-
-export type TokenResponse = {
-  __typename?: 'TokenResponse';
-  token: Scalars['String'];
-};
-
-export type UpdateInfo = {
-  __typename?: 'UpdateInfo';
-  current: Scalars['Float'];
-  dockerVersion?: Maybe<Scalars['String']>;
-  latest: Scalars['Float'];
-};
-
-export type User = {
-  __typename?: 'User';
-  createdAt: Scalars['DateTime'];
-  id: Scalars['ID'];
-  updatedAt: Scalars['DateTime'];
-  username: Scalars['String'];
-};
-
-export type UsernamePasswordInput = {
-  password: Scalars['String'];
-  username: Scalars['String'];
-};
-
-export type VersionResponse = {
-  __typename?: 'VersionResponse';
-  current: Scalars['String'];
-  latest?: Maybe<Scalars['String']>;
-};
-
-export type InstallAppMutationVariables = Exact<{
-  input: AppInputType;
-}>;
-
-export type InstallAppMutation = { __typename?: 'Mutation'; installApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
-
-export type LoginMutationVariables = Exact<{
-  input: UsernamePasswordInput;
-}>;
-
-export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
-
-export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
-
-export type LogoutMutation = { __typename?: 'Mutation'; logout: boolean };
-
-export type RegisterMutationVariables = Exact<{
-  input: UsernamePasswordInput;
-}>;
-
-export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
-
-export type RestartMutationVariables = Exact<{ [key: string]: never }>;
-
-export type RestartMutation = { __typename?: 'Mutation'; restart: boolean };
-
-export type StartAppMutationVariables = Exact<{
-  id: Scalars['String'];
-}>;
-
-export type StartAppMutation = { __typename?: 'Mutation'; startApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
-
-export type StopAppMutationVariables = Exact<{
-  id: Scalars['String'];
-}>;
-
-export type StopAppMutation = { __typename?: 'Mutation'; stopApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
-
-export type UninstallAppMutationVariables = Exact<{
-  id: Scalars['String'];
-}>;
-
-export type UninstallAppMutation = { __typename?: 'Mutation'; uninstallApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
-
-export type UpdateMutationVariables = Exact<{ [key: string]: never }>;
-
-export type UpdateMutation = { __typename?: 'Mutation'; update: boolean };
-
-export type UpdateAppMutationVariables = Exact<{
-  id: Scalars['String'];
-}>;
-
-export type UpdateAppMutation = { __typename?: 'Mutation'; updateApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
-
-export type UpdateAppConfigMutationVariables = Exact<{
-  input: AppInputType;
-}>;
-
-export type UpdateAppConfigMutation = { __typename?: 'Mutation'; updateAppConfig: { __typename: 'App'; id: string; status: AppStatusEnum } };
-
-export type GetAppQueryVariables = Exact<{
-  appId: Scalars['String'];
-}>;
-
-export type GetAppQuery = {
-  __typename?: 'Query';
-  getApp: {
-    __typename?: 'App';
-    id: string;
-    status: AppStatusEnum;
-    config: any;
-    version?: number | null;
-    exposed: boolean;
-    domain?: string | null;
-    updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
-    info?: {
-      __typename?: 'AppInfo';
-      id: string;
-      port: number;
-      name: string;
-      description: string;
-      available: boolean;
-      version?: string | null;
-      tipi_version: number;
-      short_desc: string;
-      author: string;
-      source: string;
-      categories: Array<AppCategoriesEnum>;
-      url_suffix?: string | null;
-      https?: boolean | null;
-      exposable?: boolean | null;
-      no_gui?: boolean | null;
-      form_fields: Array<{
-        __typename?: 'FormField';
-        type: FieldTypesEnum;
-        label: string;
-        max?: number | null;
-        min?: number | null;
-        hint?: string | null;
-        placeholder?: string | null;
-        required?: boolean | null;
-        env_variable: string;
-      }>;
-    } | null;
-  };
-};
-
-export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>;
-
-export type InstalledAppsQuery = {
-  __typename?: 'Query';
-  installedApps: Array<{
-    __typename?: 'App';
-    id: string;
-    status: AppStatusEnum;
-    config: any;
-    version?: number | null;
-    updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
-    info?: { __typename?: 'AppInfo'; id: string; name: string; description: string; tipi_version: number; short_desc: string; https?: boolean | null } | null;
-  }>;
-};
-
-export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
-
-export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
-
-export type ListAppsQueryVariables = Exact<{ [key: string]: never }>;
-
-export type ListAppsQuery = {
-  __typename?: 'Query';
-  listAppsInfo: {
-    __typename?: 'ListAppsResonse';
-    total: number;
-    apps: Array<{
-      __typename?: 'AppInfo';
-      id: string;
-      available: boolean;
-      tipi_version: number;
-      port: number;
-      name: string;
-      version?: string | null;
-      short_desc: string;
-      author: string;
-      categories: Array<AppCategoriesEnum>;
-      https?: boolean | null;
-    }>;
-  };
-};
-
-export type MeQueryVariables = Exact<{ [key: string]: never }>;
-
-export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
-
-export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
-
-export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
-
-export type SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
-
-export type SystemInfoQuery = {
-  __typename?: 'Query';
-  systemInfo?: {
-    __typename?: 'SystemInfoResponse';
-    cpu: { __typename?: 'Cpu'; load: number };
-    disk: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
-    memory: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
-  } | null;
-};
-
-export type VersionQueryVariables = Exact<{ [key: string]: never }>;
-
-export type VersionQuery = { __typename?: 'Query'; version: { __typename?: 'VersionResponse'; current: string; latest?: string | null } };
-
-export const InstallAppDocument = gql`
-  mutation InstallApp($input: AppInputType!) {
-    installApp(input: $input) {
-      id
-      status
-      __typename
-    }
-  }
-`;
-export type InstallAppMutationFn = Apollo.MutationFunction<InstallAppMutation, InstallAppMutationVariables>;
-
-/**
- * __useInstallAppMutation__
- *
- * To run a mutation, you first call `useInstallAppMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useInstallAppMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [installAppMutation, { data, loading, error }] = useInstallAppMutation({
- *   variables: {
- *      input: // value for 'input'
- *   },
- * });
- */
-export function useInstallAppMutation(baseOptions?: Apollo.MutationHookOptions<InstallAppMutation, InstallAppMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<InstallAppMutation, InstallAppMutationVariables>(InstallAppDocument, options);
-}
-export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutation>;
-export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
-export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
-export const LoginDocument = gql`
-  mutation Login($input: UsernamePasswordInput!) {
-    login(input: $input) {
-      token
-    }
-  }
-`;
-export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
-
-/**
- * __useLoginMutation__
- *
- * To run a mutation, you first call `useLoginMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useLoginMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [loginMutation, { data, loading, error }] = useLoginMutation({
- *   variables: {
- *      input: // value for 'input'
- *   },
- * });
- */
-export function useLoginMutation(baseOptions?: Apollo.MutationHookOptions<LoginMutation, LoginMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
-}
-export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
-export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
-export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
-export const LogoutDocument = gql`
-  mutation Logout {
-    logout
-  }
-`;
-export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
-
-/**
- * __useLogoutMutation__
- *
- * To run a mutation, you first call `useLogoutMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useLogoutMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [logoutMutation, { data, loading, error }] = useLogoutMutation({
- *   variables: {
- *   },
- * });
- */
-export function useLogoutMutation(baseOptions?: Apollo.MutationHookOptions<LogoutMutation, LogoutMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<LogoutMutation, LogoutMutationVariables>(LogoutDocument, options);
-}
-export type LogoutMutationHookResult = ReturnType<typeof useLogoutMutation>;
-export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
-export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
-export const RegisterDocument = gql`
-  mutation Register($input: UsernamePasswordInput!) {
-    register(input: $input) {
-      token
-    }
-  }
-`;
-export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
-
-/**
- * __useRegisterMutation__
- *
- * To run a mutation, you first call `useRegisterMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useRegisterMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [registerMutation, { data, loading, error }] = useRegisterMutation({
- *   variables: {
- *      input: // value for 'input'
- *   },
- * });
- */
-export function useRegisterMutation(baseOptions?: Apollo.MutationHookOptions<RegisterMutation, RegisterMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<RegisterMutation, RegisterMutationVariables>(RegisterDocument, options);
-}
-export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
-export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
-export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
-export const RestartDocument = gql`
-  mutation Restart {
-    restart
-  }
-`;
-export type RestartMutationFn = Apollo.MutationFunction<RestartMutation, RestartMutationVariables>;
-
-/**
- * __useRestartMutation__
- *
- * To run a mutation, you first call `useRestartMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useRestartMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [restartMutation, { data, loading, error }] = useRestartMutation({
- *   variables: {
- *   },
- * });
- */
-export function useRestartMutation(baseOptions?: Apollo.MutationHookOptions<RestartMutation, RestartMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<RestartMutation, RestartMutationVariables>(RestartDocument, options);
-}
-export type RestartMutationHookResult = ReturnType<typeof useRestartMutation>;
-export type RestartMutationResult = Apollo.MutationResult<RestartMutation>;
-export type RestartMutationOptions = Apollo.BaseMutationOptions<RestartMutation, RestartMutationVariables>;
-export const StartAppDocument = gql`
-  mutation StartApp($id: String!) {
-    startApp(id: $id) {
-      id
-      status
-      __typename
-    }
-  }
-`;
-export type StartAppMutationFn = Apollo.MutationFunction<StartAppMutation, StartAppMutationVariables>;
-
-/**
- * __useStartAppMutation__
- *
- * To run a mutation, you first call `useStartAppMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useStartAppMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [startAppMutation, { data, loading, error }] = useStartAppMutation({
- *   variables: {
- *      id: // value for 'id'
- *   },
- * });
- */
-export function useStartAppMutation(baseOptions?: Apollo.MutationHookOptions<StartAppMutation, StartAppMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<StartAppMutation, StartAppMutationVariables>(StartAppDocument, options);
-}
-export type StartAppMutationHookResult = ReturnType<typeof useStartAppMutation>;
-export type StartAppMutationResult = Apollo.MutationResult<StartAppMutation>;
-export type StartAppMutationOptions = Apollo.BaseMutationOptions<StartAppMutation, StartAppMutationVariables>;
-export const StopAppDocument = gql`
-  mutation StopApp($id: String!) {
-    stopApp(id: $id) {
-      id
-      status
-      __typename
-    }
-  }
-`;
-export type StopAppMutationFn = Apollo.MutationFunction<StopAppMutation, StopAppMutationVariables>;
-
-/**
- * __useStopAppMutation__
- *
- * To run a mutation, you first call `useStopAppMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useStopAppMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [stopAppMutation, { data, loading, error }] = useStopAppMutation({
- *   variables: {
- *      id: // value for 'id'
- *   },
- * });
- */
-export function useStopAppMutation(baseOptions?: Apollo.MutationHookOptions<StopAppMutation, StopAppMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<StopAppMutation, StopAppMutationVariables>(StopAppDocument, options);
-}
-export type StopAppMutationHookResult = ReturnType<typeof useStopAppMutation>;
-export type StopAppMutationResult = Apollo.MutationResult<StopAppMutation>;
-export type StopAppMutationOptions = Apollo.BaseMutationOptions<StopAppMutation, StopAppMutationVariables>;
-export const UninstallAppDocument = gql`
-  mutation UninstallApp($id: String!) {
-    uninstallApp(id: $id) {
-      id
-      status
-      __typename
-    }
-  }
-`;
-export type UninstallAppMutationFn = Apollo.MutationFunction<UninstallAppMutation, UninstallAppMutationVariables>;
-
-/**
- * __useUninstallAppMutation__
- *
- * To run a mutation, you first call `useUninstallAppMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useUninstallAppMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [uninstallAppMutation, { data, loading, error }] = useUninstallAppMutation({
- *   variables: {
- *      id: // value for 'id'
- *   },
- * });
- */
-export function useUninstallAppMutation(baseOptions?: Apollo.MutationHookOptions<UninstallAppMutation, UninstallAppMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<UninstallAppMutation, UninstallAppMutationVariables>(UninstallAppDocument, options);
-}
-export type UninstallAppMutationHookResult = ReturnType<typeof useUninstallAppMutation>;
-export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
-export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
-export const UpdateDocument = gql`
-  mutation Update {
-    update
-  }
-`;
-export type UpdateMutationFn = Apollo.MutationFunction<UpdateMutation, UpdateMutationVariables>;
-
-/**
- * __useUpdateMutation__
- *
- * To run a mutation, you first call `useUpdateMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useUpdateMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [updateMutation, { data, loading, error }] = useUpdateMutation({
- *   variables: {
- *   },
- * });
- */
-export function useUpdateMutation(baseOptions?: Apollo.MutationHookOptions<UpdateMutation, UpdateMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<UpdateMutation, UpdateMutationVariables>(UpdateDocument, options);
-}
-export type UpdateMutationHookResult = ReturnType<typeof useUpdateMutation>;
-export type UpdateMutationResult = Apollo.MutationResult<UpdateMutation>;
-export type UpdateMutationOptions = Apollo.BaseMutationOptions<UpdateMutation, UpdateMutationVariables>;
-export const UpdateAppDocument = gql`
-  mutation UpdateApp($id: String!) {
-    updateApp(id: $id) {
-      id
-      status
-      __typename
-    }
-  }
-`;
-export type UpdateAppMutationFn = Apollo.MutationFunction<UpdateAppMutation, UpdateAppMutationVariables>;
-
-/**
- * __useUpdateAppMutation__
- *
- * To run a mutation, you first call `useUpdateAppMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useUpdateAppMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [updateAppMutation, { data, loading, error }] = useUpdateAppMutation({
- *   variables: {
- *      id: // value for 'id'
- *   },
- * });
- */
-export function useUpdateAppMutation(baseOptions?: Apollo.MutationHookOptions<UpdateAppMutation, UpdateAppMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<UpdateAppMutation, UpdateAppMutationVariables>(UpdateAppDocument, options);
-}
-export type UpdateAppMutationHookResult = ReturnType<typeof useUpdateAppMutation>;
-export type UpdateAppMutationResult = Apollo.MutationResult<UpdateAppMutation>;
-export type UpdateAppMutationOptions = Apollo.BaseMutationOptions<UpdateAppMutation, UpdateAppMutationVariables>;
-export const UpdateAppConfigDocument = gql`
-  mutation UpdateAppConfig($input: AppInputType!) {
-    updateAppConfig(input: $input) {
-      id
-      status
-      __typename
-    }
-  }
-`;
-export type UpdateAppConfigMutationFn = Apollo.MutationFunction<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
-
-/**
- * __useUpdateAppConfigMutation__
- *
- * To run a mutation, you first call `useUpdateAppConfigMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useUpdateAppConfigMutation` returns a tuple that includes:
- * - A mutate function that you can call at any time to execute the mutation
- * - An object with fields that represent the current status of the mutation's execution
- *
- * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
- *
- * @example
- * const [updateAppConfigMutation, { data, loading, error }] = useUpdateAppConfigMutation({
- *   variables: {
- *      input: // value for 'input'
- *   },
- * });
- */
-export function useUpdateAppConfigMutation(baseOptions?: Apollo.MutationHookOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>(UpdateAppConfigDocument, options);
-}
-export type UpdateAppConfigMutationHookResult = ReturnType<typeof useUpdateAppConfigMutation>;
-export type UpdateAppConfigMutationResult = Apollo.MutationResult<UpdateAppConfigMutation>;
-export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
-export const GetAppDocument = gql`
-  query GetApp($appId: String!) {
-    getApp(id: $appId) {
-      id
-      status
-      config
-      version
-      exposed
-      domain
-      updateInfo {
-        current
-        latest
-        dockerVersion
-      }
-      info {
-        id
-        port
-        name
-        description
-        available
-        version
-        tipi_version
-        short_desc
-        author
-        source
-        categories
-        url_suffix
-        https
-        exposable
-        no_gui
-        form_fields {
-          type
-          label
-          max
-          min
-          hint
-          placeholder
-          required
-          env_variable
-        }
-      }
-    }
-  }
-`;
-
-/**
- * __useGetAppQuery__
- *
- * To run a query within a React component, call `useGetAppQuery` and pass it any options that fit your needs.
- * When your component renders, `useGetAppQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useGetAppQuery({
- *   variables: {
- *      appId: // value for 'appId'
- *   },
- * });
- */
-export function useGetAppQuery(baseOptions: Apollo.QueryHookOptions<GetAppQuery, GetAppQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<GetAppQuery, GetAppQueryVariables>(GetAppDocument, options);
-}
-export function useGetAppLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAppQuery, GetAppQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<GetAppQuery, GetAppQueryVariables>(GetAppDocument, options);
-}
-export type GetAppQueryHookResult = ReturnType<typeof useGetAppQuery>;
-export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
-export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
-export const InstalledAppsDocument = gql`
-  query InstalledApps {
-    installedApps {
-      id
-      status
-      config
-      version
-      updateInfo {
-        current
-        latest
-        dockerVersion
-      }
-      info {
-        id
-        name
-        description
-        tipi_version
-        short_desc
-        https
-      }
-    }
-  }
-`;
-
-/**
- * __useInstalledAppsQuery__
- *
- * To run a query within a React component, call `useInstalledAppsQuery` and pass it any options that fit your needs.
- * When your component renders, `useInstalledAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useInstalledAppsQuery({
- *   variables: {
- *   },
- * });
- */
-export function useInstalledAppsQuery(baseOptions?: Apollo.QueryHookOptions<InstalledAppsQuery, InstalledAppsQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<InstalledAppsQuery, InstalledAppsQueryVariables>(InstalledAppsDocument, options);
-}
-export function useInstalledAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<InstalledAppsQuery, InstalledAppsQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<InstalledAppsQuery, InstalledAppsQueryVariables>(InstalledAppsDocument, options);
-}
-export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQuery>;
-export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
-export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
-export const ConfiguredDocument = gql`
-  query Configured {
-    isConfigured
-  }
-`;
-
-/**
- * __useConfiguredQuery__
- *
- * To run a query within a React component, call `useConfiguredQuery` and pass it any options that fit your needs.
- * When your component renders, `useConfiguredQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useConfiguredQuery({
- *   variables: {
- *   },
- * });
- */
-export function useConfiguredQuery(baseOptions?: Apollo.QueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
-}
-export function useConfiguredLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
-}
-export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
-export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
-export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
-export const ListAppsDocument = gql`
-  query ListApps {
-    listAppsInfo {
-      apps {
-        id
-        available
-        tipi_version
-        port
-        name
-        version
-        short_desc
-        author
-        categories
-        https
-      }
-      total
-    }
-  }
-`;
-
-/**
- * __useListAppsQuery__
- *
- * To run a query within a React component, call `useListAppsQuery` and pass it any options that fit your needs.
- * When your component renders, `useListAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useListAppsQuery({
- *   variables: {
- *   },
- * });
- */
-export function useListAppsQuery(baseOptions?: Apollo.QueryHookOptions<ListAppsQuery, ListAppsQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<ListAppsQuery, ListAppsQueryVariables>(ListAppsDocument, options);
-}
-export function useListAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ListAppsQuery, ListAppsQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<ListAppsQuery, ListAppsQueryVariables>(ListAppsDocument, options);
-}
-export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
-export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
-export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
-export const MeDocument = gql`
-  query Me {
-    me {
-      id
-    }
-  }
-`;
-
-/**
- * __useMeQuery__
- *
- * To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs.
- * When your component renders, `useMeQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useMeQuery({
- *   variables: {
- *   },
- * });
- */
-export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
-}
-export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
-}
-export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
-export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
-export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
-export const RefreshTokenDocument = gql`
-  query RefreshToken {
-    refreshToken {
-      token
-    }
-  }
-`;
-
-/**
- * __useRefreshTokenQuery__
- *
- * To run a query within a React component, call `useRefreshTokenQuery` and pass it any options that fit your needs.
- * When your component renders, `useRefreshTokenQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useRefreshTokenQuery({
- *   variables: {
- *   },
- * });
- */
-export function useRefreshTokenQuery(baseOptions?: Apollo.QueryHookOptions<RefreshTokenQuery, RefreshTokenQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<RefreshTokenQuery, RefreshTokenQueryVariables>(RefreshTokenDocument, options);
-}
-export function useRefreshTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RefreshTokenQuery, RefreshTokenQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<RefreshTokenQuery, RefreshTokenQueryVariables>(RefreshTokenDocument, options);
-}
-export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery>;
-export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
-export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
-export const SystemInfoDocument = gql`
-  query SystemInfo {
-    systemInfo {
-      cpu {
-        load
-      }
-      disk {
-        available
-        used
-        total
-      }
-      memory {
-        available
-        used
-        total
-      }
-    }
-  }
-`;
-
-/**
- * __useSystemInfoQuery__
- *
- * To run a query within a React component, call `useSystemInfoQuery` and pass it any options that fit your needs.
- * When your component renders, `useSystemInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useSystemInfoQuery({
- *   variables: {
- *   },
- * });
- */
-export function useSystemInfoQuery(baseOptions?: Apollo.QueryHookOptions<SystemInfoQuery, SystemInfoQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<SystemInfoQuery, SystemInfoQueryVariables>(SystemInfoDocument, options);
-}
-export function useSystemInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SystemInfoQuery, SystemInfoQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<SystemInfoQuery, SystemInfoQueryVariables>(SystemInfoDocument, options);
-}
-export type SystemInfoQueryHookResult = ReturnType<typeof useSystemInfoQuery>;
-export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
-export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
-export const VersionDocument = gql`
-  query Version {
-    version {
-      current
-      latest
-    }
-  }
-`;
-
-/**
- * __useVersionQuery__
- *
- * To run a query within a React component, call `useVersionQuery` and pass it any options that fit your needs.
- * When your component renders, `useVersionQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useVersionQuery({
- *   variables: {
- *   },
- * });
- */
-export function useVersionQuery(baseOptions?: Apollo.QueryHookOptions<VersionQuery, VersionQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<VersionQuery, VersionQueryVariables>(VersionDocument, options);
-}
-export function useVersionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<VersionQuery, VersionQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<VersionQuery, VersionQueryVariables>(VersionDocument, options);
-}
-export type VersionQueryHookResult = ReturnType<typeof useVersionQuery>;
-export type VersionLazyQueryHookResult = ReturnType<typeof useVersionLazyQuery>;
-export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;

+ 0 - 7
packages/dashboard/src/graphql/mutations/installApp.graphql

@@ -1,7 +0,0 @@
-mutation InstallApp($input: AppInputType!) {
-  installApp(input: $input) {
-    id
-    status
-    __typename
-  }
-}

+ 0 - 5
packages/dashboard/src/graphql/mutations/login.graphql

@@ -1,5 +0,0 @@
-mutation Login($input: UsernamePasswordInput!) {
-  login(input: $input) {
-    token
-  }
-}

+ 0 - 3
packages/dashboard/src/graphql/mutations/logout.graphql

@@ -1,3 +0,0 @@
-mutation Logout {
-  logout
-}

+ 0 - 5
packages/dashboard/src/graphql/mutations/register.graphql

@@ -1,5 +0,0 @@
-mutation Register($input: UsernamePasswordInput!) {
-  register(input: $input) {
-    token
-  }
-}

+ 0 - 3
packages/dashboard/src/graphql/mutations/restart.graphql

@@ -1,3 +0,0 @@
-mutation Restart {
-  restart
-}

+ 0 - 7
packages/dashboard/src/graphql/mutations/startApp.graphql

@@ -1,7 +0,0 @@
-mutation StartApp($id: String!) {
-  startApp(id: $id) {
-    id
-    status
-    __typename
-  }
-}

+ 0 - 7
packages/dashboard/src/graphql/mutations/stopApp.graphql

@@ -1,7 +0,0 @@
-mutation StopApp($id: String!) {
-  stopApp(id: $id) {
-    id
-    status
-    __typename
-  }
-}

+ 0 - 7
packages/dashboard/src/graphql/mutations/unintallApp.graphql

@@ -1,7 +0,0 @@
-mutation UninstallApp($id: String!) {
-  uninstallApp(id: $id) {
-    id
-    status
-    __typename
-  }
-}

+ 0 - 3
packages/dashboard/src/graphql/mutations/update.graphql

@@ -1,3 +0,0 @@
-mutation Update {
-  update
-}

+ 0 - 7
packages/dashboard/src/graphql/mutations/updateApp.graphql

@@ -1,7 +0,0 @@
-mutation UpdateApp($id: String!) {
-  updateApp(id: $id) {
-    id
-    status
-    __typename
-  }
-}

+ 0 - 7
packages/dashboard/src/graphql/mutations/updateAppConfig.graphql

@@ -1,7 +0,0 @@
-mutation UpdateAppConfig($input: AppInputType!) {
-  updateAppConfig(input: $input) {
-    id
-    status
-    __typename
-  }
-}

+ 0 - 42
packages/dashboard/src/graphql/queries/getApp.graphql

@@ -1,42 +0,0 @@
-query GetApp($appId: String!) {
-  getApp(id: $appId) {
-    id
-    status
-    config
-    version
-    exposed
-    domain
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-    info {
-      id
-      port
-      name
-      description
-      available
-      version
-      tipi_version
-      short_desc
-      author
-      source
-      categories
-      url_suffix
-      https
-      exposable
-      no_gui
-      form_fields {
-        type
-        label
-        max
-        min
-        hint
-        placeholder
-        required
-        env_variable
-      }
-    }
-  }
-}

+ 0 - 21
packages/dashboard/src/graphql/queries/installedApps.graphql

@@ -1,21 +0,0 @@
-query InstalledApps {
-  installedApps {
-    id
-    status
-    config
-    version
-    updateInfo {
-      current
-      latest
-      dockerVersion
-    }
-    info {
-      id
-      name
-      description
-      tipi_version
-      short_desc
-      https
-    }
-  }
-}

+ 0 - 3
packages/dashboard/src/graphql/queries/isConfigured.graphql

@@ -1,3 +0,0 @@
-query Configured {
-  isConfigured
-}

+ 0 - 18
packages/dashboard/src/graphql/queries/listApps.graphql

@@ -1,18 +0,0 @@
-# Write your query or mutation here
-query ListApps {
-  listAppsInfo {
-    apps {
-      id
-      available
-      tipi_version
-      port
-      name
-      version
-      short_desc
-      author
-      categories
-      https
-    }
-    total
-  }
-}

+ 0 - 5
packages/dashboard/src/graphql/queries/me.graphql

@@ -1,5 +0,0 @@
-query Me {
-  me {
-    id
-  }
-}

+ 0 - 5
packages/dashboard/src/graphql/queries/refreshToken.graphql

@@ -1,5 +0,0 @@
-query RefreshToken {
-  refreshToken {
-    token
-  }
-}

+ 0 - 17
packages/dashboard/src/graphql/queries/systemInfo.graphql

@@ -1,17 +0,0 @@
-query SystemInfo {
-  systemInfo {
-    cpu {
-      load
-    }
-    disk {
-      available
-      used
-      total
-    }
-    memory {
-      available
-      used
-      total
-    }
-  }
-}

+ 0 - 6
packages/dashboard/src/graphql/queries/version.graphql

@@ -1,6 +0,0 @@
-query Version {
-  version {
-    current
-    latest
-  }
-}

+ 0 - 32
packages/dashboard/src/hooks/useCachedRessources.ts

@@ -1,32 +0,0 @@
-import { useEffect, useState } from 'react';
-import { ApolloClient } from '@apollo/client';
-import { createApolloClient } from '../core/apollo/client';
-
-interface IReturnProps {
-  client?: ApolloClient<unknown>;
-  isLoadingComplete?: boolean;
-}
-
-export default function useCachedResources(): IReturnProps {
-  const [isLoadingComplete, setLoadingComplete] = useState(false);
-  const [client, setClient] = useState<ApolloClient<unknown>>();
-
-  async function loadResourcesAndDataAsync() {
-    try {
-      const restoredClient = createApolloClient();
-
-      setClient(restoredClient);
-    } catch (error) {
-      // We might want to provide this error information to an error reporting service
-      console.error(error);
-    } finally {
-      setLoadingComplete(true);
-    }
-  }
-
-  useEffect(() => {
-    loadResourcesAndDataAsync();
-  }, []);
-
-  return { client, isLoadingComplete };
-}

+ 0 - 133
packages/dashboard/src/mocks/handlers.ts

@@ -1,133 +0,0 @@
-import { graphql, rest } from 'msw';
-import {
-  ConfiguredQuery,
-  LoginMutation,
-  LogoutMutationResult,
-  MeQuery,
-  RefreshTokenQuery,
-  RegisterMutation,
-  RegisterMutationVariables,
-  UsernamePasswordInput,
-  VersionQuery,
-  SystemInfoQuery,
-} from '../generated/graphql';
-import appHandlers from './handlers/appHandlers';
-
-const restHandlers = [
-  rest.get('/api/status', (req, res, ctx) =>
-    res(
-      ctx.delay(200),
-      ctx.status(200),
-      ctx.json({
-        status: 'RUNNING',
-      }),
-    ),
-  ),
-];
-const graphqlHandlers = [
-  // Handles a "Login" mutation
-  graphql.mutation('Login', (req, res, ctx) => {
-    const { username } = req.variables as UsernamePasswordInput;
-    sessionStorage.setItem('is-authenticated', username);
-
-    const result: LoginMutation = {
-      login: { token: 'token' },
-    };
-
-    return res(ctx.delay(), ctx.data(result));
-  }),
-
-  // Handles a "Logout" mutation
-  graphql.mutation('Logout', (req, res, ctx) => {
-    sessionStorage.removeItem('is-authenticated');
-
-    const result: LogoutMutationResult['data'] = {
-      logout: true,
-    };
-
-    return res(ctx.delay(), ctx.data(result));
-  }),
-
-  // Handles me query
-  graphql.query('Me', (req, res, ctx) => {
-    const isAuthenticated = sessionStorage.getItem('is-authenticated');
-    if (!isAuthenticated) {
-      return res(ctx.errors([{ message: 'Not authenticated' }]));
-    }
-    const result: MeQuery = {
-      me: { id: '1' },
-    };
-
-    return res(ctx.delay(), ctx.data(result));
-  }),
-
-  graphql.query('RefreshToken', (req, res, ctx) => {
-    const result: RefreshTokenQuery = {
-      refreshToken: { token: 'token' },
-    };
-
-    return res(ctx.delay(), ctx.data(result));
-  }),
-
-  graphql.mutation('Register', (req, res, ctx) => {
-    const {
-      input: { username },
-    } = req.variables as RegisterMutationVariables;
-
-    const result: RegisterMutation = {
-      register: { token: 'token' },
-    };
-
-    if (username === 'error@error.com') {
-      return res(ctx.errors([{ message: 'Username is already taken' }]));
-    }
-
-    return res(ctx.data(result));
-  }),
-  appHandlers.listApps,
-  appHandlers.getApp,
-  appHandlers.installedApps,
-  appHandlers.installApp,
-  graphql.query('Version', (req, res, ctx) => {
-    const result: VersionQuery = {
-      version: {
-        current: '1.0.0',
-        latest: '1.0.0',
-      },
-    };
-
-    return res(ctx.data(result));
-  }),
-
-  graphql.query('Configured', (req, res, ctx) => {
-    const result: ConfiguredQuery = {
-      isConfigured: true,
-    };
-
-    return res(ctx.data(result));
-  }),
-
-  graphql.query('SystemInfo', (req, res, ctx) => {
-    const result: SystemInfoQuery = {
-      systemInfo: {
-        cpu: {
-          load: 50,
-        },
-        disk: {
-          available: 1000000000,
-          total: 2000000000,
-          used: 1000000000,
-        },
-        memory: {
-          available: 1000000000,
-          total: 2000000000,
-          used: 1000000000,
-        },
-      },
-    };
-
-    return res(ctx.data(result));
-  }),
-];
-
-export const handlers = [...graphqlHandlers, ...restHandlers];

+ 0 - 173
packages/dashboard/src/mocks/handlers/appHandlers.ts

@@ -1,173 +0,0 @@
-import { graphql } from 'msw';
-import { faker } from '@faker-js/faker';
-import { createAppsRandomly } from '../fixtures/app.fixtures';
-import { AppInputType, AppStatusEnum, GetAppQuery, InstallAppMutation, InstalledAppsQuery, ListAppsQuery } from '../../generated/graphql';
-
-// eslint-disable-next-line no-promise-executor-return
-const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
-export const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => value !== null && value !== undefined;
-
-const removeDuplicates = <T extends { id: string }>(array: T[]) =>
-  array.filter((a, i) => {
-    const index = array.findIndex((_a) => _a.id === a.id);
-    return index === i;
-  });
-
-export const mockedApps = removeDuplicates(createAppsRandomly(faker.datatype.number({ min: 20, max: 30 })));
-
-export const mockInstalledAppIds = mockedApps.slice(0, faker.datatype.number({ min: 5, max: 8 })).map((a) => a.id);
-const stoppedAppsIds = mockInstalledAppIds.slice(0, faker.datatype.number({ min: 1, max: 3 }));
-
-/**
- * GetApp handler
- */
-const getApp = graphql.query('GetApp', (req, res, ctx) => {
-  const { appId } = req.variables as { appId: string };
-
-  const app = mockedApps.find((a) => a.id === appId);
-
-  if (!app) {
-    return res(ctx.errors([{ message: 'App not found' }]));
-  }
-
-  const isInstalled = mockInstalledAppIds.includes(appId);
-
-  let status = AppStatusEnum.Missing;
-  if (isInstalled) {
-    status = AppStatusEnum.Running;
-  }
-  if (isInstalled && stoppedAppsIds.includes(appId)) {
-    status = AppStatusEnum.Stopped;
-  }
-
-  const result: GetAppQuery = {
-    getApp: {
-      id: app.id,
-      status,
-      info: app,
-      __typename: 'App',
-      config: {},
-      exposed: false,
-      updateInfo: null,
-      domain: null,
-      version: 1,
-    },
-  };
-
-  return res(ctx.data(result));
-});
-
-const getAppError = graphql.query('GetApp', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
-
-/**
- * ListApps handler
- */
-const listApps = graphql.query('ListApps', async (req, res, ctx) => {
-  const result: ListAppsQuery = {
-    listAppsInfo: {
-      apps: mockedApps,
-      total: mockedApps.length,
-    },
-  };
-
-  await wait(100);
-
-  return res(ctx.data(result));
-});
-
-const listAppsEmpty = graphql.query('ListApps', (req, res, ctx) => {
-  const result: ListAppsQuery = {
-    listAppsInfo: {
-      apps: [],
-      total: 0,
-    },
-  };
-  return res(ctx.data(result));
-});
-
-const listAppsError = graphql.query('ListApps', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
-
-/**
- * InstalledApps handler
- */
-const installedApps = graphql.query('InstalledApps', (req, res, ctx) => {
-  const apps: InstalledAppsQuery['installedApps'] = mockInstalledAppIds
-    .map((id) => {
-      const app = mockedApps.find((a) => a.id === id);
-      if (!app) return null;
-
-      let status = AppStatusEnum.Running;
-      if (stoppedAppsIds.includes(id)) {
-        status = AppStatusEnum.Stopped;
-      }
-
-      return {
-        __typename: 'App' as const,
-        id: app.id,
-        status,
-        config: {},
-        info: app,
-        version: 1,
-        updateInfo: null,
-      };
-    })
-    .filter(notEmpty);
-
-  const result: InstalledAppsQuery = {
-    installedApps: apps,
-  };
-
-  return res(ctx.data(result));
-});
-
-const installedAppsEmpty = graphql.query('InstalledApps', (req, res, ctx) => {
-  const result: InstalledAppsQuery = {
-    installedApps: [],
-  };
-
-  return res(ctx.data(result));
-});
-
-const installedAppsError = graphql.query('InstalledApps', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
-
-const installedAppsNoInfo = graphql.query('InstalledApps', (req, res, ctx) => {
-  const result: InstalledAppsQuery = {
-    installedApps: [
-      {
-        __typename: 'App' as const,
-        id: 'app-id',
-        status: AppStatusEnum.Running,
-        config: {},
-        info: null,
-        version: 1,
-        updateInfo: null,
-      },
-    ],
-  };
-  return res(ctx.data(result));
-});
-
-/**
- * Install app handler
- */
-const installApp = graphql.mutation('InstallApp', (req, res, ctx) => {
-  const { input } = req.variables as { input: AppInputType };
-
-  const app = mockedApps.find((a) => a.id === input.id);
-
-  if (!app) {
-    return res(ctx.errors([{ message: 'App not found' }]));
-  }
-
-  const result: InstallAppMutation = {
-    installApp: {
-      __typename: 'App' as const,
-      id: app.id,
-      status: AppStatusEnum.Running,
-    },
-  };
-
-  return res(ctx.data(result));
-});
-
-export default { getApp, getAppError, listApps, listAppsEmpty, listAppsError, installedApps, installedAppsEmpty, installedAppsError, installedAppsNoInfo, installApp };

+ 0 - 22
packages/dashboard/src/modules/Apps/components/InstallModal/InstallModal.tsx

@@ -1,22 +0,0 @@
-import React from 'react';
-import { InstallForm } from '../InstallForm';
-import { AppInfo } from '../../../../generated/graphql';
-import { Modal, ModalBody, ModalHeader } from '../../../../components/ui/Modal';
-
-interface IProps {
-  app: Pick<AppInfo, 'name' | 'form_fields' | 'exposable'>;
-  isOpen: boolean;
-  onClose: () => void;
-  onSubmit: (values: Record<string, any>) => void;
-}
-
-export const InstallModal: React.FC<IProps> = ({ app, isOpen, onClose, onSubmit }) => (
-  <Modal onClose={onClose} isOpen={isOpen}>
-    <ModalHeader>
-      <h5 className="modal-title">Install {app.name}</h5>
-    </ModalHeader>
-    <ModalBody>
-      <InstallForm onSubmit={onSubmit} formFields={app.form_fields} exposable={app.exposable} />
-    </ModalBody>
-  </Modal>
-);

+ 0 - 25
packages/dashboard/src/modules/Apps/components/UpdateSettingsModal.tsx

@@ -1,25 +0,0 @@
-import React from 'react';
-import { InstallForm } from './InstallForm';
-import { App, AppInfo } from '../../../generated/graphql';
-import { Modal, ModalBody, ModalHeader } from '../../../components/ui/Modal';
-
-interface IProps {
-  app: AppInfo;
-  config: App['config'];
-  isOpen: boolean;
-  exposed?: boolean;
-  domain?: string;
-  onClose: () => void;
-  onSubmit: (values: Record<string, any>) => void;
-}
-
-export const UpdateSettingsModal: React.FC<IProps> = ({ app, config, isOpen, onClose, onSubmit, exposed, domain }) => (
-  <Modal onClose={onClose} isOpen={isOpen}>
-    <ModalHeader>
-      <h5 className="modal-title">Update {app.name} config</h5>
-    </ModalHeader>
-    <ModalBody>
-      <InstallForm onSubmit={onSubmit} formFields={app.form_fields} exposable={app.exposable} initalValues={{ ...config, exposed, domain }} />
-    </ModalBody>
-  </Modal>
-);

+ 0 - 153
packages/dashboard/src/modules/Apps/containers/AppDetailsContainer/AppDetailsContainer.test.tsx

@@ -1,153 +0,0 @@
-import { graphql } from 'msw';
-import React from 'react';
-import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../tests/test-utils';
-import { AppStatusEnum } from '../../../../generated/graphql';
-import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
-import { server } from '../../../../mocks/server';
-import { useToastStore } from '../../../../state/toastStore';
-import { AppDetailsContainer } from './AppDetailsContainer';
-
-describe('Test: AppDetailsContainer', () => {
-  describe('Test: UI', () => {
-    it('should render', async () => {
-      // Arrange
-      const app = createAppEntity({});
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.getByText(app.info.short_desc)).toBeInTheDocument();
-    });
-
-    it('should display update button when update is available', async () => {
-      // Arrange
-      const app = createAppEntity({ overrides: { updateInfo: { current: 2, latest: 3 } } });
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.getByTestId('action-button-update')).toBeInTheDocument();
-    });
-
-    it('should display install button when app is not installed', async () => {
-      // Arrange
-      const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
-
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.getByTestId('action-button-install')).toBeInTheDocument();
-    });
-
-    it('should display uninstall and start button when app is stopped', async () => {
-      // Arrange
-      const app = createAppEntity({ overrides: { status: AppStatusEnum.Stopped } });
-
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.getByTestId('action-button-remove')).toBeInTheDocument();
-      expect(screen.getByTestId('action-button-start')).toBeInTheDocument();
-    });
-
-    it('should display stop, open and settings buttons when app is running', async () => {
-      // Arrange
-      const app = createAppEntity({ overrides: { status: AppStatusEnum.Running } });
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.getByTestId('action-button-stop')).toBeInTheDocument();
-      expect(screen.getByTestId('action-button-open')).toBeInTheDocument();
-      expect(screen.getByTestId('action-button-settings')).toBeInTheDocument();
-    });
-
-    it('should not display update button when update is not available', async () => {
-      // Arrange
-      const app = createAppEntity({ overrides: { updateInfo: { current: 3, latest: 3 } } });
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.queryByTestId('action-button-update')).not.toBeInTheDocument();
-    });
-
-    it('should not display open button when app has no_gui set to true', async () => {
-      // Arrange
-      const app = createAppEntity({ overridesInfo: { no_gui: true } });
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Assert
-      expect(screen.queryByTestId('action-button-open')).not.toBeInTheDocument();
-    });
-  });
-
-  describe('Test: Open app', () => {
-    it('should call window.open with the correct url when open button is clicked', async () => {
-      // Arrange
-      const app = createAppEntity({});
-      const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Act
-      const openButton = screen.getByTestId('action-button-open');
-      openButton.click();
-
-      // Assert
-      expect(spy).toHaveBeenCalledWith(`http://localhost:${app.info.port}`, '_blank', 'noreferrer');
-    });
-
-    it('should open with https when app info has https set to true', async () => {
-      // Arrange
-      const app = createAppEntity({ overridesInfo: { https: true } });
-      const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Act
-      const openButton = screen.getByTestId('action-button-open');
-      openButton.click();
-
-      // Assert
-      expect(spy).toHaveBeenCalledWith(`https://localhost:${app.info.port}`, '_blank', 'noreferrer');
-    });
-  });
-
-  describe('Test: Install app', () => {
-    const installFn = jest.fn();
-    const fakeInstallHandler = graphql.mutation('InstallApp', (req, res, ctx) => {
-      installFn(req.variables);
-      return res(ctx.data({ installApp: { id: 'id', status: '', __typename: '' } }));
-    });
-
-    it('should call install mutation when install form is submitted', async () => {
-      // Arrange
-      server.use(fakeInstallHandler);
-      const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Act
-      const installForm = screen.getByTestId('install-form');
-      fireEvent.submit(installForm);
-
-      await waitFor(() => {
-        expect(installFn).toHaveBeenCalledWith({
-          input: { id: app.id, form: {}, exposed: false, domain: '' },
-        });
-      });
-    });
-
-    it('should display a toast error when install mutation fails', async () => {
-      // Arrange
-      const { result } = renderHook(() => useToastStore());
-      server.use(graphql.mutation('InstallApp', (req, res, ctx) => res(ctx.errors([{ message: 'my big error' }]))));
-      const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
-      render(<AppDetailsContainer app={app} info={app.info} />);
-
-      // Act
-      const installForm = screen.getByTestId('install-form');
-      fireEvent.submit(installForm);
-
-      await waitFor(() => {
-        expect(result.current.toasts).toHaveLength(1);
-        expect(result.current.toasts[0].description).toEqual('my big error');
-        expect(result.current.toasts[0].status).toEqual('error');
-      });
-    });
-  });
-});

+ 0 - 196
packages/dashboard/src/modules/Apps/containers/AppDetailsContainer/AppDetailsContainer.tsx

@@ -1,196 +0,0 @@
-import React from 'react';
-import { useDisclosure } from '../../../../hooks/useDisclosure';
-import { useToastStore } from '../../../../state/toastStore';
-import { AppLogo } from '../../../../components/AppLogo/AppLogo';
-import { AppStatus } from '../../../../components/AppStatus';
-import {
-  App,
-  AppInfo,
-  AppStatusEnum,
-  GetAppDocument,
-  InstalledAppsDocument,
-  useInstallAppMutation,
-  useStartAppMutation,
-  useStopAppMutation,
-  useUninstallAppMutation,
-  useUpdateAppConfigMutation,
-  useUpdateAppMutation,
-} from '../../../../generated/graphql';
-import { AppActions } from '../../components/AppActions';
-import { AppDetailsTabs } from '../../components/AppDetailsTabs';
-import { InstallModal } from '../../components/InstallModal';
-import { StopModal } from '../../components/StopModal';
-import { UninstallModal } from '../../components/UninstallModal';
-import { UpdateModal } from '../../components/UpdateModal';
-import { UpdateSettingsModal } from '../../components/UpdateSettingsModal';
-import { FormValues } from '../../components/InstallForm/InstallForm';
-
-interface IProps {
-  app: Pick<App, 'id' | 'updateInfo' | 'config' | 'exposed' | 'domain' | 'status'>;
-  info: AppInfo;
-}
-
-export const AppDetailsContainer: React.FC<IProps> = ({ app, info }) => {
-  const { addToast } = useToastStore();
-  const installDisclosure = useDisclosure();
-  const uninstallDisclosure = useDisclosure();
-  const stopDisclosure = useDisclosure();
-  const updateDisclosure = useDisclosure();
-  const updateSettingsDisclosure = useDisclosure();
-
-  // Mutations
-  const [install] = useInstallAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }, { query: InstalledAppsDocument }] });
-  const [update] = useUpdateAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
-  const [uninstall] = useUninstallAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }, { query: InstalledAppsDocument }] });
-  const [stop] = useStopAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
-  const [start] = useStartAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
-  const [updateConfig] = useUpdateAppConfigMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
-
-  const updateAvailable = Number(app?.updateInfo?.current || 0) < Number(app?.updateInfo?.latest);
-
-  const handleError = (error: unknown) => {
-    if (error instanceof Error) {
-      addToast({
-        title: 'Error',
-        description: error.message,
-        status: 'error',
-        position: 'top',
-        isClosable: true,
-      });
-    }
-  };
-
-  const handleInstallSubmit = async (values: FormValues) => {
-    installDisclosure.close();
-    const { exposed, domain, ...form } = values;
-
-    try {
-      await install({
-        variables: { input: { form, id: info.id, exposed: exposed || false, domain: domain || '' } },
-        optimisticResponse: { installApp: { id: info.id, status: AppStatusEnum.Installing, __typename: 'App' } },
-      });
-    } catch (error) {
-      handleError(error);
-    }
-  };
-
-  const handleUnistallSubmit = async () => {
-    uninstallDisclosure.close();
-    try {
-      await uninstall({ variables: { id: info.id }, optimisticResponse: { uninstallApp: { id: info.id, status: AppStatusEnum.Uninstalling, __typename: 'App' } } });
-    } catch (error) {
-      handleError(error);
-    }
-  };
-
-  const handleStopSubmit = async () => {
-    stopDisclosure.close();
-    try {
-      await stop({ variables: { id: info.id }, optimisticResponse: { stopApp: { id: info.id, status: AppStatusEnum.Stopping, __typename: 'App' } } });
-    } catch (error) {
-      handleError(error);
-    }
-  };
-
-  const handleStartSubmit = async () => {
-    try {
-      await start({ variables: { id: info.id }, optimisticResponse: { startApp: { id: info.id, status: AppStatusEnum.Starting, __typename: 'App' } } });
-    } catch (e: unknown) {
-      handleError(e);
-    }
-  };
-
-  const handleUpdateSettingsSubmit = async (values: FormValues) => {
-    try {
-      const { exposed, domain, ...form } = values;
-      await updateConfig({ variables: { input: { form, id: info.id, exposed: exposed || false, domain: domain || '' } } });
-      addToast({
-        title: 'Success',
-        description: 'App config updated successfully. Restart the app to apply the changes.',
-        position: 'top',
-        status: 'success',
-        isClosable: true,
-      });
-      updateSettingsDisclosure.close();
-    } catch (error) {
-      handleError(error);
-    }
-  };
-
-  const handleUpdateSubmit = async () => {
-    updateDisclosure.close();
-    try {
-      await update({ variables: { id: info.id }, optimisticResponse: { updateApp: { id: info.id, status: AppStatusEnum.Updating, __typename: 'App' } } });
-      addToast({
-        title: 'Success',
-        description: 'App updated successfully',
-        position: 'top',
-        status: 'success',
-        isClosable: true,
-      });
-    } catch (error) {
-      handleError(error);
-    }
-  };
-
-  const handleOpen = () => {
-    const { https } = info;
-    const protocol = https ? 'https' : 'http';
-
-    if (typeof window !== 'undefined') {
-      // Current domain
-      const domain = window.location.hostname;
-      window.open(`${protocol}://${domain}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
-    }
-  };
-
-  const newVersion = [app?.updateInfo?.dockerVersion ? `${app?.updateInfo?.dockerVersion}` : '', `(${String(app?.updateInfo?.latest)})`].join(' ');
-
-  return (
-    <div className="card" data-testid="app-details">
-      <InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} app={info} />
-      <StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} app={info} />
-      <UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} app={info} />
-      <UpdateModal onConfirm={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} app={info} newVersion={newVersion} />
-      <UpdateSettingsModal
-        onSubmit={handleUpdateSettingsSubmit}
-        isOpen={updateSettingsDisclosure.isOpen}
-        onClose={updateSettingsDisclosure.close}
-        app={info}
-        config={app?.config}
-        exposed={app?.exposed}
-        domain={app?.domain || ''}
-      />
-      <div className="card-header d-flex flex-column flex-md-row">
-        <AppLogo id={info.id} size={130} alt={info.name} />
-        <div className="w-100 d-flex flex-column ms-md-3 align-items-center align-items-md-start">
-          <div>
-            <span className="mt-1 me-1">Version: </span>
-            <span className="badge bg-gray mt-2">{info?.version}</span>
-          </div>
-          {app.domain && (
-            <a target="_blank" rel="noreferrer" className="mt-1" href={`https://${app.domain}`}>
-              https://{app.domain}
-            </a>
-          )}
-          <span className="mt-1 text-muted text-center mb-2">{info.short_desc}</span>
-          <div className="mb-1">{app && app?.status !== AppStatusEnum.Missing && <AppStatus status={app.status} />}</div>
-          <AppActions
-            updateAvailable={updateAvailable}
-            onUpdate={updateDisclosure.open}
-            onUpdateSettings={updateSettingsDisclosure.open}
-            onStop={stopDisclosure.open}
-            onCancel={stopDisclosure.open}
-            onUninstall={uninstallDisclosure.open}
-            onInstall={installDisclosure.open}
-            onOpen={handleOpen}
-            onStart={handleStartSubmit}
-            app={info}
-            status={app?.status}
-          />
-        </div>
-      </div>
-      <AppDetailsTabs info={info} />
-    </div>
-  );
-};

+ 0 - 51
packages/dashboard/src/modules/Auth/containers/LoginContainer/LoginContainer.tsx

@@ -1,51 +0,0 @@
-import { useApolloClient } from '@apollo/client';
-import React, { useState } from 'react';
-import { useLoginMutation } from '../../../../generated/graphql';
-import { useToastStore } from '../../../../state/toastStore';
-import { AuthFormLayout } from '../../components/AuthFormLayout';
-import { LoginForm } from '../../components/LoginForm';
-
-type FormValues = { email: string; password: string };
-
-export const LoginContainer: React.FC = () => {
-  const client = useApolloClient();
-  const [login] = useLoginMutation({});
-  const [loading, setLoading] = useState(false);
-  const { addToast } = useToastStore();
-
-  const handleError = (error: unknown) => {
-    localStorage.removeItem('token');
-    if (error instanceof Error) {
-      addToast({
-        title: 'Error',
-        description: error.message,
-        status: 'error',
-        position: 'top',
-        isClosable: true,
-      });
-    }
-  };
-
-  const handleLogin = async (values: FormValues) => {
-    try {
-      setLoading(true);
-      const { data } = await login({ variables: { input: { username: values.email, password: values.password } } });
-
-      if (data?.login?.token) {
-        localStorage.setItem('token', data.login.token);
-      }
-
-      await client.refetchQueries({ include: ['Me'] });
-    } catch (error) {
-      handleError(error);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  return (
-    <AuthFormLayout>
-      <LoginForm onSubmit={handleLogin} loading={loading} />
-    </AuthFormLayout>
-  );
-};

+ 0 - 48
packages/dashboard/src/modules/Auth/containers/RegisterContainer/RegisterContainer.tsx

@@ -1,48 +0,0 @@
-import router from 'next/router';
-import React, { useState } from 'react';
-import { useRegisterMutation } from '../../../../generated/graphql';
-import { useToastStore } from '../../../../state/toastStore';
-import { AuthFormLayout } from '../../components/AuthFormLayout';
-import { RegisterForm } from '../../components/RegisterForm';
-
-export const RegisterContainer: React.FC = () => {
-  const { addToast } = useToastStore();
-  const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
-  const [loading, setLoading] = useState(false);
-
-  const handleError = (error: unknown) => {
-    if (error instanceof Error) {
-      addToast({
-        title: 'Error',
-        description: error.message,
-        status: 'error',
-        position: 'top',
-        isClosable: true,
-      });
-    }
-  };
-
-  const handleRegister = async (values: { email: string; password: string }) => {
-    try {
-      setLoading(true);
-      const { data } = await register({ variables: { input: { username: values.email, password: values.password } } });
-
-      if (data?.register?.token) {
-        localStorage.setItem('token', data.register.token);
-        router.reload();
-      } else {
-        setLoading(false);
-        handleError(new Error('Something went wrong'));
-      }
-    } catch (error) {
-      setLoading(false);
-      handleError(error);
-    }
-  };
-
-  return (
-    <AuthFormLayout>
-      <RegisterForm onSubmit={handleRegister} loading={loading} />
-    </AuthFormLayout>
-  );
-};

+ 0 - 14
packages/dashboard/src/modules/Dashboard/pages/DashboardPage/DashboardPage.tsx

@@ -1,14 +0,0 @@
-import React from 'react';
-import type { NextPage } from 'next';
-import { Layout } from '../../../../components/Layout';
-import Dashboard from '../../containers/Dashboard';
-import { useSystemInfoQuery } from '../../../../generated/graphql';
-
-export const DashboardPage: NextPage = () => {
-  const { data, loading } = useSystemInfoQuery({ pollInterval: 10000 });
-  return (
-    <Layout title="Dashboard" loading={loading && !data}>
-      {data?.systemInfo && <Dashboard data={data.systemInfo} />}
-    </Layout>
-  );
-};

+ 0 - 122
packages/dashboard/src/modules/Settings/containers/SettingsContainer/SettingsContainer.test.tsx

@@ -1,122 +0,0 @@
-import { faker } from '@faker-js/faker';
-import { graphql } from 'msw';
-import React from 'react';
-import { act, fireEvent, render, renderHook, screen, waitFor } from '../../../../../tests/test-utils';
-import { server } from '../../../../mocks/server';
-import { useToastStore } from '../../../../state/toastStore';
-import { SettingsContainer } from './SettingsContainer';
-
-describe('Test: SettingsContainer', () => {
-  it('renders without crashing', () => {
-    const currentVersion = faker.system.semver();
-    render(<SettingsContainer currentVersion={currentVersion} latestVersion={currentVersion} />);
-
-    expect(screen.getByText('Tipi settings')).toBeInTheDocument();
-    expect(screen.getByText('Already up to date')).toBeInTheDocument();
-  });
-
-  it('should make update button disable if current version is equal to latest version', () => {
-    const currentVersion = faker.system.semver();
-    render(<SettingsContainer currentVersion={currentVersion} latestVersion={currentVersion} />);
-
-    expect(screen.getByText('Already up to date')).toBeDisabled();
-  });
-
-  it('should make update button disabled if current version is greater than latest version', () => {
-    const currentVersion = '1.0.0';
-    const latestVersion = '0.0.1';
-    render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
-
-    expect(screen.getByText('Already up to date')).toBeDisabled();
-  });
-
-  it('should display update button if current version is less than latest version', () => {
-    const currentVersion = '0.0.1';
-    const latestVersion = '1.0.0';
-
-    render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
-    expect(screen.getByText(`Update to ${latestVersion}`)).toBeInTheDocument();
-    expect(screen.getByText(`Update to ${latestVersion}`)).not.toBeDisabled();
-  });
-
-  it('should call update mutation when update button is clicked', async () => {
-    // Arrange
-
-    localStorage.setItem('token', 'token');
-    const currentVersion = '0.0.1';
-    const latestVersion = '1.0.0';
-    const updateFn = jest.fn();
-    server.use(
-      graphql.mutation('Update', async (req, res, ctx) => {
-        updateFn();
-        return res(ctx.data({ update: true }));
-      }),
-    );
-    render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
-
-    // Act
-    act(() => screen.getByText(`Update to ${latestVersion}`).click());
-
-    fireEvent.click(screen.getByText('Update'));
-    waitFor(() => expect(updateFn).toHaveBeenCalled());
-    // eslint-disable-next-line no-promise-executor-return
-    await act(() => new Promise((resolve) => setTimeout(resolve, 1500)));
-
-    // Assert
-    const token = localStorage.getItem('token');
-    expect(token).toBe(null);
-  });
-
-  it('should display error toast if update mutation fails', async () => {
-    // Arrange
-    const { result, unmount } = renderHook(() => useToastStore());
-    const currentVersion = '0.0.1';
-    const latestVersion = '1.0.0';
-    const errorMessage = 'My error';
-    server.use(graphql.mutation('Update', async (req, res, ctx) => res(ctx.errors([{ message: errorMessage }]))));
-    render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
-
-    // Act
-    act(() => screen.getByText(`Update to ${latestVersion}`).click());
-    fireEvent.click(screen.getByText('Update'));
-
-    // Assert
-    await waitFor(() => expect(result.current.toasts[0].description).toBe(errorMessage));
-    unmount();
-  });
-
-  it('should call restart mutation when restart button is clicked', async () => {
-    // Arrange
-    const restartFn = jest.fn();
-    server.use(
-      graphql.mutation('Restart', async (req, res, ctx) => {
-        restartFn();
-        return res(ctx.data({ restart: true }));
-      }),
-    );
-    render(<SettingsContainer currentVersion="1.0.0" latestVersion="1.0.0" />);
-
-    // Act
-    fireEvent.click(screen.getByTestId('settings-modal-restart-button'));
-    waitFor(() => expect(restartFn).toHaveBeenCalled());
-    // eslint-disable-next-line no-promise-executor-return
-    await new Promise((resolve) => setTimeout(resolve, 1500));
-
-    // Assert
-    const token = localStorage.getItem('token');
-    expect(token).toBe(null);
-  });
-
-  it('should display error toast if restart mutation fails', async () => {
-    // Arrange
-    const { result } = renderHook(() => useToastStore());
-    const errorMessage = 'Update error';
-    server.use(graphql.mutation('Restart', async (req, res, ctx) => res(ctx.errors([{ message: errorMessage }]))));
-    render(<SettingsContainer currentVersion="1.0.0" latestVersion="1.0.0" />);
-    // Act
-    fireEvent.click(screen.getByTestId('settings-modal-restart-button'));
-
-    // Assert
-    await waitFor(() => expect(result.current.toasts[0].description).toBe(errorMessage));
-  });
-});

+ 0 - 21
packages/dashboard/src/modules/Settings/pages/SettingsPage/SettingsPage.test.tsx

@@ -1,21 +0,0 @@
-import { graphql } from 'msw';
-import React from 'react';
-import { render, screen, waitFor } from '../../../../../tests/test-utils';
-import { server } from '../../../../mocks/server';
-import { SettingsPage } from './SettingsPage';
-
-describe('Test: SettingsPage', () => {
-  it('should render', async () => {
-    render(<SettingsPage />);
-
-    await waitFor(() => expect(screen.getByText('Tipi settings')).toBeInTheDocument());
-  });
-
-  it('should render error page if version query fails', async () => {
-    server.use(graphql.query('Version', (req, res, ctx) => res(ctx.errors([{ message: 'My error' }]))));
-
-    render(<SettingsPage />);
-
-    await waitFor(() => expect(screen.getByText('My error')).toBeInTheDocument());
-  });
-});

+ 0 - 54
packages/dashboard/src/pages/_app.tsx

@@ -1,54 +0,0 @@
-import React, { useEffect } from 'react';
-import type { AppProps } from 'next/app';
-import { ApolloProvider } from '@apollo/client';
-import Head from 'next/head';
-import useCachedResources from '../hooks/useCachedRessources';
-import '../styles/global.css';
-import '../styles/global.scss';
-import { useUIStore } from '../state/uiStore';
-import { ToastProvider } from '../components/hoc/ToastProvider';
-import { StatusProvider } from '../components/hoc/StatusProvider';
-import { AuthProvider } from '../components/hoc/AuthProvider';
-import { StatusScreen } from '../components/StatusScreen';
-
-function MyApp({ Component, pageProps }: AppProps) {
-  const { setDarkMode } = useUIStore();
-
-  // check theme on component mount
-  useEffect(() => {
-    const themeCheck = () => {
-      if (localStorage.darkMode === 'true' || (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
-        document.body.classList.add('theme-dark');
-        setDarkMode(true);
-      } else {
-        document.body.classList.remove('theme-light');
-        setDarkMode(false);
-      }
-    };
-    themeCheck();
-  }, [setDarkMode]);
-
-  const { client } = useCachedResources();
-  if (!client) {
-    return <StatusScreen title="" subtitle="" />;
-  }
-
-  return (
-    <main className="h-100">
-      <ApolloProvider client={client}>
-        <Head>
-          <title>Tipi</title>
-        </Head>
-        <ToastProvider>
-          <StatusProvider>
-            <AuthProvider>
-              <Component {...pageProps} />
-            </AuthProvider>
-          </StatusProvider>
-        </ToastProvider>
-      </ApolloProvider>
-    </main>
-  );
-}
-
-export default MyApp;

+ 0 - 1
packages/dashboard/src/pages/app-store/[id].tsx

@@ -1 +0,0 @@
-export { AppDetailsPage as default } from '../../modules/Apps/pages/AppDetailsPage';

+ 0 - 1
packages/dashboard/src/pages/app-store/index.tsx

@@ -1 +0,0 @@
-export { AppStorePage as default } from '../../modules/AppStore/pages/AppStorePage';

+ 0 - 1
packages/dashboard/src/pages/apps/[id].tsx

@@ -1 +0,0 @@
-export { AppDetailsPage as default } from '../../modules/Apps/pages/AppDetailsPage';

+ 0 - 1
packages/dashboard/src/pages/apps/index.tsx

@@ -1 +0,0 @@
-export { AppsPage as default } from '../../modules/Apps/pages/AppsPage';

+ 0 - 1
packages/dashboard/src/pages/index.tsx

@@ -1 +0,0 @@
-export { DashboardPage as default } from '../modules/Dashboard/pages/DashboardPage';

+ 0 - 1
packages/dashboard/src/pages/settings.tsx

@@ -1 +0,0 @@
-export { SettingsPage as default } from '../modules/Settings/pages/SettingsPage';

+ 0 - 17
packages/dashboard/src/state/systemStore.ts

@@ -1,17 +0,0 @@
-import create from 'zustand';
-
-export enum SystemStatus {
-  RUNNING = 'RUNNING',
-  RESTARTING = 'RESTARTING',
-  UPDATING = 'UPDATING',
-}
-
-type Store = {
-  status: SystemStatus;
-  setStatus: (status: SystemStatus) => void;
-};
-
-export const useSystemStore = create<Store>((set) => ({
-  status: SystemStatus.RUNNING,
-  setStatus: (status: SystemStatus) => set((state) => ({ ...state, status })),
-}));

+ 0 - 31
packages/dashboard/tests/test-utils.tsx

@@ -1,31 +0,0 @@
-import React, { FC, ReactElement } from 'react';
-import { render, RenderOptions, renderHook } from '@testing-library/react';
-import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
-import fetch from 'isomorphic-fetch';
-import { SWRConfig } from 'swr';
-
-const link = new HttpLink({
-  uri: 'http://localhost:3000/graphql',
-  // Use explicit `window.fetch` so tha outgoing requests
-  // are captured and deferred until the Service Worker is ready.
-  fetch: (...args) => fetch(...args),
-});
-
-// create a mock of Apollo Client
-export const mockApolloClient = new ApolloClient({
-  cache: new InMemoryCache({}),
-  link,
-});
-
-const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => (
-  <SWRConfig value={{ dedupingInterval: 0, provider: () => new Map() }}>
-    <ApolloProvider client={mockApolloClient}>{children}</ApolloProvider>
-  </SWRConfig>
-);
-
-const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => render(ui, { wrapper: AllTheProviders, ...options });
-const customRenderHook = (callback: () => any, options?: Omit<RenderOptions, 'wrapper'>) => renderHook(callback, { wrapper: AllTheProviders, ...options });
-
-export * from '@testing-library/react';
-export { customRender as render };
-export { customRenderHook as renderHook };

+ 0 - 23
packages/dashboard/tsconfig.json

@@ -1,23 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es5",
-    "lib": ["dom", "dom.iterable", "esnext"],
-    "allowJs": true,
-    "skipLibCheck": true,
-    "strict": true,
-    "forceConsistentCasingInFileNames": true,
-    "noEmit": true,
-    "esModuleInterop": true,
-    "module": "esnext",
-    "moduleResolution": "node",
-    "resolveJsonModule": true,
-    "isolatedModules": true,
-    "jsx": "preserve",
-    "incremental": true,
-    "strictNullChecks": true,
-    "allowSyntheticDefaultImports": true,
-    "types": ["jest", "@testing-library/jest-dom"]
-  },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
-  "exclude": ["node_modules"]
-}

+ 0 - 4
packages/system-api/.dockerignore

@@ -1,4 +0,0 @@
-node_modules/
-dist/
-sessions/
-logs/

部分文件因为文件数量过多而无法显示