Przeglądaj źródła

Frontend GraphQL queries

Nicolas Meienberger 3 lat temu
rodzic
commit
729c2311f5
46 zmienionych plików z 797 dodań i 641 usunięć
  1. 1 1
      packages/common/src/types/app.types.ts
  2. 13 13
      packages/dashboard/src/components/Form/validators.ts
  3. 3 3
      packages/dashboard/src/components/Layout/SideMenu.tsx
  4. 406 43
      packages/dashboard/src/generated/graphql.tsx
  5. 3 0
      packages/dashboard/src/graphql/mutations/logout.graphql
  6. 7 0
      packages/dashboard/src/graphql/mutations/register.graphql
  7. 33 0
      packages/dashboard/src/graphql/queries/getApp.graphql
  8. 7 0
      packages/dashboard/src/graphql/queries/installedApps.graphql
  9. 18 0
      packages/dashboard/src/graphql/queries/listApps.graphql
  10. 17 0
      packages/dashboard/src/graphql/queries/systemInfo.graphql
  11. 3 3
      packages/dashboard/src/modules/AppStore/components/AppStoreTable.tsx
  12. 11 3
      packages/dashboard/src/modules/AppStore/components/AppStoreTile.tsx
  13. 6 4
      packages/dashboard/src/modules/AppStore/containers/AppStoreContainer.tsx
  14. 5 3
      packages/dashboard/src/modules/AppStore/helpers/table.helpers.ts
  15. 3 1
      packages/dashboard/src/modules/AppStore/helpers/table.types.ts
  16. 8 7
      packages/dashboard/src/modules/Apps/components/AppActions.tsx
  17. 7 10
      packages/dashboard/src/modules/Apps/components/InstallForm.tsx
  18. 2 2
      packages/dashboard/src/modules/Apps/components/InstallModal.tsx
  19. 2 2
      packages/dashboard/src/modules/Apps/components/StopModal.tsx
  20. 2 2
      packages/dashboard/src/modules/Apps/components/UninstallModal.tsx
  21. 2 2
      packages/dashboard/src/modules/Apps/components/UpdateModal.tsx
  22. 23 24
      packages/dashboard/src/modules/Apps/containers/AppDetails.tsx
  23. 8 5
      packages/dashboard/src/modules/Auth/containers/Onboarding.tsx
  24. 8 18
      packages/dashboard/src/modules/Dashboard/containers/Dashboard.tsx
  25. 5 11
      packages/dashboard/src/pages/app-store/[id].tsx
  26. 5 10
      packages/dashboard/src/pages/app-store/index.tsx
  27. 5 11
      packages/dashboard/src/pages/apps/[id].tsx
  28. 3 5
      packages/dashboard/src/pages/index.tsx
  29. 0 118
      packages/dashboard/src/state/appsStore.ts
  30. 11 2
      packages/system-api/src/modules/apps/app.entity.ts
  31. 0 139
      packages/system-api/src/modules/apps/apps.controller.ts
  32. 0 17
      packages/system-api/src/modules/apps/apps.helpers.ts
  33. 4 4
      packages/system-api/src/modules/apps/apps.resolver.ts
  34. 0 15
      packages/system-api/src/modules/apps/apps.routes.ts
  35. 21 16
      packages/system-api/src/modules/apps/apps.service.ts
  36. 26 11
      packages/system-api/src/modules/apps/apps.types.ts
  37. 1 2
      packages/system-api/src/modules/auth/auth.resolver.ts
  38. 6 0
      packages/system-api/src/modules/auth/auth.service.ts
  39. 0 27
      packages/system-api/src/modules/network/network.controller.ts
  40. 0 8
      packages/system-api/src/modules/network/network.routes.ts
  41. 0 87
      packages/system-api/src/modules/system/system.controller.ts
  42. 16 0
      packages/system-api/src/modules/system/system.resolver.ts
  43. 0 11
      packages/system-api/src/modules/system/system.routes.ts
  44. 52 0
      packages/system-api/src/modules/system/system.service.ts
  45. 42 0
      packages/system-api/src/modules/system/system.types.ts
  46. 2 1
      packages/system-api/src/schema.ts

+ 1 - 1
packages/common/src/types/app.types.ts

@@ -51,7 +51,7 @@ export interface AppConfig {
     ports?: number[];
     ports?: number[];
   };
   };
   description: string;
   description: string;
-  version: string;
+  version?: string;
   image: string;
   image: string;
   form_fields: FormField[];
   form_fields: FormField[];
   short_desc: string;
   short_desc: string;

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

@@ -1,7 +1,7 @@
 import validator from 'validator';
 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) {
   if (field.required && !value) {
     return `${field.label} is required`;
     return `${field.label} is required`;
   }
   }
