diff --git a/packages/dashboard/server.ts b/packages/dashboard/server.ts new file mode 100644 index 00000000..bbe83368 --- /dev/null +++ b/packages/dashboard/server.ts @@ -0,0 +1,39 @@ +/* eslint-disable consistent-return */ +import { createServer } from 'http'; +import { parse } from 'url'; +import next from 'next'; +import { EventDispatcher } from './src/server/core/EventDispatcher'; +import { getConfig, setConfig } from './src/server/core/TipiConfig'; +import { Logger } from './src/server/core/Logger'; +import { runPostgresMigrations } from './run-migration'; +import { AppServiceClass } from './src/server/services/apps/apps.service'; +import { prisma } from './src/server/db/client'; + +const port = parseInt(process.env.PORT || '3000', 10); +const dev = process.env.NODE_ENV !== 'production'; +const nextApp = next({ dev }); +const handle = nextApp.getRequestHandler(); + +nextApp.prepare().then(async () => { + createServer(async (req, res) => { + const parsedUrl = parse(req.url!, true); + + handle(req, res, parsedUrl); + }).listen(port); + + const appService = new AppServiceClass(prisma); + EventDispatcher.clear(); + + // Run database migrations + await runPostgresMigrations(); + + // startJobs(); + setConfig('status', 'RUNNING'); + + await EventDispatcher.dispatchEventAsync('clone_repo', [getConfig().appsRepoUrl]); + await EventDispatcher.dispatchEventAsync('update_repo', [getConfig().appsRepoUrl]); + + appService.startAllApps(); + + Logger.info(`> Server listening at http://localhost:${port} as ${dev ? 'development' : process.env.NODE_ENV}`); +}); diff --git a/packages/dashboard/src/client/components/AppStatus/AppStatus.tsx b/packages/dashboard/src/client/components/AppStatus/AppStatus.tsx index 228ea8e5..d687a9bc 100644 --- a/packages/dashboard/src/client/components/AppStatus/AppStatus.tsx +++ b/packages/dashboard/src/client/components/AppStatus/AppStatus.tsx @@ -1,14 +1,14 @@ import clsx from 'clsx'; import React from 'react'; +import * as AppTypes from '../../core/types'; import styles from './AppStatus.module.scss'; -import { AppStatusEnum } from '../../generated/graphql'; -export const AppStatus: React.FC<{ status: AppStatusEnum; lite?: boolean }> = ({ status, lite }) => { +export const AppStatus: React.FC<{ status: AppTypes.AppStatus; lite?: boolean }> = ({ status, lite }) => { const formattedStatus = `${status[0]}${status.substring(1, status.length).toLowerCase()}`; const classes = clsx('status-dot status-gray', { - 'status-dot-animated status-green': status === AppStatusEnum.Running, - 'status-red': status === AppStatusEnum.Stopped, + 'status-dot-animated status-green': status === 'running', + 'status-red': status === 'stopped', }); return ( diff --git a/packages/dashboard/src/client/components/AppTile/AppTile.tsx b/packages/dashboard/src/client/components/AppTile/AppTile.tsx index 53574c95..348bf824 100644 --- a/packages/dashboard/src/client/components/AppTile/AppTile.tsx +++ b/packages/dashboard/src/client/components/AppTile/AppTile.tsx @@ -4,8 +4,8 @@ import { IconDownload } from '@tabler/icons'; import { AppStatus } from '../AppStatus'; import { AppLogo } from '../AppLogo/AppLogo'; import { limitText } from '../../modules/AppStore/helpers/table.helpers'; -import { AppInfo, AppStatusEnum } from '../../generated/graphql'; import styles from './AppTile.module.scss'; +import { AppInfo, AppStatus as AppStatusEnum } from '../../core/types'; type AppTileInfo = Pick; diff --git a/packages/dashboard/src/client/generated/graphql.tsx b/packages/dashboard/src/client/generated/graphql.tsx deleted file mode 100644 index 411316cf..00000000 --- a/packages/dashboard/src/client/generated/graphql.tsx +++ /dev/null @@ -1,681 +0,0 @@ -import { gql } from '@apollo/client'; -import * as Apollo from '@apollo/client'; - -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -const defaultOptions = {} as const; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: string; - String: string; - Boolean: boolean; - Int: number; - Float: number; - /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ - DateTime: any; - /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ - JSONObject: any; -}; - -export type App = { - __typename?: 'App'; - config: Scalars['JSONObject']; - createdAt: Scalars['DateTime']; - domain?: Maybe; - exposed: Scalars['Boolean']; - id: Scalars['String']; - info?: Maybe; - lastOpened: Scalars['DateTime']; - numOpened: Scalars['Float']; - status: AppStatusEnum; - updateInfo?: Maybe; - updatedAt: Scalars['DateTime']; - version?: Maybe; -}; - -export enum AppCategoriesEnum { - Automation = 'AUTOMATION', - Books = 'BOOKS', - Data = 'DATA', - Development = 'DEVELOPMENT', - Featured = 'FEATURED', - Finance = 'FINANCE', - Gaming = 'GAMING', - Media = 'MEDIA', - Music = 'MUSIC', - Network = 'NETWORK', - Photography = 'PHOTOGRAPHY', - Security = 'SECURITY', - Social = 'SOCIAL', - Utilities = 'UTILITIES', -} - -export type AppInfo = { - __typename?: 'AppInfo'; - author: Scalars['String']; - available: Scalars['Boolean']; - categories: Array; - description: Scalars['String']; - exposable?: Maybe; - form_fields: Array; - https?: Maybe; - id: Scalars['String']; - name: Scalars['String']; - no_gui?: Maybe; - port: Scalars['Float']; - short_desc: Scalars['String']; - source: Scalars['String']; - supported_architectures?: Maybe>; - tipi_version: Scalars['Float']; - url_suffix?: Maybe; - version?: Maybe; -}; - -export type AppInputType = { - domain: Scalars['String']; - exposed: Scalars['Boolean']; - form: Scalars['JSONObject']; - id: Scalars['String']; -}; - -export enum AppStatusEnum { - Installing = 'INSTALLING', - Missing = 'MISSING', - Running = 'RUNNING', - Starting = 'STARTING', - Stopped = 'STOPPED', - Stopping = 'STOPPING', - Uninstalling = 'UNINSTALLING', - Updating = 'UPDATING', -} - -export enum AppSupportedArchitecturesEnum { - Amd64 = 'AMD64', - Arm = 'ARM', - Arm64 = 'ARM64', -} - -export enum FieldTypesEnum { - Email = 'email', - Fqdn = 'fqdn', - Fqdnip = 'fqdnip', - Ip = 'ip', - Number = 'number', - Password = 'password', - Random = 'random', - Text = 'text', - Url = 'url', -} - -export type FormField = { - __typename?: 'FormField'; - env_variable: Scalars['String']; - hint?: Maybe; - label: Scalars['String']; - max?: Maybe; - min?: Maybe; - placeholder?: Maybe; - required?: Maybe; - type: FieldTypesEnum; -}; - -export type ListAppsResonse = { - __typename?: 'ListAppsResonse'; - apps: Array; - total: Scalars['Float']; -}; - -export type Mutation = { - __typename?: 'Mutation'; - installApp: App; - startApp: App; - stopApp: App; - uninstallApp: App; - updateApp: App; - updateAppConfig: App; -}; - -export type MutationInstallAppArgs = { - input: AppInputType; -}; - -export type MutationStartAppArgs = { - id: Scalars['String']; -}; - -export type MutationStopAppArgs = { - id: Scalars['String']; -}; - -export type MutationUninstallAppArgs = { - id: Scalars['String']; -}; - -export type MutationUpdateAppArgs = { - id: Scalars['String']; -}; - -export type MutationUpdateAppConfigArgs = { - input: AppInputType; -}; - -export type Query = { - __typename?: 'Query'; - getApp: App; - installedApps: Array; - listAppsInfo: ListAppsResonse; -}; - -export type QueryGetAppArgs = { - id: Scalars['String']; -}; - -export type UpdateInfo = { - __typename?: 'UpdateInfo'; - current: Scalars['Float']; - dockerVersion?: Maybe; - latest: Scalars['Float']; -}; - -export type InstallAppMutationVariables = Exact<{ - input: AppInputType; -}>; - -export type InstallAppMutation = { __typename?: 'Mutation'; installApp: { __typename: 'App'; id: string; status: AppStatusEnum } }; - -export type StartAppMutationVariables = Exact<{ - id: Scalars['String']; -}>; - -export type StartAppMutation = { __typename?: 'Mutation'; startApp: { __typename: 'App'; id: string; status: AppStatusEnum } }; - -export type StopAppMutationVariables = Exact<{ - id: Scalars['String']; -}>; - -export type StopAppMutation = { __typename?: 'Mutation'; stopApp: { __typename: 'App'; id: string; status: AppStatusEnum } }; - -export type UninstallAppMutationVariables = Exact<{ - id: Scalars['String']; -}>; - -export type UninstallAppMutation = { __typename?: 'Mutation'; uninstallApp: { __typename: 'App'; id: string; status: AppStatusEnum } }; - -export type UpdateAppMutationVariables = Exact<{ - id: Scalars['String']; -}>; - -export type UpdateAppMutation = { __typename?: 'Mutation'; updateApp: { __typename: 'App'; id: string; status: AppStatusEnum } }; - -export type UpdateAppConfigMutationVariables = Exact<{ - input: AppInputType; -}>; - -export type UpdateAppConfigMutation = { __typename?: 'Mutation'; updateAppConfig: { __typename: 'App'; id: string; status: AppStatusEnum } }; - -export type GetAppQueryVariables = Exact<{ - appId: Scalars['String']; -}>; - -export type GetAppQuery = { - __typename?: 'Query'; - getApp: { - __typename?: 'App'; - id: string; - status: AppStatusEnum; - config: any; - version?: number | null; - exposed: boolean; - domain?: string | null; - updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null; - info?: { - __typename?: 'AppInfo'; - id: string; - port: number; - name: string; - description: string; - available: boolean; - version?: string | null; - tipi_version: number; - short_desc: string; - author: string; - source: string; - categories: Array; - url_suffix?: string | null; - https?: boolean | null; - exposable?: boolean | null; - no_gui?: boolean | null; - form_fields: Array<{ - __typename?: 'FormField'; - type: FieldTypesEnum; - label: string; - max?: number | null; - min?: number | null; - hint?: string | null; - placeholder?: string | null; - required?: boolean | null; - env_variable: string; - }>; - } | null; - }; -}; - -export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>; - -export type InstalledAppsQuery = { - __typename?: 'Query'; - installedApps: Array<{ - __typename?: 'App'; - id: string; - status: AppStatusEnum; - config: any; - version?: number | null; - updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null; - info?: { __typename?: 'AppInfo'; id: string; name: string; description: string; tipi_version: number; short_desc: string; https?: boolean | null } | null; - }>; -}; - -export type ListAppsQueryVariables = Exact<{ [key: string]: never }>; - -export type ListAppsQuery = { - __typename?: 'Query'; - listAppsInfo: { - __typename?: 'ListAppsResonse'; - total: number; - apps: Array<{ - __typename?: 'AppInfo'; - id: string; - available: boolean; - tipi_version: number; - port: number; - name: string; - version?: string | null; - short_desc: string; - author: string; - categories: Array; - https?: boolean | null; - }>; - }; -}; - -export const InstallAppDocument = gql` - mutation InstallApp($input: AppInputType!) { - installApp(input: $input) { - id - status - __typename - } - } -`; -export type InstallAppMutationFn = Apollo.MutationFunction; - -/** - * __useInstallAppMutation__ - * - * To run a mutation, you first call `useInstallAppMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useInstallAppMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [installAppMutation, { data, loading, error }] = useInstallAppMutation({ - * variables: { - * input: // value for 'input' - * }, - * }); - */ -export function useInstallAppMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation(InstallAppDocument, options); -} -export type InstallAppMutationHookResult = ReturnType; -export type InstallAppMutationResult = Apollo.MutationResult; -export type InstallAppMutationOptions = Apollo.BaseMutationOptions; -export const StartAppDocument = gql` - mutation StartApp($id: String!) { - startApp(id: $id) { - id - status - __typename - } - } -`; -export type StartAppMutationFn = Apollo.MutationFunction; - -/** - * __useStartAppMutation__ - * - * To run a mutation, you first call `useStartAppMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useStartAppMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [startAppMutation, { data, loading, error }] = useStartAppMutation({ - * variables: { - * id: // value for 'id' - * }, - * }); - */ -export function useStartAppMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation(StartAppDocument, options); -} -export type StartAppMutationHookResult = ReturnType; -export type StartAppMutationResult = Apollo.MutationResult; -export type StartAppMutationOptions = Apollo.BaseMutationOptions; -export const StopAppDocument = gql` - mutation StopApp($id: String!) { - stopApp(id: $id) { - id - status - __typename - } - } -`; -export type StopAppMutationFn = Apollo.MutationFunction; - -/** - * __useStopAppMutation__ - * - * To run a mutation, you first call `useStopAppMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useStopAppMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [stopAppMutation, { data, loading, error }] = useStopAppMutation({ - * variables: { - * id: // value for 'id' - * }, - * }); - */ -export function useStopAppMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation(StopAppDocument, options); -} -export type StopAppMutationHookResult = ReturnType; -export type StopAppMutationResult = Apollo.MutationResult; -export type StopAppMutationOptions = Apollo.BaseMutationOptions; -export const UninstallAppDocument = gql` - mutation UninstallApp($id: String!) { - uninstallApp(id: $id) { - id - status - __typename - } - } -`; -export type UninstallAppMutationFn = Apollo.MutationFunction; - -/** - * __useUninstallAppMutation__ - * - * To run a mutation, you first call `useUninstallAppMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUninstallAppMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [uninstallAppMutation, { data, loading, error }] = useUninstallAppMutation({ - * variables: { - * id: // value for 'id' - * }, - * }); - */ -export function useUninstallAppMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation(UninstallAppDocument, options); -} -export type UninstallAppMutationHookResult = ReturnType; -export type UninstallAppMutationResult = Apollo.MutationResult; -export type UninstallAppMutationOptions = Apollo.BaseMutationOptions; -export const UpdateAppDocument = gql` - mutation UpdateApp($id: String!) { - updateApp(id: $id) { - id - status - __typename - } - } -`; -export type UpdateAppMutationFn = Apollo.MutationFunction; - -/** - * __useUpdateAppMutation__ - * - * To run a mutation, you first call `useUpdateAppMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUpdateAppMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [updateAppMutation, { data, loading, error }] = useUpdateAppMutation({ - * variables: { - * id: // value for 'id' - * }, - * }); - */ -export function useUpdateAppMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation(UpdateAppDocument, options); -} -export type UpdateAppMutationHookResult = ReturnType; -export type UpdateAppMutationResult = Apollo.MutationResult; -export type UpdateAppMutationOptions = Apollo.BaseMutationOptions; -export const UpdateAppConfigDocument = gql` - mutation UpdateAppConfig($input: AppInputType!) { - updateAppConfig(input: $input) { - id - status - __typename - } - } -`; -export type UpdateAppConfigMutationFn = Apollo.MutationFunction; - -/** - * __useUpdateAppConfigMutation__ - * - * To run a mutation, you first call `useUpdateAppConfigMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUpdateAppConfigMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [updateAppConfigMutation, { data, loading, error }] = useUpdateAppConfigMutation({ - * variables: { - * input: // value for 'input' - * }, - * }); - */ -export function useUpdateAppConfigMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation(UpdateAppConfigDocument, options); -} -export type UpdateAppConfigMutationHookResult = ReturnType; -export type UpdateAppConfigMutationResult = Apollo.MutationResult; -export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions; -export const GetAppDocument = gql` - query GetApp($appId: String!) { - getApp(id: $appId) { - id - status - config - version - exposed - domain - updateInfo { - current - latest - dockerVersion - } - info { - id - port - name - description - available - version - tipi_version - short_desc - author - source - categories - url_suffix - https - exposable - no_gui - form_fields { - type - label - max - min - hint - placeholder - required - env_variable - } - } - } - } -`; - -/** - * __useGetAppQuery__ - * - * To run a query within a React component, call `useGetAppQuery` and pass it any options that fit your needs. - * When your component renders, `useGetAppQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetAppQuery({ - * variables: { - * appId: // value for 'appId' - * }, - * }); - */ -export function useGetAppQuery(baseOptions: Apollo.QueryHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery(GetAppDocument, options); -} -export function useGetAppLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery(GetAppDocument, options); -} -export type GetAppQueryHookResult = ReturnType; -export type GetAppLazyQueryHookResult = ReturnType; -export type GetAppQueryResult = Apollo.QueryResult; -export const InstalledAppsDocument = gql` - query InstalledApps { - installedApps { - id - status - config - version - updateInfo { - current - latest - dockerVersion - } - info { - id - name - description - tipi_version - short_desc - https - } - } - } -`; - -/** - * __useInstalledAppsQuery__ - * - * To run a query within a React component, call `useInstalledAppsQuery` and pass it any options that fit your needs. - * When your component renders, `useInstalledAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useInstalledAppsQuery({ - * variables: { - * }, - * }); - */ -export function useInstalledAppsQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery(InstalledAppsDocument, options); -} -export function useInstalledAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery(InstalledAppsDocument, options); -} -export type InstalledAppsQueryHookResult = ReturnType; -export type InstalledAppsLazyQueryHookResult = ReturnType; -export type InstalledAppsQueryResult = Apollo.QueryResult; -export const ListAppsDocument = gql` - query ListApps { - listAppsInfo { - apps { - id - available - tipi_version - port - name - version - short_desc - author - categories - https - } - total - } - } -`; - -/** - * __useListAppsQuery__ - * - * To run a query within a React component, call `useListAppsQuery` and pass it any options that fit your needs. - * When your component renders, `useListAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useListAppsQuery({ - * variables: { - * }, - * }); - */ -export function useListAppsQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery(ListAppsDocument, options); -} -export function useListAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery(ListAppsDocument, options); -} -export type ListAppsQueryHookResult = ReturnType; -export type ListAppsLazyQueryHookResult = ReturnType; -export type ListAppsQueryResult = Apollo.QueryResult; diff --git a/packages/dashboard/src/client/graphql/mutations/installApp.graphql b/packages/dashboard/src/client/graphql/mutations/installApp.graphql deleted file mode 100644 index 79716361..00000000 --- a/packages/dashboard/src/client/graphql/mutations/installApp.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation InstallApp($input: AppInputType!) { - installApp(input: $input) { - id - status - __typename - } -} diff --git a/packages/dashboard/src/client/graphql/mutations/startApp.graphql b/packages/dashboard/src/client/graphql/mutations/startApp.graphql deleted file mode 100644 index a5d601e1..00000000 --- a/packages/dashboard/src/client/graphql/mutations/startApp.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation StartApp($id: String!) { - startApp(id: $id) { - id - status - __typename - } -} diff --git a/packages/dashboard/src/client/graphql/mutations/stopApp.graphql b/packages/dashboard/src/client/graphql/mutations/stopApp.graphql deleted file mode 100644 index 879e3ad1..00000000 --- a/packages/dashboard/src/client/graphql/mutations/stopApp.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation StopApp($id: String!) { - stopApp(id: $id) { - id - status - __typename - } -} diff --git a/packages/dashboard/src/client/graphql/mutations/unintallApp.graphql b/packages/dashboard/src/client/graphql/mutations/unintallApp.graphql deleted file mode 100644 index 17eafc3a..00000000 --- a/packages/dashboard/src/client/graphql/mutations/unintallApp.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation UninstallApp($id: String!) { - uninstallApp(id: $id) { - id - status - __typename - } -} diff --git a/packages/dashboard/src/client/graphql/mutations/updateApp.graphql b/packages/dashboard/src/client/graphql/mutations/updateApp.graphql deleted file mode 100644 index 2658f48f..00000000 --- a/packages/dashboard/src/client/graphql/mutations/updateApp.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation UpdateApp($id: String!) { - updateApp(id: $id) { - id - status - __typename - } -} diff --git a/packages/dashboard/src/client/graphql/mutations/updateAppConfig.graphql b/packages/dashboard/src/client/graphql/mutations/updateAppConfig.graphql deleted file mode 100644 index d6fd633d..00000000 --- a/packages/dashboard/src/client/graphql/mutations/updateAppConfig.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation UpdateAppConfig($input: AppInputType!) { - updateAppConfig(input: $input) { - id - status - __typename - } -} diff --git a/packages/dashboard/src/client/graphql/queries/getApp.graphql b/packages/dashboard/src/client/graphql/queries/getApp.graphql deleted file mode 100644 index bb8bf697..00000000 --- a/packages/dashboard/src/client/graphql/queries/getApp.graphql +++ /dev/null @@ -1,42 +0,0 @@ -query GetApp($appId: String!) { - getApp(id: $appId) { - id - status - config - version - exposed - domain - updateInfo { - current - latest - dockerVersion - } - info { - id - port - name - description - available - version - tipi_version - short_desc - author - source - categories - url_suffix - https - exposable - no_gui - form_fields { - type - label - max - min - hint - placeholder - required - env_variable - } - } - } -} diff --git a/packages/dashboard/src/client/graphql/queries/installedApps.graphql b/packages/dashboard/src/client/graphql/queries/installedApps.graphql deleted file mode 100644 index 399840c7..00000000 --- a/packages/dashboard/src/client/graphql/queries/installedApps.graphql +++ /dev/null @@ -1,21 +0,0 @@ -query InstalledApps { - installedApps { - id - status - config - version - updateInfo { - current - latest - dockerVersion - } - info { - id - name - description - tipi_version - short_desc - https - } - } -} diff --git a/packages/dashboard/src/client/graphql/queries/listApps.graphql b/packages/dashboard/src/client/graphql/queries/listApps.graphql deleted file mode 100644 index 51909676..00000000 --- a/packages/dashboard/src/client/graphql/queries/listApps.graphql +++ /dev/null @@ -1,18 +0,0 @@ -# Write your query or mutation here -query ListApps { - listAppsInfo { - apps { - id - available - tipi_version - port - name - version - short_desc - author - categories - https - } - total - } -} diff --git a/packages/dashboard/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx b/packages/dashboard/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx index 7181a8bc..8c598f55 100644 --- a/packages/dashboard/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx +++ b/packages/dashboard/src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx @@ -2,14 +2,14 @@ import clsx from 'clsx'; import Link from 'next/link'; import React from 'react'; import { AppLogo } from '../../../../components/AppLogo/AppLogo'; -import { AppCategoriesEnum } from '../../../../generated/graphql'; +import { AppCategory } from '../../../../core/types'; import { colorSchemeForCategory, limitText } from '../../helpers/table.helpers'; import styles from './AppStoreTile.module.scss'; type App = { id: string; name: string; - categories: string[]; + categories: AppCategory[]; short_desc: string; }; @@ -21,7 +21,7 @@ const AppStoreTile: React.FC<{ app: App }> = ({ app }) => (

{limitText(app.name, 20)}

{limitText(app.short_desc, 30)}

{app.categories?.map((category) => ( -
+
{category.toLocaleLowerCase()}
))} diff --git a/packages/dashboard/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx b/packages/dashboard/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx index 53224174..9241b45b 100644 --- a/packages/dashboard/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx +++ b/packages/dashboard/src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx @@ -1,16 +1,16 @@ import React from 'react'; import Select, { SingleValue } from 'react-select'; import { APP_CATEGORIES } from '../../../../core/constants'; -import { AppCategoriesEnum } from '../../../../generated/graphql'; +import { AppCategory } from '../../../../core/types'; import { useUIStore } from '../../../../state/uiStore'; interface IProps { - onSelect: (value?: AppCategoriesEnum) => void; + onSelect: (value?: AppCategory) => void; className?: string; - initialValue?: AppCategoriesEnum; + initialValue?: AppCategory; } -type OptionsType = { value: AppCategoriesEnum; label: string }; +type OptionsType = { value: AppCategory; label: string }; const CategorySelector: React.FC = ({ onSelect, className, initialValue }) => { const { darkMode } = useUIStore(); diff --git a/packages/dashboard/src/client/modules/AppStore/helpers/table.helpers.ts b/packages/dashboard/src/client/modules/AppStore/helpers/table.helpers.ts index 74c4a2ec..c0c2c761 100644 --- a/packages/dashboard/src/client/modules/AppStore/helpers/table.helpers.ts +++ b/packages/dashboard/src/client/modules/AppStore/helpers/table.helpers.ts @@ -1,11 +1,11 @@ -import { AppCategoriesEnum, AppInfo } from '../../../generated/graphql'; +import { AppCategory, AppInfo } from '../../../core/types'; import { AppTableData } from './table.types'; type SortParams = { data: AppTableData; col: keyof Pick; direction: 'asc' | 'desc'; - category?: AppCategoriesEnum; + category?: AppCategory; search: string; }; @@ -32,19 +32,19 @@ export const sortTable = (params: SortParams) => { export const limitText = (text: string, limit: number) => (text.length > limit ? `${text.substring(0, limit)}...` : text); -export const colorSchemeForCategory: Record = { - [AppCategoriesEnum.Network]: 'blue', - [AppCategoriesEnum.Media]: 'azure', - [AppCategoriesEnum.Automation]: 'indigo', - [AppCategoriesEnum.Development]: 'red', - [AppCategoriesEnum.Utilities]: 'muted', - [AppCategoriesEnum.Photography]: 'purple', - [AppCategoriesEnum.Security]: 'organge', - [AppCategoriesEnum.Social]: 'yellow', - [AppCategoriesEnum.Featured]: 'lime', - [AppCategoriesEnum.Data]: 'green', - [AppCategoriesEnum.Books]: 'teal', - [AppCategoriesEnum.Music]: 'cyan', - [AppCategoriesEnum.Finance]: 'dark', - [AppCategoriesEnum.Gaming]: 'pink', +export const colorSchemeForCategory: Record = { + network: 'blue', + media: 'azure', + automation: 'indigo', + development: 'red', + utilities: 'muted', + photography: 'purple', + security: 'organge', + social: 'yellow', + featured: 'lime', + data: 'green', + books: 'teal', + music: 'cyan', + finance: 'dark', + gaming: 'pink', }; diff --git a/packages/dashboard/src/client/modules/AppStore/helpers/table.types.ts b/packages/dashboard/src/client/modules/AppStore/helpers/table.types.ts index 56331a4f..71cf73dc 100644 --- a/packages/dashboard/src/client/modules/AppStore/helpers/table.types.ts +++ b/packages/dashboard/src/client/modules/AppStore/helpers/table.types.ts @@ -1,4 +1,4 @@ -import { AppInfo } from '../../../generated/graphql'; +import { AppInfo } from '../../../core/types'; export type SortableColumns = keyof Pick; export type SortDirection = 'asc' | 'desc'; diff --git a/packages/dashboard/src/client/modules/AppStore/state/appStoreState.ts b/packages/dashboard/src/client/modules/AppStore/state/appStoreState.ts index b54ba0a5..35535422 100644 --- a/packages/dashboard/src/client/modules/AppStore/state/appStoreState.ts +++ b/packages/dashboard/src/client/modules/AppStore/state/appStoreState.ts @@ -1,12 +1,12 @@ import create from 'zustand'; -import { AppCategoriesEnum } from '../../../generated/graphql'; +import { AppCategory } from '../../../core/types'; import { SortableColumns } from '../helpers/table.types'; type Store = { search: string; setSearch: (textSearch: string) => void; - category?: AppCategoriesEnum; - setCategory: (selectedCategories?: AppCategoriesEnum) => void; + category?: AppCategory; + setCategory: (selectedCategories?: AppCategory) => void; sort: SortableColumns; setSort: (sort: SortableColumns) => void; sortDirection: 'asc' | 'desc'; diff --git a/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.test.tsx b/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.test.tsx index 281152e4..8219f61a 100644 --- a/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.test.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.test.tsx @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import React from 'react'; -import '@testing-library/jest-dom/extend-expect'; import { AppActions } from './AppActions'; -import { AppInfo, AppStatusEnum } from '../../../../generated/graphql'; import { cleanup, fireEvent, render, screen } from '../../../../../../tests/test-utils'; +import { AppInfo } from '../../../../core/types'; afterEach(cleanup); @@ -19,7 +18,7 @@ describe('Test: AppActions', () => { const onStart = jest.fn(); const onRemove = jest.fn(); // @ts-expect-error - const { getByText } = render(); + const { getByText } = render(); // Act fireEvent.click(getByText('Start')); @@ -34,7 +33,7 @@ describe('Test: AppActions', () => { it('should render the correct buttons when app status is running', () => { // @ts-expect-error - const { getByText } = render(); + const { getByText } = render(); expect(getByText('Stop')).toBeInTheDocument(); expect(getByText('Open')).toBeInTheDocument(); expect(getByText('Settings')).toBeInTheDocument(); @@ -42,42 +41,42 @@ describe('Test: AppActions', () => { it('should render the correct buttons when app status is starting', () => { // @ts-expect-error - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByTestId('action-button-loading')).toBeInTheDocument(); }); it('should render the correct buttons when app status is stopping', () => { // @ts-expect-error - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByTestId('action-button-loading')).toBeInTheDocument(); }); it('should render the correct buttons when app status is removing', () => { // @ts-expect-error - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByTestId('action-button-loading')).toBeInTheDocument(); }); it('should render the correct buttons when app status is installing', () => { // @ts-ignore - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByTestId('action-button-loading')).toBeInTheDocument(); }); it('should render the correct buttons when app status is updating', () => { // @ts-expect-error - render(); + render(); expect(screen.getByText('Cancel')).toBeInTheDocument(); expect(screen.getByTestId('action-button-loading')).toBeInTheDocument(); }); it('should render the correct buttons when app status is missing', () => { // @ts-expect-error - render(); + render(); expect(screen.getByText('Install')).toBeInTheDocument(); }); }); diff --git a/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.tsx b/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.tsx index aba89989..87e985a8 100644 --- a/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/AppActions/AppActions.tsx @@ -1,13 +1,14 @@ import { IconDownload, IconExternalLink, IconPlayerPause, IconPlayerPlay, IconSettings, IconTrash, IconX, TablerIcon } from '@tabler/icons'; import clsx from 'clsx'; import React from 'react'; +import type { AppStatus } from '../../../../../server/services/apps/apps.types'; import { Button } from '../../../../components/ui/Button'; -import { AppInfo, AppStatusEnum } from '../../../../generated/graphql'; +import { AppInfo } from '../../../../core/types'; interface IProps { - app: AppInfo; - status?: AppStatusEnum; + info: AppInfo; + status?: AppStatus; updateAvailable: boolean; onInstall: () => void; onUninstall: () => void; @@ -39,8 +40,8 @@ const ActionButton: React.FC = (props) => { ); }; -export const AppActions: React.FC = ({ app, status, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate, onCancel, updateAvailable, onUpdateSettings }) => { - const hasSettings = Object.keys(app.form_fields).length > 0 || app.exposable; +export const AppActions: React.FC = ({ info, status, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate, onCancel, updateAvailable, onUpdateSettings }) => { + const hasSettings = Object.keys(info.form_fields).length > 0 || info.exposable; const buttons: JSX.Element[] = []; @@ -55,7 +56,7 @@ export const AppActions: React.FC = ({ app, status, onInstall, onUninsta const UpdateButton = ; switch (status) { - case AppStatusEnum.Stopped: + case 'stopped': buttons.push(StartButton, RemoveButton); if (hasSettings) { buttons.push(SettingsButton); @@ -64,9 +65,9 @@ export const AppActions: React.FC = ({ app, status, onInstall, onUninsta buttons.push(UpdateButton); } break; - case AppStatusEnum.Running: + case 'running': buttons.push(StopButton); - if (!app.no_gui) { + if (!info.no_gui) { buttons.push(OpenButton); } if (hasSettings) { @@ -76,14 +77,14 @@ export const AppActions: React.FC = ({ app, status, onInstall, onUninsta buttons.push(UpdateButton); } break; - case AppStatusEnum.Installing: - case AppStatusEnum.Uninstalling: - case AppStatusEnum.Starting: - case AppStatusEnum.Stopping: - case AppStatusEnum.Updating: + case 'installing': + case 'uninstalling': + case 'starting': + case 'stopping': + case 'updating': buttons.push(LoadingButtion, CancelButton); break; - case AppStatusEnum.Missing: + case 'missing': buttons.push(InstallButton); break; default: diff --git a/packages/dashboard/src/client/modules/Apps/components/AppDetailsTabs.tsx b/packages/dashboard/src/client/modules/Apps/components/AppDetailsTabs.tsx index eedb5f86..5f561858 100644 --- a/packages/dashboard/src/client/modules/Apps/components/AppDetailsTabs.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/AppDetailsTabs.tsx @@ -2,7 +2,7 @@ import { IconExternalLink } from '@tabler/icons'; import React from 'react'; import { DataGrid, DataGridItem } from '../../../components/ui/DataGrid'; import Markdown from '../../../components/Markdown/Markdown'; -import { AppInfo } from '../../../generated/graphql'; +import { AppInfo } from '../../../core/types'; interface IProps { info: AppInfo; diff --git a/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx b/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx index d6185847..1b16fb8f 100644 --- a/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils'; -import { FieldTypesEnum, FormField } from '../../../../generated/graphql'; +import { FormField } from '../../../../core/types'; import { InstallForm } from './InstallForm'; describe('Test: InstallForm', () => { @@ -12,11 +12,11 @@ describe('Test: InstallForm', () => { it('should render fields with correct types', () => { const formFields: FormField[] = [ - { env_variable: 'test', label: 'test', type: FieldTypesEnum.Text }, - { env_variable: 'test2', label: 'test2', type: FieldTypesEnum.Password }, - { env_variable: 'test3', label: 'test3', type: FieldTypesEnum.Email }, - { env_variable: 'test4', label: 'test4', type: FieldTypesEnum.Url }, - { env_variable: 'test5', label: 'test5', type: FieldTypesEnum.Number }, + { env_variable: 'test', label: 'test', type: 'text', required: false }, + { env_variable: 'test2', label: 'test2', type: 'password', required: false }, + { env_variable: 'test3', label: 'test3', type: 'email', required: false }, + { env_variable: 'test4', label: 'test4', type: 'url', required: false }, + { env_variable: 'test5', label: 'test5', type: 'number', required: false }, ]; render(); @@ -29,7 +29,7 @@ describe('Test: InstallForm', () => { }); it('should call submit function with correct values', async () => { - const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: FieldTypesEnum.Text }]; + const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: 'text', required: false }]; const onSubmit = jest.fn(); @@ -46,7 +46,7 @@ describe('Test: InstallForm', () => { }); it('should show validation error when required field is empty', async () => { - const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: FieldTypesEnum.Text, required: true }]; + const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true }]; const onSubmit = jest.fn(); @@ -60,7 +60,7 @@ describe('Test: InstallForm', () => { }); it('should pre-fill fields if initialValues are provided', () => { - const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: FieldTypesEnum.Text, required: true }]; + const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true }]; const onSubmit = jest.fn(); @@ -70,7 +70,7 @@ describe('Test: InstallForm', () => { }); it('should render expose switch when app is exposable', () => { - const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: FieldTypesEnum.Text, required: true }]; + const formFields: FormField[] = [{ env_variable: 'test-env', label: 'test-field', type: 'text', required: true }]; const onSubmit = jest.fn(); diff --git a/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.tsx b/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.tsx index 2dea5300..3732a47d 100644 --- a/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/InstallForm/InstallForm.tsx @@ -1,16 +1,16 @@ import React, { useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { AppInfo, FormField } from '../../../../generated/graphql'; import { Button } from '../../../../components/ui/Button'; import { Switch } from '../../../../components/ui/Switch'; import { Input } from '../../../../components/ui/Input'; import { validateAppConfig } from '../../utils/validators'; +import { FormField } from '../../../../core/types'; interface IProps { - formFields: AppInfo['form_fields']; - onSubmit: (values: Record) => void; - initalValues?: { exposed?: boolean; domain?: string } & { [key: string]: string }; + formFields: FormField[]; + onSubmit: (values: FormValues) => void; + initalValues?: { exposed?: boolean; domain?: string } & { [key: string]: string | boolean | undefined }; loading?: boolean; exposable?: boolean | null; } diff --git a/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx b/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx index 72e57302..9bb47daf 100644 --- a/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.test.tsx @@ -1,26 +1,26 @@ import React from 'react'; import { InstallModal } from './InstallModal'; -import { FieldTypesEnum } from '../../../../generated/graphql'; import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils'; +import { AppInfo } from '../../../../core/types'; describe('InstallModal', () => { const app = { name: 'My App', form_fields: [ - { name: 'hostname', label: 'Hostname', type: FieldTypesEnum.Text, required: true, env_variable: 'test_hostname' }, - { name: 'password', label: 'Password', type: FieldTypesEnum.Text, required: true, env_variable: 'test_password' }, + { name: 'hostname', label: 'Hostname', type: 'text', required: true, env_variable: 'test_hostname' }, + { name: 'password', label: 'Password', type: 'text', required: true, env_variable: 'test_password' }, ], exposable: true, - }; + } as unknown as AppInfo; it('renders with the correct title', () => { - render(); + render(); expect(screen.getByText(`Install ${app.name}`)).toBeInTheDocument(); }); it('renders the InstallForm with the correct props', () => { - render(); + render(); expect(screen.getByLabelText(app.form_fields[0]?.label || '')).toBeInTheDocument(); expect(screen.getByLabelText(app.form_fields[1]?.label || '')).toBeInTheDocument(); @@ -28,7 +28,7 @@ describe('InstallModal', () => { it('calls onClose when the close button is clicked', () => { const onClose = jest.fn(); - render(); + render(); fireEvent.click(screen.getByTestId('modal-close-button')); expect(onClose).toHaveBeenCalled(); @@ -36,7 +36,7 @@ describe('InstallModal', () => { it('calls onSubmit with the correct values when the form is submitted', async () => { const onSubmit = jest.fn(); - render(); + render(); const hostnameInput = screen.getByLabelText(app.form_fields[0]?.label || ''); const passwordInput = screen.getByLabelText(app.form_fields[1]?.label || ''); diff --git a/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.tsx b/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.tsx index 3205ae8e..2c4bc631 100644 --- a/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/InstallModal/InstallModal.tsx @@ -1,22 +1,23 @@ import React from 'react'; import { InstallForm } from '../InstallForm'; -import { AppInfo } from '../../../../generated/graphql'; import { Modal, ModalBody, ModalHeader } from '../../../../components/ui/Modal'; +import { AppInfo } from '../../../../core/types'; +import { FormValues } from '../InstallForm/InstallForm'; interface IProps { - app: Pick; + info: AppInfo; isOpen: boolean; onClose: () => void; - onSubmit: (values: Record) => void; + onSubmit: (values: FormValues) => void; } -export const InstallModal: React.FC = ({ app, isOpen, onClose, onSubmit }) => ( +export const InstallModal: React.FC = ({ info, isOpen, onClose, onSubmit }) => ( -
Install {app.name}
+
Install {info.name}
- +
); diff --git a/packages/dashboard/src/client/modules/Apps/components/StopModal.tsx b/packages/dashboard/src/client/modules/Apps/components/StopModal.tsx index f21473b2..efa65d32 100644 --- a/packages/dashboard/src/client/modules/Apps/components/StopModal.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/StopModal.tsx @@ -1,25 +1,25 @@ import React from 'react'; -import { AppInfo } from '../../../generated/graphql'; import { Button } from '../../../components/ui/Button'; import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../components/ui/Modal'; +import { AppInfo } from '../../../core/types'; interface IProps { - app: AppInfo; + info: AppInfo; isOpen: boolean; onClose: () => void; onConfirm: () => void; } -export const StopModal: React.FC = ({ app, isOpen, onClose, onConfirm }) => ( +export const StopModal: React.FC = ({ info, isOpen, onClose, onConfirm }) => ( -
Stop {app.name} ?
+
Stop {info.name} ?
All data will be retained
- diff --git a/packages/dashboard/src/client/modules/Apps/components/UninstallModal.tsx b/packages/dashboard/src/client/modules/Apps/components/UninstallModal.tsx index ff6fd341..d7bcd8be 100644 --- a/packages/dashboard/src/client/modules/Apps/components/UninstallModal.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/UninstallModal.tsx @@ -2,20 +2,19 @@ import { IconAlertTriangle } from '@tabler/icons'; import React from 'react'; import { Button } from '../../../components/ui/Button'; import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../components/ui/Modal'; - -import { AppInfo } from '../../../generated/graphql'; +import { AppInfo } from '../../../core/types'; interface IProps { - app: AppInfo; + info: AppInfo; isOpen: boolean; onClose: () => void; onConfirm: () => void; } -export const UninstallModal: React.FC = ({ app, isOpen, onClose, onConfirm }) => ( +export const UninstallModal: React.FC = ({ info, isOpen, onClose, onConfirm }) => ( -
Uninstall {app.name} ?
+
Uninstall {info.name} ?
diff --git a/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.test.tsx b/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.test.tsx index aa9723f6..21256c7a 100644 --- a/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.test.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.test.tsx @@ -8,7 +8,7 @@ describe('UpdateModal', () => { it('renders with the correct title and version number', () => { // Arrange - render(); + render(); // Assert expect(screen.getByText(`Update ${app.name} ?`)).toBeInTheDocument(); @@ -17,7 +17,7 @@ describe('UpdateModal', () => { it('should not render when isOpen is false', () => { // Arrange - render(); + render(); const modal = screen.queryByTestId('modal'); // Assert (modal should have style display: none) @@ -27,7 +27,7 @@ describe('UpdateModal', () => { it('calls onClose when the close button is clicked', () => { // Arrange const onClose = jest.fn(); - render(); + render(); // Act const closeButton = screen.getByTestId('modal-close-button'); @@ -38,7 +38,7 @@ describe('UpdateModal', () => { it('calls onConfirm when the update button is clicked', () => { // Arrange const onConfirm = jest.fn(); - render(); + render(); // Act const updateButton = screen.getByText('Update'); diff --git a/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx b/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx index b989352f..7dcf1892 100644 --- a/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/UpdateModal/UpdateModal.tsx @@ -1,21 +1,20 @@ import React from 'react'; import { Button } from '../../../../components/ui/Button'; import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../../components/ui/Modal'; - -import { AppInfo } from '../../../../generated/graphql'; +import { AppInfo } from '../../../../core/types'; interface IProps { newVersion: string; - app: Pick; + info: Pick; isOpen: boolean; onClose: () => void; onConfirm: () => void; } -export const UpdateModal: React.FC = ({ app, newVersion, isOpen, onClose, onConfirm }) => ( +export const UpdateModal: React.FC = ({ info, newVersion, isOpen, onClose, onConfirm }) => ( -
Update {app.name} ?
+
Update {info.name} ?
@@ -24,7 +23,7 @@ export const UpdateModal: React.FC = ({ app, newVersion, isOpen, onClose
- diff --git a/packages/dashboard/src/client/modules/Apps/components/UpdateSettingsModal.tsx b/packages/dashboard/src/client/modules/Apps/components/UpdateSettingsModal.tsx index 4edbd25d..abc76813 100644 --- a/packages/dashboard/src/client/modules/Apps/components/UpdateSettingsModal.tsx +++ b/packages/dashboard/src/client/modules/Apps/components/UpdateSettingsModal.tsx @@ -1,25 +1,26 @@ import React from 'react'; import { InstallForm } from './InstallForm'; -import { App, AppInfo } from '../../../generated/graphql'; import { Modal, ModalBody, ModalHeader } from '../../../components/ui/Modal'; +import { AppInfo } from '../../../core/types'; +import { FormValues } from './InstallForm/InstallForm'; interface IProps { - app: AppInfo; - config: App['config']; + info: AppInfo; + config: Record; isOpen: boolean; exposed?: boolean; domain?: string; onClose: () => void; - onSubmit: (values: Record) => void; + onSubmit: (values: FormValues) => void; } -export const UpdateSettingsModal: React.FC = ({ app, config, isOpen, onClose, onSubmit, exposed, domain }) => ( +export const UpdateSettingsModal: React.FC = ({ info, config, isOpen, onClose, onSubmit, exposed, domain }) => ( -
Update {app.name} config
+
Update {info.name} config
- +
); diff --git a/packages/dashboard/src/client/modules/Apps/utils/validators/validators.test.tsx b/packages/dashboard/src/client/modules/Apps/utils/validators/validators.test.tsx index 15908cb9..401ddbbc 100644 --- a/packages/dashboard/src/client/modules/Apps/utils/validators/validators.test.tsx +++ b/packages/dashboard/src/client/modules/Apps/utils/validators/validators.test.tsx @@ -1,4 +1,4 @@ -import { FieldTypesEnum, FormField } from '../../../../generated/graphql'; +import { FormField } from '../../../../core/types'; import { validateAppConfig, validateField } from './validators'; describe('Test: validateField', () => { @@ -7,7 +7,7 @@ describe('Test: validateField', () => { label: 'Username', required: true, env_variable: 'test', - type: FieldTypesEnum.Text, + type: 'text', }; const value: string | undefined | boolean = undefined; const result = validateField(field, value); @@ -17,9 +17,10 @@ describe('Test: validateField', () => { it('should return "field label must be less than field.max characters" if the field type is text and the value is longer than the max value', () => { const field: FormField = { label: 'Description', - type: FieldTypesEnum.Text, + type: 'text', max: 10, env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'This value is too long'; const result = validateField(field, value); @@ -29,9 +30,10 @@ describe('Test: validateField', () => { it('should return "field label must be at least field.min characters" if the field type is text and the value is shorter than the min value', () => { const field: FormField = { label: 'Description', - type: FieldTypesEnum.Text, + type: 'text', min: 20, env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'This is too short'; const result = validateField(field, value); @@ -42,10 +44,11 @@ describe('Test: validateField', () => { it('should return "field label must be between field.min and field.max characters" if the field type is password and the value is not between the min and max values', () => { const field: FormField = { label: 'Password', - type: FieldTypesEnum.Password, + type: 'password', min: 6, max: 10, env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'pass'; const result = validateField(field, value); @@ -55,8 +58,9 @@ describe('Test: validateField', () => { it('should return "field label must be a valid email address" if the field type is email and the value is not a valid email', () => { const field: FormField = { label: 'Email', - type: FieldTypesEnum.Email, + type: 'email', env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'invalid-email'; const result = validateField(field, value); @@ -66,8 +70,9 @@ describe('Test: validateField', () => { it('should return "field label must be a number" if the field type is number and the value is not a number', () => { const field: FormField = { label: 'Age', - type: FieldTypesEnum.Number, + type: 'number', env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'not a number'; const result = validateField(field, value); @@ -77,8 +82,9 @@ describe('Test: validateField', () => { it('should return "field label must be a valid domain" if the field type is fqdn and the value is not a valid domain', () => { const field: FormField = { label: 'Domain', - type: FieldTypesEnum.Fqdn, + type: 'fqdn', env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'not.a.valid.c'; const result = validateField(field, value); @@ -88,8 +94,9 @@ describe('Test: validateField', () => { it('should return "field label must be a valid IP address" if the field type is ip and the value is not a valid IP address', () => { const field: FormField = { label: 'IP Address', - type: FieldTypesEnum.Ip, + type: 'ip', env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'not a valid IP'; const result = validateField(field, value); @@ -99,8 +106,9 @@ describe('Test: validateField', () => { it('should return "field label must be a valid domain or IP address" if the field type is fqdnip and the value is not a valid domain or IP address', () => { const field: FormField = { label: 'Domain or IP', - type: FieldTypesEnum.Fqdnip, + type: 'fqdnip', env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'not a valid domain or IP'; const result = validateField(field, value); @@ -110,8 +118,9 @@ describe('Test: validateField', () => { it('should return "field label must be a valid URL" if the field type is url and the value is not a valid URL', () => { const field: FormField = { label: 'Website', - type: FieldTypesEnum.Url, + type: 'url', env_variable: 'test', + required: false, }; const value: string | undefined | boolean = 'not a valid URL'; const result = validateField(field, value); @@ -123,7 +132,7 @@ describe('Test: validateField', () => { label: 'Username', required: false, env_variable: 'test', - type: FieldTypesEnum.Text, + type: 'text', }; const value: string | undefined | boolean = undefined; const result = validateField(field, value); @@ -135,7 +144,7 @@ describe('Test: validateField', () => { label: 'Username', required: true, env_variable: 'test', - type: FieldTypesEnum.Text, + type: 'text', }; const value: string | undefined | boolean = true; const result = validateField(field, value); @@ -155,13 +164,13 @@ describe('Test: validateAppConfig', () => { const fields: FormField[] = [ { label: 'Username', - type: FieldTypesEnum.Text, + type: 'text', required: true, env_variable: 'username', }, { label: 'Password', - type: FieldTypesEnum.Password, + type: 'password', required: true, min: 6, max: 10, @@ -169,7 +178,7 @@ describe('Test: validateAppConfig', () => { }, { label: 'Email', - type: FieldTypesEnum.Email, + type: 'email', required: true, env_variable: 'email', }, @@ -194,13 +203,13 @@ describe('Test: validateAppConfig', () => { const fields: FormField[] = [ { label: 'Username', - type: FieldTypesEnum.Text, + type: 'text', required: true, env_variable: 'username', }, { label: 'Password', - type: FieldTypesEnum.Password, + type: 'password', required: true, min: 6, max: 10, @@ -208,7 +217,7 @@ describe('Test: validateAppConfig', () => { }, { label: 'Email', - type: FieldTypesEnum.Email, + type: 'email', required: true, env_variable: 'email', }, @@ -226,7 +235,7 @@ describe('Test: validateAppConfig', () => { const fields: FormField[] = [ { label: 'Username', - type: FieldTypesEnum.Text, + type: 'text', required: false, env_variable: 'username', }, @@ -245,7 +254,7 @@ describe('Test: validateAppConfig', () => { const fields: FormField[] = [ { label: 'Username', - type: FieldTypesEnum.Text, + type: 'text', required: true, env_variable: 'username', }, diff --git a/packages/dashboard/src/client/modules/Apps/utils/validators/validators.ts b/packages/dashboard/src/client/modules/Apps/utils/validators/validators.ts index a004b011..7f3de2d4 100644 --- a/packages/dashboard/src/client/modules/Apps/utils/validators/validators.ts +++ b/packages/dashboard/src/client/modules/Apps/utils/validators/validators.ts @@ -1,5 +1,5 @@ import validator from 'validator'; -import { FieldTypesEnum, FormField } from '../../../../generated/graphql'; +import type { FormField } from '../../../../core/types'; export const validateField = (field: FormField, value: string | undefined | boolean): string | undefined => { if (field.required && !value) { @@ -11,7 +11,7 @@ export const validateField = (field: FormField, value: string | undefined | bool } switch (field.type) { - case FieldTypesEnum.Text: + case 'text': if (field.max && value.length > field.max) { return `${field.label} must be less than ${field.max} characters`; } @@ -19,37 +19,37 @@ export const validateField = (field: FormField, value: string | undefined | bool return `${field.label} must be at least ${field.min} characters`; } break; - case FieldTypesEnum.Password: + case 'password': if (!validator.isLength(value, { min: field.min || 0, max: field.max || 100 })) { return `${field.label} must be between ${String(field.min)} and ${String(field.max)} characters`; } break; - case FieldTypesEnum.Email: + case 'email': if (!validator.isEmail(value)) { return `${field.label} must be a valid email address`; } break; - case FieldTypesEnum.Number: + case 'number': if (!validator.isNumeric(value)) { return `${field.label} must be a number`; } break; - case FieldTypesEnum.Fqdn: + case 'fqdn': if (!validator.isFQDN(value)) { return `${field.label} must be a valid domain`; } break; - case FieldTypesEnum.Ip: + case 'ip': if (!validator.isIP(value)) { return `${field.label} must be a valid IP address`; } break; - case FieldTypesEnum.Fqdnip: + case 'fqdnip': if (!validator.isFQDN(value || '') && !validator.isIP(value)) { return `${field.label} must be a valid domain or IP address`; } break; - case FieldTypesEnum.Url: + case 'url': if (!validator.isURL(value)) { return `${field.label} must be a valid URL`; }