feat: specify which app have no GUI and therefore don't show the "open" button

This commit is contained in:
Nicolas Meienberger 2022-11-12 15:39:47 +01:00
parent 63c2af8c91
commit bed2404541
17 changed files with 482 additions and 327 deletions

View file

@ -1,5 +1,5 @@
overwrite: true
schema: "http://localhost:3001/graphql"
schema: "http://localhost:3000/api/graphql"
documents: "src/graphql/**/*.graphql"
generates:
src/generated/graphql.tsx:

View file

@ -10,12 +10,14 @@ interface IProps {
className?: string;
isInvalid?: boolean;
size?: Parameters<typeof Input>[0]['size'];
hint?: string;
}
const FormInput: React.FC<IProps> = ({ placeholder, error, type, label, className, isInvalid, size, ...rest }) => {
const FormInput: React.FC<IProps> = ({ placeholder, error, type, label, className, isInvalid, size, hint, ...rest }) => {
return (
<div className={clsx('transition-all', className)}>
{label && <label>{label}</label>}
{label && <label className="mb-1">{label}</label>}
{hint && <div className="text-sm text-gray-500 mb-1">{hint}</div>}
<Input type={type} placeholder={placeholder} isInvalid={isInvalid} size={size} {...rest} />
{isInvalid && <span className="text-red-500 text-sm">{error}</span>}
</div>

View file

@ -63,6 +63,7 @@ export type AppInfo = {
https?: Maybe<Scalars['Boolean']>;
id: Scalars['String'];
name: Scalars['String'];
no_gui?: Maybe<Scalars['Boolean']>;
port: Scalars['Float'];
requirements?: Maybe<Scalars['JSONObject']>;
short_desc: Scalars['String'];
@ -153,42 +154,34 @@ export type Mutation = {
updateAppConfig: App;
};
export type MutationInstallAppArgs = {
input: AppInputType;
};
export type MutationLoginArgs = {
input: UsernamePasswordInput;
};
export type MutationRegisterArgs = {
input: UsernamePasswordInput;
};
export type MutationStartAppArgs = {
id: Scalars['String'];
};
export type MutationStopAppArgs = {
id: Scalars['String'];
};
export type MutationUninstallAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppConfigArgs = {
input: AppInputType;
};
@ -205,7 +198,6 @@ export type Query = {
version: VersionResponse;
};
export type QueryGetAppArgs = {
id: Scalars['String'];
};
@ -252,125 +244,183 @@ export type InstallAppMutationVariables = Exact<{
input: AppInputType;
}>;
export type InstallAppMutation = { __typename?: 'Mutation', installApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type InstallAppMutation = { __typename?: 'Mutation'; installApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type LoginMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'TokenResponse', token: string } };
export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
export type LogoutMutationVariables = Exact<{ [key: string]: never; }>;
export type LogoutMutation = { __typename?: 'Mutation', logout: boolean };
export type LogoutMutation = { __typename?: 'Mutation'; logout: boolean };
export type RegisterMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
export type RegisterMutation = { __typename?: 'Mutation', register: { __typename?: 'TokenResponse', token: string } };
export type RestartMutationVariables = Exact<{ [key: string]: never }>;
export type RestartMutationVariables = Exact<{ [key: string]: never; }>;
export type RestartMutation = { __typename?: 'Mutation', restart: boolean };
export type RestartMutation = { __typename?: 'Mutation'; restart: boolean };
export type StartAppMutationVariables = Exact<{
id: Scalars['String'];
}>;
export type StartAppMutation = { __typename?: 'Mutation', startApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type 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 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 UninstallAppMutation = { __typename?: 'Mutation', uninstallApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type UpdateMutationVariables = Exact<{ [key: string]: never }>;
export type UpdateMutationVariables = Exact<{ [key: string]: never; }>;
export type UpdateMutation = { __typename?: 'Mutation', update: boolean };
export type UpdateMutation = { __typename?: 'Mutation'; update: boolean };
export type UpdateAppMutationVariables = Exact<{
id: Scalars['String'];
}>;
export type UpdateAppMutation = { __typename?: 'Mutation', updateApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type 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 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;
required?: boolean | null;
env_variable: string;
}>;
} | null;
};
};
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, form_fields: Array<{ __typename?: 'FormField', type: FieldTypesEnum, label: string, max?: number | null, min?: number | null, hint?: string | null, required?: boolean | null, env_variable: string }> } | null } };
export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>;
export type InstalledAppsQueryVariables = Exact<{ [key: string]: never; }>;
export type InstalledAppsQuery = {
__typename?: 'Query';
installedApps: Array<{
__typename?: 'App';
id: string;
status: AppStatusEnum;
config: any;
version?: number | null;
updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
info?: { __typename?: 'AppInfo'; id: string; name: string; description: string; tipi_version: number; short_desc: string; https?: boolean | null } | null;
}>;
};
export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
export type 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 ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
export type ConfiguredQueryVariables = Exact<{ [key: string]: never; }>;
export type ListAppsQueryVariables = Exact<{ [key: string]: never }>;
export type ListAppsQuery = {
__typename?: 'Query';
listAppsInfo: {
__typename?: 'ListAppsResonse';
total: number;
apps: Array<{
__typename?: 'AppInfo';
id: string;
available: boolean;
tipi_version: number;
port: number;
name: string;
version?: string | null;
short_desc: string;
author: string;
categories: Array<AppCategoriesEnum>;
https?: boolean | null;
}>;
};
};
export type ConfiguredQuery = { __typename?: 'Query', isConfigured: boolean };
export type MeQueryVariables = Exact<{ [key: string]: never }>;
export type ListAppsQueryVariables = Exact<{ [key: string]: never; }>;
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
export type ListAppsQuery = { __typename?: 'Query', listAppsInfo: { __typename?: 'ListAppsResonse', total: number, apps: Array<{ __typename?: 'AppInfo', id: string, available: boolean, tipi_version: number, port: number, name: string, version?: string | null, short_desc: string, author: string, categories: Array<AppCategoriesEnum>, https?: boolean | null }> } };
export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
export type SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
export type SystemInfoQuery = {
__typename?: 'Query';
systemInfo?: {
__typename?: 'SystemInfoResponse';
cpu: { __typename?: 'Cpu'; load: number };
disk: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
memory: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
} | null;
};
export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null };
export type RefreshTokenQueryVariables = Exact<{ [key: string]: never; }>;
export type RefreshTokenQuery = { __typename?: 'Query', refreshToken?: { __typename?: 'TokenResponse', token: string } | null };
export type SystemInfoQueryVariables = Exact<{ [key: string]: never; }>;
export type SystemInfoQuery = { __typename?: 'Query', systemInfo?: { __typename?: 'SystemInfoResponse', cpu: { __typename?: 'Cpu', load: number }, disk: { __typename?: 'DiskMemory', available: number, used: number, total: number }, memory: { __typename?: 'DiskMemory', available: number, used: number, total: number } } | null };
export type VersionQueryVariables = Exact<{ [key: string]: never; }>;
export type VersionQuery = { __typename?: 'Query', version: { __typename?: 'VersionResponse', current: string, latest?: string | null } };
export type VersionQueryVariables = Exact<{ [key: string]: never }>;
export type VersionQuery = { __typename?: 'Query'; version: { __typename?: 'VersionResponse'; current: string; latest?: string | null } };
export const InstallAppDocument = gql`
mutation InstallApp($input: AppInputType!) {
installApp(input: $input) {
id
status
__typename
mutation InstallApp($input: AppInputType!) {
installApp(input: $input) {
id
status
__typename
}
}
}
`;
`;
export type InstallAppMutationFn = Apollo.MutationFunction<InstallAppMutation, InstallAppMutationVariables>;
/**
@ -398,12 +448,12 @@ export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutati
export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
export const LoginDocument = gql`
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
token
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
token
}
}
}
`;
`;
export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
/**
@ -431,10 +481,10 @@ export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
export const LogoutDocument = gql`
mutation Logout {
logout
}
`;
mutation Logout {
logout
}
`;
export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
/**
@ -461,12 +511,12 @@ export type LogoutMutationHookResult = ReturnType<typeof useLogoutMutation>;
export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
export const RegisterDocument = gql`
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
token
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
token
}
}
}
`;
`;
export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
/**
@ -494,10 +544,10 @@ export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
export const RestartDocument = gql`
mutation Restart {
restart
}
`;
mutation Restart {
restart
}
`;
export type RestartMutationFn = Apollo.MutationFunction<RestartMutation, RestartMutationVariables>;
/**
@ -524,14 +574,14 @@ export type RestartMutationHookResult = ReturnType<typeof useRestartMutation>;
export type RestartMutationResult = Apollo.MutationResult<RestartMutation>;
export type RestartMutationOptions = Apollo.BaseMutationOptions<RestartMutation, RestartMutationVariables>;
export const StartAppDocument = gql`
mutation StartApp($id: String!) {
startApp(id: $id) {
id
status
__typename
mutation StartApp($id: String!) {
startApp(id: $id) {
id
status
__typename
}
}
}
`;
`;
export type StartAppMutationFn = Apollo.MutationFunction<StartAppMutation, StartAppMutationVariables>;
/**
@ -559,14 +609,14 @@ 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
mutation StopApp($id: String!) {
stopApp(id: $id) {
id
status
__typename
}
}
}
`;
`;
export type StopAppMutationFn = Apollo.MutationFunction<StopAppMutation, StopAppMutationVariables>;
/**
@ -594,14 +644,14 @@ 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
mutation UninstallApp($id: String!) {
uninstallApp(id: $id) {
id
status
__typename
}
}
}
`;
`;
export type UninstallAppMutationFn = Apollo.MutationFunction<UninstallAppMutation, UninstallAppMutationVariables>;
/**
@ -629,10 +679,10 @@ export type UninstallAppMutationHookResult = ReturnType<typeof useUninstallAppMu
export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
export const UpdateDocument = gql`
mutation Update {
update
}
`;
mutation Update {
update
}
`;
export type UpdateMutationFn = Apollo.MutationFunction<UpdateMutation, UpdateMutationVariables>;
/**
@ -659,14 +709,14 @@ export type UpdateMutationHookResult = ReturnType<typeof useUpdateMutation>;
export type UpdateMutationResult = Apollo.MutationResult<UpdateMutation>;
export type UpdateMutationOptions = Apollo.BaseMutationOptions<UpdateMutation, UpdateMutationVariables>;
export const UpdateAppDocument = gql`
mutation UpdateApp($id: String!) {
updateApp(id: $id) {
id
status
__typename
mutation UpdateApp($id: String!) {
updateApp(id: $id) {
id
status
__typename
}
}
}
`;
`;
export type UpdateAppMutationFn = Apollo.MutationFunction<UpdateAppMutation, UpdateAppMutationVariables>;
/**
@ -694,14 +744,14 @@ 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
mutation UpdateAppConfig($input: AppInputType!) {
updateAppConfig(input: $input) {
id
status
__typename
}
}
}
`;
`;
export type UpdateAppConfigMutationFn = Apollo.MutationFunction<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
/**
@ -729,47 +779,48 @@ export type UpdateAppConfigMutationHookResult = ReturnType<typeof useUpdateAppCo
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 {
query GetApp($appId: String!) {
getApp(id: $appId) {
id
port
name
description
available
status
config
version
tipi_version
short_desc
author
source
categories
url_suffix
https
exposable
form_fields {
type
label
max
min
hint
required
env_variable
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
required
env_variable
}
}
}
}
}
`;
`;
/**
* __useGetAppQuery__
@ -799,28 +850,28 @@ 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 {
query InstalledApps {
installedApps {
id
name
description
tipi_version
short_desc
https
status
config
version
updateInfo {
current
latest
dockerVersion
}
info {
id
name
description
tipi_version
short_desc
https
}
}
}
}
`;
`;
/**
* __useInstalledAppsQuery__
@ -849,10 +900,10 @@ export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQue
export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
export const ConfiguredDocument = gql`
query Configured {
isConfigured
}
`;
query Configured {
isConfigured
}
`;
/**
* __useConfiguredQuery__
@ -881,24 +932,24 @@ export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
export const ListAppsDocument = gql`
query ListApps {
listAppsInfo {
apps {
id
available
tipi_version
port
name
version
short_desc
author
categories
https
query ListApps {
listAppsInfo {
apps {
id
available
tipi_version
port
name
version
short_desc
author
categories
https
}
total
}
total
}
}
`;
`;
/**
* __useListAppsQuery__
@ -927,12 +978,12 @@ export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
export const MeDocument = gql`
query Me {
me {
id
query Me {
me {
id
}
}
}
`;
`;
/**
* __useMeQuery__
@ -961,12 +1012,12 @@ export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
export const RefreshTokenDocument = gql`
query RefreshToken {
refreshToken {
token
query RefreshToken {
refreshToken {
token
}
}
}
`;
`;
/**
* __useRefreshTokenQuery__
@ -995,24 +1046,24 @@ export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery
export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
export const SystemInfoDocument = gql`
query SystemInfo {
systemInfo {
cpu {
load
}
disk {
available
used
total
}
memory {
available
used
total
query SystemInfo {
systemInfo {
cpu {
load
}
disk {
available
used
total
}
memory {
available
used
total
}
}
}
}
`;
`;
/**
* __useSystemInfoQuery__
@ -1041,13 +1092,13 @@ export type SystemInfoQueryHookResult = ReturnType<typeof useSystemInfoQuery>;
export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
export const VersionDocument = gql`
query Version {
version {
current
latest
query Version {
version {
current
latest
}
}
}
`;
`;
/**
* __useVersionQuery__
@ -1074,4 +1125,4 @@ export function useVersionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ve
}
export type VersionQueryHookResult = ReturnType<typeof useVersionQuery>;
export type VersionLazyQueryHookResult = ReturnType<typeof useVersionLazyQuery>;
export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;
export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;

View file

@ -26,6 +26,7 @@ query GetApp($appId: String!) {
url_suffix
https
exposable
no_gui
form_fields {
type
label

View file

@ -76,7 +76,10 @@ const AppActions: React.FC<IProps> = ({ app, status, onInstall, onUninstall, onS
}
break;
case AppStatusEnum.Running:
buttons.push(StopButton, OpenButton);
buttons.push(StopButton);
if (!app.no_gui) {
buttons.push(OpenButton);
}
if (hasSettings) {
buttons.push(SettingsButton);
}

View file

@ -28,7 +28,9 @@ const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValues, exp
<Field
key={field.env_variable}
name={field.env_variable}
render={({ input, meta }) => <FormInput className="mb-3" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} label={field.label} {...input} />}
render={({ input, meta }) => (
<FormInput hint={field.hint || ''} className="mb-3" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} label={field.label} {...input} />
)}
/>
);
};

View file

@ -112,10 +112,11 @@ class Config {
this.config = configSchema.parse(newConf);
if (writeFile) {
const currentJsonConf = readJsonFile<Partial<z.infer<typeof configSchema>>>('/runtipi/state/settings.json') || {};
currentJsonConf[key] = value;
const partialConfig = configSchema.partial();
const parsed = partialConfig.parse(currentJsonConf);
const currentJsonConf = readJsonFile('/runtipi/state/settings.json') || {};
const parsedConf = configSchema.partial().parse(currentJsonConf);
parsedConf[key] = value;
const parsed = configSchema.partial().parse(parsedConf);
fs.writeFileSync('/runtipi/state/settings.json', JSON.stringify(parsed));
}

View file

@ -48,7 +48,7 @@ describe('Test: setConfig', () => {
expect(config).toBeDefined();
expect(config.appsRepoUrl).toBe(randomWord);
const settingsJson = readJsonFile<any>('/runtipi/state/settings.json');
const settingsJson = readJsonFile('/runtipi/state/settings.json') as { [key: string]: string };
expect(settingsJson).toBeDefined();
expect(settingsJson.appsRepoUrl).toBe(randomWord);

View file

@ -1,12 +1,15 @@
import { z } from 'zod';
import logger from '../../config/logger/logger';
import App from '../../modules/apps/app.entity';
import { AppInfo, AppStatusEnum } from '../../modules/apps/apps.types';
import { appInfoSchema } from '../../modules/apps/apps.helpers';
import { AppStatusEnum } from '../../modules/apps/apps.types';
import User from '../../modules/auth/user.entity';
import { deleteFolder, fileExists, readFile, readJsonFile } from '../../modules/fs/fs.helpers';
import Update, { UpdateStatusEnum } from '../../modules/system/update.entity';
import { getConfig } from '../config/TipiConfig';
type AppsState = { installed: string };
const appStateSchema = z.object({ installed: z.string().optional().default('') });
const userStateSchema = z.object({ email: z.string(), password: z.string() }).array();
const UPDATE_NAME = 'v040';
@ -25,17 +28,21 @@ const migrateApp = async (appId: string): Promise<void> => {
const form: Record<string, string> = {};
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
configFile?.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);
const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (envVarValue) {
form[field.env_variable] = envVarValue;
}
});
if (parsedConfig.success) {
parsedConfig.data.form_fields.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);
await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
if (envVarValue) {
form[field.env_variable] = envVarValue;
}
});
await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
}
} else {
logger.info('App already migrated');
}
@ -56,19 +63,25 @@ export const updateV040 = async (): Promise<void> => {
// Migrate apps
if (fileExists('/runtipi/state/apps.json')) {
const state: AppsState = await readJsonFile('/runtipi/state/apps.json');
const installed: string[] = state.installed.split(' ').filter(Boolean);
const state = await readJsonFile('/runtipi/state/apps.json');
const parsedState = appStateSchema.safeParse(state);
await Promise.all(installed.map((appId) => migrateApp(appId)));
deleteFolder('/runtipi/state/apps.json');
if (parsedState.success) {
const installed: string[] = parsedState.data.installed.split(' ').filter(Boolean);
await Promise.all(installed.map((appId) => migrateApp(appId)));
deleteFolder('/runtipi/state/apps.json');
}
}
// Migrate users
if (fileExists('/state/users.json')) {
const state: { email: string; password: string }[] = await readJsonFile('/runtipi/state/users.json');
const state = await readJsonFile('/runtipi/state/users.json');
const parsedState = userStateSchema.safeParse(state);
await Promise.all(state.map((user) => migrateUser(user)));
deleteFolder('/runtipi/state/users.json');
if (parsedState.success) {
await Promise.all(parsedState.data.map((user) => migrateUser(user)));
deleteFolder('/runtipi/state/users.json');
}
}
await Update.create({ name: UPDATE_NAME, status: UpdateStatusEnum.SUCCESS }).save();

View file

@ -1,3 +1,5 @@
const objectKeys = <T extends object>(obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
export const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => value !== null && value !== undefined;
export default { objectKeys };

View file

@ -13,6 +13,23 @@ interface IProps {
supportedArchitectures?: AppSupportedArchitecturesEnum[];
}
type CreateConfigParams = {
id?: string;
};
const createAppConfig = (props?: CreateConfigParams): AppInfo => ({
id: props?.id || faker.random.alphaNumeric(32),
available: true,
port: faker.datatype.number(),
name: faker.random.alphaNumeric(32),
description: faker.random.alphaNumeric(32),
tipi_version: 1,
short_desc: faker.random.alphaNumeric(32),
author: faker.random.alphaNumeric(32),
source: faker.internet.url(),
categories: [AppCategoriesEnum.AUTOMATION],
});
const createApp = async (props: IProps) => {
const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false, supportedArchitectures } = props;
@ -82,4 +99,4 @@ const createApp = async (props: IProps) => {
return { appInfo, MockFiles, appEntity };
};
export { createApp };
export { createApp, createAppConfig };

View file

@ -5,8 +5,8 @@ import logger from '../../../config/logger/logger';
import { setupConnection, teardownConnection } from '../../../test/connection';
import App from '../app.entity';
import { checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
import { AppInfo } from '../apps.types';
import { createApp } from './apps.factory';
import { AppCategoriesEnum, AppInfo } from '../apps.types';
import { createApp, createAppConfig } from './apps.factory';
jest.mock('fs-extra');
jest.mock('child_process');
@ -39,7 +39,7 @@ describe('checkAppRequirements', () => {
});
it('Should throw an error if app does not exist', async () => {
await expect(checkAppRequirements('not-existing-app')).rejects.toThrow('App not-existing-app not found');
await expect(checkAppRequirements('not-existing-app')).rejects.toThrow('App not-existing-app has invalid config.json file');
});
});
@ -163,7 +163,7 @@ describe('Test: generateEnvFile', () => {
} catch (e: unknown) {
if (e instanceof Error) {
expect(e).toBeDefined();
expect(e.message).toBe('App not-existing-app not found');
expect(e.message).toBe('App not-existing-app has invalid config.json file');
} else {
fail('Should throw an error');
}
@ -257,9 +257,7 @@ describe('Test: getAppInfo', () => {
// @ts-ignore
fs.__createMockFiles(MockFiles);
const newConfig = {
id: faker.random.alphaNumeric(32),
};
const newConfig = createAppConfig();
fs.writeFileSync(`/runtipi/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));
@ -273,10 +271,7 @@ describe('Test: getAppInfo', () => {
// @ts-ignore
fs.__createMockFiles(MockFiles);
const newConfig = {
id: faker.random.alphaNumeric(32),
available: true,
};
const newConfig = createAppConfig();
fs.writeFileSync(`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`, JSON.stringify(newConfig));

View file

@ -182,7 +182,7 @@ describe('InstallApp', () => {
variableValues: { input: { id: 'not-existing', form: { TEST_FIELD: 'hello' }, exposed: false, domain: '' } },
});
expect(errors?.[0].message).toBe('App not-existing not found');
expect(errors?.[0].message).toBe('App not-existing has invalid config.json file');
expect(data?.installApp).toBeUndefined();
});

View file

@ -1,19 +1,52 @@
import crypto from 'crypto';
import fs from 'fs-extra';
import { z } from 'zod';
import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../fs/fs.helpers';
import { AppInfo, AppStatusEnum } from './apps.types';
import { AppCategoriesEnum, AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum, FieldTypes } from './apps.types';
import logger from '../../config/logger/logger';
import { getConfig } from '../../core/config/TipiConfig';
import { AppEntityType } from './app.types';
export const checkAppRequirements = async (appName: string) => {
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
const formFieldSchema = z.object({
type: z.nativeEnum(FieldTypes),
label: z.string(),
max: z.number().optional(),
min: z.number().optional(),
hint: z.string().optional(),
required: z.boolean().optional().default(false),
env_variable: z.string(),
});
if (!configFile) {
throw new Error(`App ${appName} not found`);
export const appInfoSchema = z.object({
id: z.string(),
available: z.boolean(),
port: z.number().min(1).max(65535),
name: z.string(),
description: z.string().optional().default(''),
version: z.string().optional().default('latest'),
tipi_version: z.number(),
short_desc: z.string(),
author: z.string(),
source: z.string(),
website: z.string().optional(),
categories: z.nativeEnum(AppCategoriesEnum).array(),
url_suffix: z.string().optional(),
form_fields: z.array(formFieldSchema).optional().default([]),
https: z.boolean().optional().default(false),
exposable: z.boolean().optional().default(false),
no_gui: z.boolean().optional().default(false),
supported_architectures: z.nativeEnum(AppSupportedArchitecturesEnum).array().optional(),
});
export const checkAppRequirements = async (appName: string) => {
const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!parsedConfig.success) {
throw new Error(`App ${appName} has invalid config.json file`);
}
if (configFile?.supported_architectures && !configFile.supported_architectures.includes(getConfig().architecture)) {
if (parsedConfig.data.supported_architectures && !parsedConfig.data.supported_architectures.includes(getConfig().architecture)) {
throw new Error(`App ${appName} is not supported on this architecture`);
}
@ -34,10 +67,16 @@ export const getEnvMap = (appName: string): Map<string, string> => {
};
export const checkEnvFile = (appName: string) => {
const configFile: AppInfo | null = readJsonFile(`/runtipi/apps/${appName}/config.json`);
const configFile = readJsonFile(`/runtipi/apps/${appName}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!parsedConfig.success) {
throw new Error(`App ${appName} has invalid config.json file`);
}
const envMap = getEnvMap(appName);
configFile?.form_fields?.forEach((field) => {
parsedConfig.data.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envMap.get(envVar);
@ -54,17 +93,18 @@ const getEntropy = (name: string, length: number) => {
};
export const generateEnvFile = (app: AppEntityType) => {
const configFile: AppInfo | null = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
const configFile = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (!configFile) {
throw new Error(`App ${app.id} not found`);
if (!parsedConfig.success) {
throw new Error(`App ${app.id} has invalid config.json file`);
}
const baseEnvFile = readFile('/runtipi/.env').toString();
let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
let envFile = `${baseEnvFile}\nAPP_PORT=${parsedConfig.data.port}\n`;
const envMap = getEnvMap(app.id);
configFile.form_fields?.forEach((field) => {
parsedConfig.data.form_fields.forEach((field) => {
const formValue = app.config[field.env_variable];
const envVar = field.env_variable;
@ -89,7 +129,7 @@ export const generateEnvFile = (app: AppEntityType) => {
envFile += `APP_DOMAIN=${app.domain}\n`;
envFile += 'APP_PROTOCOL=https\n';
} else {
envFile += `APP_DOMAIN=${getConfig().internalIp}:${configFile.port}\n`;
envFile += `APP_DOMAIN=${getConfig().internalIp}:${parsedConfig.data.port}\n`;
}
// Create app-data folder if it doesn't exist
@ -107,9 +147,10 @@ export const getAvailableApps = async (): Promise<string[]> => {
appsDir.forEach((app) => {
if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)) {
const configFile = readJsonFile<AppInfo>(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (configFile?.available) {
if (parsedConfig.success && parsedConfig.data.available) {
apps.push(app);
}
}
@ -124,23 +165,22 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
if (installed && fileExists(`/runtipi/apps/${id}/config.json`)) {
const configFile = readJsonFile<AppInfo>(`/runtipi/apps/${id}/config.json`);
const configFile = readJsonFile(`/runtipi/apps/${id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (configFile) {
configFile.description = readFile(`/runtipi/apps/${id}/metadata/description.md`).toString();
if (parsedConfig.success && parsedConfig.data.available) {
const description = readFile(`/runtipi/apps/${id}/metadata/description.md`);
return { ...parsedConfig.data, description };
}
return configFile;
}
if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile = readJsonFile<AppInfo>(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(configFile);
if (configFile) {
configFile.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
}
if (configFile?.available) {
return configFile;
if (parsedConfig.success && parsedConfig.data.available) {
const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
return { ...parsedConfig.data, description };
}
}
@ -158,17 +198,18 @@ export const getUpdateInfo = async (id: string, version: number) => {
return null;
}
const repoConfig = readJsonFile<AppInfo>(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
const repoConfig = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
const parsedConfig = appInfoSchema.safeParse(repoConfig);
if (!repoConfig?.tipi_version) {
return null;
if (parsedConfig.success) {
return {
current: version || 0,
latest: parsedConfig.data.tipi_version,
dockerVersion: parsedConfig.data.version,
};
}
return {
current: version || 0,
latest: repoConfig?.tipi_version,
dockerVersion: repoConfig?.version,
};
return null;
};
export const ensureAppFolder = (appName: string, cleanup = false) => {

View file

@ -1,12 +1,13 @@
import validator from 'validator';
import { Not } from 'typeorm';
import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder } from './apps.helpers';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, appInfoSchema } from './apps.helpers';
import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
import App from './app.entity';
import logger from '../../config/logger/logger';
import { getConfig } from '../../core/config/TipiConfig';
import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
import { notEmpty } from '../../helpers/helpers';
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
const filterApp = (app: AppInfo): boolean => {
@ -115,9 +116,14 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
// Create app folder
createFolder(`/app/storage/app-data/${id}`);
const appInfo: AppInfo | null = await readJsonFile(`/runtipi/apps/${id}/config.json`);
const appInfo = await readJsonFile(`/runtipi/apps/${id}/config.json`);
const parsedAppInfo = appInfoSchema.safeParse(appInfo);
if (!appInfo?.exposable && exposed) {
if (!parsedAppInfo.success) {
throw new Error(`App ${id} config.json is not valid`);
}
if (!parsedAppInfo.data.exposable && exposed) {
throw new Error(`App ${id} is not exposable`);
}
@ -128,7 +134,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
}
}
app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form, version: Number(appInfo?.tipi_version || 0), exposed: exposed || false, domain }).save();
app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form, version: parsedAppInfo.data.tipi_version, exposed: exposed || false, domain }).save();
// Create env file
generateEnvFile(app);
@ -155,12 +161,23 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
const listApps = async (): Promise<ListAppsResonse> => {
const folders: string[] = await getAvailableApps();
const apps: AppInfo[] = folders.map((app) => readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)).filter(Boolean);
const apps = folders.map((app) => readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)).filter(Boolean);
const filteredApps = filterApps(apps).map((app) => {
const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
return { ...app, description };
});
const parsedApps = apps
.map((app) => {
const result = appInfoSchema.safeParse(app);
if (!result.success) {
logger.error(`App ${JSON.stringify(app)} has invalid config.json`);
return null;
}
const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${result.data.id}/metadata/description.md`);
return { ...result.data, description };
})
.filter(notEmpty);
const filteredApps = filterApps(parsedApps);
return { apps: filteredApps, total: apps.length };
};
@ -182,9 +199,20 @@ const updateAppConfig = async (id: string, form: Record<string, string>, exposed
throw new Error(`Domain ${domain} is not valid`);
}
const appInfo: AppInfo | null = await readJsonFile(`/runtipi/apps/${id}/config.json`);
let app = await App.findOne({ where: { id } });
if (!appInfo?.exposable && exposed) {
if (!app) {
throw new Error(`App ${id} not found`);
}
const appInfo = await readJsonFile(`/runtipi/apps/${id}/config.json`);
const parsedAppInfo = appInfoSchema.safeParse(appInfo);
if (!parsedAppInfo.success) {
throw new Error(`App ${id} has invalid config.json`);
}
if (!parsedAppInfo.data.exposable && exposed) {
throw new Error(`App ${id} is not exposable`);
}
@ -195,12 +223,6 @@ const updateAppConfig = async (id: string, form: Record<string, string>, exposed
}
}
let app = await App.findOne({ where: { id } });
if (!app) {
throw new Error(`App ${id} not found`);
}
await App.update({ id }, { config: form, exposed: exposed || false, domain });
app = (await App.findOne({ where: { id } })) as App;
@ -309,8 +331,10 @@ const updateApp = async (id: string) => {
const { success, stdout } = await eventDispatcher.dispatchEventAsync(EventTypes.APP, ['update', id]);
if (success) {
const appInfo: AppInfo | null = await readJsonFile(`/runtipi/apps/${id}/config.json`);
await App.update({ id }, { status: AppStatusEnum.RUNNING, version: Number(appInfo?.tipi_version) });
const appInfo = await readJsonFile(`/runtipi/apps/${id}/config.json`);
const parsedAppInfo = appInfoSchema.parse(appInfo);
await App.update({ id }, { status: AppStatusEnum.RUNNING, version: parsedAppInfo.tipi_version });
} else {
await App.update({ id }, { status: AppStatusEnum.STOPPED });
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);

View file

@ -139,6 +139,9 @@ class AppInfo {
@Field(() => Boolean, { nullable: true })
exposable?: boolean;
@Field(() => Boolean, { nullable: true })
no_gui?: boolean;
@Field(() => [AppSupportedArchitecturesEnum], { nullable: true })
supported_architectures?: AppSupportedArchitecturesEnum[];
}

View file

@ -1,6 +1,6 @@
import fs from 'fs-extra';
export const readJsonFile = <T>(path: string): T | null => {
export const readJsonFile = (path: string): unknown | null => {
try {
const rawFile = fs.readFileSync(path).toString();