@@ -11,7 +11,7 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
   }
   }
 
 
   switch (field.type) {
   switch (field.type) {
-    case FieldTypes.text:
+    case FieldTypesEnum.Text:
       if (field.max && value.length > field.max) {
       if (field.max && value.length > field.max) {
         return `${field.label} must be less than ${field.max} characters`;
         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`;
         return `${field.label} must be at least ${field.min} characters`;
       }
       }
       break;
       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`;
         return `${field.label} must be between ${field.min} and ${field.max} characters`;
       }
       }
       break;
       break;
-    case FieldTypes.email:
+    case FieldTypesEnum.Email:
       if (!validator.isEmail(value)) {
       if (!validator.isEmail(value)) {
         return `${field.label} must be a valid email address`;
         return `${field.label} must be a valid email address`;
       }
       }
       break;
       break;
-    case FieldTypes.number:
+    case FieldTypesEnum.Number:
       if (!validator.isNumeric(value)) {
       if (!validator.isNumeric(value)) {
         return `${field.label} must be a number`;
         return `${field.label} must be a number`;
       }
       }
       break;
       break;
-    case FieldTypes.fqdn:
+    case FieldTypesEnum.Fqdn:
       if (!validator.isFQDN(value)) {
       if (!validator.isFQDN(value)) {
         return `${field.label} must be a valid domain`;
         return `${field.label} must be a valid domain`;
       }
       }
       break;
       break;
-    case FieldTypes.ip:
+    case FieldTypesEnum.Ip:
       if (!validator.isIP(value)) {
       if (!validator.isIP(value)) {
         return `${field.label} must be a valid IP address`;
         return `${field.label} must be a valid IP address`;
       }
       }
       break;
       break;
-    case FieldTypes.fqdnip:
+    case FieldTypesEnum.Fqdnip:
       if (!validator.isFQDN(value || '') && !validator.isIP(value)) {
       if (!validator.isFQDN(value || '') && !validator.isIP(value)) {
         return `${field.label} must be a valid domain or IP address`;
         return `${field.label} must be a valid domain or IP address`;
       }
       }
       break;
       break;
-    case FieldTypes.url:
+    case FieldTypesEnum.Url:
       if (!validator.isURL(value)) {
       if (!validator.isURL(value)) {
         return `${field.label} must be a valid URL`;
         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 = {};
   const errors: any = {};
 
 
   fields.forEach((field) => {
   fields.forEach((field) => {
-    errors[field.id] = validateField(field, values[field.id]);
+    errors[field.env_variable] = validateField(field, values[field.env_variable]);
   });
   });
 
 
   return errors;
   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 clsx from 'clsx';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { IconType } from 'react-icons';
 import { IconType } from 'react-icons';
-import { useAuthStore } from '../../state/authStore';
+import { useLogoutMutation } from '../../generated/graphql';
 
 
 const SideMenu: React.FC = () => {
 const SideMenu: React.FC = () => {
   const router = useRouter();
   const router = useRouter();
   const { colorMode, setColorMode } = useColorMode();
   const { colorMode, setColorMode } = useColorMode();
-  const { logout } = useAuthStore();
+  const [logout] = useLogoutMutation({ refetchQueries: ['Me'] });
   const path = router.pathname.split('/')[1];
   const path = router.pathname.split('/')[1];
 
 
   const renderMenuItem = (title: string, name: string, Icon: IconType) => {
   const renderMenuItem = (title: string, name: string, Icon: IconType) => {
@@ -53,7 +53,7 @@ const SideMenu: React.FC = () => {
       <Flex flex="1" />
       <Flex flex="1" />
       <List>
       <List>
         <div className="mx-3">
         <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" />
             <FiLogOut size={20} className="mr-3" />
             <p className="flex-1">Log out</p>
             <p className="flex-1">Log out</p>
           </ListItem>
           </ListItem>

+ 406 - 43
packages/dashboard/src/generated/graphql.tsx

@@ -21,19 +21,34 @@ export type Scalars = {
 
 
 export type App = {
 export type App = {
   __typename?: 'App';
   __typename?: 'App';
+  config: Scalars['JSONObject'];
   createdAt: Scalars['DateTime'];
   createdAt: Scalars['DateTime'];
   id: Scalars['String'];
   id: Scalars['String'];
   lastOpened: Scalars['DateTime'];
   lastOpened: Scalars['DateTime'];
   numOpened: Scalars['Float'];
   numOpened: Scalars['Float'];
-  status: Scalars['String'];
+  status: AppStatusEnum;
   updatedAt: Scalars['DateTime'];
   updatedAt: Scalars['DateTime'];
 };
 };
 
 
-export type AppConfig = {
-  __typename?: 'AppConfig';
+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'];
   author: Scalars['String'];
   available: Scalars['Boolean'];
   available: Scalars['Boolean'];
-  categories: Array<Scalars['String']>;
+  categories: Array<AppCategoriesEnum>;
   description: Scalars['String'];
   description: Scalars['String'];
   form_fields: Array<FormField>;
   form_fields: Array<FormField>;
   id: Scalars['String'];
   id: Scalars['String'];
@@ -43,7 +58,6 @@ export type AppConfig = {
   port: Scalars['Float'];
   port: Scalars['Float'];
   short_desc: Scalars['String'];
   short_desc: Scalars['String'];
   source: Scalars['String'];
   source: Scalars['String'];
-  status: Scalars['String'];
   url_suffix?: Maybe<Scalars['String']>;
   url_suffix?: Maybe<Scalars['String']>;
   version?: Maybe<Scalars['String']>;
   version?: Maybe<Scalars['String']>;
 };
 };
@@ -53,6 +67,44 @@ export type AppInputType = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+export type AppResponse = {
+  __typename?: 'AppResponse';
+  app?: Maybe<App>;
+  info: AppInfo;
+};
+
+export enum AppStatusEnum {
+  Installing = 'INSTALLING',
+  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 = {
 export type FormField = {
   __typename?: 'FormField';
   __typename?: 'FormField';
   env_variable: Scalars['String'];
   env_variable: Scalars['String'];
@@ -61,12 +113,12 @@ export type FormField = {
   max?: Maybe<Scalars['Float']>;
   max?: Maybe<Scalars['Float']>;
   min?: Maybe<Scalars['Float']>;
   min?: Maybe<Scalars['Float']>;
   required?: Maybe<Scalars['Boolean']>;
   required?: Maybe<Scalars['Boolean']>;
-  type: Scalars['String'];
+  type: FieldTypesEnum;
 };
 };
 
 
 export type ListAppsResonse = {
 export type ListAppsResonse = {
   __typename?: 'ListAppsResonse';
   __typename?: 'ListAppsResonse';
-  apps: Array<AppConfig>;
+  apps: Array<AppInfo>;
   total: Scalars['Float'];
   total: Scalars['Float'];
 };
 };
 
 
@@ -82,47 +134,64 @@ export type Mutation = {
   updateAppConfig: App;
   updateAppConfig: App;
 };
 };
 
 
+
 export type MutationInstallAppArgs = {
 export type MutationInstallAppArgs = {
   input: AppInputType;
   input: AppInputType;
 };
 };
 
 
+
 export type MutationLoginArgs = {
 export type MutationLoginArgs = {
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 };
 };
 
 
+
 export type MutationRegisterArgs = {
 export type MutationRegisterArgs = {
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 };
 };
 
 
+
 export type MutationStartAppArgs = {
 export type MutationStartAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationStopAppArgs = {
 export type MutationStopAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationUninstallAppArgs = {
 export type MutationUninstallAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+
 export type MutationUpdateAppConfigArgs = {
 export type MutationUpdateAppConfigArgs = {
   input: AppInputType;
   input: AppInputType;
 };
 };
 
 
 export type Query = {
 export type Query = {
   __typename?: 'Query';
   __typename?: 'Query';
-  getAppInfo: AppConfig;
+  getApp: AppResponse;
   installedApps: Array<App>;
   installedApps: Array<App>;
   isConfigured: Scalars['Boolean'];
   isConfigured: Scalars['Boolean'];
   listAppsInfo: ListAppsResonse;
   listAppsInfo: ListAppsResonse;
   me?: Maybe<User>;
   me?: Maybe<User>;
+  systemInfo?: Maybe<SystemInfoResponse>;
+  version: Scalars['String'];
 };
 };
 
 
-export type QueryGetAppInfoArgs = {
+
+export type QueryGetAppArgs = {
   id: Scalars['String'];
   id: Scalars['String'];
 };
 };
 
 
+export type SystemInfoResponse = {
+  __typename?: 'SystemInfoResponse';
+  cpu: Cpu;
+  disk: DiskMemory;
+  memory: DiskMemory;
+};
+
 export type User = {
 export type User = {
   __typename?: 'User';
   __typename?: 'User';
   createdAt: Scalars['DateTime'];
   createdAt: Scalars['DateTime'];
@@ -145,25 +214,63 @@ export type LoginMutationVariables = Exact<{
   input: UsernamePasswordInput;
   input: UsernamePasswordInput;
 }>;
 }>;
 
 
-export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
 
 
-export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
+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 GetAppQueryVariables = Exact<{
+  appId: Scalars['String'];
+}>;
+
+
+export type GetAppQuery = { __typename?: 'Query', getApp: { __typename?: 'AppResponse', app?: { __typename?: 'App', id: string, status: AppStatusEnum, config: any } | null, 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, installed: boolean, 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 }> };
+
+export type ConfiguredQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type ConfiguredQuery = { __typename?: 'Query', isConfigured: boolean };
+
+export type ListAppsQueryVariables = Exact<{ [key: string]: never; }>;
 
 
-export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
 
 
-export type MeQueryVariables = Exact<{ [key: string]: never }>;
+export type ListAppsQuery = { __typename?: 'Query', listAppsInfo: { __typename?: 'ListAppsResonse', total: number, apps: Array<{ __typename?: 'AppInfo', id: string, available: boolean, installed: 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 MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
 
 
 export const LoginDocument = gql`
 export const LoginDocument = gql`
-  mutation Login($input: UsernamePasswordInput!) {
-    login(input: $input) {
-      user {
-        id
-      }
+    mutation Login($input: UsernamePasswordInput!) {
+  login(input: $input) {
+    user {
+      id
     }
     }
   }
   }
-`;
+}
+    `;
 export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
 export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
 
 
 /**
 /**
@@ -184,17 +291,181 @@ export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutati
  * });
  * });
  */
  */
 export function useLoginMutation(baseOptions?: Apollo.MutationHookOptions<LoginMutation, LoginMutationVariables>) {
 export function useLoginMutation(baseOptions?: Apollo.MutationHookOptions<LoginMutation, LoginMutationVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
-}
+        const options = {...defaultOptions, ...baseOptions}
+        return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
+      }
 export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
 export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
 export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
 export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
 export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
 export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
-export const ConfiguredDocument = gql`
-  query Configured {
-    isConfigured
+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 GetAppDocument = gql`
+    query GetApp($appId: String!) {
+  getApp(id: $appId) {
+    app {
+      id
+      status
+      config
+    }
+    info {
+      id
+      port
+      name
+      description
+      available
+      version
+      image
+      short_desc
+      author
+      source
+      installed
+      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
   }
   }
-`;
+}
+    `;
+
+/**
+ * __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__
  * __useConfiguredQuery__
@@ -212,23 +483,69 @@ export const ConfiguredDocument = gql`
  * });
  * });
  */
  */
 export function useConfiguredQuery(baseOptions?: Apollo.QueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
 export function useConfiguredQuery(baseOptions?: Apollo.QueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
-}
+        const options = {...defaultOptions, ...baseOptions}
+        return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
+      }
 export function useConfiguredLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
 export function useConfiguredLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
-}
+          const options = {...defaultOptions, ...baseOptions}
+          return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
+        }
 export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
 export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
 export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
 export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
 export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
 export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
-export const MeDocument = gql`
-  query Me {
-    me {
+export const ListAppsDocument = gql`
+    query ListApps {
+  listAppsInfo {
+    apps {
       id
       id
+      available
+      installed
+      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__
  * __useMeQuery__
@@ -246,13 +563,59 @@ export const MeDocument = gql`
  * });
  * });
  */
  */
 export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
 export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
-}
+        const options = {...defaultOptions, ...baseOptions}
+        return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
+      }
 export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
 export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
-  const options = { ...defaultOptions, ...baseOptions };
-  return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
-}
+          const options = {...defaultOptions, ...baseOptions}
+          return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
+        }
 export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
 export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
 export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
 export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
 export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
 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>;

+ 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
+    }
+  }
+}

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

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

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

@@ -0,0 +1,7 @@
+query InstalledApps {
+  installedApps {
+    id
+    status
+    config
+  }
+}

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

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

+ 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
+    }
+  }
+}

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

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

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

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

+ 6 - 4
packages/dashboard/src/modules/AppStore/containers/AppStoreContainer.tsx

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

+ 5 - 3
packages/dashboard/src/modules/AppStore/helpers/table.helpers.ts

@@ -1,6 +1,8 @@
-import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
+import { AppCategoriesEnum } from '@runtipi/common';
+import { AppConfig } 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<AppConfig, 'name'>, direction: 'asc' | 'desc', categories: AppCategoriesEnum[], search: string) => {
   const sortedData = [...data].sort((a, b) => {
   const sortedData = [...data].sort((a, b) => {
     const aVal = a[col];
     const aVal = a[col];
     const bVal = b[col];
     const bVal = b[col];
@@ -14,7 +16,7 @@ export const sortTable = (data: AppConfig[], col: keyof Pick<AppConfig, 'name'>,
   });
   });
 
 
   if (categories.length > 0) {
   if (categories.length > 0) {
-    return sortedData.filter((app) => app.categories.some((c) => categories.includes(c))).filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
+    return sortedData.filter((app) => app.categories.some((c) => categories.includes(c as AppCategoriesEnum))).filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
   } else {
   } else {
     return sortedData.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
     return sortedData.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
   }
   }

+ 3 - 1
packages/dashboard/src/modules/AppStore/helpers/table.types.ts

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

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

@@ -1,10 +1,11 @@
 import { Button } from '@chakra-ui/react';
 import { Button } from '@chakra-ui/react';
 import React from 'react';
 import React from 'react';
 import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
 import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
-import { AppConfig, AppStatusEnum } from '@runtipi/common';
+import { AppInfo, AppStatusEnum } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
+  status?: AppStatusEnum;
   onInstall: () => void;
   onInstall: () => void;
   onUninstall: () => void;
   onUninstall: () => void;
   onStart: () => void;
   onStart: () => void;
@@ -13,10 +14,10 @@ interface IProps {
   onUpdate: () => void;
   onUpdate: () => 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 }) => {
   const hasSettings = Object.keys(app.form_fields).length > 0;
   const hasSettings = Object.keys(app.form_fields).length > 0;
 
 
-  if (app?.installed && app.status === AppStatusEnum.STOPPED) {
+  if (status === AppStatusEnum.Stopped) {
     return (
     return (
       <div className="flex flex-wrap justify-center">
       <div className="flex flex-wrap justify-center">
         <Button onClick={onStart} width={150} colorScheme="green" className="mt-3 mr-2">
         <Button onClick={onStart} width={150} colorScheme="green" className="mt-3 mr-2">
@@ -35,7 +36,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
         )}
         )}
       </div>
       </div>
     );
     );
-  } else if (app?.installed && app.status === AppStatusEnum.RUNNING) {
+  } else if (status === AppStatusEnum.Running) {
     return (
     return (
       <div>
       <div>
         <Button onClick={onOpen} width={150} colorScheme="gray" className="mt-3 mr-2">
         <Button onClick={onOpen} width={150} colorScheme="gray" className="mt-3 mr-2">
@@ -48,14 +49,14 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
         </Button>
         </Button>
       </div>
       </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 (
     return (
       <div className="flex items-center sm:items-start flex-col md:flex-row">
       <div className="flex items-center sm:items-start flex-col md:flex-row">
         <Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
         <Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
           Install
           Install
           <FiPlay className="ml-1" />
           <FiPlay className="ml-1" />
         </Button>
         </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>
+        <span className="text-gray-500 text-sm ml-2 mt-3 self-center text-center sm:text-left">{`App is ${status} please wait and don't refresh page...`}</span>
       </div>
       </div>
     );
     );
   }
   }

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

@@ -3,23 +3,20 @@ import React from 'react';
 import { Form, Field } from 'react-final-form';
 import { Form, Field } from 'react-final-form';
 import FormInput from '../../../components/Form/FormInput';
 import FormInput from '../../../components/Form/FormInput';
 import { validateAppConfig } from '../../../components/Form/validators';
 import { validateAppConfig } from '../../../components/Form/validators';
-import { AppConfig } from '@runtipi/common';
-import { objectKeys } from '../../../utils/typescript';
+import { AppInfo } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  formFields: AppConfig['form_fields'];
+  formFields: AppInfo['form_fields'];
   onSubmit: (values: Record<string, unknown>) => void;
   onSubmit: (values: Record<string, unknown>) => void;
   initalValues?: Record<string, string>;
   initalValues?: Record<string, string>;
 }
 }
 
 
 const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValues }) => {
 const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValues }) => {
-  const fields = objectKeys(formFields).map((key) => ({ ...formFields[key], id: key }));
-
-  const renderField = (field: typeof fields[0]) => {
+  const renderField = (field: typeof formFields[0]) => {
     return (
     return (
       <Field
       <Field
-        key={field.id}
-        name={field.id}
+        key={field.env_variable}
+        name={field.env_variable}
         render={({ input, meta }) => <FormInput className="mb-3" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} label={field.label} {...input} />}
         render={({ input, meta }) => <FormInput className="mb-3" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} label={field.label} {...input} />}
       />
       />
     );
     );
@@ -30,10 +27,10 @@ const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValues }) =
       initialValues={initalValues}
       initialValues={initalValues}
       onSubmit={onSubmit}
       onSubmit={onSubmit}
       validateOnBlur={true}
       validateOnBlur={true}
-      validate={(values) => validateAppConfig(values, fields)}
+      validate={(values) => validateAppConfig(values, formFields)}
       render={({ handleSubmit, validating, submitting }) => (
       render={({ handleSubmit, validating, submitting }) => (
         <form className="flex flex-col" onSubmit={handleSubmit}>
         <form className="flex flex-col" onSubmit={handleSubmit}>
-          {fields.map(renderField)}
+          {formFields.map(renderField)}
           <Button isLoading={validating || submitting} className="self-end mb-2" colorScheme="green" type="submit">
           <Button isLoading={validating || submitting} className="self-end mb-2" colorScheme="green" type="submit">
             {initalValues ? 'Update' : 'Install'}
             {initalValues ? 'Update' : 'Install'}
           </Button>
           </Button>

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

@@ -1,10 +1,10 @@
 import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
 import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
 import React from 'react';
 import React from 'react';
-import { AppConfig } from '@runtipi/common';
 import InstallForm from './InstallForm';
 import InstallForm from './InstallForm';
+import { AppInfo } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
   isOpen: boolean;
   isOpen: boolean;
   onClose: () => void;
   onClose: () => void;
   onSubmit: (values: Record<string, any>) => void;
   onSubmit: (values: Record<string, any>) => void;

+ 2 - 2
packages/dashboard/src/modules/Apps/components/StopModal.tsx

@@ -1,9 +1,9 @@
 import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
 import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
 import React from 'react';
 import React from 'react';
-import { AppConfig } from '@runtipi/common';
+import { AppInfo } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
   isOpen: boolean;
   isOpen: boolean;
   onClose: () => void;
   onClose: () => void;
   onConfirm: () => void;
   onConfirm: () => void;

+ 2 - 2
packages/dashboard/src/modules/Apps/components/UninstallModal.tsx

@@ -1,9 +1,9 @@
 import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
 import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
 import React from 'react';
 import React from 'react';
-import { AppConfig } from '@runtipi/common';
+import { AppInfo } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
   isOpen: boolean;
   isOpen: boolean;
   onClose: () => void;
   onClose: () => void;
   onConfirm: () => void;
   onConfirm: () => void;

+ 2 - 2
packages/dashboard/src/modules/Apps/components/UpdateModal.tsx

@@ -2,11 +2,11 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve
 import React, { useEffect } from 'react';
 import React, { useEffect } from 'react';
 import useSWR from 'swr';
 import useSWR from 'swr';
 import fetcher from '../../../core/fetcher';
 import fetcher from '../../../core/fetcher';
-import { AppConfig } from '@runtipi/common';
 import InstallForm from './InstallForm';
 import InstallForm from './InstallForm';
+import { AppInfo } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  app: AppConfig;
+  app: AppInfo;
   isOpen: boolean;
   isOpen: boolean;
   onClose: () => void;
   onClose: () => void;
   onSubmit: (values: Record<string, any>) => void;
   onSubmit: (values: Record<string, any>) => void;

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

@@ -1,8 +1,6 @@
 import { SlideFade, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
 import { SlideFade, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
 import React from 'react';
 import React from 'react';
 import { FiExternalLink } from 'react-icons/fi';
 import { FiExternalLink } from 'react-icons/fi';
-import { AppConfig } from '@runtipi/common';
-import { useAppsStore } from '../../../state/appsStore';
 import { useSytemStore } from '../../../state/systemStore';
 import { useSytemStore } from '../../../state/systemStore';
 import AppActions from '../components/AppActions';
 import AppActions from '../components/AppActions';
 import InstallModal from '../components/InstallModal';
 import InstallModal from '../components/InstallModal';
@@ -11,19 +9,20 @@ import UninstallModal from '../components/UninstallModal';
 import UpdateModal from '../components/UpdateModal';
 import UpdateModal from '../components/UpdateModal';
 import AppLogo from '../../../components/AppLogo/AppLogo';
 import AppLogo from '../../../components/AppLogo/AppLogo';
 import Markdown from '../../../components/Markdown/Markdown';
 import Markdown from '../../../components/Markdown/Markdown';
+import { AppInfo, AppStatusEnum } from '../../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
-  app: AppConfig;
+  status?: AppStatusEnum;
+  info: AppInfo;
 }
 }
 
 
-const AppDetails: React.FC<IProps> = ({ app }) => {
+const AppDetails: React.FC<IProps> = ({ status, info }) => {
   const toast = useToast();
   const toast = useToast();
   const installDisclosure = useDisclosure();
   const installDisclosure = useDisclosure();
   const uninstallDisclosure = useDisclosure();
   const uninstallDisclosure = useDisclosure();
   const stopDisclosure = useDisclosure();
   const stopDisclosure = useDisclosure();
   const updateDisclosure = useDisclosure();
   const updateDisclosure = useDisclosure();
 
 
-  const { install, update, uninstall, stop, start, fetchApp } = useAppsStore();
   const { internalIp } = useSytemStore();
   const { internalIp } = useSytemStore();
 
 
   const handleError = (error: unknown) => {
   const handleError = (error: unknown) => {
@@ -35,14 +34,13 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
         position: 'top',
         position: 'top',
         isClosable: true,
         isClosable: true,
       });
       });
-      fetchApp(app.id);
     }
     }
   };
   };
 
 
   const handleInstallSubmit = async (values: Record<string, any>) => {
   const handleInstallSubmit = async (values: Record<string, any>) => {
     installDisclosure.onClose();
     installDisclosure.onClose();
     try {
     try {
-      await install(app.id, values);
+      await install(info.id, values);
     } catch (error) {
     } catch (error) {
       handleError(error);
       handleError(error);
     }
     }
@@ -51,7 +49,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
   const handleUnistallSubmit = async () => {
   const handleUnistallSubmit = async () => {
     uninstallDisclosure.onClose();
     uninstallDisclosure.onClose();
     try {
     try {
-      await uninstall(app.id);
+      await uninstall(info.id);
     } catch (error) {
     } catch (error) {
       handleError(error);
       handleError(error);
     }
     }
@@ -60,7 +58,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
   const handleStopSubmit = async () => {
   const handleStopSubmit = async () => {
     stopDisclosure.onClose();
     stopDisclosure.onClose();
     try {
     try {
-      await stop(app.id);
+      await stop(info.id);
     } catch (error) {
     } catch (error) {
       handleError(error);
       handleError(error);
     }
     }
@@ -68,7 +66,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
 
 
   const handleStartSubmit = async () => {
   const handleStartSubmit = async () => {
     try {
     try {
-      await start(app.id);
+      await start(info.id);
     } catch (e: unknown) {
     } catch (e: unknown) {
       handleError(e);
       handleError(e);
     }
     }
@@ -76,7 +74,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
 
 
   const handleUpdateSubmit = async (values: Record<string, any>) => {
   const handleUpdateSubmit = async (values: Record<string, any>) => {
     try {
     try {
-      await update(app.id, values);
+      await update(info.id, values);
       toast({
       toast({
         title: 'Success',
         title: 'Success',
         description: 'App config updated successfully',
         description: 'App config updated successfully',
@@ -90,27 +88,27 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
   };
   };
 
 
   const handleOpen = () => {
   const handleOpen = () => {
-    window.open(`http://${internalIp}:${app.port}${app.url_suffix || ''}`, '_blank', 'noreferrer');
+    window.open(`http://${internalIp}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
   };
   };
 
 
   return (
   return (
     <SlideFade in className="flex flex-1" offsetY="20px">
     <SlideFade in className="flex flex-1" offsetY="20px">
       <div className="flex flex-1  p-4 mt-3 rounded-lg flex-col">
       <div className="flex flex-1  p-4 mt-3 rounded-lg flex-col">
         <Flex className="flex-col md:flex-row">
         <Flex className="flex-col md:flex-row">
-          <AppLogo src={app?.image} size={180} className="self-center sm:self-auto" alt={app.name} />
+          <AppLogo src={info.image} size={180} className="self-center sm:self-auto" alt={info.name} />
           <VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
           <VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
             <div className="mt-3 items-center self-center flex flex-col sm:items-start sm:self-start md:mt-0">
             <div className="mt-3 items-center self-center flex flex-col sm:items-start sm:self-start md:mt-0">
-              <h1 className="font-bold text-2xl">{app?.name}</h1>
-              <h2 className="text-center md:text-left">{app?.short_desc}</h2>
-              {app.source && (
-                <a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={app?.source}>
+              <h1 className="font-bold text-2xl">{info.name}</h1>
+              <h2 className="text-center md:text-left">{info.short_desc}</h2>
+              {info.source && (
+                <a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.source}>
                   <Flex className="mt-2 items-center">
                   <Flex className="mt-2 items-center">
                     Source
                     Source
                     <FiExternalLink className="ml-1" />
                     <FiExternalLink className="ml-1" />
                   </Flex>
                   </Flex>
                 </a>
                 </a>
               )}
               )}
-              <p className="text-xs text-gray-600">By {app?.author}</p>
+              <p className="text-xs text-gray-600">By {info.author}</p>
             </div>
             </div>
             <div className="flex justify-center xs:absolute md:static top-0 right-5 self-center sm:self-auto">
             <div className="flex justify-center xs:absolute md:static top-0 right-5 self-center sm:self-auto">
               <AppActions
               <AppActions
@@ -120,17 +118,18 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
                 onStop={stopDisclosure.onOpen}
                 onStop={stopDisclosure.onOpen}
                 onUninstall={uninstallDisclosure.onOpen}
                 onUninstall={uninstallDisclosure.onOpen}
                 onInstall={installDisclosure.onOpen}
                 onInstall={installDisclosure.onOpen}
-                app={app}
+                app={info}
+                status={status}
               />
               />
             </div>
             </div>
           </VStack>
           </VStack>
         </Flex>
         </Flex>
         <Divider className="mt-5" />
         <Divider className="mt-5" />
-        <Markdown className="mt-3">{app?.description}</Markdown>
-        <InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.onClose} app={app} />
-        <UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.onClose} app={app} />
-        <StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.onClose} app={app} />
-        <UpdateModal onSubmit={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.onClose} app={app} />
+        <Markdown className="mt-3">{info.description}</Markdown>
+        <InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.onClose} app={info} />
+        <UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.onClose} app={info} />
+        <StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.onClose} app={info} />
+        <UpdateModal onSubmit={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.onClose} app={info} />
       </div>
       </div>
     </SlideFade>
     </SlideFade>
   );
   );

+ 8 - 5
packages/dashboard/src/modules/Auth/containers/Onboarding.tsx

@@ -1,12 +1,13 @@
 import { useToast } from '@chakra-ui/react';
 import { useToast } from '@chakra-ui/react';
-import React from 'react';
-import { useAuthStore } from '../../../state/authStore';
+import React, { useState } from 'react';
+import { useRegisterMutation } from '../../../generated/graphql';
 import AuthFormLayout from '../components/AuthFormLayout';
 import AuthFormLayout from '../components/AuthFormLayout';
 import RegisterForm from '../components/RegisterForm';
 import RegisterForm from '../components/RegisterForm';
 
 
 const Onboarding: React.FC = () => {
 const Onboarding: React.FC = () => {
   const toast = useToast();
   const toast = useToast();
-  const { me, register, loading } = useAuthStore();
+  const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
+  const [loading, setLoading] = useState(false);
 
 
   const handleError = (error: unknown) => {
   const handleError = (error: unknown) => {
     if (error instanceof Error) {
     if (error instanceof Error) {
@@ -22,10 +23,12 @@ const Onboarding: React.FC = () => {
 
 
   const handleRegister = async (values: { email: string; password: string }) => {
   const handleRegister = async (values: { email: string; password: string }) => {
     try {
     try {
-      await register(values.email, values.password);
-      await me();
+      setLoading(true);
+      await register({ variables: { input: { username: values.email, password: values.password } } });
     } catch (error) {
     } catch (error) {
       handleError(error);
       handleError(error);
+    } finally {
+      setLoading(false);
     }
     }
   };
   };
 
 

+ 8 - 18
packages/dashboard/src/modules/Dashboard/containers/Dashboard.tsx

@@ -1,27 +1,17 @@
 import { SimpleGrid, Text } from '@chakra-ui/react';
 import { SimpleGrid, Text } from '@chakra-ui/react';
-import React, { useEffect } from 'react';
+import React from 'react';
 import { BsCpu } from 'react-icons/bs';
 import { BsCpu } from 'react-icons/bs';
 import { FaMemory } from 'react-icons/fa';
 import { FaMemory } from 'react-icons/fa';
 import { FiHardDrive } from 'react-icons/fi';
 import { FiHardDrive } from 'react-icons/fi';
-import { useSytemStore } from '../../../state/systemStore';
+import { SystemInfoResponse } from '../../../generated/graphql';
 import SystemStat from '../components/SystemStat';
 import SystemStat from '../components/SystemStat';
 
 
-const Dashboard: React.FC = () => {
-  const { fetchDiskSpace, fetchCpuLoad, fetchMemoryLoad, disk, cpuLoad, memory } = useSytemStore();
+interface IProps {
+  data: SystemInfoResponse;
+}
 
 
-  useEffect(() => {
-    fetchDiskSpace();
-    fetchCpuLoad();
-    fetchMemoryLoad();
-
-    const interval = setInterval(() => {
-      fetchDiskSpace();
-      fetchCpuLoad();
-      fetchMemoryLoad();
-    }, 10000);
-
-    return () => clearInterval(interval);
-  }, [fetchCpuLoad, fetchDiskSpace, fetchMemoryLoad]);
+const Dashboard: React.FC<IProps> = ({ data }) => {
+  const { disk, memory, cpu } = data;
 
 
   // Convert bytes to GB
   // Convert bytes to GB
   const diskFree = Math.round(disk.available / 1024 / 1024 / 1024);
   const diskFree = Math.round(disk.available / 1024 / 1024 / 1024);
@@ -43,7 +33,7 @@ const Dashboard: React.FC = () => {
       </Text>
       </Text>
       <SimpleGrid className="mt-5" minChildWidth="180px" spacing="20px">
       <SimpleGrid className="mt-5" minChildWidth="180px" spacing="20px">
         <SystemStat title="Disk space" metric={`${diskUsed} GB`} subtitle={`Used out of ${diskSize} GB`} icon={FiHardDrive} progress={percentUsed} />
         <SystemStat title="Disk space" metric={`${diskUsed} GB`} subtitle={`Used out of ${diskSize} GB`} icon={FiHardDrive} progress={percentUsed} />
-        <SystemStat title="CPU Load" metric={`${cpuLoad.toFixed(2)}%`} subtitle="Uninstall apps if there is to much load" icon={BsCpu} progress={cpuLoad} />
+        <SystemStat title="CPU Load" metric={`${cpu.load.toFixed(2)}%`} subtitle="Uninstall apps if there is to much load" icon={BsCpu} progress={cpu.load} />
         <SystemStat title="Memory Used" metric={`${percentUsedMemory}%`} subtitle={`${memoryTotal} GB`} icon={FaMemory} progress={percentUsedMemory} />
         <SystemStat title="Memory Used" metric={`${percentUsedMemory}%`} subtitle={`${memoryTotal} GB`} icon={FaMemory} progress={percentUsedMemory} />
       </SimpleGrid>
       </SimpleGrid>
     </>
     </>

+ 5 - 11
packages/dashboard/src/pages/app-store/[id].tsx

@@ -1,29 +1,23 @@
 import type { NextPage } from 'next';
 import type { NextPage } from 'next';
-import { useEffect } from 'react';
 import Layout from '../../components/Layout';
 import Layout from '../../components/Layout';
-import { useAppsStore } from '../../state/appsStore';
 import AppDetails from '../../modules/Apps/containers/AppDetails';
 import AppDetails from '../../modules/Apps/containers/AppDetails';
+import { useGetAppQuery } from '../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
   appId: string;
   appId: string;
 }
 }
 
 
 const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
 const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
-  const { fetchApp, getApp } = useAppsStore((state) => state);
-  const app = getApp(appId);
-
-  useEffect(() => {
-    fetchApp(appId);
-  }, [appId, fetchApp]);
+  const { data, loading } = useGetAppQuery({ variables: { appId }, pollInterval: 5000 });
 
 
   const breadcrumb = [
   const breadcrumb = [
     { name: 'App Store', href: '/app-store' },
     { name: 'App Store', href: '/app-store' },
-    { name: app?.name || '', href: `/app-store/${appId}`, current: true },
+    { name: data?.getApp.info?.name || '', href: `/app-store/${appId}`, current: true },
   ];
   ];
 
 
   return (
   return (
-    <Layout breadcrumbs={breadcrumb} loading={!app}>
-      {app && <AppDetails app={app} />}
+    <Layout breadcrumbs={breadcrumb} loading={!data?.getApp && loading}>
+      {data?.getApp.info && <AppDetails status={data?.getApp.app?.status} info={data?.getApp.info} />}
     </Layout>
     </Layout>
   );
   );
 };
 };

+ 5 - 10
packages/dashboard/src/pages/app-store/index.tsx

@@ -1,20 +1,15 @@
-import React, { useEffect } from 'react';
+import React from 'react';
 import type { NextPage } from 'next';
 import type { NextPage } from 'next';
 import Layout from '../../components/Layout';
 import Layout from '../../components/Layout';
 import AppStoreContainer from '../../modules/AppStore/containers/AppStoreContainer';
 import AppStoreContainer from '../../modules/AppStore/containers/AppStoreContainer';
-import { useAppsStore } from '../../state/appsStore';
-import { RequestStatus } from '../../core/types';
+import { useListAppsQuery } from '../../generated/graphql';
 
 
 const Apps: NextPage = () => {
 const Apps: NextPage = () => {
-  const { fetch, status, apps } = useAppsStore((state) => state);
-
-  useEffect(() => {
-    fetch();
-  }, [fetch]);
+  const { loading, data } = useListAppsQuery();
 
 
   return (
   return (
-    <Layout loading={status === RequestStatus.LOADING && apps.length === 0}>
-      <AppStoreContainer />
+    <Layout loading={loading && !data}>
+      <AppStoreContainer apps={data?.listAppsInfo.apps || []} />
     </Layout>
     </Layout>
   );
   );
 };
 };

+ 5 - 11
packages/dashboard/src/pages/apps/[id].tsx

@@ -1,29 +1,23 @@
 import type { NextPage } from 'next';
 import type { NextPage } from 'next';
-import { useEffect } from 'react';
 import Layout from '../../components/Layout';
 import Layout from '../../components/Layout';
-import { useAppsStore } from '../../state/appsStore';
 import AppDetails from '../../modules/Apps/containers/AppDetails';
 import AppDetails from '../../modules/Apps/containers/AppDetails';
+import { useGetAppQuery } from '../../generated/graphql';
 
 
 interface IProps {
 interface IProps {
   appId: string;
   appId: string;
 }
 }
 
 
 const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
 const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
-  const { fetchApp, getApp } = useAppsStore((state) => state);
-  const app = getApp(appId);
-
-  useEffect(() => {
-    fetchApp(appId);
-  }, [appId, fetchApp]);
+  const { data, loading } = useGetAppQuery({ variables: { appId }, pollInterval: 5000 });
 
 
   const breadcrumb = [
   const breadcrumb = [
     { name: 'Apps', href: '/apps' },
     { name: 'Apps', href: '/apps' },
-    { name: app?.name || '', href: `/apps/${appId}`, current: true },
+    { name: data?.getApp.info.name || '', href: `/apps/${appId}`, current: true },
   ];
   ];
 
 
   return (
   return (
-    <Layout breadcrumbs={breadcrumb} loading={!app}>
-      {app && <AppDetails app={app} />}
+    <Layout breadcrumbs={breadcrumb} loading={!data?.getApp.app && loading}>
+      {data?.getApp.info && <AppDetails status={data?.getApp.app?.status} info={data.getApp.info} />}
     </Layout>
     </Layout>
   );
   );
 };
 };

+ 3 - 5
packages/dashboard/src/pages/index.tsx

@@ -1,13 +1,11 @@
 import type { NextPage } from 'next';
 import type { NextPage } from 'next';
 import Layout from '../components/Layout';
 import Layout from '../components/Layout';
+import { useSystemInfoQuery } from '../generated/graphql';
 import Dashboard from '../modules/Dashboard/containers/Dashboard';
 import Dashboard from '../modules/Dashboard/containers/Dashboard';
 
 
 const Home: NextPage = () => {
 const Home: NextPage = () => {
-  return (
-    <Layout>
-      <Dashboard />
-    </Layout>
-  );
+  const { data, loading } = useSystemInfoQuery({ pollInterval: 10000 });
+  return <Layout loading={loading && !data}>{data?.systemInfo && <Dashboard data={data.systemInfo} />}</Layout>;
 };
 };
 
 
 export default Home;
 export default Home;

+ 0 - 118
packages/dashboard/src/state/appsStore.ts

@@ -1,118 +0,0 @@
-import produce from 'immer';
-import create, { SetState } from 'zustand';
-import api from '../core/api';
-import { AppConfig, AppStatusEnum } from '@runtipi/common';
-import { RequestStatus } from '../core/types';
-
-type AppsStore = {
-  apps: AppConfig[];
-  status: RequestStatus;
-  fetch: () => void;
-  getApp: (id: string) => AppConfig | undefined;
-  fetchApp: (id: string) => void;
-  install: (id: string, form: Record<string, string>) => Promise<void>;
-  update: (id: string, form: Record<string, string>) => Promise<void>;
-  uninstall: (id: string) => Promise<void>;
-  stop: (id: string) => Promise<void>;
-  start: (id: string) => Promise<void>;
-};
-
-type Set = SetState<AppsStore>;
-
-const sortApps = (a: AppConfig, b: AppConfig) => a.name.localeCompare(b.name);
-
-const setAppStatus = (appId: string, status: AppStatusEnum, set: Set) => {
-  set((state) => {
-    return produce(state, (draft) => {
-      const app = draft.apps.find((a) => a.id === appId);
-      if (app) app.status = status;
-    });
-  });
-};
-
-/**
- * Fetch one app and add it to the list of apps.
- * @param appId
- * @param set
- */
-const fetchApp = async (appId: string, set: Set) => {
-  const response = await api.fetch<AppConfig>({
-    endpoint: `/apps/info/${appId}`,
-    method: 'get',
-  });
-
-  set((state) => {
-    const apps = state.apps.filter((app) => app.id !== appId);
-    apps.push(response);
-
-    return { ...state, apps: apps.sort(sortApps) };
-  });
-};
-
-export const useAppsStore = create<AppsStore>((set, get) => ({
-  apps: [],
-  status: RequestStatus.LOADING,
-  fetchApp: async (appId: string) => fetchApp(appId, set),
-  fetch: async () => {
-    set({ status: RequestStatus.LOADING });
-
-    const response = await api.fetch<AppConfig[]>({
-      endpoint: '/apps/list',
-      method: 'get',
-    });
-
-    const apps = response.sort(sortApps);
-
-    set({ apps, status: RequestStatus.SUCCESS });
-  },
-  getApp: (appId: string) => {
-    return get().apps.find((app) => app.id === appId);
-  },
-  install: async (appId: string, form?: Record<string, string>) => {
-    setAppStatus(appId, AppStatusEnum.INSTALLING, set);
-
-    await api.fetch({
-      endpoint: `/apps/install/${appId}`,
-      method: 'POST',
-      data: { form },
-    });
-
-    await get().fetchApp(appId);
-  },
-  update: async (appId: string, form?: Record<string, string>) => {
-    await api.fetch({
-      endpoint: `/apps/update/${appId}`,
-      method: 'POST',
-      data: { form },
-    });
-
-    await get().fetchApp(appId);
-  },
-  uninstall: async (appId: string) => {
-    setAppStatus(appId, AppStatusEnum.UNINSTALLING, set);
-
-    await api.fetch({
-      endpoint: `/apps/uninstall/${appId}`,
-    });
-
-    await get().fetchApp(appId);
-  },
-  stop: async (appId: string) => {
-    setAppStatus(appId, AppStatusEnum.STOPPING, set);
-
-    await api.fetch({
-      endpoint: `/apps/stop/${appId}`,
-    });
-
-    await get().fetchApp(appId);
-  },
-  start: async (appId: string) => {
-    setAppStatus(appId, AppStatusEnum.STARTING, set);
-
-    await api.fetch({
-      endpoint: `/apps/start/${appId}`,
-    });
-
-    await get().fetchApp(appId);
-  },
-}));

+ 11 - 2
packages/system-api/src/modules/apps/app.entity.ts

@@ -1,7 +1,12 @@
 import { AppStatusEnum } from '@runtipi/common';
 import { AppStatusEnum } from '@runtipi/common';
-import { Field, ObjectType } from 'type-graphql';
+import { GraphQLJSONObject } from 'graphql-type-json';
+import { Field, ObjectType, registerEnumType } from 'type-graphql';
 import { BaseEntity, Column, CreateDateColumn, Entity, UpdateDateColumn } from 'typeorm';
 import { BaseEntity, Column, CreateDateColumn, Entity, UpdateDateColumn } from 'typeorm';
 
 
+registerEnumType(AppStatusEnum, {
+  name: 'AppStatusEnum',
+});
+
 @ObjectType()
 @ObjectType()
 @Entity()
 @Entity()
 class App extends BaseEntity {
 class App extends BaseEntity {
@@ -9,7 +14,7 @@ class App extends BaseEntity {
   @Column({ type: 'varchar', primary: true, unique: true })
   @Column({ type: 'varchar', primary: true, unique: true })
   id!: string;
   id!: string;
 
 
-  @Field(() => String)
+  @Field(() => AppStatusEnum)
   @Column({ type: 'enum', enum: AppStatusEnum, default: AppStatusEnum.STOPPED, nullable: false })
   @Column({ type: 'enum', enum: AppStatusEnum, default: AppStatusEnum.STOPPED, nullable: false })
   status!: AppStatusEnum;
   status!: AppStatusEnum;
 
 
@@ -21,6 +26,10 @@ class App extends BaseEntity {
   @Column({ type: 'integer', default: 0, nullable: false })
   @Column({ type: 'integer', default: 0, nullable: false })
   numOpened!: number;
   numOpened!: number;
 
 
+  @Field(() => GraphQLJSONObject)
+  @Column({ type: 'jsonb', nullable: false })
+  config!: Record<string, string>;
+
   @Field(() => Date)
   @Field(() => Date)
   @CreateDateColumn()
   @CreateDateColumn()
   createdAt!: Date;
   createdAt!: Date;

+ 0 - 139
packages/system-api/src/modules/apps/apps.controller.ts

@@ -1,139 +0,0 @@
-import { NextFunction, Request, Response } from 'express';
-import { AppConfig } from '@runtipi/common';
-import AppsService from './apps.service';
-import { getInitalFormValues } from './apps.helpers';
-
-const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {
-  try {
-    const { id: appName } = req.params;
-
-    if (!appName) {
-      throw new Error('App name is required');
-    }
-
-    await AppsService.uninstallApp(appName);
-
-    res.status(200).json({ message: 'App uninstalled successfully' });
-  } catch (e) {
-    next(e);
-  }
-};
-
-const stopApp = async (req: Request, res: Response, next: NextFunction) => {
-  try {
-    const { id: appName } = req.params;
-
-    if (!appName) {
-      throw new Error('App name is required');
-    }
-
-    await AppsService.stopApp(appName);
-
-    res.status(200).json({ message: 'App stopped successfully' });
-  } catch (e) {
-    next(e);
-  }
-};
-
-const updateAppConfig = async (req: Request, res: Response, next: NextFunction) => {
-  try {
-    const { id: appName } = req.params;
-    const { form } = req.body;
-
-    if (!appName) {
-      throw new Error('App name is required');
-    }
-
-    AppsService.updateAppConfig(appName, form);
-
-    res.status(200).json({ message: 'App updated successfully' });
-  } catch (e) {
-    next(e);
-  }
-};
-
-const getAppInfo = async (req: Request, res: Response<AppConfig>, next: NextFunction) => {
-  try {
-    const { id } = req.params;
-
-    if (!id) {
-      throw new Error('App name is required');
-    }
-
-    const appInfo = await AppsService.getAppInfo(id);
-
-    res.status(200).json(appInfo);
-  } catch (e) {
-    next(e);
-  }
-};
-
-const listApps = async (_req: Request, res: Response, next: NextFunction) => {
-  try {
-    const apps = await AppsService.listApps();
-
-    res.status(200).json(apps);
-  } catch (e) {
-    next(e);
-  }
-};
-
-const startApp = async (req: Request, res: Response, next: NextFunction) => {
-  try {
-    const { id } = req.params;
-
-    if (!id) {
-      throw new Error('App name is required');
-    }
-
-    await AppsService.startApp(id);
-
-    res.status(200).json({ message: 'App started successfully' });
-  } catch (e) {
-    next(e);
-  }
-};
-
-const installApp = async (req: Request, res: Response, next: NextFunction) => {
-  try {
-    const { id } = req.params;
-    const { form } = req.body;
-
-    if (!id) {
-      throw new Error('App name is required');
-    }
-
-    await AppsService.installApp(id, form);
-
-    res.status(200).json({ message: 'App installed successfully' });
-  } catch (e) {
-    next(e);
-  }
-};
-
-const initalFormValues = (req: Request, res: Response, next: NextFunction) => {
-  try {
-    const { id } = req.params;
-
-    if (!id) {
-      throw new Error('App name is required');
-    }
-
-    res.status(200).json(getInitalFormValues(id));
-  } catch (e) {
-    next(e);
-  }
-};
-
-const AppController = {
-  uninstallApp,
-  installApp,
-  stopApp,
-  updateAppConfig,
-  getAppInfo,
-  listApps,
-  startApp,
-  initalFormValues,
-};
-
-export default AppController;

+ 0 - 17
packages/system-api/src/modules/apps/apps.helpers.ts

@@ -50,23 +50,6 @@ export const checkEnvFile = (appName: string) => {
   });
   });
 };
 };
 
 
-export const getInitalFormValues = (appName: string): Record<string, string> => {
-  const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
-  const envMap = getEnvMap(appName);
-  const formValues: Record<string, string> = {};
-
-  configFile.form_fields.forEach((field) => {
-    const envVar = field.env_variable;
-    const envVarValue = envMap.get(envVar);
-
-    if (envVarValue) {
-      formValues[field.env_variable] = envVarValue;
-    }
-  });
-
-  return formValues;
-};
-
 export const checkAppExists = (appName: string) => {
 export const checkAppExists = (appName: string) => {
   const appExists = fileExists(`/app-data/${appName}`);
   const appExists = fileExists(`/app-data/${appName}`);
 
 

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

@@ -1,6 +1,6 @@
 import { Arg, Authorized, Mutation, Query, Resolver } from 'type-graphql';
 import { Arg, Authorized, Mutation, Query, Resolver } from 'type-graphql';
 import AppsService from './apps.service';
 import AppsService from './apps.service';
-import { AppConfig, AppInputType, ListAppsResonse } from './apps.types';
+import { AppInputType, AppResponse, ListAppsResonse } from './apps.types';
 import App from './app.entity';
 import App from './app.entity';
 
 
 @Resolver()
 @Resolver()
@@ -10,9 +10,9 @@ export default class AppsResolver {
     return AppsService.listApps();
     return AppsService.listApps();
   }
   }
 
 
-  @Query(() => AppConfig)
-  getAppInfo(@Arg('id', () => String) appId: string): Promise<AppConfig> {
-    return AppsService.getAppInfo(appId);
+  @Query(() => AppResponse)
+  getApp(@Arg('id', () => String) id: string): Promise<AppResponse> {
+    return AppsService.getApp(id);
   }
   }
 
 
   @Authorized()
   @Authorized()

+ 0 - 15
packages/system-api/src/modules/apps/apps.routes.ts

@@ -1,15 +0,0 @@
-import { Router } from 'express';
-import AppController from './apps.controller';
-
-const router = Router();
-
-router.route('/install/:id').post(AppController.installApp);
-router.route('/update/:id').post(AppController.updateAppConfig);
-router.route('/uninstall/:id').get(AppController.uninstallApp);
-router.route('/stop/:id').get(AppController.stopApp);
-router.route('/start/:id').get(AppController.startApp);
-router.route('/list').get(AppController.listApps);
-router.route('/info/:id').get(AppController.getAppInfo);
-router.route('/form/:id').get(AppController.initalFormValues);
-
-export default router;

+ 21 - 16
packages/system-api/src/modules/apps/apps.service.ts

@@ -1,8 +1,7 @@
-import si from 'systeminformation';
 import { AppStatusEnum } from '@runtipi/common';
 import { AppStatusEnum } from '@runtipi/common';
 import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
 import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
-import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
-import { AppConfig, ListAppsResonse } from './apps.types';
+import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, getStateFile, runAppScript } from './apps.helpers';
+import { AppInfo, AppResponse, ListAppsResonse } from './apps.types';
 import App from './app.entity';
 import App from './app.entity';
 
 
 const startApp = async (appName: string): Promise<App> => {
 const startApp = async (appName: string): Promise<App> => {
@@ -15,8 +14,7 @@ const startApp = async (appName: string): Promise<App> => {
   checkEnvFile(appName);
   checkEnvFile(appName);
 
 
   // Regenerate env file
   // Regenerate env file
-  const form = getInitalFormValues(appName);
-  generateEnvFile(appName, form);
+  generateEnvFile(appName, app.config);
 
 
   await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
   await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
   // Run script
   // Run script
@@ -46,7 +44,7 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
     // Create env file
     // Create env file
     generateEnvFile(id, form);
     generateEnvFile(id, form);
 
 
-    await App.create({ id, status: AppStatusEnum.INSTALLING }).save();
+    app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form }).save();
 
 
     // Run script
     // Run script
     await runAppScript(['install', id]);
     await runAppScript(['install', id]);
@@ -58,7 +56,7 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
 };
 };
 
 
 const listApps = async (): Promise<ListAppsResonse> => {
 const listApps = async (): Promise<ListAppsResonse> => {
-  const apps: AppConfig[] = getAvailableApps()
+  const apps: AppInfo[] = getAvailableApps()
     .map((app) => {
     .map((app) => {
       try {
       try {
         return readJsonFile(`/apps/${app}/config.json`);
         return readJsonFile(`/apps/${app}/config.json`);
@@ -68,41 +66,39 @@ const listApps = async (): Promise<ListAppsResonse> => {
     })
     })
     .filter(Boolean);
     .filter(Boolean);
 
 
-  const dockerContainers = await si.dockerContainers();
-
   const state = getStateFile();
   const state = getStateFile();
   const installed: string[] = state.installed.split(' ').filter(Boolean);
   const installed: string[] = state.installed.split(' ').filter(Boolean);
 
 
   apps.forEach((app) => {
   apps.forEach((app) => {
     app.installed = installed.includes(app.id);
     app.installed = installed.includes(app.id);
-    app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
     app.description = readFile(`/apps/${app.id}/metadata/description.md`);
     app.description = readFile(`/apps/${app.id}/metadata/description.md`);
   });
   });
 
 
   return { apps, total: apps.length };
   return { apps, total: apps.length };
 };
 };
 
 
-const getAppInfo = async (id: string): Promise<AppConfig> => {
-  const dockerContainers = await si.dockerContainers();
-  const configFile: AppConfig = readJsonFile(`/apps/${id}/config.json`);
+const getAppInfo = (id: string): AppInfo => {
+  const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
 
 
   const state = getStateFile();
   const state = getStateFile();
   const installed: string[] = state.installed.split(' ').filter(Boolean);
   const installed: string[] = state.installed.split(' ').filter(Boolean);
   configFile.installed = installed.includes(id);
   configFile.installed = installed.includes(id);
-  configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
   configFile.description = readFile(`/apps/${id}/metadata/description.md`);
   configFile.description = readFile(`/apps/${id}/metadata/description.md`);
 
 
   return configFile;
   return configFile;
 };
 };
 
 
 const updateAppConfig = async (id: string, form: Record<string, string>): Promise<App> => {
 const updateAppConfig = async (id: string, form: Record<string, string>): Promise<App> => {
-  const app = await App.findOne({ where: { id } });
+  let app = await App.findOne({ where: { id } });
 
 
   if (!app) {
   if (!app) {
     throw new Error(`App ${id} not found`);
     throw new Error(`App ${id} not found`);
   }
   }
 
 
   generateEnvFile(id, form);
   generateEnvFile(id, form);
+  await App.update({ id }, { config: form });
+
+  app = (await App.findOne({ where: { id } })) as App;
 
 
   return app;
   return app;
 };
 };
@@ -136,4 +132,13 @@ const uninstallApp = async (id: string): Promise<boolean> => {
   return true;
   return true;
 };
 };
 
 
-export default { installApp, startApp, listApps, getAppInfo, updateAppConfig, stopApp, uninstallApp };
+const getApp = async (id: string): Promise<AppResponse> => {
+  const app = await App.findOne({ where: { id } });
+
+  return {
+    info: getAppInfo(id),
+    app,
+  };
+};
+
+export default { installApp, startApp, listApps, getApp, updateAppConfig, stopApp, uninstallApp };

+ 26 - 11
packages/system-api/src/modules/apps/apps.types.ts

@@ -1,10 +1,19 @@
-import { AppCategoriesEnum, AppStatusEnum, FieldTypes } from '@runtipi/common';
-import { Field, InputType, ObjectType } from 'type-graphql';
+import { AppCategoriesEnum, FieldTypes } from '@runtipi/common';
+import { Field, InputType, ObjectType, registerEnumType } from 'type-graphql';
 import { GraphQLJSONObject } from 'graphql-type-json';
 import { GraphQLJSONObject } from 'graphql-type-json';
+import App from './app.entity';
+
+registerEnumType(AppCategoriesEnum, {
+  name: 'AppCategoriesEnum',
+});
+
+registerEnumType(FieldTypes, {
+  name: 'FieldTypesEnum',
+});
 
 
 @ObjectType()
 @ObjectType()
 class FormField {
 class FormField {
-  @Field(() => String)
+  @Field(() => FieldTypes)
   type!: FieldTypes;
   type!: FieldTypes;
 
 
   @Field(() => String)
   @Field(() => String)
@@ -27,7 +36,7 @@ class FormField {
 }
 }
 
 
 @ObjectType()
 @ObjectType()
-class AppConfig {
+class AppInfo {
   @Field(() => String)
   @Field(() => String)
   id!: string;
   id!: string;
 
 
@@ -61,12 +70,9 @@ class AppConfig {
   @Field(() => Boolean)
   @Field(() => Boolean)
   installed!: boolean;
   installed!: boolean;
 
 
-  @Field(() => [String])
+  @Field(() => [AppCategoriesEnum])
   categories!: AppCategoriesEnum[];
   categories!: AppCategoriesEnum[];
 
 
-  @Field(() => String)
-  status!: AppStatusEnum;
-
   @Field(() => String, { nullable: true })
   @Field(() => String, { nullable: true })
   url_suffix?: string;
   url_suffix?: string;
 
 
@@ -76,13 +82,22 @@ class AppConfig {
 
 
 @ObjectType()
 @ObjectType()
 class ListAppsResonse {
 class ListAppsResonse {
-  @Field(() => [AppConfig])
-  apps!: AppConfig[];
+  @Field(() => [AppInfo])
+  apps!: AppInfo[];
 
 
   @Field(() => Number)
   @Field(() => Number)
   total!: number;
   total!: number;
 }
 }
 
 
+@ObjectType()
+class AppResponse {
+  @Field(() => App, { nullable: true })
+  app!: App | null;
+
+  @Field(() => AppInfo)
+  info!: AppInfo;
+}
+
 @InputType()
 @InputType()
 class AppInputType {
 class AppInputType {
   @Field(() => String)
   @Field(() => String)
@@ -92,4 +107,4 @@ class AppInputType {
   form!: Record<string, string>;
   form!: Record<string, string>;
 }
 }
 
 
-export { ListAppsResonse, AppConfig, AppInputType };
+export { ListAppsResonse, AppInfo, AppInputType, AppResponse };

+ 1 - 2
packages/system-api/src/modules/auth/auth.resolver.ts

@@ -1,4 +1,4 @@
-import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql';
+import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql';
 import { MyContext } from '../../types';
 import { MyContext } from '../../types';
 import { UsernamePasswordInput, UserResponse } from './auth.types';
 import { UsernamePasswordInput, UserResponse } from './auth.types';
 
 
@@ -36,7 +36,6 @@ export default class AuthResolver {
     return { user };
     return { user };
   }
   }
 
 
-  @Authorized()
   @Mutation(() => Boolean)
   @Mutation(() => Boolean)
   logout(@Ctx() { req }: MyContext): boolean {
   logout(@Ctx() { req }: MyContext): boolean {
     req.session.userId = undefined;
     req.session.userId = undefined;

+ 6 - 0
packages/system-api/src/modules/auth/auth.service.ts

@@ -27,6 +27,12 @@ const register = async (input: UsernamePasswordInput): Promise<UserResponse> =>
     throw new Error('Missing email or password');
     throw new Error('Missing email or password');
   }
   }
 
 
+  const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
+
+  if (user) {
+    throw new Error('User already exists');
+  }
+
   const hash = await argon2.hash(password);
   const hash = await argon2.hash(password);
   const newUser = await User.create({ username: username.trim().toLowerCase(), password: hash }).save();
   const newUser = await User.create({ username: username.trim().toLowerCase(), password: hash }).save();
 
 

+ 0 - 27
packages/system-api/src/modules/network/network.controller.ts

@@ -1,27 +0,0 @@
-import { Request, Response } from 'express';
-import publicIp from 'public-ip';
-import portScanner from 'node-port-scanner';
-import internalIp from 'internal-ip';
-
-const isPortOpen = async (req: Request, res: Response<boolean>) => {
-  const { port } = req.params;
-
-  const host = await publicIp.v4();
-
-  const isOpen = await portScanner(host, [port]);
-
-  res.status(200).send(isOpen);
-};
-
-const getInternalIp = async (_req: Request, res: Response<string>) => {
-  const ip = await internalIp.v4();
-
-  res.status(200).send(ip);
-};
-
-const NetworkController = {
-  isPortOpen,
-  getInternalIp,
-};
-
-export default NetworkController;

+ 0 - 8
packages/system-api/src/modules/network/network.routes.ts

@@ -1,8 +0,0 @@
-import { Router } from 'express';
-import NetworkController from './network.controller';
-
-const router = Router();
-
-router.route('/internal-ip').get(NetworkController.getInternalIp);
-
-export default router;

+ 0 - 87
packages/system-api/src/modules/system/system.controller.ts

@@ -1,87 +0,0 @@
-import axios from 'axios';
-import { Request, Response } from 'express';
-import config from '../../config';
-import TipiCache from '../../config/TipiCache';
-import { readJsonFile } from '../fs/fs.helpers';
-
-type CpuData = {
-  load: number;
-};
-
-type DiskData = {
-  total: number;
-  used: number;
-  available: number;
-};
-
-type MemoryData = {
-  total: number;
-  available: number;
-  used: number;
-};
-
-type SystemInfo = {
-  cpu: CpuData;
-  disk: DiskData;
-  memory: MemoryData;
-};
-
-/**
- *
- * @param req
- * @param res
- */
-const getCpuInfo = async (_req: Request, res: Response<CpuData>) => {
-  const systemInfo: SystemInfo = readJsonFile('/state/system-info.json');
-
-  const cpu = systemInfo.cpu;
-
-  res.status(200).send({ load: cpu.load });
-};
-
-/**
- *
- * @param req
- * @param res
- */
-const getDiskInfo = async (_req: Request, res: Response<DiskData>) => {
-  const systemInfo: SystemInfo = readJsonFile('/state/system-info.json');
-
-  const result: DiskData = systemInfo.disk;
-
-  res.status(200).send(result);
-};
-
-/**
- *
- * @param req
- * @param res
- */
-const getMemoryInfo = async (_req: Request, res: Response<MemoryData>) => {
-  const systemInfo: SystemInfo = readJsonFile('/state/system-info.json');
-
-  const result: MemoryData = systemInfo.memory;
-
-  res.status(200).json(result);
-};
-
-const getVersion = async (_req: Request, res: Response<{ current: string; latest?: string }>) => {
-  try {
-    let version = TipiCache.get<string>('latestVersion');
-
-    if (!version) {
-      const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
-
-      TipiCache.set('latestVersion', data.name);
-      version = data.name.replace('v', '');
-    }
-
-    TipiCache.set('latestVersion', version?.replace('v', ''));
-
-    res.status(200).send({ current: config.VERSION, latest: version?.replace('v', '') });
-  } catch (e) {
-    res.status(500).send({ current: config.VERSION, latest: undefined });
-  }
-};
-
-export default { getCpuInfo, getDiskInfo, getMemoryInfo, getVersion };

+ 16 - 0
packages/system-api/src/modules/system/system.resolver.ts

@@ -0,0 +1,16 @@
+import { Query, Resolver } from 'type-graphql';
+import SystemService from './system.service';
+import { SystemInfoResponse, VersionResponse } from './system.types';
+
+@Resolver()
+export default class AuthResolver {
+  @Query(() => SystemInfoResponse, { nullable: true })
+  async systemInfo(): Promise<SystemInfoResponse> {
+    return SystemService.systemInfo();
+  }
+
+  @Query(() => String)
+  async version(): Promise<VersionResponse> {
+    return SystemService.getVersion();
+  }
+}

+ 0 - 11
packages/system-api/src/modules/system/system.routes.ts

@@ -1,11 +0,0 @@
-import { Router } from 'express';
-import SystemController from './system.controller';
-
-const router = Router();
-
-router.route('/cpu').get(SystemController.getCpuInfo);
-router.route('/disk').get(SystemController.getDiskInfo);
-router.route('/memory').get(SystemController.getMemoryInfo);
-router.route('/version').get(SystemController.getVersion);
-
-export default router;

+ 52 - 0
packages/system-api/src/modules/system/system.service.ts

@@ -0,0 +1,52 @@
+import axios from 'axios';
+import config from '../../config';
+import TipiCache from '../../config/TipiCache';
+import { readJsonFile } from '../fs/fs.helpers';
+
+type SystemInfo = {
+  cpu: {
+    load: number;
+  };
+  disk: {
+    total: number;
+    used: number;
+    available: number;
+  };
+  memory: {
+    total: number;
+    available: number;
+    used: number;
+  };
+};
+
+const systemInfo = (): SystemInfo => {
+  const info: SystemInfo = readJsonFile('/state/system-info.json');
+
+  return info;
+};
+
+const getVersion = async (): Promise<{ current: string; latest?: string }> => {
+  try {
+    let version = TipiCache.get<string>('latestVersion');
+
+    if (!version) {
+      const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
+
+      TipiCache.set('latestVersion', data.name);
+      version = data.name.replace('v', '');
+    }
+
+    TipiCache.set('latestVersion', version?.replace('v', ''));
+
+    return { current: config.VERSION, latest: version?.replace('v', '') };
+  } catch (e) {
+    return { current: config.VERSION, latest: undefined };
+  }
+};
+
+const SystemService = {
+  systemInfo,
+  getVersion,
+};
+
+export default SystemService;

+ 42 - 0
packages/system-api/src/modules/system/system.types.ts

@@ -0,0 +1,42 @@
+import { Field, ObjectType } from 'type-graphql';
+
+@ObjectType()
+class Cpu {
+  @Field(() => Number, { nullable: false })
+  load!: number;
+}
+
+@ObjectType()
+class DiskMemory {
+  @Field(() => Number, { nullable: false })
+  total!: number;
+
+  @Field(() => Number, { nullable: false })
+  used!: number;
+
+  @Field(() => Number, { nullable: false })
+  available!: number;
+}
+
+@ObjectType()
+class SystemInfoResponse {
+  @Field(() => Cpu, { nullable: false })
+  cpu!: Cpu;
+
+  @Field(() => DiskMemory, { nullable: false })
+  disk!: DiskMemory;
+
+  @Field(() => DiskMemory, { nullable: false })
+  memory!: DiskMemory;
+}
+
+@ObjectType()
+class VersionResponse {
+  @Field(() => String, { nullable: false })
+  current!: string;
+
+  @Field(() => String, { nullable: true })
+  latest?: string;
+}
+
+export { SystemInfoResponse, VersionResponse };

+ 2 - 1
packages/system-api/src/schema.ts

@@ -3,10 +3,11 @@ import { buildSchema } from 'type-graphql';
 import { customAuthChecker } from './core/middlewares/authChecker';
 import { customAuthChecker } from './core/middlewares/authChecker';
 import AppsResolver from './modules/apps/apps.resolver';
 import AppsResolver from './modules/apps/apps.resolver';
 import AuthResolver from './modules/auth/auth.resolver';
 import AuthResolver from './modules/auth/auth.resolver';
+import SystemResolver from './modules/system/system.resolver';
 
 
 const createSchema = (): Promise<GraphQLSchema> =>
 const createSchema = (): Promise<GraphQLSchema> =>
   buildSchema({
   buildSchema({
-    resolvers: [AppsResolver, AuthResolver],
+    resolvers: [AppsResolver, AuthResolver, SystemResolver],
     validate: true,
     validate: true,
     authChecker: customAuthChecker,
     authChecker: customAuthChecker,
   });
   });