refactor: removal and replace usage of old graphql generated types

This commit is contained in:
Nicolas Meienberger 2023-02-02 07:55:46 +01:00 committed by Nicolas Meienberger
parent 783c80714b
commit d72526ab8a
32 changed files with 191 additions and 947 deletions

View file

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

View file

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

View file

@ -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<AppInfo, 'id' | 'name' | 'description' | 'short_desc'>;

View file

@ -1,681 +0,0 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
const defaultOptions = {} as const;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
/** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
DateTime: any;
/** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSONObject: any;
};
export type App = {
__typename?: 'App';
config: Scalars['JSONObject'];
createdAt: Scalars['DateTime'];
domain?: Maybe<Scalars['String']>;
exposed: Scalars['Boolean'];
id: Scalars['String'];
info?: Maybe<AppInfo>;
lastOpened: Scalars['DateTime'];
numOpened: Scalars['Float'];
status: AppStatusEnum;
updateInfo?: Maybe<UpdateInfo>;
updatedAt: Scalars['DateTime'];
version?: Maybe<Scalars['Float']>;
};
export enum AppCategoriesEnum {
Automation = 'AUTOMATION',
Books = 'BOOKS',
Data = 'DATA',
Development = 'DEVELOPMENT',
Featured = 'FEATURED',
Finance = 'FINANCE',
Gaming = 'GAMING',
Media = 'MEDIA',
Music = 'MUSIC',
Network = 'NETWORK',
Photography = 'PHOTOGRAPHY',
Security = 'SECURITY',
Social = 'SOCIAL',
Utilities = 'UTILITIES',
}
export type AppInfo = {
__typename?: 'AppInfo';
author: Scalars['String'];
available: Scalars['Boolean'];
categories: Array<AppCategoriesEnum>;
description: Scalars['String'];
exposable?: Maybe<Scalars['Boolean']>;
form_fields: Array<FormField>;
https?: Maybe<Scalars['Boolean']>;
id: Scalars['String'];
name: Scalars['String'];
no_gui?: Maybe<Scalars['Boolean']>;
port: Scalars['Float'];
short_desc: Scalars['String'];
source: Scalars['String'];
supported_architectures?: Maybe<Array<AppSupportedArchitecturesEnum>>;
tipi_version: Scalars['Float'];
url_suffix?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
};
export type AppInputType = {
domain: Scalars['String'];
exposed: Scalars['Boolean'];
form: Scalars['JSONObject'];
id: Scalars['String'];
};
export enum AppStatusEnum {
Installing = 'INSTALLING',
Missing = 'MISSING',
Running = 'RUNNING',
Starting = 'STARTING',
Stopped = 'STOPPED',
Stopping = 'STOPPING',
Uninstalling = 'UNINSTALLING',
Updating = 'UPDATING',
}
export enum AppSupportedArchitecturesEnum {
Amd64 = 'AMD64',
Arm = 'ARM',
Arm64 = 'ARM64',
}
export enum FieldTypesEnum {
Email = 'email',
Fqdn = 'fqdn',
Fqdnip = 'fqdnip',
Ip = 'ip',
Number = 'number',
Password = 'password',
Random = 'random',
Text = 'text',
Url = 'url',
}
export type FormField = {
__typename?: 'FormField';
env_variable: Scalars['String'];
hint?: Maybe<Scalars['String']>;
label: Scalars['String'];
max?: Maybe<Scalars['Float']>;
min?: Maybe<Scalars['Float']>;
placeholder?: Maybe<Scalars['String']>;
required?: Maybe<Scalars['Boolean']>;
type: FieldTypesEnum;
};
export type ListAppsResonse = {
__typename?: 'ListAppsResonse';
apps: Array<AppInfo>;
total: Scalars['Float'];
};
export type Mutation = {
__typename?: 'Mutation';
installApp: App;
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<App>;
listAppsInfo: ListAppsResonse;
};
export type QueryGetAppArgs = {
id: Scalars['String'];
};
export type UpdateInfo = {
__typename?: 'UpdateInfo';
current: Scalars['Float'];
dockerVersion?: Maybe<Scalars['String']>;
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<AppCategoriesEnum>;
url_suffix?: string | null;
https?: boolean | null;
exposable?: boolean | null;
no_gui?: boolean | null;
form_fields: Array<{
__typename?: 'FormField';
type: FieldTypesEnum;
label: string;
max?: number | null;
min?: number | null;
hint?: string | null;
placeholder?: string | null;
required?: boolean | null;
env_variable: string;
}>;
} | null;
};
};
export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>;
export type InstalledAppsQuery = {
__typename?: 'Query';
installedApps: Array<{
__typename?: 'App';
id: string;
status: AppStatusEnum;
config: any;
version?: number | null;
updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
info?: { __typename?: 'AppInfo'; id: string; name: string; description: string; tipi_version: number; short_desc: string; https?: boolean | null } | null;
}>;
};
export type ListAppsQueryVariables = Exact<{ [key: string]: never }>;
export type ListAppsQuery = {
__typename?: 'Query';
listAppsInfo: {
__typename?: 'ListAppsResonse';
total: number;
apps: Array<{
__typename?: 'AppInfo';
id: string;
available: boolean;
tipi_version: number;
port: number;
name: string;
version?: string | null;
short_desc: string;
author: string;
categories: Array<AppCategoriesEnum>;
https?: boolean | null;
}>;
};
};
export const InstallAppDocument = gql`
mutation InstallApp($input: AppInputType!) {
installApp(input: $input) {
id
status
__typename
}
}
`;
export type InstallAppMutationFn = Apollo.MutationFunction<InstallAppMutation, InstallAppMutationVariables>;
/**
* __useInstallAppMutation__
*
* To run a mutation, you first call `useInstallAppMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInstallAppMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [installAppMutation, { data, loading, error }] = useInstallAppMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useInstallAppMutation(baseOptions?: Apollo.MutationHookOptions<InstallAppMutation, InstallAppMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<InstallAppMutation, InstallAppMutationVariables>(InstallAppDocument, options);
}
export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutation>;
export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
export const StartAppDocument = gql`
mutation StartApp($id: String!) {
startApp(id: $id) {
id
status
__typename
}
}
`;
export type StartAppMutationFn = Apollo.MutationFunction<StartAppMutation, StartAppMutationVariables>;
/**
* __useStartAppMutation__
*
* To run a mutation, you first call `useStartAppMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useStartAppMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [startAppMutation, { data, loading, error }] = useStartAppMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useStartAppMutation(baseOptions?: Apollo.MutationHookOptions<StartAppMutation, StartAppMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<StartAppMutation, StartAppMutationVariables>(StartAppDocument, options);
}
export type StartAppMutationHookResult = ReturnType<typeof useStartAppMutation>;
export type StartAppMutationResult = Apollo.MutationResult<StartAppMutation>;
export type StartAppMutationOptions = Apollo.BaseMutationOptions<StartAppMutation, StartAppMutationVariables>;
export const StopAppDocument = gql`
mutation StopApp($id: String!) {
stopApp(id: $id) {
id
status
__typename
}
}
`;
export type StopAppMutationFn = Apollo.MutationFunction<StopAppMutation, StopAppMutationVariables>;
/**
* __useStopAppMutation__
*
* To run a mutation, you first call `useStopAppMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useStopAppMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [stopAppMutation, { data, loading, error }] = useStopAppMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useStopAppMutation(baseOptions?: Apollo.MutationHookOptions<StopAppMutation, StopAppMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<StopAppMutation, StopAppMutationVariables>(StopAppDocument, options);
}
export type StopAppMutationHookResult = ReturnType<typeof useStopAppMutation>;
export type StopAppMutationResult = Apollo.MutationResult<StopAppMutation>;
export type StopAppMutationOptions = Apollo.BaseMutationOptions<StopAppMutation, StopAppMutationVariables>;
export const UninstallAppDocument = gql`
mutation UninstallApp($id: String!) {
uninstallApp(id: $id) {
id
status
__typename
}
}
`;
export type UninstallAppMutationFn = Apollo.MutationFunction<UninstallAppMutation, UninstallAppMutationVariables>;
/**
* __useUninstallAppMutation__
*
* To run a mutation, you first call `useUninstallAppMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUninstallAppMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [uninstallAppMutation, { data, loading, error }] = useUninstallAppMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useUninstallAppMutation(baseOptions?: Apollo.MutationHookOptions<UninstallAppMutation, UninstallAppMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<UninstallAppMutation, UninstallAppMutationVariables>(UninstallAppDocument, options);
}
export type UninstallAppMutationHookResult = ReturnType<typeof useUninstallAppMutation>;
export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
export const UpdateAppDocument = gql`
mutation UpdateApp($id: String!) {
updateApp(id: $id) {
id
status
__typename
}
}
`;
export type UpdateAppMutationFn = Apollo.MutationFunction<UpdateAppMutation, UpdateAppMutationVariables>;
/**
* __useUpdateAppMutation__
*
* To run a mutation, you first call `useUpdateAppMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateAppMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateAppMutation, { data, loading, error }] = useUpdateAppMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useUpdateAppMutation(baseOptions?: Apollo.MutationHookOptions<UpdateAppMutation, UpdateAppMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<UpdateAppMutation, UpdateAppMutationVariables>(UpdateAppDocument, options);
}
export type UpdateAppMutationHookResult = ReturnType<typeof useUpdateAppMutation>;
export type UpdateAppMutationResult = Apollo.MutationResult<UpdateAppMutation>;
export type UpdateAppMutationOptions = Apollo.BaseMutationOptions<UpdateAppMutation, UpdateAppMutationVariables>;
export const UpdateAppConfigDocument = gql`
mutation UpdateAppConfig($input: AppInputType!) {
updateAppConfig(input: $input) {
id
status
__typename
}
}
`;
export type UpdateAppConfigMutationFn = Apollo.MutationFunction<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
/**
* __useUpdateAppConfigMutation__
*
* To run a mutation, you first call `useUpdateAppConfigMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateAppConfigMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateAppConfigMutation, { data, loading, error }] = useUpdateAppConfigMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUpdateAppConfigMutation(baseOptions?: Apollo.MutationHookOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>(UpdateAppConfigDocument, options);
}
export type UpdateAppConfigMutationHookResult = ReturnType<typeof useUpdateAppConfigMutation>;
export type UpdateAppConfigMutationResult = Apollo.MutationResult<UpdateAppConfigMutation>;
export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
export const GetAppDocument = gql`
query GetApp($appId: String!) {
getApp(id: $appId) {
id
status
config
version
exposed
domain
updateInfo {
current
latest
dockerVersion
}
info {
id
port
name
description
available
version
tipi_version
short_desc
author
source
categories
url_suffix
https
exposable
no_gui
form_fields {
type
label
max
min
hint
placeholder
required
env_variable
}
}
}
}
`;
/**
* __useGetAppQuery__
*
* To run a query within a React component, call `useGetAppQuery` and pass it any options that fit your needs.
* When your component renders, `useGetAppQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetAppQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetAppQuery(baseOptions: Apollo.QueryHookOptions<GetAppQuery, GetAppQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetAppQuery, GetAppQueryVariables>(GetAppDocument, options);
}
export function useGetAppLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAppQuery, GetAppQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<GetAppQuery, GetAppQueryVariables>(GetAppDocument, options);
}
export type GetAppQueryHookResult = ReturnType<typeof useGetAppQuery>;
export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
export const InstalledAppsDocument = gql`
query InstalledApps {
installedApps {
id
status
config
version
updateInfo {
current
latest
dockerVersion
}
info {
id
name
description
tipi_version
short_desc
https
}
}
}
`;
/**
* __useInstalledAppsQuery__
*
* To run a query within a React component, call `useInstalledAppsQuery` and pass it any options that fit your needs.
* When your component renders, `useInstalledAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useInstalledAppsQuery({
* variables: {
* },
* });
*/
export function useInstalledAppsQuery(baseOptions?: Apollo.QueryHookOptions<InstalledAppsQuery, InstalledAppsQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<InstalledAppsQuery, InstalledAppsQueryVariables>(InstalledAppsDocument, options);
}
export function useInstalledAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<InstalledAppsQuery, InstalledAppsQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<InstalledAppsQuery, InstalledAppsQueryVariables>(InstalledAppsDocument, options);
}
export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQuery>;
export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
export const ListAppsDocument = gql`
query ListApps {
listAppsInfo {
apps {
id
available
tipi_version
port
name
version
short_desc
author
categories
https
}
total
}
}
`;
/**
* __useListAppsQuery__
*
* To run a query within a React component, call `useListAppsQuery` and pass it any options that fit your needs.
* When your component renders, `useListAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useListAppsQuery({
* variables: {
* },
* });
*/
export function useListAppsQuery(baseOptions?: Apollo.QueryHookOptions<ListAppsQuery, ListAppsQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<ListAppsQuery, ListAppsQueryVariables>(ListAppsDocument, options);
}
export function useListAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ListAppsQuery, ListAppsQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<ListAppsQuery, ListAppsQueryVariables>(ListAppsDocument, options);
}
export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }) => (
<h3 className="text-bold h-3 mb-2">{limitText(app.name, 20)}</h3>
<p className="text-muted text-nowrap mb-2">{limitText(app.short_desc, 30)}</p>
{app.categories?.map((category) => (
<div className={`badge me-1 bg-${colorSchemeForCategory[category as AppCategoriesEnum]}`} key={`${app.id}-${category}`}>
<div className={`badge me-1 bg-${colorSchemeForCategory[category]}`} key={`${app.id}-${category}`}>
{category.toLocaleLowerCase()}
</div>
))}

View file

@ -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<IProps> = ({ onSelect, className, initialValue }) => {
const { darkMode } = useUIStore();

View file

@ -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<AppInfo, 'name'>;
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, string> = {
[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<AppCategory, string> = {
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',
};

View file

@ -1,4 +1,4 @@
import { AppInfo } from '../../../generated/graphql';
import { AppInfo } from '../../../core/types';
export type SortableColumns = keyof Pick<AppInfo, 'name'>;
export type SortDirection = 'asc' | 'desc';

View file

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

View file

@ -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(<AppActions status={AppStatusEnum.Stopped} app={app} onStart={onStart} onUninstall={onRemove} />);
const { getByText } = render(<AppActions status="stopped" info={app} onStart={onStart} onUninstall={onRemove} />);
// 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(<AppActions status={AppStatusEnum.Running} app={app} />);
const { getByText } = render(<AppActions status="running" info={app} />);
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(<AppActions status={AppStatusEnum.Starting} app={app} />);
render(<AppActions status="starting" info={app} />);
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(<AppActions status={AppStatusEnum.Stopping} app={app} />);
render(<AppActions status="stopping" info={app} />);
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(<AppActions status={AppStatusEnum.Uninstalling} app={app} />);
render(<AppActions status="uninstalling" info={app} />);
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(<AppActions status={AppStatusEnum.Installing} app={app} />);
render(<AppActions status="installing" info={app} />);
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(<AppActions status={AppStatusEnum.Updating} app={app} />);
render(<AppActions status="updating" info={app} />);
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(<AppActions status={AppStatusEnum.Missing} app={app} />);
render(<AppActions status="missing" info={app} />);
expect(screen.getByText('Install')).toBeInTheDocument();
});
});

