Frontend GraphQL queries
This commit is contained in:
parent
537cdcd811
commit
729c2311f5
46 changed files with 797 additions and 641 deletions
|
@ -51,7 +51,7 @@ export interface AppConfig {
|
|||
ports?: number[];
|
||||
};
|
||||
description: string;
|
||||
version: string;
|
||||
version?: string;
|
||||
image: string;
|
||||
form_fields: FormField[];
|
||||
short_desc: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import validator from 'validator';
|
||||
import { AppConfig, FieldTypes } from '@runtipi/common';
|
||||
import { FieldTypesEnum, FormField } from '../../generated/graphql';
|
||||
|
||||
const validateField = (field: AppConfig['form_fields'][0], value: string): string | undefined => {
|
||||
const validateField = (field: FormField, value: string): string | undefined => {
|
||||
if (field.required && !value) {
|
||||
return `${field.label} is required`;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
|
|||
}
|
||||
|
||||
switch (field.type) {
|
||||
case FieldTypes.text:
|
||||
case FieldTypesEnum.Text:
|
||||
if (field.max && value.length > field.max) {
|
||||
return `${field.label} must be less than ${field.max} characters`;
|
||||
}
|
||||
|
@ -19,37 +19,37 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
|
|||
return `${field.label} must be at least ${field.min} characters`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.password:
|
||||
if (!validator.isLength(value, { min: field.min, max: field.max })) {
|
||||
case FieldTypesEnum.Password:
|
||||
if (!validator.isLength(value, { min: field.min || 0, max: field.max || 100 })) {
|
||||
return `${field.label} must be between ${field.min} and ${field.max} characters`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.email:
|
||||
case FieldTypesEnum.Email:
|
||||
if (!validator.isEmail(value)) {
|
||||
return `${field.label} must be a valid email address`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.number:
|
||||
case FieldTypesEnum.Number:
|
||||
if (!validator.isNumeric(value)) {
|
||||
return `${field.label} must be a number`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.fqdn:
|
||||
case FieldTypesEnum.Fqdn:
|
||||
if (!validator.isFQDN(value)) {
|
||||
return `${field.label} must be a valid domain`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.ip:
|
||||
case FieldTypesEnum.Ip:
|
||||
if (!validator.isIP(value)) {
|
||||
return `${field.label} must be a valid IP address`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.fqdnip:
|
||||
case FieldTypesEnum.Fqdnip:
|
||||
if (!validator.isFQDN(value || '') && !validator.isIP(value)) {
|
||||
return `${field.label} must be a valid domain or IP address`;
|
||||
}
|
||||
break;
|
||||
case FieldTypes.url:
|
||||
case FieldTypesEnum.Url:
|
||||
if (!validator.isURL(value)) {
|
||||
return `${field.label} must be a valid URL`;
|
||||
}
|
||||
|
@ -59,11 +59,11 @@ const validateField = (field: AppConfig['form_fields'][0], value: string): strin
|
|||
}
|
||||
};
|
||||
|
||||
export const validateAppConfig = (values: Record<string, string>, fields: (AppConfig['form_fields'][0] & { id: string })[]) => {
|
||||
export const validateAppConfig = (values: Record<string, string>, fields: FormField[]) => {
|
||||
const errors: any = {};
|
||||
|
||||
fields.forEach((field) => {
|
||||
errors[field.id] = validateField(field, values[field.id]);
|
||||
errors[field.env_variable] = validateField(field, values[field.env_variable]);
|
||||
});
|
||||
|
||||
return errors;
|
||||
|
|
|
@ -8,12 +8,12 @@ import Link from 'next/link';
|
|||
import clsx from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconType } from 'react-icons';
|
||||
import { useAuthStore } from '../../state/authStore';
|
||||
import { useLogoutMutation } from '../../generated/graphql';
|
||||
|
||||
const SideMenu: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
const { logout } = useAuthStore();
|
||||
const [logout] = useLogoutMutation({ refetchQueries: ['Me'] });
|
||||
const path = router.pathname.split('/')[1];
|
||||
|
||||
const renderMenuItem = (title: string, name: string, Icon: IconType) => {
|
||||
|
@ -53,7 +53,7 @@ const SideMenu: React.FC = () => {
|
|||
<Flex flex="1" />
|
||||
<List>
|
||||
<div className="mx-3">
|
||||
<ListItem onClick={logout} className="cursor-pointer hover:font-bold flex items-center mb-5">
|
||||
<ListItem onClick={() => logout()} className="cursor-pointer hover:font-bold flex items-center mb-5">
|
||||
<FiLogOut size={20} className="mr-3" />
|
||||
<p className="flex-1">Log out</p>
|
||||
</ListItem>
|
||||
|
|
|
@ -21,19 +21,34 @@ export type Scalars = {
|
|||
|
||||
export type App = {
|
||||
__typename?: 'App';
|
||||
config: Scalars['JSONObject'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['String'];
|
||||
lastOpened: Scalars['DateTime'];
|
||||
numOpened: Scalars['Float'];
|
||||
status: Scalars['String'];
|
||||
status: AppStatusEnum;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type AppConfig = {
|
||||
__typename?: 'AppConfig';
|
||||
export enum AppCategoriesEnum {
|
||||
Automation = 'AUTOMATION',
|
||||
Books = 'BOOKS',
|
||||
Data = 'DATA',
|
||||
Development = 'DEVELOPMENT',
|
||||
Featured = 'FEATURED',
|
||||
Media = 'MEDIA',
|
||||
Network = 'NETWORK',
|
||||
Photography = 'PHOTOGRAPHY',
|
||||
Security = 'SECURITY',
|
||||
Social = 'SOCIAL',
|
||||
Utilities = 'UTILITIES'
|
||||
}
|
||||
|
||||
export type AppInfo = {
|
||||
__typename?: 'AppInfo';
|
||||
author: Scalars['String'];
|
||||
available: Scalars['Boolean'];
|
||||
categories: Array<Scalars['String']>;
|
||||
categories: Array<AppCategoriesEnum>;
|
||||
description: Scalars['String'];
|
||||
form_fields: Array<FormField>;
|
||||
id: Scalars['String'];
|
||||
|
@ -43,7 +58,6 @@ export type AppConfig = {
|
|||
port: Scalars['Float'];
|
||||
short_desc: Scalars['String'];
|
||||
source: Scalars['String'];
|
||||
status: Scalars['String'];
|
||||
url_suffix?: Maybe<Scalars['String']>;
|
||||
version?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
@ -53,6 +67,44 @@ export type AppInputType = {
|
|||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AppResponse = {
|
||||
__typename?: 'AppResponse';
|
||||
app?: Maybe<App>;
|
||||
info: AppInfo;
|
||||
};
|
||||
|
||||
export enum AppStatusEnum {
|
||||
Installing = 'INSTALLING',
|
||||
Running = 'RUNNING',
|
||||
Starting = 'STARTING',
|
||||
Stopped = 'STOPPED',
|
||||
Stopping = 'STOPPING',
|
||||
Uninstalling = 'UNINSTALLING'
|
||||
}
|
||||
|
||||
export type Cpu = {
|
||||
__typename?: 'Cpu';
|
||||
load: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type DiskMemory = {
|
||||
__typename?: 'DiskMemory';
|
||||
available: Scalars['Float'];
|
||||
total: Scalars['Float'];
|
||||
used: Scalars['Float'];
|
||||
};
|
||||
|
||||
export enum FieldTypesEnum {
|
||||
Email = 'email',
|
||||
Fqdn = 'fqdn',
|
||||
Fqdnip = 'fqdnip',
|
||||
Ip = 'ip',
|
||||
Number = 'number',
|
||||
Password = 'password',
|
||||
Text = 'text',
|
||||
Url = 'url'
|
||||
}
|
||||
|
||||
export type FormField = {
|
||||
__typename?: 'FormField';
|
||||
env_variable: Scalars['String'];
|
||||
|
@ -61,12 +113,12 @@ export type FormField = {
|
|||
max?: Maybe<Scalars['Float']>;
|
||||
min?: Maybe<Scalars['Float']>;
|
||||
required?: Maybe<Scalars['Boolean']>;
|
||||
type: Scalars['String'];
|
||||
type: FieldTypesEnum;
|
||||
};
|
||||
|
||||
export type ListAppsResonse = {
|
||||
__typename?: 'ListAppsResonse';
|
||||
apps: Array<AppConfig>;
|
||||
apps: Array<AppInfo>;
|
||||
total: Scalars['Float'];
|
||||
};
|
||||
|
||||
|
@ -82,47 +134,64 @@ 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 MutationUpdateAppConfigArgs = {
|
||||
input: AppInputType;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
getAppInfo: AppConfig;
|
||||
getApp: AppResponse;
|
||||
installedApps: Array<App>;
|
||||
isConfigured: Scalars['Boolean'];
|
||||
listAppsInfo: ListAppsResonse;
|
||||
me?: Maybe<User>;
|
||||
systemInfo?: Maybe<SystemInfoResponse>;
|
||||
version: Scalars['String'];
|
||||
};
|
||||
|
||||
export type QueryGetAppInfoArgs = {
|
||||
|
||||
export type QueryGetAppArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
export type SystemInfoResponse = {
|
||||
__typename?: 'SystemInfoResponse';
|
||||
cpu: Cpu;
|
||||
disk: DiskMemory;
|
||||
memory: DiskMemory;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
createdAt: Scalars['DateTime'];
|
||||
|
@ -145,25 +214,63 @@ export type LoginMutationVariables = Exact<{
|
|||
input: UsernamePasswordInput;
|
||||
}>;
|
||||
|
||||
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
|
||||
|
||||
export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
|
||||
export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'UserResponse', user?: { __typename?: 'User', id: string } | null } };
|
||||
|
||||
export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
|
||||
export type LogoutMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
|
||||
export type LogoutMutation = { __typename?: 'Mutation', logout: boolean };
|
||||
|
||||
export type RegisterMutationVariables = Exact<{
|
||||
input: UsernamePasswordInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type RegisterMutation = { __typename?: 'Mutation', register: { __typename?: 'UserResponse', user?: { __typename?: 'User', id: string } | null } };
|
||||
|
||||
export type GetAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAppQuery = { __typename?: 'Query', getApp: { __typename?: 'AppResponse', app?: { __typename?: 'App', id: string, status: AppStatusEnum, config: any } | null, info: { __typename?: 'AppInfo', id: string, port: number, name: string, description: string, available: boolean, version?: string | null, image: string, short_desc: string, author: string, source: string, installed: boolean, categories: Array<AppCategoriesEnum>, url_suffix?: string | null, form_fields: Array<{ __typename?: 'FormField', type: FieldTypesEnum, label: string, max?: number | null, min?: number | null, hint?: string | null, required?: boolean | null, env_variable: string }> } } };
|
||||
|
||||
export type InstalledAppsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type InstalledAppsQuery = { __typename?: 'Query', installedApps: Array<{ __typename?: 'App', id: string, status: AppStatusEnum, config: any }> };
|
||||
|
||||
export type ConfiguredQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ConfiguredQuery = { __typename?: 'Query', isConfigured: boolean };
|
||||
|
||||
export type ListAppsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ListAppsQuery = { __typename?: 'Query', listAppsInfo: { __typename?: 'ListAppsResonse', total: number, apps: Array<{ __typename?: 'AppInfo', id: string, available: boolean, installed: boolean, image: string, port: number, name: string, version?: string | null, short_desc: string, author: string, categories: Array<AppCategoriesEnum> }> } };
|
||||
|
||||
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null };
|
||||
|
||||
export type SystemInfoQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type SystemInfoQuery = { __typename?: 'Query', systemInfo?: { __typename?: 'SystemInfoResponse', cpu: { __typename?: 'Cpu', load: number }, disk: { __typename?: 'DiskMemory', available: number, used: number, total: number }, memory: { __typename?: 'DiskMemory', available: number, used: number, total: number } } | null };
|
||||
|
||||
|
||||
export const LoginDocument = gql`
|
||||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
|
||||
|
||||
/**
|
||||
|
@ -184,17 +291,181 @@ export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutati
|
|||
* });
|
||||
*/
|
||||
export function useLoginMutation(baseOptions?: Apollo.MutationHookOptions<LoginMutation, LoginMutationVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
|
||||
}
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
|
||||
}
|
||||
export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
|
||||
export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
|
||||
export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
|
||||
export const ConfiguredDocument = gql`
|
||||
query Configured {
|
||||
isConfigured
|
||||
export const LogoutDocument = gql`
|
||||
mutation Logout {
|
||||
logout
|
||||
}
|
||||
`;
|
||||
export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useLogoutMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useLogoutMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useLogoutMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [logoutMutation, { data, loading, error }] = useLogoutMutation({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useLogoutMutation(baseOptions?: Apollo.MutationHookOptions<LogoutMutation, LogoutMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<LogoutMutation, LogoutMutationVariables>(LogoutDocument, options);
|
||||
}
|
||||
export type LogoutMutationHookResult = ReturnType<typeof useLogoutMutation>;
|
||||
export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
|
||||
export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
|
||||
export const RegisterDocument = gql`
|
||||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useRegisterMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useRegisterMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useRegisterMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [registerMutation, { data, loading, error }] = useRegisterMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRegisterMutation(baseOptions?: Apollo.MutationHookOptions<RegisterMutation, RegisterMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<RegisterMutation, RegisterMutationVariables>(RegisterDocument, options);
|
||||
}
|
||||
export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
|
||||
export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
|
||||
export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
|
||||
export const GetAppDocument = gql`
|
||||
query GetApp($appId: String!) {
|
||||
getApp(id: $appId) {
|
||||
app {
|
||||
id
|
||||
status
|
||||
config
|
||||
}
|
||||
info {
|
||||
id
|
||||
port
|
||||
name
|
||||
description
|
||||
available
|
||||
version
|
||||
image
|
||||
short_desc
|
||||
author
|
||||
source
|
||||
installed
|
||||
categories
|
||||
url_suffix
|
||||
form_fields {
|
||||
type
|
||||
label
|
||||
max
|
||||
min
|
||||
hint
|
||||
required
|
||||
env_variable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetAppQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetAppQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetAppQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetAppQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetAppQuery(baseOptions: Apollo.QueryHookOptions<GetAppQuery, GetAppQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetAppQuery, GetAppQueryVariables>(GetAppDocument, options);
|
||||
}
|
||||
export function useGetAppLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAppQuery, GetAppQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetAppQuery, GetAppQueryVariables>(GetAppDocument, options);
|
||||
}
|
||||
export type GetAppQueryHookResult = ReturnType<typeof useGetAppQuery>;
|
||||
export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
|
||||
export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
|
||||
export const InstalledAppsDocument = gql`
|
||||
query InstalledApps {
|
||||
installedApps {
|
||||
id
|
||||
status
|
||||
config
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useInstalledAppsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useInstalledAppsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useInstalledAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useInstalledAppsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useInstalledAppsQuery(baseOptions?: Apollo.QueryHookOptions<InstalledAppsQuery, InstalledAppsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<InstalledAppsQuery, InstalledAppsQueryVariables>(InstalledAppsDocument, options);
|
||||
}
|
||||
export function useInstalledAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<InstalledAppsQuery, InstalledAppsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<InstalledAppsQuery, InstalledAppsQueryVariables>(InstalledAppsDocument, options);
|
||||
}
|
||||
export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQuery>;
|
||||
export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
|
||||
export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
|
||||
export const ConfiguredDocument = gql`
|
||||
query Configured {
|
||||
isConfigured
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useConfiguredQuery__
|
||||
|
@ -212,23 +483,69 @@ export const ConfiguredDocument = gql`
|
|||
* });
|
||||
*/
|
||||
export function useConfiguredQuery(baseOptions?: Apollo.QueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
|
||||
}
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
|
||||
}
|
||||
export function useConfiguredLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
|
||||
}
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
|
||||
}
|
||||
export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
|
||||
export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
|
||||
export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
|
||||
export const MeDocument = gql`
|
||||
query Me {
|
||||
me {
|
||||
export const ListAppsDocument = gql`
|
||||
query ListApps {
|
||||
listAppsInfo {
|
||||
apps {
|
||||
id
|
||||
available
|
||||
installed
|
||||
image
|
||||
port
|
||||
name
|
||||
version
|
||||
short_desc
|
||||
author
|
||||
categories
|
||||
}
|
||||
total
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useListAppsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useListAppsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useListAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useListAppsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useListAppsQuery(baseOptions?: Apollo.QueryHookOptions<ListAppsQuery, ListAppsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ListAppsQuery, ListAppsQueryVariables>(ListAppsDocument, options);
|
||||
}
|
||||
export function useListAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ListAppsQuery, ListAppsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ListAppsQuery, ListAppsQueryVariables>(ListAppsDocument, options);
|
||||
}
|
||||
export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
|
||||
export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
|
||||
export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
|
||||
export const MeDocument = gql`
|
||||
query Me {
|
||||
me {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMeQuery__
|
||||
|
@ -246,13 +563,59 @@ export const MeDocument = gql`
|
|||
* });
|
||||
*/
|
||||
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
|
||||
}
|
||||
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
||||
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
||||
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
|
||||
export const SystemInfoDocument = gql`
|
||||
query SystemInfo {
|
||||
systemInfo {
|
||||
cpu {
|
||||
load
|
||||
}
|
||||
disk {
|
||||
available
|
||||
used
|
||||
total
|
||||
}
|
||||
memory {
|
||||
available
|
||||
used
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useSystemInfoQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useSystemInfoQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useSystemInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useSystemInfoQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSystemInfoQuery(baseOptions?: Apollo.QueryHookOptions<SystemInfoQuery, SystemInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<SystemInfoQuery, SystemInfoQueryVariables>(SystemInfoDocument, options);
|
||||
}
|
||||
export function useSystemInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SystemInfoQuery, SystemInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<SystemInfoQuery, SystemInfoQueryVariables>(SystemInfoDocument, options);
|
||||
}
|
||||
export type SystemInfoQueryHookResult = ReturnType<typeof useSystemInfoQuery>;
|
||||
export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
|
||||
export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
|
3
packages/dashboard/src/graphql/mutations/logout.graphql
Normal file
3
packages/dashboard/src/graphql/mutations/logout.graphql
Normal file
|
@ -0,0 +1,3 @@
|
|||
mutation Logout {
|
||||
logout
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
33
packages/dashboard/src/graphql/queries/getApp.graphql
Normal file
33
packages/dashboard/src/graphql/queries/getApp.graphql
Normal file
|
@ -0,0 +1,33 @@
|
|||
query GetApp($appId: String!) {
|
||||
getApp(id: $appId) {
|
||||
app {
|
||||
id
|
||||
status
|
||||
config
|
||||
}
|
||||
info {
|
||||
id
|
||||
port
|
||||
name
|
||||
description
|
||||
available
|
||||
version
|
||||
image
|
||||
short_desc
|
||||
author
|
||||
source
|
||||
installed
|
||||
categories
|
||||
url_suffix
|
||||
form_fields {
|
||||
type
|
||||
label
|
||||
max
|
||||
min
|
||||
hint
|
||||
required
|
||||
env_variable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
query InstalledApps {
|
||||
installedApps {
|
||||
id
|
||||
status
|
||||
config
|
||||
}
|
||||
}
|
18
packages/dashboard/src/graphql/queries/listApps.graphql
Normal file
18
packages/dashboard/src/graphql/queries/listApps.graphql
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Write your query or mutation here
|
||||
query ListApps {
|
||||
listAppsInfo {
|
||||
apps {
|
||||
id
|
||||
available
|
||||
installed
|
||||
image
|
||||
port
|
||||
name
|
||||
version
|
||||
short_desc
|
||||
author
|
||||
categories
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
17
packages/dashboard/src/graphql/queries/systemInfo.graphql
Normal file
17
packages/dashboard/src/graphql/queries/systemInfo.graphql
Normal file
|
@ -0,0 +1,17 @@
|
|||
query SystemInfo {
|
||||
systemInfo {
|
||||
cpu {
|
||||
load
|
||||
}
|
||||
disk {
|
||||
available
|
||||
used
|
||||
total
|
||||
}
|
||||
memory {
|
||||
available
|
||||
used
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import { Flex, Input, SimpleGrid } from '@chakra-ui/react';
|
||||
import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
|
||||
import { AppCategoriesEnum } from '@runtipi/common';
|
||||
import React from 'react';
|
||||
import { SortableColumns, SortDirection } from '../helpers/table.types';
|
||||
import { AppTableData, SortableColumns, SortDirection } from '../helpers/table.types';
|
||||
import AppStoreTile from './AppStoreTile';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
interface IProps {
|
||||
data: AppConfig[];
|
||||
data: AppTableData;
|
||||
onSearch: (value: string) => void;
|
||||
onSelectCategories: (value: AppCategoriesEnum[]) => void;
|
||||
onSortBy: (value: SortableColumns) => void;
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { Tag, TagLabel } from '@chakra-ui/react';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { AppCategoriesEnum } from '@runtipi/common';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import AppLogo from '../../../components/AppLogo/AppLogo';
|
||||
import { colorSchemeForCategory, limitText } from '../helpers/table.helpers';
|
||||
|
||||
const AppStoreTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
||||
type App = {
|
||||
id: string;
|
||||
name: string;
|
||||
categories: string[];
|
||||
short_desc: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
const AppStoreTile: React.FC<{ app: App }> = ({ app }) => {
|
||||
return (
|
||||
<Link href={`/app-store/${app.id}`} passHref>
|
||||
<div key={app.id} className="p-2 rounded-md app-store-tile flex items-center group">
|
||||
|
@ -14,7 +22,7 @@ const AppStoreTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
|||
<div className="font-bold">{limitText(app.name, 20)}</div>
|
||||
<div className="text-sm mb-1">{limitText(app.short_desc, 45)}</div>
|
||||
{app.categories?.map((category) => (
|
||||
<Tag colorScheme={colorSchemeForCategory[category]} className="mr-1" borderRadius="full" key={`${app.id}-${category}`} size="sm" variant="solid">
|
||||
<Tag colorScheme={colorSchemeForCategory[category as AppCategoriesEnum]} className="mr-1" borderRadius="full" key={`${app.id}-${category}`} size="sm" variant="solid">
|
||||
<TagLabel>{category}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { Flex } from '@chakra-ui/react';
|
||||
import { AppCategoriesEnum } from '@runtipi/common';
|
||||
import React from 'react';
|
||||
import { useAppsStore } from '../../../state/appsStore';
|
||||
import AppStoreTable from '../components/AppStoreTable';
|
||||
import { sortTable } from '../helpers/table.helpers';
|
||||
import { SortableColumns, SortDirection } from '../helpers/table.types';
|
||||
import { AppTableData, SortableColumns, SortDirection } from '../helpers/table.types';
|
||||
|
||||
// function nonNullable<T>(value: T): value is NonNullable<T> {
|
||||
// return value !== null && value !== undefined;
|
||||
// }
|
||||
|
||||
const AppStoreContainer = () => {
|
||||
const { apps } = useAppsStore();
|
||||
interface IProps {
|
||||
apps: AppTableData;
|
||||
}
|
||||
|
||||
const AppStoreContainer: React.FC<IProps> = ({ apps }) => {
|
||||
const [search, setSearch] = React.useState('');
|
||||
const [categories, setCategories] = React.useState<AppCategoriesEnum[]>([]);
|
||||
const [sort, setSort] = React.useState<SortableColumns>('name');
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { AppCategoriesEnum, AppConfig } from '@runtipi/common';
|
||||
import { AppCategoriesEnum } from '@runtipi/common';
|
||||
import { AppConfig } from '../../../generated/graphql';
|
||||
import { AppTableData } from './table.types';
|
||||
|
||||
export const sortTable = (data: AppConfig[], col: keyof Pick<AppConfig, 'name'>, direction: 'asc' | 'desc', categories: AppCategoriesEnum[], search: string) => {
|
||||
export const sortTable = (data: AppTableData, col: keyof Pick<AppConfig, 'name'>, direction: 'asc' | 'desc', categories: AppCategoriesEnum[], search: string) => {
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
const aVal = a[col];
|
||||
const bVal = b[col];
|
||||
|
@ -14,7 +16,7 @@ export const sortTable = (data: AppConfig[], col: keyof Pick<AppConfig, 'name'>,
|
|||
});
|
||||
|
||||
if (categories.length > 0) {
|
||||
return sortedData.filter((app) => app.categories.some((c) => categories.includes(c))).filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
|
||||
return sortedData.filter((app) => app.categories.some((c) => categories.includes(c as AppCategoriesEnum))).filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
|
||||
} else {
|
||||
return sortedData.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { AppConfig } from '@runtipi/common';
|
||||
import { AppConfig } from '../../../generated/graphql';
|
||||
|
||||
export type SortableColumns = keyof Pick<AppConfig, 'name'>;
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export type AppTableData = Omit<AppConfig, 'description' | 'form_fields' | 'source' | 'status' | 'url_suffix' | 'version'>[];
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FiExternalLink, FiPause, FiPlay, FiSettings, FiTrash2 } from 'react-icons/fi';
|
||||
import { AppConfig, AppStatusEnum } from '@runtipi/common';
|
||||
import { AppInfo, AppStatusEnum } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
app: AppInfo;
|
||||
status?: AppStatusEnum;
|
||||
onInstall: () => void;
|
||||
onUninstall: () => void;
|
||||
onStart: () => void;
|
||||
|
@ -13,10 +14,10 @@ interface IProps {
|
|||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
|
||||
const AppActions: React.FC<IProps> = ({ app, status, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate }) => {
|
||||
const hasSettings = Object.keys(app.form_fields).length > 0;
|
||||
|
||||
if (app?.installed && app.status === AppStatusEnum.STOPPED) {
|
||||
if (status === AppStatusEnum.Stopped) {
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center">
|
||||
<Button onClick={onStart} width={150} colorScheme="green" className="mt-3 mr-2">
|
||||
|
@ -35,7 +36,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (app?.installed && app.status === AppStatusEnum.RUNNING) {
|
||||
} else if (status === AppStatusEnum.Running) {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={onOpen} width={150} colorScheme="gray" className="mt-3 mr-2">
|
||||
|
@ -48,14 +49,14 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
|
|||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else if (app.status === AppStatusEnum.INSTALLING || app.status === AppStatusEnum.UNINSTALLING || app.status === AppStatusEnum.STARTING || app.status === AppStatusEnum.STOPPING) {
|
||||
} else if (status === AppStatusEnum.Installing || status === AppStatusEnum.Uninstalling || status === AppStatusEnum.Starting || status === AppStatusEnum.Stopping) {
|
||||
return (
|
||||
<div className="flex items-center sm:items-start flex-col md:flex-row">
|
||||
<Button isLoading onClick={() => null} width={160} colorScheme="green" className="mt-3">
|
||||
Install
|
||||
<FiPlay className="ml-1" />
|
||||
</Button>
|
||||
<span className="text-gray-500 text-sm ml-2 mt-3 self-center text-center sm:text-left">{`App is ${app.status} please wait and don't refresh page...`}</span>
|
||||
<span className="text-gray-500 text-sm ml-2 mt-3 self-center text-center sm:text-left">{`App is ${status} please wait and don't refresh page...`}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,23 +3,20 @@ import React from 'react';
|
|||
import { Form, Field } from 'react-final-form';
|
||||
import FormInput from '../../../components/Form/FormInput';
|
||||
import { validateAppConfig } from '../../../components/Form/validators';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { objectKeys } from '../../../utils/typescript';
|
||||
import { AppInfo } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
formFields: AppConfig['form_fields'];
|
||||
formFields: AppInfo['form_fields'];
|
||||
onSubmit: (values: Record<string, unknown>) => void;
|
||||
initalValues?: Record<string, string>;
|
||||
}
|
||||
|
||||
const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValues }) => {
|
||||
const fields = objectKeys(formFields).map((key) => ({ ...formFields[key], id: key }));
|
||||
|
||||
const renderField = (field: typeof fields[0]) => {
|
||||
const renderField = (field: typeof formFields[0]) => {
|
||||
return (
|
||||
<Field
|
||||
key={field.id}
|
||||
name={field.id}
|
||||
key={field.env_variable}
|
||||
name={field.env_variable}
|
||||
render={({ input, meta }) => <FormInput className="mb-3" error={meta.error} isInvalid={meta.invalid && (meta.submitError || meta.submitFailed)} label={field.label} {...input} />}
|
||||
/>
|
||||
);
|
||||
|
@ -30,10 +27,10 @@ const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValues }) =
|
|||
initialValues={initalValues}
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur={true}
|
||||
validate={(values) => validateAppConfig(values, fields)}
|
||||
validate={(values) => validateAppConfig(values, formFields)}
|
||||
render={({ handleSubmit, validating, submitting }) => (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
{fields.map(renderField)}
|
||||
{formFields.map(renderField)}
|
||||
<Button isLoading={validating || submitting} className="self-end mb-2" colorScheme="green" type="submit">
|
||||
{initalValues ? 'Update' : 'Install'}
|
||||
</Button>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import InstallForm from './InstallForm';
|
||||
import { AppInfo } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
app: AppInfo;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: Record<string, any>) => void;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { AppInfo } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
app: AppInfo;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { AppInfo } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
app: AppInfo;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
|
|
|
@ -2,11 +2,11 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve
|
|||
import React, { useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import fetcher from '../../../core/fetcher';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import InstallForm from './InstallForm';
|
||||
import { AppInfo } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
app: AppInfo;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: Record<string, any>) => void;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { SlideFade, VStack, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { FiExternalLink } from 'react-icons/fi';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import { useAppsStore } from '../../../state/appsStore';
|
||||
import { useSytemStore } from '../../../state/systemStore';
|
||||
import AppActions from '../components/AppActions';
|
||||
import InstallModal from '../components/InstallModal';
|
||||
|
@ -11,19 +9,20 @@ import UninstallModal from '../components/UninstallModal';
|
|||
import UpdateModal from '../components/UpdateModal';
|
||||
import AppLogo from '../../../components/AppLogo/AppLogo';
|
||||
import Markdown from '../../../components/Markdown/Markdown';
|
||||
import { AppInfo, AppStatusEnum } from '../../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
app: AppConfig;
|
||||
status?: AppStatusEnum;
|
||||
info: AppInfo;
|
||||
}
|
||||
|
||||
const AppDetails: React.FC<IProps> = ({ app }) => {
|
||||
const AppDetails: React.FC<IProps> = ({ status, info }) => {
|
||||
const toast = useToast();
|
||||
const installDisclosure = useDisclosure();
|
||||
const uninstallDisclosure = useDisclosure();
|
||||
const stopDisclosure = useDisclosure();
|
||||
const updateDisclosure = useDisclosure();
|
||||
|
||||
const { install, update, uninstall, stop, start, fetchApp } = useAppsStore();
|
||||
const { internalIp } = useSytemStore();
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
|
@ -35,14 +34,13 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
fetchApp(app.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallSubmit = async (values: Record<string, any>) => {
|
||||
installDisclosure.onClose();
|
||||
try {
|
||||
await install(app.id, values);
|
||||
await install(info.id, values);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
@ -51,7 +49,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
const handleUnistallSubmit = async () => {
|
||||
uninstallDisclosure.onClose();
|
||||
try {
|
||||
await uninstall(app.id);
|
||||
await uninstall(info.id);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
@ -60,7 +58,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
const handleStopSubmit = async () => {
|
||||
stopDisclosure.onClose();
|
||||
try {
|
||||
await stop(app.id);
|
||||
await stop(info.id);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
@ -68,7 +66,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
|
||||
const handleStartSubmit = async () => {
|
||||
try {
|
||||
await start(app.id);
|
||||
await start(info.id);
|
||||
} catch (e: unknown) {
|
||||
handleError(e);
|
||||
}
|
||||
|
@ -76,7 +74,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
|
||||
const handleUpdateSubmit = async (values: Record<string, any>) => {
|
||||
try {
|
||||
await update(app.id, values);
|
||||
await update(info.id, values);
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'App config updated successfully',
|
||||
|
@ -90,27 +88,27 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
window.open(`http://${internalIp}:${app.port}${app.url_suffix || ''}`, '_blank', 'noreferrer');
|
||||
window.open(`http://${internalIp}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||
<div className="flex flex-1 p-4 mt-3 rounded-lg flex-col">
|
||||
<Flex className="flex-col md:flex-row">
|
||||
<AppLogo src={app?.image} size={180} className="self-center sm:self-auto" alt={app.name} />
|
||||
<AppLogo src={info.image} size={180} className="self-center sm:self-auto" alt={info.name} />
|
||||
<VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
|
||||
<div className="mt-3 items-center self-center flex flex-col sm:items-start sm:self-start md:mt-0">
|
||||
<h1 className="font-bold text-2xl">{app?.name}</h1>
|
||||
<h2 className="text-center md:text-left">{app?.short_desc}</h2>
|
||||
{app.source && (
|
||||
<a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={app?.source}>
|
||||
<h1 className="font-bold text-2xl">{info.name}</h1>
|
||||
<h2 className="text-center md:text-left">{info.short_desc}</h2>
|
||||
{info.source && (
|
||||
<a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.source}>
|
||||
<Flex className="mt-2 items-center">
|
||||
Source
|
||||
<FiExternalLink className="ml-1" />
|
||||
</Flex>
|
||||
</a>
|
||||
)}
|
||||
<p className="text-xs text-gray-600">By {app?.author}</p>
|
||||
<p className="text-xs text-gray-600">By {info.author}</p>
|
||||
</div>
|
||||
<div className="flex justify-center xs:absolute md:static top-0 right-5 self-center sm:self-auto">
|
||||
<AppActions
|
||||
|
@ -120,17 +118,18 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
|||
onStop={stopDisclosure.onOpen}
|
||||
onUninstall={uninstallDisclosure.onOpen}
|
||||
onInstall={installDisclosure.onOpen}
|
||||
app={app}
|
||||
app={info}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
</VStack>
|
||||
</Flex>
|
||||
<Divider className="mt-5" />
|
||||
<Markdown className="mt-3">{app?.description}</Markdown>
|
||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.onClose} app={app} />
|
||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.onClose} app={app} />
|
||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.onClose} app={app} />
|
||||
<UpdateModal onSubmit={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.onClose} app={app} />
|
||||
<Markdown className="mt-3">{info.description}</Markdown>
|
||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.onClose} app={info} />
|
||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.onClose} app={info} />
|
||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.onClose} app={info} />
|
||||
<UpdateModal onSubmit={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.onClose} app={info} />
|
||||
</div>
|
||||
</SlideFade>
|
||||
);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { useToast } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useAuthStore } from '../../../state/authStore';
|
||||
import React, { useState } from 'react';
|
||||
import { useRegisterMutation } from '../../../generated/graphql';
|
||||
import AuthFormLayout from '../components/AuthFormLayout';
|
||||
import RegisterForm from '../components/RegisterForm';
|
||||
|
||||
const Onboarding: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const { me, register, loading } = useAuthStore();
|
||||
const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
|
@ -22,10 +23,12 @@ const Onboarding: React.FC = () => {
|
|||
|
||||
const handleRegister = async (values: { email: string; password: string }) => {
|
||||
try {
|
||||
await register(values.email, values.password);
|
||||
await me();
|
||||
setLoading(true);
|
||||
await register({ variables: { input: { username: values.email, password: values.password } } });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,27 +1,17 @@
|
|||
import { SimpleGrid, Text } from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { BsCpu } from 'react-icons/bs';
|
||||
import { FaMemory } from 'react-icons/fa';
|
||||
import { FiHardDrive } from 'react-icons/fi';
|
||||
import { useSytemStore } from '../../../state/systemStore';
|
||||
import { SystemInfoResponse } from '../../../generated/graphql';
|
||||
import SystemStat from '../components/SystemStat';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { fetchDiskSpace, fetchCpuLoad, fetchMemoryLoad, disk, cpuLoad, memory } = useSytemStore();
|
||||
interface IProps {
|
||||
data: SystemInfoResponse;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiskSpace();
|
||||
fetchCpuLoad();
|
||||
fetchMemoryLoad();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDiskSpace();
|
||||
fetchCpuLoad();
|
||||
fetchMemoryLoad();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchCpuLoad, fetchDiskSpace, fetchMemoryLoad]);
|
||||
const Dashboard: React.FC<IProps> = ({ data }) => {
|
||||
const { disk, memory, cpu } = data;
|
||||
|
||||
// Convert bytes to GB
|
||||
const diskFree = Math.round(disk.available / 1024 / 1024 / 1024);
|
||||
|
@ -43,7 +33,7 @@ const Dashboard: React.FC = () => {
|
|||
</Text>
|
||||
<SimpleGrid className="mt-5" minChildWidth="180px" spacing="20px">
|
||||
<SystemStat title="Disk space" metric={`${diskUsed} GB`} subtitle={`Used out of ${diskSize} GB`} icon={FiHardDrive} progress={percentUsed} />
|
||||
<SystemStat title="CPU Load" metric={`${cpuLoad.toFixed(2)}%`} subtitle="Uninstall apps if there is to much load" icon={BsCpu} progress={cpuLoad} />
|
||||
<SystemStat title="CPU Load" metric={`${cpu.load.toFixed(2)}%`} subtitle="Uninstall apps if there is to much load" icon={BsCpu} progress={cpu.load} />
|
||||
<SystemStat title="Memory Used" metric={`${percentUsedMemory}%`} subtitle={`${memoryTotal} GB`} icon={FaMemory} progress={percentUsedMemory} />
|
||||
</SimpleGrid>
|
||||
</>
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
import type { NextPage } from 'next';
|
||||
import { useEffect } from 'react';
|
||||
import Layout from '../../components/Layout';
|
||||
import { useAppsStore } from '../../state/appsStore';
|
||||
import AppDetails from '../../modules/Apps/containers/AppDetails';
|
||||
import { useGetAppQuery } from '../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
|
||||
const { fetchApp, getApp } = useAppsStore((state) => state);
|
||||
const app = getApp(appId);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApp(appId);
|
||||
}, [appId, fetchApp]);
|
||||
const { data, loading } = useGetAppQuery({ variables: { appId }, pollInterval: 5000 });
|
||||
|
||||
const breadcrumb = [
|
||||
{ name: 'App Store', href: '/app-store' },
|
||||
{ name: app?.name || '', href: `/app-store/${appId}`, current: true },
|
||||
{ name: data?.getApp.info?.name || '', href: `/app-store/${appId}`, current: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout breadcrumbs={breadcrumb} loading={!app}>
|
||||
{app && <AppDetails app={app} />}
|
||||
<Layout breadcrumbs={breadcrumb} loading={!data?.getApp && loading}>
|
||||
{data?.getApp.info && <AppDetails status={data?.getApp.app?.status} info={data?.getApp.info} />}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import Layout from '../../components/Layout';
|
||||
import AppStoreContainer from '../../modules/AppStore/containers/AppStoreContainer';
|
||||
import { useAppsStore } from '../../state/appsStore';
|
||||
import { RequestStatus } from '../../core/types';
|
||||
import { useListAppsQuery } from '../../generated/graphql';
|
||||
|
||||
const Apps: NextPage = () => {
|
||||
const { fetch, status, apps } = useAppsStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, [fetch]);
|
||||
const { loading, data } = useListAppsQuery();
|
||||
|
||||
return (
|
||||
<Layout loading={status === RequestStatus.LOADING && apps.length === 0}>
|
||||
<AppStoreContainer />
|
||||
<Layout loading={loading && !data}>
|
||||
<AppStoreContainer apps={data?.listAppsInfo.apps || []} />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
import type { NextPage } from 'next';
|
||||
import { useEffect } from 'react';
|
||||
import Layout from '../../components/Layout';
|
||||
import { useAppsStore } from '../../state/appsStore';
|
||||
import AppDetails from '../../modules/Apps/containers/AppDetails';
|
||||
import { useGetAppQuery } from '../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
const AppDetailsPage: NextPage<IProps> = ({ appId }) => {
|
||||
const { fetchApp, getApp } = useAppsStore((state) => state);
|
||||
const app = getApp(appId);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApp(appId);
|
||||
}, [appId, fetchApp]);
|
||||
const { data, loading } = useGetAppQuery({ variables: { appId }, pollInterval: 5000 });
|
||||
|
||||
const breadcrumb = [
|
||||
{ name: 'Apps', href: '/apps' },
|
||||
{ name: app?.name || '', href: `/apps/${appId}`, current: true },
|
||||
{ name: data?.getApp.info.name || '', href: `/apps/${appId}`, current: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout breadcrumbs={breadcrumb} loading={!app}>
|
||||
{app && <AppDetails app={app} />}
|
||||
<Layout breadcrumbs={breadcrumb} loading={!data?.getApp.app && loading}>
|
||||
{data?.getApp.info && <AppDetails status={data?.getApp.app?.status} info={data.getApp.info} />}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import type { NextPage } from 'next';
|
||||
import Layout from '../components/Layout';
|
||||
import { useSystemInfoQuery } from '../generated/graphql';
|
||||
import Dashboard from '../modules/Dashboard/containers/Dashboard';
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Dashboard />
|
||||
</Layout>
|
||||
);
|
||||
const { data, loading } = useSystemInfoQuery({ pollInterval: 10000 });
|
||||
return <Layout loading={loading && !data}>{data?.systemInfo && <Dashboard data={data.systemInfo} />}</Layout>;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
import produce from 'immer';
|
||||
import create, { SetState } from 'zustand';
|
||||
import api from '../core/api';
|
||||
import { AppConfig, AppStatusEnum } from '@runtipi/common';
|
||||
import { RequestStatus } from '../core/types';
|
||||
|
||||
type AppsStore = {
|
||||
apps: AppConfig[];
|
||||
status: RequestStatus;
|
||||
fetch: () => void;
|
||||
getApp: (id: string) => AppConfig | undefined;
|
||||
fetchApp: (id: string) => void;
|
||||
install: (id: string, form: Record<string, string>) => Promise<void>;
|
||||
update: (id: string, form: Record<string, string>) => Promise<void>;
|
||||
uninstall: (id: string) => Promise<void>;
|
||||
stop: (id: string) => Promise<void>;
|
||||
start: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
type Set = SetState<AppsStore>;
|
||||
|
||||
const sortApps = (a: AppConfig, b: AppConfig) => a.name.localeCompare(b.name);
|
||||
|
||||
const setAppStatus = (appId: string, status: AppStatusEnum, set: Set) => {
|
||||
set((state) => {
|
||||
return produce(state, (draft) => {
|
||||
const app = draft.apps.find((a) => a.id === appId);
|
||||
if (app) app.status = status;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch one app and add it to the list of apps.
|
||||
* @param appId
|
||||
* @param set
|
||||
*/
|
||||
const fetchApp = async (appId: string, set: Set) => {
|
||||
const response = await api.fetch<AppConfig>({
|
||||
endpoint: `/apps/info/${appId}`,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
set((state) => {
|
||||
const apps = state.apps.filter((app) => app.id !== appId);
|
||||
apps.push(response);
|
||||
|
||||
return { ...state, apps: apps.sort(sortApps) };
|
||||
});
|
||||
};
|
||||
|
||||
export const useAppsStore = create<AppsStore>((set, get) => ({
|
||||
apps: [],
|
||||
status: RequestStatus.LOADING,
|
||||
fetchApp: async (appId: string) => fetchApp(appId, set),
|
||||
fetch: async () => {
|
||||
set({ status: RequestStatus.LOADING });
|
||||
|
||||
const response = await api.fetch<AppConfig[]>({
|
||||
endpoint: '/apps/list',
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
const apps = response.sort(sortApps);
|
||||
|
||||
set({ apps, status: RequestStatus.SUCCESS });
|
||||
},
|
||||
getApp: (appId: string) => {
|
||||
return get().apps.find((app) => app.id === appId);
|
||||
},
|
||||
install: async (appId: string, form?: Record<string, string>) => {
|
||||
setAppStatus(appId, AppStatusEnum.INSTALLING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/install/${appId}`,
|
||||
method: 'POST',
|
||||
data: { form },
|
||||
});
|
||||
|
||||
await get().fetchApp(appId);
|
||||
},
|
||||
update: async (appId: string, form?: Record<string, string>) => {
|
||||
await api.fetch({
|
||||
endpoint: `/apps/update/${appId}`,
|
||||
method: 'POST',
|
||||
data: { form },
|
||||
});
|
||||
|
||||
await get().fetchApp(appId);
|
||||
},
|
||||
uninstall: async (appId: string) => {
|
||||
setAppStatus(appId, AppStatusEnum.UNINSTALLING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/uninstall/${appId}`,
|
||||
});
|
||||
|
||||
await get().fetchApp(appId);
|
||||
},
|
||||
stop: async (appId: string) => {
|
||||
setAppStatus(appId, AppStatusEnum.STOPPING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/stop/${appId}`,
|
||||
});
|
||||
|
||||
await get().fetchApp(appId);
|
||||
},
|
||||
start: async (appId: string) => {
|
||||
setAppStatus(appId, AppStatusEnum.STARTING, set);
|
||||
|
||||
await api.fetch({
|
||||
endpoint: `/apps/start/${appId}`,
|
||||
});
|
||||
|
||||
await get().fetchApp(appId);
|
||||
},
|
||||
}));
|
|
@ -1,7 +1,12 @@
|
|||
import { AppStatusEnum } from '@runtipi/common';
|
||||
import { Field, ObjectType } from 'type-graphql';
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { Field, ObjectType, registerEnumType } from 'type-graphql';
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
registerEnumType(AppStatusEnum, {
|
||||
name: 'AppStatusEnum',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
@Entity()
|
||||
class App extends BaseEntity {
|
||||
|
@ -9,7 +14,7 @@ class App extends BaseEntity {
|
|||
@Column({ type: 'varchar', primary: true, unique: true })
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@Field(() => AppStatusEnum)
|
||||
@Column({ type: 'enum', enum: AppStatusEnum, default: AppStatusEnum.STOPPED, nullable: false })
|
||||
status!: AppStatusEnum;
|
||||
|
||||
|
@ -21,6 +26,10 @@ class App extends BaseEntity {
|
|||
@Column({ type: 'integer', default: 0, nullable: false })
|
||||
numOpened!: number;
|
||||
|
||||
@Field(() => GraphQLJSONObject)
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
config!: Record<string, string>;
|
||||
|
||||
@Field(() => Date)
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
|
|
@ -1,139 +0,0 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import { AppConfig } from '@runtipi/common';
|
||||
import AppsService from './apps.service';
|
||||
import { getInitalFormValues } from './apps.helpers';
|
||||
|
||||
const uninstallApp = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id: appName } = req.params;
|
||||
|
||||
if (!appName) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
await AppsService.uninstallApp(appName);
|
||||
|
||||
res.status(200).json({ message: 'App uninstalled successfully' });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const stopApp = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id: appName } = req.params;
|
||||
|
||||
if (!appName) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
await AppsService.stopApp(appName);
|
||||
|
||||
res.status(200).json({ message: 'App stopped successfully' });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAppConfig = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id: appName } = req.params;
|
||||
const { form } = req.body;
|
||||
|
||||
if (!appName) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
AppsService.updateAppConfig(appName, form);
|
||||
|
||||
res.status(200).json({ message: 'App updated successfully' });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const getAppInfo = async (req: Request, res: Response<AppConfig>, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
const appInfo = await AppsService.getAppInfo(id);
|
||||
|
||||
res.status(200).json(appInfo);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const listApps = async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const apps = await AppsService.listApps();
|
||||
|
||||
res.status(200).json(apps);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const startApp = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
await AppsService.startApp(id);
|
||||
|
||||
res.status(200).json({ message: 'App started successfully' });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const installApp = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { form } = req.body;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
await AppsService.installApp(id, form);
|
||||
|
||||
res.status(200).json({ message: 'App installed successfully' });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const initalFormValues = (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('App name is required');
|
||||
}
|
||||
|
||||
res.status(200).json(getInitalFormValues(id));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const AppController = {
|
||||
uninstallApp,
|
||||
installApp,
|
||||
stopApp,
|
||||
updateAppConfig,
|
||||
getAppInfo,
|
||||
listApps,
|
||||
startApp,
|
||||
initalFormValues,
|
||||
};
|
||||
|
||||
export default AppController;
|
|
@ -50,23 +50,6 @@ export const checkEnvFile = (appName: string) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getInitalFormValues = (appName: string): Record<string, string> => {
|
||||
const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
|
||||
const envMap = getEnvMap(appName);
|
||||
const formValues: Record<string, string> = {};
|
||||
|
||||
configFile.form_fields.forEach((field) => {
|
||||
const envVar = field.env_variable;
|
||||
const envVarValue = envMap.get(envVar);
|
||||
|
||||
if (envVarValue) {
|
||||
formValues[field.env_variable] = envVarValue;
|
||||
}
|
||||
});
|
||||
|
||||
return formValues;
|
||||
};
|
||||
|
||||
export const checkAppExists = (appName: string) => {
|
||||
const appExists = fileExists(`/app-data/${appName}`);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Arg, Authorized, Mutation, Query, Resolver } from 'type-graphql';
|
||||
import AppsService from './apps.service';
|
||||
import { AppConfig, AppInputType, ListAppsResonse } from './apps.types';
|
||||
import { AppInputType, AppResponse, ListAppsResonse } from './apps.types';
|
||||
import App from './app.entity';
|
||||
|
||||
@Resolver()
|
||||
|
@ -10,9 +10,9 @@ export default class AppsResolver {
|
|||
return AppsService.listApps();
|
||||
}
|
||||
|
||||
@Query(() => AppConfig)
|
||||
getAppInfo(@Arg('id', () => String) appId: string): Promise<AppConfig> {
|
||||
return AppsService.getAppInfo(appId);
|
||||
@Query(() => AppResponse)
|
||||
getApp(@Arg('id', () => String) id: string): Promise<AppResponse> {
|
||||
return AppsService.getApp(id);
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { Router } from 'express';
|
||||
import AppController from './apps.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.route('/install/:id').post(AppController.installApp);
|
||||
router.route('/update/:id').post(AppController.updateAppConfig);
|
||||
router.route('/uninstall/:id').get(AppController.uninstallApp);
|
||||
router.route('/stop/:id').get(AppController.stopApp);
|
||||
router.route('/start/:id').get(AppController.startApp);
|
||||
router.route('/list').get(AppController.listApps);
|
||||
router.route('/info/:id').get(AppController.getAppInfo);
|
||||
router.route('/form/:id').get(AppController.initalFormValues);
|
||||
|
||||
export default router;
|
|
@ -1,8 +1,7 @@
|
|||
import si from 'systeminformation';
|
||||
import { AppStatusEnum } from '@runtipi/common';
|
||||
import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
|
||||
import { AppConfig, ListAppsResonse } from './apps.types';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, getStateFile, runAppScript } from './apps.helpers';
|
||||
import { AppInfo, AppResponse, ListAppsResonse } from './apps.types';
|
||||
import App from './app.entity';
|
||||
|
||||
const startApp = async (appName: string): Promise<App> => {
|
||||
|
@ -15,8 +14,7 @@ const startApp = async (appName: string): Promise<App> => {
|
|||
checkEnvFile(appName);
|
||||
|
||||
// Regenerate env file
|
||||
const form = getInitalFormValues(appName);
|
||||
generateEnvFile(appName, form);
|
||||
generateEnvFile(appName, app.config);
|
||||
|
||||
await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
|
||||
// Run script
|
||||
|
@ -46,7 +44,7 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
|
|||
// Create env file
|
||||
generateEnvFile(id, form);
|
||||
|
||||
await App.create({ id, status: AppStatusEnum.INSTALLING }).save();
|
||||
app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form }).save();
|
||||
|
||||
// Run script
|
||||
await runAppScript(['install', id]);
|
||||
|
@ -58,7 +56,7 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
|
|||
};
|
||||
|
||||
const listApps = async (): Promise<ListAppsResonse> => {
|
||||
const apps: AppConfig[] = getAvailableApps()
|
||||
const apps: AppInfo[] = getAvailableApps()
|
||||
.map((app) => {
|
||||
try {
|
||||
return readJsonFile(`/apps/${app}/config.json`);
|
||||
|
@ -68,41 +66,39 @@ const listApps = async (): Promise<ListAppsResonse> => {
|
|||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const dockerContainers = await si.dockerContainers();
|
||||
|
||||
const state = getStateFile();
|
||||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
|
||||
apps.forEach((app) => {
|
||||
app.installed = installed.includes(app.id);
|
||||
app.status = (dockerContainers.find((container) => container.name === `${app.id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
|
||||
app.description = readFile(`/apps/${app.id}/metadata/description.md`);
|
||||
});
|
||||
|
||||
return { apps, total: apps.length };
|
||||
};
|
||||
|
||||
const getAppInfo = async (id: string): Promise<AppConfig> => {
|
||||
const dockerContainers = await si.dockerContainers();
|
||||
const configFile: AppConfig = readJsonFile(`/apps/${id}/config.json`);
|
||||
const getAppInfo = (id: string): AppInfo => {
|
||||
const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
|
||||
|
||||
const state = getStateFile();
|
||||
const installed: string[] = state.installed.split(' ').filter(Boolean);
|
||||
configFile.installed = installed.includes(id);
|
||||
configFile.status = (dockerContainers.find((container) => container.name === `${id}`)?.state as AppStatusEnum) || AppStatusEnum.STOPPED;
|
||||
configFile.description = readFile(`/apps/${id}/metadata/description.md`);
|
||||
|
||||
return configFile;
|
||||
};
|
||||
|
||||
const updateAppConfig = async (id: string, form: Record<string, string>): Promise<App> => {
|
||||
const app = await App.findOne({ where: { id } });
|
||||
let app = await App.findOne({ where: { id } });
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
}
|
||||
|
||||
generateEnvFile(id, form);
|
||||
await App.update({ id }, { config: form });
|
||||
|
||||
app = (await App.findOne({ where: { id } })) as App;
|
||||
|
||||
return app;
|
||||
};
|
||||
|
@ -136,4 +132,13 @@ const uninstallApp = async (id: string): Promise<boolean> => {
|
|||
return true;
|
||||
};
|
||||
|
||||
export default { installApp, startApp, listApps, getAppInfo, updateAppConfig, stopApp, uninstallApp };
|
||||
const getApp = async (id: string): Promise<AppResponse> => {
|
||||
const app = await App.findOne({ where: { id } });
|
||||
|
||||
return {
|
||||
info: getAppInfo(id),
|
||||
app,
|
||||
};
|
||||
};
|
||||
|
||||
export default { installApp, startApp, listApps, getApp, updateAppConfig, stopApp, uninstallApp };
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import { AppCategoriesEnum, AppStatusEnum, FieldTypes } from '@runtipi/common';
|
||||
import { Field, InputType, ObjectType } from 'type-graphql';
|
||||
import { AppCategoriesEnum, FieldTypes } from '@runtipi/common';
|
||||
import { Field, InputType, ObjectType, registerEnumType } from 'type-graphql';
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import App from './app.entity';
|
||||
|
||||
registerEnumType(AppCategoriesEnum, {
|
||||
name: 'AppCategoriesEnum',
|
||||
});
|
||||
|
||||
registerEnumType(FieldTypes, {
|
||||
name: 'FieldTypesEnum',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
class FormField {
|
||||
@Field(() => String)
|
||||
@Field(() => FieldTypes)
|
||||
type!: FieldTypes;
|
||||
|
||||
@Field(() => String)
|
||||
|
@ -27,7 +36,7 @@ class FormField {
|
|||
}
|
||||
|
||||
@ObjectType()
|
||||
class AppConfig {
|
||||
class AppInfo {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
|
@ -61,12 +70,9 @@ class AppConfig {
|
|||
@Field(() => Boolean)
|
||||
installed!: boolean;
|
||||
|
||||
@Field(() => [String])
|
||||
@Field(() => [AppCategoriesEnum])
|
||||
categories!: AppCategoriesEnum[];
|
||||
|
||||
@Field(() => String)
|
||||
status!: AppStatusEnum;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
url_suffix?: string;
|
||||
|
||||
|
@ -76,13 +82,22 @@ class AppConfig {
|
|||
|
||||
@ObjectType()
|
||||
class ListAppsResonse {
|
||||
@Field(() => [AppConfig])
|
||||
apps!: AppConfig[];
|
||||
@Field(() => [AppInfo])
|
||||
apps!: AppInfo[];
|
||||
|
||||
@Field(() => Number)
|
||||
total!: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class AppResponse {
|
||||
@Field(() => App, { nullable: true })
|
||||
app!: App | null;
|
||||
|
||||
@Field(() => AppInfo)
|
||||
info!: AppInfo;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
class AppInputType {
|
||||
@Field(() => String)
|
||||
|
@ -92,4 +107,4 @@ class AppInputType {
|
|||
form!: Record<string, string>;
|
||||
}
|
||||
|
||||
export { ListAppsResonse, AppConfig, AppInputType };
|
||||
export { ListAppsResonse, AppInfo, AppInputType, AppResponse };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql';
|
||||
import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql';
|
||||
import { MyContext } from '../../types';
|
||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
||||
|
||||
|
@ -36,7 +36,6 @@ export default class AuthResolver {
|
|||
return { user };
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => Boolean)
|
||||
logout(@Ctx() { req }: MyContext): boolean {
|
||||
req.session.userId = undefined;
|
||||
|
|
|
@ -27,6 +27,12 @@ const register = async (input: UsernamePasswordInput): Promise<UserResponse> =>
|
|||
throw new Error('Missing email or password');
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
|
||||
|
||||
if (user) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hash = await argon2.hash(password);
|
||||
const newUser = await User.create({ username: username.trim().toLowerCase(), password: hash }).save();
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { Request, Response } from 'express';
|
||||
import publicIp from 'public-ip';
|
||||
import portScanner from 'node-port-scanner';
|
||||
import internalIp from 'internal-ip';
|
||||
|
||||
const isPortOpen = async (req: Request, res: Response<boolean>) => {
|
||||
const { port } = req.params;
|
||||
|
||||
const host = await publicIp.v4();
|
||||
|
||||
const isOpen = await portScanner(host, [port]);
|
||||
|
||||
res.status(200).send(isOpen);
|
||||
};
|
||||
|
||||
const getInternalIp = async (_req: Request, res: Response<string>) => {
|
||||
const ip = await internalIp.v4();
|
||||
|
||||
res.status(200).send(ip);
|
||||
};
|
||||
|
||||
const NetworkController = {
|
||||
isPortOpen,
|
||||
getInternalIp,
|
||||
};
|
||||
|
||||
export default NetworkController;
|
|
@ -1,8 +0,0 @@
|
|||
import { Router } from 'express';
|
||||
import NetworkController from './network.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.route('/internal-ip').get(NetworkController.getInternalIp);
|
||||
|
||||
export default router;
|
|
@ -1,87 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { Request, Response } from 'express';
|
||||
import config from '../../config';
|
||||
import TipiCache from '../../config/TipiCache';
|
||||
import { readJsonFile } from '../fs/fs.helpers';
|
||||
|
||||
type CpuData = {
|
||||
load: number;
|
||||
};
|
||||
|
||||
type DiskData = {
|
||||
total: number;
|
||||
used: number;
|
||||
available: number;
|
||||
};
|
||||
|
||||
type MemoryData = {
|
||||
total: number;
|
||||
available: number;
|
||||
used: number;
|
||||
};
|
||||
|
||||
type SystemInfo = {
|
||||
cpu: CpuData;
|
||||
disk: DiskData;
|
||||
memory: MemoryData;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
const getCpuInfo = async (_req: Request, res: Response<CpuData>) => {
|
||||
const systemInfo: SystemInfo = readJsonFile('/state/system-info.json');
|
||||
|
||||
const cpu = systemInfo.cpu;
|
||||
|
||||
res.status(200).send({ load: cpu.load });
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
const getDiskInfo = async (_req: Request, res: Response<DiskData>) => {
|
||||
const systemInfo: SystemInfo = readJsonFile('/state/system-info.json');
|
||||
|
||||
const result: DiskData = systemInfo.disk;
|
||||
|
||||
res.status(200).send(result);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
const getMemoryInfo = async (_req: Request, res: Response<MemoryData>) => {
|
||||
const systemInfo: SystemInfo = readJsonFile('/state/system-info.json');
|
||||
|
||||
const result: MemoryData = systemInfo.memory;
|
||||
|
||||
res.status(200).json(result);
|
||||
};
|
||||
|
||||
const getVersion = async (_req: Request, res: Response<{ current: string; latest?: string }>) => {
|
||||
try {
|
||||
let version = TipiCache.get<string>('latestVersion');
|
||||
|
||||
if (!version) {
|
||||
const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
|
||||
|
||||
TipiCache.set('latestVersion', data.name);
|
||||
version = data.name.replace('v', '');
|
||||
}
|
||||
|
||||
TipiCache.set('latestVersion', version?.replace('v', ''));
|
||||
|
||||
res.status(200).send({ current: config.VERSION, latest: version?.replace('v', '') });
|
||||
} catch (e) {
|
||||
res.status(500).send({ current: config.VERSION, latest: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
export default { getCpuInfo, getDiskInfo, getMemoryInfo, getVersion };
|
16
packages/system-api/src/modules/system/system.resolver.ts
Normal file
16
packages/system-api/src/modules/system/system.resolver.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Query, Resolver } from 'type-graphql';
|
||||
import SystemService from './system.service';
|
||||
import { SystemInfoResponse, VersionResponse } from './system.types';
|
||||
|
||||
@Resolver()
|
||||
export default class AuthResolver {
|
||||
@Query(() => SystemInfoResponse, { nullable: true })
|
||||
async systemInfo(): Promise<SystemInfoResponse> {
|
||||
return SystemService.systemInfo();
|
||||
}
|
||||
|
||||
@Query(() => String)
|
||||
async version(): Promise<VersionResponse> {
|
||||
return SystemService.getVersion();
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Router } from 'express';
|
||||
import SystemController from './system.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.route('/cpu').get(SystemController.getCpuInfo);
|
||||
router.route('/disk').get(SystemController.getDiskInfo);
|
||||
router.route('/memory').get(SystemController.getMemoryInfo);
|
||||
router.route('/version').get(SystemController.getVersion);
|
||||
|
||||
export default router;
|
52
packages/system-api/src/modules/system/system.service.ts
Normal file
52
packages/system-api/src/modules/system/system.service.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import axios from 'axios';
|
||||
import config from '../../config';
|
||||
import TipiCache from '../../config/TipiCache';
|
||||
import { readJsonFile } from '../fs/fs.helpers';
|
||||
|
||||
type SystemInfo = {
|
||||
cpu: {
|
||||
load: number;
|
||||
};
|
||||
disk: {
|
||||
total: number;
|
||||
used: number;
|
||||
available: number;
|
||||
};
|
||||
memory: {
|
||||
total: number;
|
||||
available: number;
|
||||
used: number;
|
||||
};
|
||||
};
|
||||
|
||||
const systemInfo = (): SystemInfo => {
|
||||
const info: SystemInfo = readJsonFile('/state/system-info.json');
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
const getVersion = async (): Promise<{ current: string; latest?: string }> => {
|
||||
try {
|
||||
let version = TipiCache.get<string>('latestVersion');
|
||||
|
||||
if (!version) {
|
||||
const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
|
||||
|
||||
TipiCache.set('latestVersion', data.name);
|
||||
version = data.name.replace('v', '');
|
||||
}
|
||||
|
||||
TipiCache.set('latestVersion', version?.replace('v', ''));
|
||||
|
||||
return { current: config.VERSION, latest: version?.replace('v', '') };
|
||||
} catch (e) {
|
||||
return { current: config.VERSION, latest: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const SystemService = {
|
||||
systemInfo,
|
||||
getVersion,
|
||||
};
|
||||
|
||||
export default SystemService;
|
42
packages/system-api/src/modules/system/system.types.ts
Normal file
42
packages/system-api/src/modules/system/system.types.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Field, ObjectType } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
class Cpu {
|
||||
@Field(() => Number, { nullable: false })
|
||||
load!: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class DiskMemory {
|
||||
@Field(() => Number, { nullable: false })
|
||||
total!: number;
|
||||
|
||||
@Field(() => Number, { nullable: false })
|
||||
used!: number;
|
||||
|
||||
@Field(() => Number, { nullable: false })
|
||||
available!: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class SystemInfoResponse {
|
||||
@Field(() => Cpu, { nullable: false })
|
||||
cpu!: Cpu;
|
||||
|
||||
@Field(() => DiskMemory, { nullable: false })
|
||||
disk!: DiskMemory;
|
||||
|
||||
@Field(() => DiskMemory, { nullable: false })
|
||||
memory!: DiskMemory;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class VersionResponse {
|
||||
@Field(() => String, { nullable: false })
|
||||
current!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
latest?: string;
|
||||
}
|
||||
|
||||
export { SystemInfoResponse, VersionResponse };
|
|
@ -3,10 +3,11 @@ import { buildSchema } from 'type-graphql';
|
|||
import { customAuthChecker } from './core/middlewares/authChecker';
|
||||
import AppsResolver from './modules/apps/apps.resolver';
|
||||
import AuthResolver from './modules/auth/auth.resolver';
|
||||
import SystemResolver from './modules/system/system.resolver';
|
||||
|
||||
const createSchema = (): Promise<GraphQLSchema> =>
|
||||
buildSchema({
|
||||
resolvers: [AppsResolver, AuthResolver],
|
||||
resolvers: [AppsResolver, AuthResolver, SystemResolver],
|
||||
validate: true,
|
||||
authChecker: customAuthChecker,
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue