Parcourir la source

Merge pull request #129 from meienberger/feature/database

Feature/database
Nicolas Meienberger il y a 3 ans
Parent
commit
7f3ebb372c
100 fichiers modifiés avec 1501 ajouts et 438 suppressions
  1. 3 0
      .dockerignore
  2. 19 10
      .github/workflows/ci.yml
  3. 2 5
      .github/workflows/release-candidate.yml
  4. 1 1
      .github/workflows/release.yml
  5. 1 0
      .gitignore
  6. 1 1
      .husky/pre-commit
  7. 19 43
      Dockerfile
  8. 5 27
      Dockerfile.dev
  9. 1 1
      apps/adguard/config.json
  10. 8 6
      apps/anonaddy/config.json
  11. 0 1
      apps/anonaddy/docker-compose.yml
  12. 1 1
      apps/calibre-web/config.json
  13. 3 3
      apps/code-server/config.json
  14. 2 1
      apps/docker-compose.common.yml
  15. 1 1
      apps/filebrowser/config.json
  16. 1 1
      apps/filerun/config.json
  17. 1 1
      apps/freshrss/config.json
  18. 1 1
      apps/gitea/config.json
  19. 1 1
      apps/homarr/config.json
  20. 1 1
      apps/homeassistant/config.json
  21. 1 1
      apps/invidious/config.json
  22. 1 1
      apps/jackett/config.json
  23. 1 1
      apps/jellyfin/config.json
  24. 1 1
      apps/joplin/config.json
  25. 1 1
      apps/libreddit/config.json
  26. 1 1
      apps/mealie/config.json
  27. 1 1
      apps/n8n/config.json
  28. 4 4
      apps/nextcloud/config.json
  29. 1 1
      apps/nitter/config.json
  30. 1 1
      apps/nodered/config.json
  31. 3 3
      apps/photoprism/config.json
  32. 3 3
      apps/pihole/config.json
  33. 1 1
      apps/plex/config.json
  34. 1 1
      apps/prowlarr/config.json
  35. 1 1
      apps/radarr/config.json
  36. 1 1
      apps/resilio-sync/config.json
  37. 1 1
      apps/simple-torrent/config.json
  38. 1 1
      apps/sonarr/config.json
  39. 1 1
      apps/syncthing/config.json
  40. 1 1
      apps/tailscale/config.json
  41. 1 1
      apps/tautulli/config.json
  42. 4 4
      apps/transmission/config.json
  43. 26 27
      apps/ttyd/config.json
  44. 3 3
      apps/vaultwarden/config.json
  45. 5 5
      apps/wg-easy/config.json
  46. 40 15
      docker-compose.dev.yml
  47. 33 5
      docker-compose.yml
  48. 3 1
      package.json
  49. 0 4
      packages/common/.eslintignore
  50. 0 18
      packages/common/.eslintrc.js
  51. 0 3
      packages/common/.gitignore
  52. 0 1
      packages/common/.npmignore
  53. 0 6
      packages/common/.prettierrc.cjs
  54. 0 23
      packages/common/package.json
  55. 0 16
      packages/common/src/constants/app.constants.ts
  56. 0 1
      packages/common/src/constants/index.ts
  57. 0 2
      packages/common/src/index.ts
  58. 0 64
      packages/common/src/types/app.types.ts
  59. 0 1
      packages/common/src/types/index.ts
  60. 0 8
      packages/common/tsconfig.build.json
  61. 0 22
      packages/common/tsconfig.json
  62. 3 0
      packages/dashboard/.dockerignore
  63. 9 0
      packages/dashboard/codegen.yml
  64. 1 1
      packages/dashboard/jest.config.js
  65. 11 4
      packages/dashboard/package.json
  66. 2 2
      packages/dashboard/src/components/AppTile/AppStatus.tsx
  67. 7 7
      packages/dashboard/src/components/AppTile/index.tsx
  68. 13 13
      packages/dashboard/src/components/Form/validators.ts
  69. 3 3
      packages/dashboard/src/components/Layout/SideMenu.tsx
  70. 11 0
      packages/dashboard/src/core/apollo/client.ts
  71. 14 0
      packages/dashboard/src/core/apollo/links/errorLink.ts
  72. 9 0
      packages/dashboard/src/core/apollo/links/httpLink.ts
  73. 9 0
      packages/dashboard/src/core/apollo/links/index.ts
  74. 15 0
      packages/dashboard/src/core/constants.ts
  75. 920 0
      packages/dashboard/src/generated/graphql.tsx
  76. 7 0
      packages/dashboard/src/graphql/mutations/installApp.graphql
  77. 7 0
      packages/dashboard/src/graphql/mutations/login.graphql
  78. 3 0
      packages/dashboard/src/graphql/mutations/logout.graphql
  79. 7 0
      packages/dashboard/src/graphql/mutations/register.graphql
  80. 7 0
      packages/dashboard/src/graphql/mutations/startApp.graphql
  81. 7 0
      packages/dashboard/src/graphql/mutations/stopApp.graphql
  82. 7 0
      packages/dashboard/src/graphql/mutations/unintallApp.graphql
  83. 7 0
      packages/dashboard/src/graphql/mutations/updateAppConfig.graphql
  84. 30 0
      packages/dashboard/src/graphql/queries/getApp.graphql
  85. 14 0
      packages/dashboard/src/graphql/queries/installedApps.graphql
  86. 3 0
      packages/dashboard/src/graphql/queries/isConfigured.graphql
  87. 17 0
      packages/dashboard/src/graphql/queries/listApps.graphql
  88. 5 0
      packages/dashboard/src/graphql/queries/me.graphql
  89. 17 0
      packages/dashboard/src/graphql/queries/systemInfo.graphql
  90. 6 0
      packages/dashboard/src/graphql/queries/version.graphql
  91. 49 0
      packages/dashboard/src/hooks/useCachedRessources.ts
  92. 3 3
      packages/dashboard/src/modules/AppStore/components/AppStoreTable.tsx
  93. 12 4
      packages/dashboard/src/modules/AppStore/components/AppStoreTile.tsx
  94. 2 1
      packages/dashboard/src/modules/AppStore/components/CategorySelect.tsx
  95. 2 2
      packages/dashboard/src/modules/AppStore/components/FeaturedApps.tsx
  96. 3 3
      packages/dashboard/src/modules/AppStore/components/FeaturedCard.tsx
  97. 7 5
      packages/dashboard/src/modules/AppStore/containers/AppStoreContainer.tsx
  98. 14 13
      packages/dashboard/src/modules/AppStore/helpers/table.helpers.ts
  99. 4 2
      packages/dashboard/src/modules/AppStore/helpers/table.types.ts
  100. 20 12
      packages/dashboard/src/modules/Apps/components/AppActions.tsx

+ 3 - 0
.dockerignore

@@ -4,3 +4,6 @@
 /.next
 node_modules
 .next
+dist/
+**/dist/
+**/next/

+ 19 - 10
.github/workflows/ci.yml

@@ -10,6 +10,19 @@ env:
 jobs:
   ci:
     runs-on: ubuntu-latest
+    services:
+      postgres:
+        image: postgres:latest
+        env:
+          POSTGRES_PASSWORD: postgres
+        ports:
+          - 5432:5432
+        # set health checks to wait until postgres has started
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
     steps:
       - name: Checkout
         uses: actions/checkout@v3
@@ -42,15 +55,6 @@ jobs:
       - name: Install dependencies
         run: pnpm install
 
-      - name: Build packages/common
-        run: |
-          cd ./packages/common
-          npm run build
-          cd ../..
-
-      - name: Install dependencies
-        run: pnpm install
-
       - name: Build packages
         run: pnpm -r build
 
@@ -61,4 +65,9 @@ jobs:
         run: pnpm -r lint
       
       - name: Run tests
-        run: pnpm -r test
+        run: pnpm -r test
+
+      - uses: codecov/codecov-action@v2
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          files: ./packages/system-api/coverage/clover.xml,./packages/dashboard/coverage/clover.xml

+ 2 - 5
.github/workflows/release-candidate.yml

@@ -28,7 +28,7 @@ jobs:
       - name: Get tag from VERSION file
         id: meta
         run: |