View file

@ -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<BtnProps> = (props) => {
);
};
export const AppActions: React.FC<IProps> = ({ 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<IProps> = ({ 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<IProps> = ({ app, status, onInstall, onUninsta
const UpdateButton = <ActionButton key="update" Icon={IconDownload} onClick={onUpdate} width={null} title="Update" color="success" />;
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<IProps> = ({ 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<IProps> = ({ 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:

View file

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

View file

@ -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(<InstallForm formFields={formFields} onSubmit={jest.fn} />);
@ -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();

View file

@ -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<string, unknown>) => 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;
}

View file

@ -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(<InstallModal app={app} isOpen onClose={jest.fn()} onSubmit={jest.fn()} />);
render(<InstallModal info={app} isOpen onClose={jest.fn()} onSubmit={jest.fn()} />);
expect(screen.getByText(`Install ${app.name}`)).toBeInTheDocument();
});
it('renders the InstallForm with the correct props', () => {
render(<InstallModal app={app} isOpen onClose={jest.fn()} onSubmit={jest.fn()} />);
render(<InstallModal info={app} isOpen onClose={jest.fn()} onSubmit={jest.fn()} />);
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(<InstallModal app={app} isOpen onClose={onClose} onSubmit={jest.fn()} />);
render(<InstallModal info={app} isOpen onClose={onClose} onSubmit={jest.fn()} />);
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(<InstallModal app={app} isOpen onClose={jest.fn()} onSubmit={onSubmit} />);
render(<InstallModal info={app} isOpen onClose={jest.fn()} onSubmit={onSubmit} />);
const hostnameInput = screen.getByLabelText(app.form_fields[0]?.label || '');
const passwordInput = screen.getByLabelText(app.form_fields[1]?.label || '');

View file

@ -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<AppInfo, 'name' | 'form_fields' | 'exposable'>;
info: AppInfo;
isOpen: boolean;
onClose: () => void;
onSubmit: (values: Record<string, any>) => void;
onSubmit: (values: FormValues) => void;
}
export const InstallModal: React.FC<IProps> = ({ app, isOpen, onClose, onSubmit }) => (
export const InstallModal: React.FC<IProps> = ({ info, isOpen, onClose, onSubmit }) => (
<Modal onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Install {app.name}</h5>
<h5 className="modal-title">Install {info.name}</h5>
</ModalHeader>
<ModalBody>
<InstallForm onSubmit={onSubmit} formFields={app.form_fields} exposable={app.exposable} />
<InstallForm onSubmit={onSubmit} formFields={info.form_fields} exposable={info.exposable} />
</ModalBody>
</Modal>
);

View file

@ -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<IProps> = ({ app, isOpen, onClose, onConfirm }) => (
export const StopModal: React.FC<IProps> = ({ info, isOpen, onClose, onConfirm }) => (
<Modal size="sm" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Stop {app.name} ?</h5>
<h5 className="modal-title">Stop {info.name} ?</h5>
</ModalHeader>
<ModalBody>
<div className="text-muted">All data will be retained</div>
</ModalBody>
<ModalFooter>
<Button onClick={onConfirm} className="btn-danger">
<Button data-testid="modal-stop-button" onClick={onConfirm} className="btn-danger">
Stop
</Button>
</ModalFooter>

View file

@ -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<IProps> = ({ app, isOpen, onClose, onConfirm }) => (
export const UninstallModal: React.FC<IProps> = ({ info, isOpen, onClose, onConfirm }) => (
<Modal size="sm" type="danger" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Uninstall {app.name} ?</h5>
<h5 className="modal-title">Uninstall {info.name} ?</h5>
</ModalHeader>
<ModalBody className="text-center py-4">
<IconAlertTriangle className="icon mb-2 text-danger icon-lg" />

View file

@ -8,7 +8,7 @@ describe('UpdateModal', () => {
it('renders with the correct title and version number', () => {
// Arrange
render(<UpdateModal app={app} newVersion={newVersion} isOpen onClose={jest.fn()} onConfirm={jest.fn()} />);
render(<UpdateModal info={app} newVersion={newVersion} isOpen onClose={jest.fn()} onConfirm={jest.fn()} />);
// Assert
expect(screen.getByText(`Update ${app.name} ?`)).toBeInTheDocument();
@ -17,7 +17,7 @@ describe('UpdateModal', () => {
it('should not render when isOpen is false', () => {
// Arrange
render(<UpdateModal app={app} newVersion={newVersion} isOpen={false} onClose={jest.fn()} onConfirm={jest.fn()} />);
render(<UpdateModal info={app} newVersion={newVersion} isOpen={false} onClose={jest.fn()} onConfirm={jest.fn()} />);
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(<UpdateModal app={app} newVersion={newVersion} isOpen onClose={onClose} onConfirm={jest.fn()} />);
render(<UpdateModal info={app} newVersion={newVersion} isOpen onClose={onClose} onConfirm={jest.fn()} />);
// 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(<UpdateModal app={app} newVersion={newVersion} isOpen onClose={jest.fn()} onConfirm={onConfirm} />);
render(<UpdateModal info={app} newVersion={newVersion} isOpen onClose={jest.fn()} onConfirm={onConfirm} />);
// Act
const updateButton = screen.getByText('Update');

View file

@ -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<AppInfo, 'name'>;
info: Pick<AppInfo, 'name'>;
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
}
export const UpdateModal: React.FC<IProps> = ({ app, newVersion, isOpen, onClose, onConfirm }) => (
export const UpdateModal: React.FC<IProps> = ({ info, newVersion, isOpen, onClose, onConfirm }) => (
<Modal size="sm" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Update {app.name} ?</h5>
<h5 className="modal-title">Update {info.name} ?</h5>
</ModalHeader>
<ModalBody>
<div className="text-muted">
@ -24,7 +23,7 @@ export const UpdateModal: React.FC<IProps> = ({ app, newVersion, isOpen, onClose
</div>
</ModalBody>
<ModalFooter>
<Button onClick={onConfirm} className="btn-success">
<Button data-testid="modal-update-button" onClick={onConfirm} className="btn-success">
Update
</Button>
</ModalFooter>

View file

@ -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<string, unknown>;
isOpen: boolean;
exposed?: boolean;
domain?: string;
onClose: () => void;
onSubmit: (values: Record<string, any>) => void;
onSubmit: (values: FormValues) => void;
}
export const UpdateSettingsModal: React.FC<IProps> = ({ app, config, isOpen, onClose, onSubmit, exposed, domain }) => (
export const UpdateSettingsModal: React.FC<IProps> = ({ info, config, isOpen, onClose, onSubmit, exposed, domain }) => (
<Modal onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Update {app.name} config</h5>
<h5 className="modal-title">Update {info.name} config</h5>
</ModalHeader>
<ModalBody>
<InstallForm onSubmit={onSubmit} formFields={app.form_fields} exposable={app.exposable} initalValues={{ ...config, exposed, domain }} />
<InstallForm onSubmit={onSubmit} formFields={info.form_fields} exposable={info.exposable} initalValues={{ ...config, exposed, domain }} />
</ModalBody>
</Modal>
);

View file

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

View file

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