-          VERSION=$(cat VERSION)
+          VERSION=$(npm run version --silent)
           TAG=${VERSION}
           echo "::set-output name=tag::${TAG}"
       
@@ -40,7 +40,4 @@ jobs:
           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

+ 1 - 1
.github/workflows/release.yml

@@ -52,7 +52,7 @@ jobs:
       - name: Get tag from VERSION file
         id: meta
         run: |
-          VERSION=$(cat VERSION)
+          VERSION=$(npm run version --silent)
           TAG=${VERSION}
           echo "::set-output name=tag::${TAG}"
       

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@ data/
 github.secrets
 node_modules/
 app-data/*
+data/
 traefik/ssl/*
 !traefik/ssl/.gitkeep
 !app-data/.gitkeep

+ 1 - 1
.husky/pre-commit

@@ -3,4 +3,4 @@
 
 pnpm test
 pnpm -r test
-pnpm -r lint
+pnpm -r lint:fix

+ 19 - 43
Dockerfile

@@ -2,68 +2,44 @@ FROM node:18 AS build
 
 RUN npm install node-gyp -g
 
-WORKDIR /common
-COPY ./packages/common /common
-RUN npm i
-RUN npm run build
-
 WORKDIR /api
 COPY ./packages/system-api/package.json /api/package.json
 RUN npm i
-COPY ./packages/system-api /api
-RUN npm run build
-
+# ---
 WORKDIR /dashboard
 COPY ./packages/dashboard/package.json /dashboard/package.json
 RUN npm i
+
+WORKDIR /api
+COPY ./packages/system-api /api
+RUN npm run build
+# ---
+WORKDIR /dashboard
 COPY ./packages/dashboard /dashboard
 RUN npm run build
 
 
-FROM ubuntu:20.04
-ARG DEBIAN_FRONTEND=noninteractive
+FROM alpine:3.16.0 as app
 
 WORKDIR /
 
-# Install docker
-RUN apt-get update && apt-get install -y \
-    ca-certificates \
-    curl \
-    gnupg \
-    lsb-release
+# Install dependencies
+RUN apk --no-cache add docker-compose nodejs npm bash g++ make
 
-RUN apt-get install -y \
-    g++ gcc make python
-
-RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
-
-RUN echo \
-    "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
-    $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
-
-RUN apt-get update
-RUN apt-get install -y docker-ce docker-ce-cli containerd.io
-
-# Install node
-RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
-RUN apt-get install -y nodejs
 RUN npm install node-gyp -g
 
-# Install docker-compose
-RUN curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
-RUN chmod +x /usr/local/bin/docker-compose
-
-COPY --from=build /common /common
-
 WORKDIR /api
-COPY ./packages/system-api/package.json /api/package.json
-RUN npm install --omit=dev
+COPY ./packages/system-api/package*.json /api/
+RUN npm install --production
 
 WORKDIR /dashboard
-COPY ./packages/dashboard/package.json /dashboard/package.json
-RUN npm install --omit=dev
+COPY ./packages/dashboard/package*.json /dashboard/
+RUN npm install --production
 
-COPY --from=build /api /api
-COPY --from=build /dashboard /dashboard
+COPY --from=build /api/dist /api/dist
+COPY ./packages/system-api /api
+
+COPY --from=build /dashboard/.next /dashboard/.next
+COPY ./packages/dashboard /dashboard
 
 WORKDIR /

+ 5 - 27
Dockerfile.dev

@@ -1,40 +1,18 @@
-FROM ubuntu:20.04
-ARG DEBIAN_FRONTEND=noninteractive
+FROM alpine:3.16.0 as app
 
 WORKDIR /
 
 # Install docker
-RUN apt-get update && apt-get install -y \
-    ca-certificates \
-    curl \
-    gnupg \
-    lsb-release
+RUN apk --no-cache add docker-compose nodejs npm bash
 
-RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
-
-RUN echo \
-    "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
-    $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
-
-RUN apt-get update
-RUN apt-get install -y docker-ce docker-ce-cli containerd.io
-
-# Install node
-RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
-RUN apt-get install -y nodejs
-
-# Install docker-compose
-RUN curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
-RUN chmod +x /usr/local/bin/docker-compose
-
-COPY ./packages/common /common
+RUN npm install node-gyp -g
 
 WORKDIR /api
-COPY ./packages/system-api/package.json /api/package.json
+COPY ./packages/system-api/package*.json /api/
 RUN npm install
 
 WORKDIR /dashboard
-COPY ./packages/dashboard/package.json /dashboard/package.json
+COPY ./packages/dashboard/package*.json /dashboard/
 RUN npm install
 
 COPY ./packages/system-api /api

+ 1 - 1
apps/adguard/config.json

@@ -12,5 +12,5 @@
   "requirements": {
     "ports": [53]
   },
-  "form_fields": {}
+  "form_fields": []
 }

+ 8 - 6
apps/anonaddy/config.json

@@ -1,6 +1,8 @@
 {
   "name": "Anonaddy",
   "port": 8084,
+  "available": false,
+  "categories": ["utilities"],
   "id": "anonaddy",
   "description": "",
   "short_desc": "Anonymous email forwarding",
@@ -10,21 +12,21 @@
   "requirements": {
     "ports": [25]
   },
-  "form_fields": {
-    "username": {
+  "form_fields": [
+    {
       "type": "text",
       "label": "Username",
       "required": true,
       "env_variable": "ANONADDY_USERNAME"
     },
-    "key": {
+    {
       "type": "text",
       "label": "App key",
       "hint": "Application key for encrypter service. Generate one with : echo \"base64:$(openssl rand -base64 32)\"",
       "required": true,
       "env_variable": "ANONADDY_KEY"
     },
-    "domain": {
+    {
       "type": "fqdn",
       "label": "Your email domain (eg. example.com)",
       "max": 50,
@@ -32,7 +34,7 @@
       "required": true,
       "env_variable": "ANONADDY_DOMAIN"
     },
-    "secret": {
+    {
       "type": "text",
       "label": "App secret",
       "hint": "Long random string used when hashing data for the anonymous replies",
@@ -41,5 +43,5 @@
       "required": true,
       "env_variable": "ANONADDY_SECRET"
     }
-  }
+  ]
 }

+ 0 - 1
apps/anonaddy/docker-compose.yml

@@ -45,7 +45,6 @@ services:
       ANONADDY_DOMAIN: ${ANONADDY_DOMAIN}
       ANONADDY_SECRET: ${ANONADDY_SECRET}
       ANONADDY_ADMIN_USERNAME: ${ANONADDY_USERNAME}
-      POSTFIX_DEBUG: true
     restart: unless-stopped
     networks:
       - tipi_main_network

+ 1 - 1
apps/calibre-web/config.json

@@ -9,5 +9,5 @@
   "author": "https://github.com/janeczku/",
   "source": "https://github.com/janeczku/calibre-web",
   "image": "/logos/apps/calibre-web.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 3 - 3
apps/code-server/config.json

@@ -9,8 +9,8 @@
   "author": "https://github.com/coder",
   "source": "https://github.com/linuxserver/docker-code-server",
   "image": "https://avatars.githubusercontent.com/u/95932066",
-  "form_fields": {
-    "password": {
+  "form_fields": [
+    {
       "type": "password",
       "label": "Password",
       "max": 50,
@@ -18,5 +18,5 @@
       "required": true,
       "env_variable": "CODESERVER_PASSWORD"
     }
-  }
+  ]
 }

+ 2 - 1
apps/docker-compose.common.yml

@@ -2,4 +2,5 @@ version: "3.7"
 
 networks:
   tipi_main_network:
-      name: runtipi_tipi_main_network
+      external:
+        name: runtipi_tipi_main_network

+ 1 - 1
apps/filebrowser/config.json

@@ -10,5 +10,5 @@
   "website": "https://filebrowser.org/",
   "source": "https://github.com/filebrowser/filebrowser",
   "image": "/logos/apps/filebrowser.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/filerun/config.json

@@ -8,5 +8,5 @@
   "author": "FileRun, LDA - Portugal",
   "source": "https://www.filerun.com/",
   "image": "https://avatars.githubusercontent.com/u/6422152?v=4",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/freshrss/config.json

@@ -9,5 +9,5 @@
   "author": "https://freshrss.org/",
   "source": "https://github.com/FreshRSS/FreshRSS",
   "image": "/logos/apps/freshrss.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/gitea/config.json

@@ -9,5 +9,5 @@
   "author": "go-gitea",
   "source": "https://github.com/go-gitea/gitea",
   "image": "/logos/apps/gitea.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/homarr/config.json

@@ -10,5 +10,5 @@
   "source": "https://github.com/ajnart/homarr",
   "website": "https://discord.gg/C2WTXkzkwK",
   "image": "/logos/apps/homarr.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/homeassistant/config.json

@@ -9,5 +9,5 @@
   "author": "ArneNaessens",
   "source": "https://github.com/home-assistant/core",
   "image": "/logos/apps/homeassistant.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/invidious/config.json

@@ -9,5 +9,5 @@
   "author": "iv-org",
   "source": "https://github.com/iv-org/invidious",
   "image": "https://raw.githubusercontent.com/iv-org/invidious/master/assets/invidious-colored-vector.svg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/jackett/config.json

@@ -9,5 +9,5 @@
   "author": "",
   "source": "https://github.com/Jackett/Jackett",
   "image": "/logos/apps/jackett.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/jellyfin/config.json

@@ -9,5 +9,5 @@
   "author": "jellyfin.org",
   "source": "https://github.com/jellyfin/jellyfin",
   "image": "https://avatars.githubusercontent.com/u/45698031?s=200&v=4",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/joplin/config.json

@@ -10,5 +10,5 @@
   "source": "https://github.com/laurent22/joplin",
   "website": "https://joplinapp.org",
   "image": "/logos/apps/joplin.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/libreddit/config.json

@@ -9,5 +9,5 @@
   "author": "spikecodes",
   "source": "https://github.com/spikecodes/libreddit",
   "image": "/logos/apps/libreddit.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/mealie/config.json

@@ -9,5 +9,5 @@
   "categories": [],
   "source": "https://github.com/hay-kot/mealie",
   "image": "https://raw.githubusercontent.com/hay-kot/mealie/mealie-next/frontend/static/icons/android-chrome-512x512.png",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/n8n/config.json

@@ -10,5 +10,5 @@
   "source": "https://github.com/n8n-io/n8n",
   "website": "https://n8n.io/",
   "image": "/logos/apps/n8n.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 4 - 4
apps/nextcloud/config.json

@@ -9,8 +9,8 @@
   "author": "Nextcloud GmbH",
   "source": "https://github.com/nextcloud/server",
   "image": "https://avatars.githubusercontent.com/u/19211038?s=200&v=4",
-  "form_fields": {
-    "username": {
+  "form_fields": [
+    {
       "type": "text",
       "label": "Username",
       "max": 50,
@@ -18,7 +18,7 @@
       "required": true,
       "env_variable": "NEXTCLOUD_ADMIN_USER"
     },
-    "password": {
+    {
       "type": "password",
       "label": "Password",
       "max": 50,
@@ -26,5 +26,5 @@
       "required": true,
       "env_variable": "NEXTCLOUD_ADMIN_PASSWORD"
     }
-  }
+  ]
 }

+ 1 - 1
apps/nitter/config.json

@@ -9,5 +9,5 @@
   "author": "zedeus",
   "source": "https://github.com/zedeus/nitter",
   "image": "/logos/apps/nitter.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/nodered/config.json

@@ -9,5 +9,5 @@
   "author": "node-red",
   "source": "https://github.com/node-red/node-red",
   "image": "https://avatars.githubusercontent.com/u/5375661?s=200&v=4",
-  "form_fields": {}
+  "form_fields": []
 }

+ 3 - 3
apps/photoprism/config.json

@@ -9,8 +9,8 @@
   "author": "PhotoPrism",
   "source": "https://github.com/photoprism/photoprism",
   "image": "/logos/apps/photoprism.jpg",
-  "form_fields": {
-    "password": {
+  "form_fields": [
+    {
       "type": "password",
       "label": "Photoprism admin password",
       "max": 50,
@@ -18,5 +18,5 @@
       "required": true,
       "env_variable": "PHOTOPRISM_ADMIN_PASSWORD"
     }
-  }
+  ]
 }

+ 3 - 3
apps/pihole/config.json

@@ -12,8 +12,8 @@
   "author": "pi-hole.net",
   "source": "https://github.com/pi-hole/pi-hole",
   "image": "/logos/apps/pihole.jpg",
-  "form_fields": {
-    "password": {
+  "form_fields": [
+    {
       "type": "password",
       "label": "Password",
       "max": 50,
@@ -21,5 +21,5 @@
       "required": true,
       "env_variable": "APP_PASSWORD"
     }
-  }
+  ]
 }

+ 1 - 1
apps/plex/config.json

@@ -10,5 +10,5 @@
   "author": "plexinc",
   "source": "https://github.com/plexinc/pms-docker",
   "image": "/logos/apps/plex.png",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/prowlarr/config.json

@@ -9,5 +9,5 @@
   "author": "Prowlarr",
   "source": "https://github.com/Prowlarr/Prowlarr/",
   "image": "/logos/apps/prowlarr.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/radarr/config.json

@@ -9,5 +9,5 @@
   "author": "radarr.video",
   "source": "https://github.com/Radarr/Radarr",
   "image": "/logos/apps/radarr.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/resilio-sync/config.json

@@ -9,5 +9,5 @@
   "author": "Resilio, Inc.",
   "source": "https://github.com/bt-sync",
   "image": "/logos/apps/resilio-sync.png",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/simple-torrent/config.json

@@ -7,5 +7,5 @@
   "author": "",
   "source": "https://github.com/boypt/simple-torrent",
   "image": "https://getumbrel.github.io/umbrel-apps-gallery/simple-torrent/icon.svg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/sonarr/config.json

@@ -9,5 +9,5 @@
   "author": "sonarr.tv",
   "source": "https://github.com/Sonarr/Sonarr",
   "image": "/logos/apps/sonarr.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/syncthing/config.json

@@ -10,5 +10,5 @@
   "source": "https://github.com/syncthing",
   "website": "https://syncthing.net",
   "image": "/logos/apps/syncthing.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/tailscale/config.json

@@ -10,5 +10,5 @@
   "source": "https://github.com/tailscale/tailscale",
   "website": "https://tailscale.com/",
   "image": "https://avatars.githubusercontent.com/u/48932923?s=200&v=4",
-  "form_fields": {}
+  "form_fields": []
 }

+ 1 - 1
apps/tautulli/config.json

@@ -9,5 +9,5 @@
   "author": "JonnyWong16",
   "source": "https://github.com/Tautulli/Tautulli",
   "image": "/logos/apps/tautulli.jpg",
-  "form_fields": {}
+  "form_fields": []
 }

+ 4 - 4
apps/transmission/config.json

@@ -12,8 +12,8 @@
   "author": "Transmission Project",
   "source": "https://github.com/transmission/transmission",
   "image": "https://avatars.githubusercontent.com/u/223312?s=200&v=4",
-  "form_fields": {
-    "username": {
+  "form_fields": [
+    {
       "type": "text",
       "label": "Username",
       "max": 50,
@@ -21,7 +21,7 @@
       "required": true,
       "env_variable": "TRANSMISSION_USERNAME"
     },
-    "password": {
+    {
       "type": "password",
       "label": "Password",
       "max": 50,
@@ -29,5 +29,5 @@
       "required": true,
       "env_variable": "TRANSMISSION_PASSWORD"
     }
-  }
+  ]
 }

+ 26 - 27
apps/ttyd/config.json

@@ -1,29 +1,28 @@
 {
-    "name": "TTYD - Web terminal",
-    "port": 8092,
-    "id": "ttyd",
-    "description": "",
-    "short_desc": "A utility that allows you to access a command line from your web browser",
-    "author": "",
-    "source": "",
-    "image": "",
-    "form_fields": {
-      "username": {
-        "type": "text",
-        "label": "Username",
-        "max": 50,
-        "min": 3,
-        "required": true,
-        "env_variable": "TRANSMISSION_USERNAME"
-      },
-      "password": {
-        "type": "password",
-        "label": "Password",
-        "max": 50,
-        "min": 3,
-        "required": true,
-        "env_variable": "TRANSMISSION_PASSWORD"
-      }
+  "name": "TTYD - Web terminal",
+  "port": 8092,
+  "id": "ttyd",
+  "description": "",
+  "short_desc": "A utility that allows you to access a command line from your web browser",
+  "author": "",
+  "source": "",
+  "image": "",
+  "form_fields": [
+    {
+      "type": "text",
+      "label": "Username",
+      "max": 50,
+      "min": 3,
+      "required": true,
+      "env_variable": "TRANSMISSION_USERNAME"
+    },
+    {
+      "type": "password",
+      "label": "Password",
+      "max": 50,
+      "min": 3,
+      "required": true,
+      "env_variable": "TRANSMISSION_PASSWORD"
     }
-  }
-  
+  ]
+}

+ 3 - 3
apps/vaultwarden/config.json

@@ -9,8 +9,8 @@
   "author": "Daniel García",
   "source": "https://github.com/dani-garcia/vaultwarden",
   "image": "/logos/apps/vaultwarden.jpg",
-  "form_fields": {
-    "admin_password": {
+  "form_fields": [
+    {
       "type": "password",
       "label": "Admin Panel Password",
       "max": 50,
@@ -18,5 +18,5 @@
       "required": true,
       "env_variable": "VAULTWARDEN_ADMIN_PASSWORD"
     }
-  }
+  ]
 }

+ 5 - 5
apps/wg-easy/config.json

@@ -12,14 +12,14 @@
   "author": "WeeJeWel",
   "source": "https://github.com/WeeJeWel/wg-easy/",
   "image": "/logos/apps/wireguard.jpg",
-  "form_fields": {
-    "host": {
+  "form_fields": [
+    {
       "type": "fqdnip",
       "label": "Your public IP address or domain name",
       "required": true,
       "env_variable": "WIREGUARD_HOST"
     },
-    "password": {
+    {
       "type": "password",
       "label": "Password",
       "max": 50,
@@ -27,11 +27,11 @@
       "required": true,
       "env_variable": "WIREGUARD_PASSWORD"
     },
-    "dns": {
+    {
       "type": "ip",
       "label": "Default DNS server",
       "required": false,
       "env_variable": "WIREGUARD_DNS"
     }
-  }
+  ]
 }

+ 40 - 15
docker-compose.dev.yml

@@ -1,11 +1,35 @@
 version: "3.7"
 
 services:
+  tipi-db:
+    container_name: tipi-db
+    image: postgres:latest
+    restart: on-failure
+    stop_grace_period: 1m
+    volumes:
+      - ./data/postgres:/var/lib/postgresql/data
+    ports:
+      - 5432:5432
+    environment:
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+      POSTGRES_USER: tipi
+      POSTGRES_DB: tipi
+    healthcheck:
+      test: [ "CMD-SHELL", "pg_isready -d tipi -U tipi" ]
+      interval: 5s
+      timeout: 10s
+      retries: 120
+    networks:
+      - tipi_main_network
+
   api:
     build:
       context: .
       dockerfile: Dockerfile.dev
-    command: bash -c "cd /api && npm run dev"
+    command: /bin/sh -c "cd /api && npm run build && npm run dev"
+    depends_on:
+      tipi-db:
+        condition: service_healthy
     container_name: api
     ports:
       - 3001:3001
@@ -13,14 +37,18 @@ services:
       ## Docker sock
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - ${PWD}:/tipi
-      - ${PWD}/packages/system-api:/api
-      - /api/node_modules
+      - ${PWD}/packages/system-api/src:/api/src
+      # - /api/node_modules
     environment:
-      - INTERNAL_IP=${INTERNAL_IP}
-      - TIPI_VERSION=${TIPI_VERSION}
-      - JWT_SECRET=${JWT_SECRET}
-      - ROOT_FOLDER_HOST=${ROOT_FOLDER_HOST}
-      - NGINX_PORT=${NGINX_PORT}
+      INTERNAL_IP: ${INTERNAL_IP}
+      TIPI_VERSION: ${TIPI_VERSION}
+      JWT_SECRET: ${JWT_SECRET}
+      ROOT_FOLDER_HOST: ${ROOT_FOLDER_HOST}
+      NGINX_PORT: ${NGINX_PORT}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+      POSTGRES_USERNAME: tipi
+      POSTGRES_DBNAME: tipi
+      POSTGRES_HOST: tipi-db
     networks:
       - tipi_main_network
 
@@ -28,7 +56,7 @@ services:
     build:
       context: .
       dockerfile: Dockerfile.dev
-    command: bash -c "cd /dashboard && npm run dev"
+    command: /bin/sh -c "cd /dashboard && npm run dev"
     container_name: dashboard
     ports:
       - 3000:3000
@@ -37,9 +65,9 @@ services:
     environment:
       - INTERNAL_IP=${INTERNAL_IP}
     volumes:
-      - ${PWD}/packages/dashboard:/dashboard
-      - /dashboard/node_modules
-      - /dashboard/.next
+      - ${PWD}/packages/dashboard/src:/dashboard/src
+      # - /dashboard/node_modules
+      # - /dashboard/.next
     labels:
       traefik.enable: true
       traefik.http.routers.dashboard.rule: PathPrefix("/") # Host(`tipi.local`) &&
@@ -50,9 +78,6 @@ services:
 networks:
   tipi_main_network:
     driver: bridge
-    driver_opts:
-      com.docker.network.bridge.enable_ip_masquerade: "true"
-      com.docker.network.bridge.enable_icc: "true"
     ipam:
       driver: default
       config:

+ 33 - 5
docker-compose.yml

@@ -15,6 +15,27 @@ services:
     networks:
       - tipi_main_network
 
+  tipi-db:
+    container_name: tipi-db
+    image: postgres:latest
+    restart: on-failure
+    stop_grace_period: 1m
+    volumes:
+      - ./data/postgres:/var/lib/postgresql/data
+    ports:
+      - 5432:5432
+    environment:
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+      POSTGRES_USER: tipi
+      POSTGRES_DB: tipi
+    healthcheck:
+      test: [ "CMD-SHELL", "pg_isready -d tipi -U tipi" ]
+      interval: 5s
+      timeout: 10s
+      retries: 120
+    networks:
+      - tipi_main_network
+
   api:
     image: meienberger/runtipi:${TIPI_VERSION}
     command: bash -c "cd /api && npm run start"
@@ -22,16 +43,23 @@ services:
     container_name: api
     ports:
       - 3001:3001
+    depends_on:
+      tipi-db:
+        condition: service_healthy
     volumes:
       ## Docker sock
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - ${PWD}:/tipi
     environment:
-      - INTERNAL_IP=${INTERNAL_IP}
-      - TIPI_VERSION=${TIPI_VERSION}
-      - JWT_SECRET=${JWT_SECRET}
-      - ROOT_FOLDER_HOST=${ROOT_FOLDER_HOST}
-      - NGINX_PORT=${NGINX_PORT}
+      INTERNAL_IP: ${INTERNAL_IP}
+      TIPI_VERSION: ${TIPI_VERSION}
+      JWT_SECRET: ${JWT_SECRET}
+      ROOT_FOLDER_HOST: ${ROOT_FOLDER_HOST}
+      NGINX_PORT: ${NGINX_PORT}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+      POSTGRES_USERNAME: tipi
+      POSTGRES_DBNAME: tipi
+      POSTGRES_HOST: tipi-db
     networks:
       - tipi_main_network
 

+ 3 - 1
package.json

@@ -10,7 +10,9 @@
     "start:dev": "docker-compose -f docker-compose.dev.yml --env-file .env.dev up --build",
     "start:rc": "docker-compose -f docker-compose.rc.yml --env-file .env up --build",
     "start:prod": "docker-compose --env-file .env up --build",
-    "build:common": "cd packages/common && npm run build"
+    "build:common": "cd packages/common && npm run build",
+    "start:pg": "docker run --name test-db -p 5432:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres",
+    "version": "echo $npm_package_version"
   },
   "devDependencies": {
     "@types/jest": "^27.5.0",

+ 0 - 4
packages/common/.eslintignore

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

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

@@ -1,18 +0,0 @@
-module.exports = {
-  env: { node: true, jest: true },
-  extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
-  parser: '@typescript-eslint/parser',
-  parserOptions: {
-    project: './tsconfig.json',
-    tsconfigRootDir: __dirname,
-    ecmaVersion: 'latest',
-    sourceType: 'module',
-  },
-  plugins: ['@typescript-eslint', 'import', 'react'],
-  rules: {
-    'arrow-body-style': 0,
-    'no-restricted-exports': 0,
-    'max-len': [1, { code: 200 }],
-    'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
-  },
-};

+ 0 - 3
packages/common/.gitignore

@@ -1,3 +0,0 @@
-node_modules/
-dist/
-coverage/

+ 0 - 1
packages/common/.npmignore

@@ -1 +0,0 @@
-

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

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

+ 0 - 23
packages/common/package.json

@@ -1,23 +0,0 @@
-{
-  "name": "@runtipi/common",
-  "version": "0.2.8",
-  "main": "./dist/index.js",
-  "files": [
-    "dist"
-  ],
-  "scripts": {
-    "test": "jest --coverage --passWithNoTests",
-    "build": "tsc -b tsconfig.build.json"
-  },
-  "author": "",
-  "license": "ISC",
-  "devDependencies": {
-    "esbuild": "^0.14.38",
-    "typescript": "4.6.4"
-  },
-  "dependencies": {},
-  "description": "",
-  "publishConfig": {
-    "access": "public"
-  }
-}

+ 0 - 16
packages/common/src/constants/app.constants.ts

@@ -1,16 +0,0 @@
-import { AppCategoriesEnum } from '../types';
-
-// Icons should come from FontAwesome https://react-icons.github.io/react-icons/icons?name=fa
-export const APP_CATEGORIES = [
-  { name: 'Network', id: AppCategoriesEnum.NETWORK, icon: 'FaNetworkWired' },
-  { name: 'Media', id: AppCategoriesEnum.MEDIA, icon: 'FaVideo' },
-  { name: 'Development', id: AppCategoriesEnum.DEVELOPMENT, icon: 'FaCode' },
-  { name: 'Automation', id: AppCategoriesEnum.AUTOMATION, icon: 'FaRobot' },
-  { name: 'Social', id: AppCategoriesEnum.SOCIAL, icon: 'FaUserFriends' },
-  { name: 'Utilities', id: AppCategoriesEnum.UTILITIES, icon: 'FaWrench' },
-  { name: 'Photography', id: AppCategoriesEnum.PHOTOGRAPHY, icon: 'FaCamera' },
-  { name: 'Security', id: AppCategoriesEnum.SECURITY, icon: 'FaShieldAlt' },
-  { name: 'Featured', id: AppCategoriesEnum.FEATURED, icon: 'FaStar' },
-  { name: 'Books', id: AppCategoriesEnum.BOOKS, icon: 'FaBook' },
-  { name: 'Data', id: AppCategoriesEnum.DATA, icon: 'FaDatabase' },
-];

+ 0 - 1
packages/common/src/constants/index.ts

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

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

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

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

@@ -1,64 +0,0 @@
-export enum AppCategoriesEnum {
-  NETWORK = 'network',
-  MEDIA = 'media',
-  DEVELOPMENT = 'development',
-  AUTOMATION = 'automation',
-  SOCIAL = 'social',
-  UTILITIES = 'utilities',
-  PHOTOGRAPHY = 'photography',
-  SECURITY = 'security',
-  FEATURED = 'featured',
-  BOOKS = 'books',
-  DATA = 'data',
-}
-
-export enum FieldTypes {
-  text = 'text',
-  password = 'password',
-  email = 'email',
-  number = 'number',
-  fqdn = 'fqdn',
-  ip = 'ip',
-  fqdnip = 'fqdnip',
-  url = 'url',
-}
-
-interface FormField {
-  type: FieldTypes;
-  label: string;
-  max?: number;
-  min?: number;
-  hint?: string;
-  required?: boolean;
-  env_variable: string;
-}
-
-export enum AppStatusEnum {
-  RUNNING = 'running',
-  STOPPED = 'stopped',
-  INSTALLING = 'installing',
-  UNINSTALLING = 'uninstalling',
-  STOPPING = 'stopping',
-  STARTING = 'starting',
-}
-
-export interface AppConfig {
-  id: string;
-  available: boolean;
-  port: number;
-  name: string;
-  requirements?: {
-    ports?: number[];
-  };
-  description: string;
-  version: string;
-  image: string;
-  form_fields: Record<string, FormField>;
-  short_desc: string;
-  author: string;
-  source: string;
-  installed: boolean;
-  categories: AppCategoriesEnum[];
-  status: AppStatusEnum;
-  url_suffix: string;
-}

+ 0 - 1
packages/common/src/types/index.ts

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

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

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

+ 0 - 22
packages/common/tsconfig.json

@@ -1,22 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es6",
-    "lib": ["dom", "dom.iterable", "esnext"],
-    "allowJs": true,
-    "skipLibCheck": true,
-    "strict": true,
-    "forceConsistentCasingInFileNames": true,
-    "noEmit": false,
-    "esModuleInterop": true,
-    "module": "commonjs",
-    "moduleResolution": "node",
-    "resolveJsonModule": true,
-    "isolatedModules": false,
-    "jsx": "preserve",
-    "incremental": false,
-    "declaration": true,
-    "outDir": "./dist"
-  },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "jest.config.cjs"],
-  "exclude": ["node_modules", "dist"]
-}

+ 3 - 0
packages/dashboard/.dockerignore

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

+ 9 - 0
packages/dashboard/codegen.yml

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

+ 1 - 1
packages/dashboard/jest.config.js

@@ -7,6 +7,6 @@ module.exports = {
   // setupFiles: ['<rootDir>/tests/dotenv-config.ts'],
   collectCoverage: true,
   collectCoverageFrom: ['src/**/*.{ts,tsx}'],
-  coverageProvider: 'v8',
+  // coverageProvider: 'v8',
   passWithNoTests: true,
 };

+ 11 - 4
packages/dashboard/package.json

@@ -7,39 +7,46 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "next lint",
+    "lint:fix": "next lint --fix",
+    "gen": "graphql-codegen --config codegen.yml"
   },
   "dependencies": {
+    "@apollo/client": "^3.6.8",
     "@chakra-ui/react": "^2.1.2",
     "@emotion/react": "^11",
     "@emotion/styled": "^11",
     "@fontsource/open-sans": "^4.5.8",
-    "@runtipi/common": "file:../common",
     "axios": "^0.26.1",
     "clsx": "^1.1.1",
     "final-form": "^4.20.6",
     "framer-motion": "^6",
+    "graphql": "^15.8.0",
+    "graphql-tag": "^2.12.6",
     "immer": "^9.0.12",
     "js-cookie": "^3.0.1",
     "next": "12.1.6",
-    "npm": "^8.12.1",
     "react": "18.1.0",
     "react-dom": "18.1.0",
     "react-final-form": "^6.5.9",
     "react-icons": "^4.3.1",
     "react-markdown": "^8.0.3",
     "react-select": "^5.3.2",
-    "react-syntax-highlighter": "^15.5.0",
     "remark-breaks": "^3.0.2",
     "remark-gfm": "^3.0.1",
     "remark-mdx": "^2.1.1",
     "swr": "^1.3.0",
     "systeminformation": "^5.11.9",
+    "tslib": "^2.4.0",
     "validator": "^13.7.0",
     "zustand": "^3.7.2"
   },
   "devDependencies": {
     "@babel/core": "^7.0.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",
     "@types/js-cookie": "^3.0.2",
     "@types/node": "17.0.31",
     "@types/react": "18.0.8",

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

@@ -1,9 +1,9 @@
 import React from 'react';
 import { FiPauseCircle, FiPlayCircle } from 'react-icons/fi';
-import { AppStatusEnum } from '@runtipi/common';
+import { AppStatusEnum } from '../../generated/graphql';
 
 const AppStatus: React.FC<{ status: AppStatusEnum }> = ({ status }) => {
-  if (status === AppStatusEnum.RUNNING) {
+  if (status === AppStatusEnum.Running) {
     return (
       <>
         <FiPlayCircle className="text-green-500 mr-1" size={20} />

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

@@ -2,12 +2,14 @@ import { Box, SlideFade, useColorModeValue } from '@chakra-ui/react';
 import Link from 'next/link';
 import React from 'react';
 import { FiChevronRight } from 'react-icons/fi';
-import { AppConfig } from '@runtipi/common';
 import AppStatus from './AppStatus';
 import AppLogo from '../AppLogo/AppLogo';
 import { limitText } from '../../modules/AppStore/helpers/table.helpers';
+import { AppInfo, AppStatusEnum } from '../../generated/graphql';
 
-const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
+type AppTileInfo = Pick<AppInfo, 'id' | 'name' | 'description' | 'image' | 'short_desc'>;
+
+const AppTile: React.FC<{ app: AppTileInfo; status: AppStatusEnum }> = ({ app, status }) => {
   const bg = useColorModeValue('white', '#1a202c');
 
   return (
@@ -18,11 +20,9 @@ const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
           <div className="mr-3 flex-1">
             <h3 className="font-bold text-xl">{app.name}</h3>
             <span>{limitText(app.short_desc, 50)}</span>
-            {app.installed && (
-              <div className="flex mt-1">
-                <AppStatus status={app.status} />
-              </div>
-            )}
+            <div className="flex mt-1">
+              <AppStatus status={status} />
+            </div>
           </div>
           <FiChevronRight className="text-slate-300" size={30} />
         </Box>

+ 13 - 13
packages/dashboard/src/components/Form/validators.ts

@@ -1,7 +1,7 @@
 import validator from 'validator';
-import { AppConfig, FieldTypes } from '@runtipi/common';
+import { FieldTypesEnum, FormField } from '../../generated/graphql';
 
-const validateField = (field: AppConfig['form_fields'][0], value: string): string | undefined => {
+const validateField = (field: FormField, value: string): string | undefined => {
   if (field.required && !value) {
     return `${field.label} is required`;
   }
@@ -11,7 +11,7 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
   }
 
   switch (field.type) {
-    case FieldTypes.text:
+    case FieldTypesEnum.Text:
       if (field.max && value.length > field.max) {
         return `${field.label} must be less than ${field.max} characters`;
       }
@@ -19,37 +19,37 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
         return `${field.label} must be at least ${field.min} characters`;
       }
       break;
-    case FieldTypes.password:
-      if (!validator.isLength(value, { min: field.min, max: field.max })) {
+    case FieldTypesEnum.Password:
+      if (!validator.isLength(value, { min: field.min || 0, max: field.max || 100 })) {
         return `${field.label} must be between ${field.min} and ${field.max} characters`;
       }
       break;
-    case FieldTypes.email:
+    case FieldTypesEnum.Email:
       if (!validator.isEmail(value)) {
         return `${field.label} must be a valid email address`;
       }
       break;
-    case FieldTypes.number:
+    case FieldTypesEnum.Number:
       if (!validator.isNumeric(value)) {
         return `${field.label} must be a number`;
       }
       break;
-    case FieldTypes.fqdn:
+    case FieldTypesEnum.Fqdn:
       if (!validator.isFQDN(value)) {
         return `${field.label} must be a valid domain`;
       }
       break;
-    case FieldTypes.ip:
+    case FieldTypesEnum.Ip:
       if (!validator.isIP(value)) {
         return `${field.label} must be a valid IP address`;
       }
       break;
-    case FieldTypes.fqdnip:
+    case FieldTypesEnum.Fqdnip:
       if (!validator.isFQDN(value || '') && !validator.isIP(value)) {
         return `${field.label} must be a valid domain or IP address`;
       }
       break;
-    case FieldTypes.url:
+    case FieldTypesEnum.Url:
       if (!validator.isURL(value)) {
         return `${field.label} must be a valid URL`;
       }
@@ -59,11 +59,11 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
   }
 };
 
-export const validateAppConfig = (values: Record<string, string>, fields: (AppConfig['form_fields'][0] & { id: string })[]) => {
+export const validateAppConfig = (values: Record<string, string>, fields: FormField[]) => {
   const errors: any = {};
 
   fields.forEach((field) => {
-    errors[field.id] = validateField(field, values[field.id]);
+    errors[field.env_variable] = validateField(field, values[field.env_variable]);
   });
 
   return errors;

+ 3 - 3
packages/dashboard/src/components/Layout/SideMenu.tsx

@@ -8,12 +8,12 @@ import Link from 'next/link';
 import clsx from 'clsx';
 import { useRouter } from 'next/router';
 import { IconType } from 'react-icons';
-import { useAuthStore } from '../../state/authStore';
+import { useLogoutMutation } from '../../generated/graphql';
 
 const SideMenu: React.FC = () => {
   const router = useRouter();
   const { colorMode, setColorMode } = useColorMode();
-  const { logout } = useAuthStore();
+  const [logout] = useLogoutMutation({ refetchQueries: ['Me'] });
   const path = router.pathname.split('/')[1];
 
   const renderMenuItem = (title: string, name: string, Icon: IconType) => {
@@ -53,7 +53,7 @@ const SideMenu: React.FC = () => {
       <Flex flex="1" />
       <List>
         <div className="mx-3">
-          <ListItem onClick={logout} className="cursor-pointer hover:font-bold flex items-center mb-5">
+          <ListItem onClick={() => logout()} className="cursor-pointer hover:font-bold flex items-center mb-5">
             <FiLogOut size={20} className="mr-3" />
             <p className="flex-1">Log out</p>
           </ListItem>

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

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

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

@@ -0,0 +1,14 @@
+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;

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

@@ -0,0 +1,9 @@
+import { HttpLink } from '@apollo/client';
+
+const httpLink = (ip: string) =>
+  new HttpLink({
+    uri: `http://${ip}:3001/graphql`,
+    credentials: 'include',
+  });
+
+export default httpLink;

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

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

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

@@ -0,0 +1,15 @@
+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' },
+];

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

@@ -0,0 +1,920 @@
+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'];
+  id: Scalars['String'];
+  info: AppInfo;
+  lastOpened: Scalars['DateTime'];
+  numOpened: Scalars['Float'];
+  status: AppStatusEnum;
+  updatedAt: Scalars['DateTime'];
+};
+
+export enum AppCategoriesEnum {
+  Automation = 'AUTOMATION',
+  Books = 'BOOKS',
+  Data = 'DATA',
+  Development = 'DEVELOPMENT',
+  Featured = 'FEATURED',
+  Media = 'MEDIA',
+  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'];
+  form_fields: Array<FormField>;
+  id: Scalars['String'];
+  image: Scalars['String'];
+  name: Scalars['String'];
+  port: Scalars['Float'];
+  requirements?: Maybe<Scalars['JSONObject']>;
+  short_desc: Scalars['String'];
+  source: Scalars['String'];
+  url_suffix?: Maybe<Scalars['String']>;
+  version?: Maybe<Scalars['String']>;
+};
+
+export type AppInputType = {
+  form: Scalars['JSONObject'];
+  id: Scalars['String'];
+};
+
+export enum AppStatusEnum {
+  Installing = 'INSTALLING',
+  Missing = 'MISSING',
+  Running = 'RUNNING',
+  Starting = 'STARTING',
+  Stopped = 'STOPPED',
+  Stopping = 'STOPPING',
+  Uninstalling = 'UNINSTALLING',
+}
+
+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',
+  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']>;
+  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: UserResponse;
+  logout: Scalars['Boolean'];
+  register: UserResponse;
+  startApp: App;
+  stopApp: App;
+  uninstallApp: 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 MutationUpdateAppConfigArgs = {
+  input: AppInputType;
+};
+
+export type Query = {
+  __typename?: 'Query';
+  getApp: App;
+  installedApps: Array<App>;
+  isConfigured: Scalars['Boolean'];
+  listAppsInfo: ListAppsResonse;
+  me?: Maybe<User>;
+  systemInfo?: Maybe<SystemInfoResponse>;
+  version: VersionResponse;
+};
+
+export type QueryGetAppArgs = {
+  id: Scalars['String'];
+};
+
+export type SystemInfoResponse = {
+  __typename?: 'SystemInfoResponse';
+  cpu: Cpu;
+  disk: DiskMemory;
+  memory: DiskMemory;
+};
+
+export type User = {
+  __typename?: 'User';
+  createdAt: Scalars['DateTime'];
+  id: Scalars['ID'];
+  updatedAt: Scalars['DateTime'];
+  username: Scalars['String'];
+};
+
+export type UserResponse = {
+  __typename?: 'UserResponse';
+  user?: Maybe<User>;
+};
+
+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?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
+
+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?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
+
+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 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;
+    info: {
+      __typename?: 'AppInfo';
+      id: string;
+      port: number;
+      name: string;
+      description: string;
+      available: boolean;
+      version?: string | null;
+      image: string;
+      short_desc: string;
+      author: string;
+      source: string;
+      categories: Array<AppCategoriesEnum>;
+      url_suffix?: string | null;
+      form_fields: Array<{
+        __typename?: 'FormField';
+        type: FieldTypesEnum;
+        label: string;
+        max?: number | null;
+        min?: number | null;
+        hint?: string | null;
+        required?: boolean | null;
+        env_variable: string;
+      }>;
+    };
+  };
+};
+
+export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>;
+
+export type InstalledAppsQuery = {
+  __typename?: 'Query';
+  installedApps: Array<{
+    __typename?: 'App';
+    id: string;
+    status: AppStatusEnum;
+    config: any;
+    info: { __typename?: 'AppInfo'; id: string; name: string; description: string; image: string; short_desc: string };
+  }>;
+};
+
+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;
+      image: string;
+      port: number;
+      name: string;
+      version?: string | null;
+      short_desc: string;
+      author: string;
+      categories: Array<AppCategoriesEnum>;
+    }>;
+  };
+};
+
+export type MeQueryVariables = Exact<{ [key: string]: never }>;
+
+export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: 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) {
+      user {
+        id
+      }
+    }
+  }
+`;
+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) {
+      user {
+        id
+      }
+    }
+  }
+`;
+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 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 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
+      info {
+        id
+        port
+        name
+        description
+        available
+        version
+        image
+        short_desc
+        author
+        source
+        categories
+        url_suffix
+        form_fields {
+          type
+          label
+          max
+          min
+          hint
+          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
+      info {
+        id
+        name
+        description
+        image
+        short_desc
+      }
+    }
+  }
+`;
+
+/**
+ * __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
+        image
+        port
+        name
+        version
+        short_desc
+        author
+        categories
+      }
+      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 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>;

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

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

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

@@ -0,0 +1,7 @@
+mutation Login($input: UsernamePasswordInput!) {
+  login(input: $input) {
+    user {
+      id
+    }
+  }
+}

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

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

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

@@ -0,0 +1,7 @@
+mutation Register($input: UsernamePasswordInput!) {
+  register(input: $input) {
+    user {
+      id
+    }
+  }
+}

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,30 @@
+query GetApp($appId: String!) {
+  getApp(id: $appId) {
+    id
+    status
+    config
+    info {
+      id
+      port
+      name
+      description
+      available
+      version
+      image
+      short_desc
+      author
+      source
+      categories
+      url_suffix
+      form_fields {
+        type
+        label
+        max
+        min
+        hint
+        required
+        env_variable
+      }
+    }
+  }
+}

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

@@ -0,0 +1,14 @@
+query InstalledApps {
+  installedApps {
+    id
+    status
+    config
+    info {
+      id
+      name
+      description
+      image
+      short_desc
+    }
+  }
+}

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,49 @@
+import { useEffect, useState } from 'react';
+import { ApolloClient } from '@apollo/client';
+import axios from 'axios';
+import useSWR, { BareFetcher } from 'swr';
+import { createApolloClient } from '../core/apollo/client';
+import { useSytemStore } from '../state/systemStore';
+
+interface IReturnProps {
+  client?: ApolloClient<unknown>;
+  isLoadingComplete?: boolean;
+}
+
+const fetcher: BareFetcher<any> = (url: string) => {
+  return axios.get(url).then((res) => res.data);
+};
+
+export default function useCachedResources(): IReturnProps {
+  const { data } = useSWR('/api/ip', fetcher);
+  const { internalIp, setInternalIp } = useSytemStore();
+  const [isLoadingComplete, setLoadingComplete] = useState(false);
+  const [client, setClient] = useState<ApolloClient<unknown>>();
+
+  async function loadResourcesAndDataAsync(ip: string) {
+    try {
+      const restoredClient = await createApolloClient(ip);
+
+      setClient(restoredClient);
+    } catch (error) {
+      // We might want to provide this error information to an error reporting service
+      console.warn(error);
+    } finally {
+      setLoadingComplete(true);
+    }
+  }
+
+  useEffect(() => {
+    if (data?.ip && !internalIp) {
+      setInternalIp(data.ip);
+    }
+  }, [data?.ip, internalIp, setInternalIp]);
+
+  useEffect(() => {
+    if (internalIp) {
+      loadResourcesAndDataAsync(internalIp);
+    }
+  }, [internalIp]);
+
+  return { client, isLoadingComplete };
+}

+ 3 - 3
packages/dashboard/src/modules/AppStore/components/AppStoreTable.tsx

@@ -1,12 +1,12 @@
 import { Flex, Input, SimpleGrid } from '@chakra-ui/react';
-import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
 import React from 'react';
-import { SortableColumns, SortDirection } from '../helpers/table.types';
+import { AppCategoriesEnum } from '../../../generated/graphql';
+import { AppTableData, SortableColumns, SortDirection } from '../helpers/table.types';
 import AppStoreTile from './AppStoreTile';
 import CategorySelect from './CategorySelect';
 
 interface IProps {
-  data: AppConfig[];
+  data: AppTableData;
   onSearch: (value: string) => void;
   onSelectCategories: (value: AppCategoriesEnum[]) => void;
   onSortBy: (value: SortableColumns) => void;

+ 12 - 4
packages/dashboard/src/modules/AppStore/components/AppStoreTile.tsx

@@ -1,11 +1,19 @@
 import { Tag, TagLabel } from '@chakra-ui/react';
-import { AppConfig } from '@runtipi/common';
 import Link from 'next/link';
 import React from 'react';
 import AppLogo from '../../../components/AppLogo/AppLogo';
+import { AppCategoriesEnum } from '../../../generated/graphql';
 import { colorSchemeForCategory, limitText } from '../helpers/table.helpers';
 
-const AppStoreTile: React.FC<{ app: AppConfig }> = ({ app }) => {
+type App = {
+  id: string;
+  name: string;
+  categories: string[];
+  short_desc: string;
+  image: string;
+};
+
+const AppStoreTile: React.FC<{ app: App }> = ({ app }) => {
   return (
     <Link href={`/app-store/${app.id}`} passHref>
       <div key={app.id} className="p-2 rounded-md app-store-tile flex items-center group">
@@ -14,8 +22,8 @@ const AppStoreTile: React.FC<{ app: AppConfig }> = ({ app }) => {
           <div className="font-bold">{limitText(app.name, 20)}</div>
           <div className="text-sm mb-1">{limitText(app.short_desc, 45)}</div>
           {app.categories?.map((category) => (
-            <Tag colorScheme={colorSchemeForCategory[category]} className="mr-1" borderRadius="full" key={`${app.id}-${category}`} size="sm" variant="solid">
-              <TagLabel>{category}</TagLabel>
+            <Tag colorScheme={colorSchemeForCategory[category as AppCategoriesEnum]} className="mr-1" borderRadius="full" key={`${app.id}-${category}`} size="sm" variant="solid">
+              <TagLabel>{category.toLocaleLowerCase()}</TagLabel>
             </Tag>
           ))}
         </div>

+ 2 - 1
packages/dashboard/src/modules/AppStore/components/CategorySelect.tsx

@@ -1,7 +1,8 @@
 import { useColorModeValue } from '@chakra-ui/react';
-import { AppCategoriesEnum, APP_CATEGORIES } from '@runtipi/common';
 import React from 'react';
 import Select, { Options } from 'react-select';
+import { APP_CATEGORIES } from '../../../core/constants';
+import { AppCategoriesEnum } from '../../../generated/graphql';
 
 interface IProps {
   onSelect: (value: AppCategoriesEnum[]) => void;

+ 2 - 2
packages/dashboard/src/modules/AppStore/components/FeaturedApps.tsx

@@ -1,10 +1,10 @@
-import { AppConfig } from '@runtipi/common';
 import React from 'react';
 import { Box, Button, Flex } from '@chakra-ui/react';
 import FeaturedCard from './FeaturedCard';
+import { AppInfo } from '../../../generated/graphql';
 
 interface IProps {
-  apps: AppConfig[];
+  apps: AppInfo[];
 }
 
 const FeaturedApps: React.FC<IProps> = ({ apps }) => {

+ 3 - 3
packages/dashboard/src/modules/AppStore/components/FeaturedCard.tsx

@@ -1,9 +1,9 @@
 import { Flex, ScaleFade } from '@chakra-ui/react';
-import { AppConfig } from '@runtipi/common';
 import React from 'react';
+import { AppInfo } from '../../../generated/graphql';
 
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
   show: boolean;
 }
 
@@ -16,7 +16,7 @@ const FeaturedCard: React.FC<IProps> = ({ app, show }) => {
         rounded="md"
         shadow="md"
         style={{
-          backgroundImage: `url(https://images.unsplash.com/photo-1488590528505-98d2b5aba04b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80)`,
+          backgroundImage: 'url(https://images.unsplash.com/photo-1488590528505-98d2b5aba04b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80)',
         }}
       >
         <div className="relative flex flex-1 w-max lg:bg-gradient-to-r from-white via-white">

+ 7 - 5
packages/dashboard/src/modules/AppStore/containers/AppStoreContainer.tsx

@@ -1,17 +1,19 @@
 import { Flex } from '@chakra-ui/react';
-import { AppCategoriesEnum } from '@runtipi/common';
 import React from 'react';
-import { useAppsStore } from '../../../state/appsStore';
+import { AppCategoriesEnum } from '../../../generated/graphql';
 import AppStoreTable from '../components/AppStoreTable';
 import { sortTable } from '../helpers/table.helpers';
-import { SortableColumns, SortDirection } from '../helpers/table.types';
+import { AppTableData, SortableColumns, SortDirection } from '../helpers/table.types';
 
 // function nonNullable<T>(value: T): value is NonNullable<T> {
 //   return value !== null && value !== undefined;
 // }
 
-const AppStoreContainer = () => {
-  const { apps } = useAppsStore();
+interface IProps {
+  apps: AppTableData;
+}
+
+const AppStoreContainer: React.FC<IProps> = ({ apps }) => {
   const [search, setSearch] = React.useState('');
   const [categories, setCategories] = React.useState<AppCategoriesEnum[]>([]);
   const [sort, setSort] = React.useState<SortableColumns>('name');

+ 14 - 13
packages/dashboard/src/modules/AppStore/helpers/table.helpers.ts

@@ -1,6 +1,7 @@
-import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
+import { AppCategoriesEnum, AppInfo } from '../../../generated/graphql';
+import { AppTableData } from './table.types';
 
-export const sortTable = (data: AppConfig[], col: keyof Pick<AppConfig, 'name'>, direction: 'asc' | 'desc', categories: AppCategoriesEnum[], search: string) => {
+export const sortTable = (data: AppTableData, col: keyof Pick<AppInfo, 'name'>, direction: 'asc' | 'desc', categories: AppCategoriesEnum[], search: string) => {
   const sortedData = [...data].sort((a, b) => {
     const aVal = a[col];
     const bVal = b[col];
@@ -25,15 +26,15 @@ export const limitText = (text: string, limit: number) => {
 };
 
 export const colorSchemeForCategory: Record<AppCategoriesEnum, string> = {
-  [AppCategoriesEnum.NETWORK]: 'blue',
-  [AppCategoriesEnum.MEDIA]: 'green',
-  [AppCategoriesEnum.AUTOMATION]: 'orange',
-  [AppCategoriesEnum.DEVELOPMENT]: 'purple',
-  [AppCategoriesEnum.UTILITIES]: 'gray',
-  [AppCategoriesEnum.PHOTOGRAPHY]: 'red',
-  [AppCategoriesEnum.SECURITY]: 'yellow',
-  [AppCategoriesEnum.SOCIAL]: 'teal',
-  [AppCategoriesEnum.FEATURED]: 'pink',
-  [AppCategoriesEnum.DATA]: 'red',
-  [AppCategoriesEnum.BOOKS]: 'blue',
+  [AppCategoriesEnum.Network]: 'blue',
+  [AppCategoriesEnum.Media]: 'green',
+  [AppCategoriesEnum.Automation]: 'orange',
+  [AppCategoriesEnum.Development]: 'purple',
+  [AppCategoriesEnum.Utilities]: 'gray',
+  [AppCategoriesEnum.Photography]: 'red',
+  [AppCategoriesEnum.Security]: 'yellow',
+  [AppCategoriesEnum.Social]: 'teal',
+  [AppCategoriesEnum.Featured]: 'pink',
+  [AppCategoriesEnum.Data]: 'red',
+  [AppCategoriesEnum.Books]: 'blue',
 };

+ 4 - 2
packages/dashboard/src/modules/AppStore/helpers/table.types.ts

@@ -1,4 +1,6 @@
-import { AppConfig } from '@runtipi/common';
+import { AppInfo } from '../../../generated/graphql';
 
-export type SortableColumns = keyof Pick<AppConfig, 'name'>;
+export type SortableColumns = keyof Pick<AppInfo, 'name'>;
 export type SortDirection = 'asc' | 'desc';
+
+export type AppTableData = Omit<AppInfo, 'description' | 'form_fields' | 'source' | 'status' | 'url_suffix' | 'version'>[];

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

@@ -1,22 +1,25 @@
 import { Button } from '@chakra-ui/react';
 import React from 'react';
 import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
-import { AppConfig, AppStatusEnum } from '@runtipi/common';
+import { TiCancel } from 'react-icons/ti';
+import { AppInfo, AppStatusEnum } from '../../../generated/graphql';
 
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
+  status?: AppStatusEnum;
   onInstall: () => void;
   onUninstall: () => void;
   onStart: () => void;
   onStop: () => void;
   onOpen: () => void;
   onUpdate: () => void;
+  onCancel: () => void;
 }
 
-const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
+const AppActions: React.FC<IProps> = ({ app, status, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate, onCancel }) => {
   const hasSettings = Object.keys(app.form_fields).length > 0;
 
-  if (app?.installed && app.status === AppStatusEnum.STOPPED) {
+  if (status === AppStatusEnum.Stopped) {
     return (
       <div className="flex flex-wrap justify-center">
         <Button onClick={onStart} width={150} colorScheme="green" className="mt-3 mr-2">
@@ -35,7 +38,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
         )}
       </div>
     );
-  } else if (app?.installed && app.status === AppStatusEnum.RUNNING) {
+  } else if (status === AppStatusEnum.Running) {
     return (
       <div>
         <Button onClick={onOpen} width={150} colorScheme="gray" className="mt-3 mr-2">
@@ -48,23 +51,28 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
         </Button>
       </div>
     );
-  } else if (app.status === AppStatusEnum.INSTALLING || app.status === AppStatusEnum.UNINSTALLING || app.status === AppStatusEnum.STARTING || app.status === AppStatusEnum.STOPPING) {
+  } else if (status === AppStatusEnum.Installing || status === AppStatusEnum.Uninstalling || status === AppStatusEnum.Starting || status === AppStatusEnum.Stopping) {
     return (
       <div className="flex items-center sm:items-start flex-col md:flex-row">
         <Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
           Install
           <FiPlay className="ml-1" />
         </Button>
-        <span className="text-gray-500 text-sm ml-2 mt-3 self-center text-center sm:text-left">{`App is ${app.status} please wait and don't refresh page...`}</span>
+        <Button onClick={onCancel} colorScheme="gray" className="mt-3 mr-2 ml-2">
+          <TiCancel />
+        </Button>
+        <span className="text-gray-500 text-sm ml-2 mt-3 self-center text-center sm:text-left">{`App is ${status.toLowerCase()} please wait and don't refresh page...`}</span>
       </div>
     );
+  } else if (status === AppStatusEnum.Missing) {
+    return (
+      <Button onClick={onInstall} width={160} colorScheme="green" className="mt-3">
+        Install
+      </Button>
+    );
   }
 
-  return (
-    <Button onClick={onInstall} width={160} colorScheme="green" className="mt-3">
-      Install
-    </Button>
-  );
+  return null;
 };
 
 export default AppActions;

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff