diff --git a/src/components/Form/CodeEditor/CodeEditor.tsx b/src/components/Form/CodeEditor/CodeEditor.tsx new file mode 100644 index 0000000..e0ad33d --- /dev/null +++ b/src/components/Form/CodeEditor/CodeEditor.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { highlight, languages } from 'prismjs'; +import { useController } from 'react-hook-form'; +import Editor from 'react-simple-code-editor'; + +/* eslint-disable react/react-in-jsx-scope */ +export const CodeEditor = ({ control, name, required }: CodeEditorProps) => { + const { + field, + // fieldState: { invalid, isTouched, isDirty }, + // formState: { touchedFields, dirtyFields }, + } = useController({ + name, + control, + rules: { required }, + defaultValue: '', + }); + + return ( + <> + highlight(value, languages.js, 'yaml')} + preClassName="font-mono whitespace-normal font-light" + textareaClassName="font-mono overflow-auto font-light" + className="font-mono text-sm font-light" + /> + + ); +}; + +type CodeEditorProps = { + control: any; + name: string; + required?: boolean; +}; diff --git a/src/components/Form/CodeEditor/index.ts b/src/components/Form/CodeEditor/index.ts new file mode 100644 index 0000000..9f66485 --- /dev/null +++ b/src/components/Form/CodeEditor/index.ts @@ -0,0 +1 @@ +export { CodeEditor } from './CodeEditor'; diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index 1fc764c..5276586 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -1,3 +1,4 @@ export { Input } from './Input'; export { Select } from './Select'; export { Switch } from './Switch'; +export { CodeEditor } from './CodeEditor'; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 157b728..95f2256 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -166,7 +166,7 @@ export const Table = >({ /> -

Loading users

+

Loading...

diff --git a/src/components/UserModal/consts.ts b/src/components/UserModal/consts.ts index 7304a41..3d36fc7 100644 --- a/src/components/UserModal/consts.ts +++ b/src/components/UserModal/consts.ts @@ -5,21 +5,25 @@ export const appAccessList = [ name: 'wekan', image: '/assets/wekan.svg', label: 'Wekan', + defaultSubdomain: 'wekan.{domain}', }, { name: 'wordpress', image: '/assets/wordpress.svg', label: 'Wordpress', + defaultSubdomain: 'www.{domain}', }, { name: 'nextcloud', image: '/assets/nextcloud.svg', label: 'Nextcloud', + defaultSubdomain: 'files.{domain}', }, { name: 'zulip', image: '/assets/zulip.svg', label: 'Zulip', + defaultSubdomain: 'zulip.{domain}', }, ]; diff --git a/src/modules/apps/Apps.tsx b/src/modules/apps/Apps.tsx index 74039a0..945146a 100644 --- a/src/modules/apps/Apps.tsx +++ b/src/modules/apps/Apps.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useNavigate } from 'react-router'; import { SearchIcon } from '@heroicons/react/solid'; +import { showToast, ToastType } from 'src/common/util/show-toast'; import _, { debounce } from 'lodash'; import { Table } from 'src/components'; import { appAccessList } from 'src/components/UserModal/consts'; @@ -61,7 +62,16 @@ export const Apps: React.FC = () => { return (
-
{status}
+ {status === AppStatus.Installing ? ( +
showToast('Installing an app can take up to 10 minutes.', ToastType.Success)} + > + {status} +
+ ) : ( +
{status}
+ )}
); }, @@ -147,7 +157,9 @@ export const Apps: React.FC = () => {
- setInstallModalOpen(false)} open={installModalOpen} /> + {installModalOpen && ( + setInstallModalOpen(false)} open={installModalOpen} /> + )} ); }; diff --git a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx index e69937e..4a2b925 100644 --- a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx +++ b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx @@ -1,36 +1,22 @@ import React, { useEffect, useState } from 'react'; -import Editor from 'react-simple-code-editor'; -import { highlight, languages } from 'prismjs'; +import { useForm } from 'react-hook-form'; import _ from 'lodash'; -import { useApps } from 'src/services/apps'; +import { AppForm, useApps } from 'src/services/apps'; import { Modal, Tabs } from 'src/components'; +import { CodeEditor, Input } from 'src/components/Form'; +import { appAccessList } from 'src/components/UserModal/consts'; import { AppInstallModalProps } from './types'; - -const initialCode = `luck: except -natural: still -near: though -search: - - feature - - - 1980732354.689713 - - hour - - butter: - ordinary: 995901949.8974948 - teeth: true - whole: - - -952367353 - - - talk: -1773961379 - temperature: false - oxygen: true - laugh: - flag: - in: 2144751662 - hospital: -1544066384.1973226 - law: congress - great: stomach`; +import { initialAppForm, initialCode } from './consts'; export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps) => { - const [code, setCode] = useState(initialCode); - const { app, appLoading, loadApp } = useApps(); + const [appName, setAppName] = useState(''); + const { app, appLoading, installApp, loadApp, clearSelectedApp } = useApps(); + + const { control, reset, handleSubmit } = useForm({ + defaultValues: initialAppForm, + }); + + const getDefaultSubdomain = () => _.get(_.find(appAccessList, ['name', appSlug]), 'defaultSubdomain', ''); useEffect(() => { if (appSlug) { @@ -39,9 +25,31 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps // eslint-disable-next-line react-hooks/exhaustive-deps }, [appSlug, open]); + useEffect(() => { + if (!_.isEmpty(app)) { + setAppName(app.name); + reset({ subdomain: getDefaultSubdomain(), configuration: initialCode }); + } + + return () => { + reset({ subdomain: getDefaultSubdomain(), configuration: initialCode }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [app, reset, open]); + + const handleClose = () => { + clearSelectedApp(); + reset(); + onClose(); + }; + const handleSave = async () => { - _.noop(); - // todo: implement + try { + await handleSubmit((data) => installApp(data))(); + } catch (e: any) { + // Continue + } + handleClose(); }; const handleKeyPress = (e: any) => { @@ -54,7 +62,7 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps return (
- +
); @@ -65,40 +73,10 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps
- Current Configuration + App Configuration
- setCode(value)} - highlight={(value) => highlight(value, languages.js, 'yaml')} - preClassName="font-mono whitespace-normal font-light" - textareaClassName="font-mono overflow-auto font-light" - className="font-mono text-sm font-light" - /> -
-              {`luck: except
-natural: still
-near: though
-search:
-  - feature
-  - - 1980732354.689713
-    - hour
-    - butter:
-        ordinary: 995901949.8974948
-        teeth: true
-        whole:
-          - -952367353
-          - - talk: -1773961379
-              temperature: false
-              oxygen: true
-              laugh:
-                flag:
-                  in: 2144751662
-                  hospital: -1544066384.1973226
-                  law: congress
-                  great: stomach`}
-            
+
@@ -110,10 +88,6 @@ search: { name: 'Advanced Configuration', component: renderConfiguration() }, ]; - const handleClose = () => { - onClose(); - }; - return ( <> @@ -121,7 +95,7 @@ search:
-

Install app {_.get(app, 'name')}

+

Install app {appName}

diff --git a/src/modules/apps/components/AppInstallModal/consts.ts b/src/modules/apps/components/AppInstallModal/consts.ts new file mode 100644 index 0000000..ee4b0b4 --- /dev/null +++ b/src/modules/apps/components/AppInstallModal/consts.ts @@ -0,0 +1,28 @@ +import { AppForm } from 'src/services/apps'; + +export const initialCode = `luck: except +natural: still +near: though +search: + - feature + - - 1980732354.689713 + - hour + - butter: + ordinary: 995901949.8974948 + teeth: true + whole: + - -952367353 + - - talk: -1773961379 + temperature: false + oxygen: true + laugh: + flag: + in: 2144751662 + hospital: -1544066384.1973226 + law: congress + great: stomach`; + +export const initialAppForm = { + subdomain: '', + configuration: initialCode, +} as AppForm; diff --git a/src/services/apps/hooks/use-apps.ts b/src/services/apps/hooks/use-apps.ts index 135fb04..9137f36 100644 --- a/src/services/apps/hooks/use-apps.ts +++ b/src/services/apps/hooks/use-apps.ts @@ -1,5 +1,5 @@ import { useDispatch, useSelector } from 'react-redux'; -import { fetchApps, fetchAppBySlug, updateAppBySlug, installAppBySlug } from '../redux'; +import { fetchApps, fetchAppBySlug, updateAppBySlug, installAppBySlug, clearCurrentApp } from '../redux'; import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors'; export function useApps() { @@ -25,6 +25,10 @@ export function useApps() { return dispatch(installAppBySlug(data)); } + function clearSelectedApp() { + return dispatch(clearCurrentApp()); + } + return { apps, app, @@ -34,5 +38,6 @@ export function useApps() { appLoading, appTableLoading, installApp, + clearSelectedApp, }; } diff --git a/src/services/apps/redux/actions.ts b/src/services/apps/redux/actions.ts index 69c00ec..47c365f 100644 --- a/src/services/apps/redux/actions.ts +++ b/src/services/apps/redux/actions.ts @@ -8,6 +8,7 @@ export enum AppActionTypes { FETCH_APP = 'apps/fetch_app', UPDATE_APP = 'apps/update_app', INSTALL_APP = 'apps/install_app', + CLEAR_APP = 'apps/clear_app', SET_APP_LOADING = 'apps/app_loading', SET_APPS_LOADING = 'apps/apps_loading', } @@ -97,7 +98,7 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch) => try { const { data } = await performApiCall({ path: `/apps/${app.slug}/install`, - method: 'POST', + method: 'PATCH', body: transformInstallAppRequest(app), }); @@ -117,3 +118,10 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch) => dispatch(setAppLoading(false)); }; + +export const clearCurrentApp = () => (dispatch: Dispatch) => { + dispatch({ + type: AppActionTypes.CLEAR_APP, + payload: {}, + }); +}; diff --git a/src/services/apps/redux/reducers.ts b/src/services/apps/redux/reducers.ts index 0258928..c978377 100644 --- a/src/services/apps/redux/reducers.ts +++ b/src/services/apps/redux/reducers.ts @@ -31,6 +31,11 @@ const appsReducer = (state: any = initialUsersState, action: any) => { ...state, currentApp: action.payload, }; + case AppActionTypes.CLEAR_APP: + return { + ...state, + currentApp: {}, + }; default: return state; } diff --git a/src/services/apps/transformations.ts b/src/services/apps/transformations.ts index fd41caa..0d66b28 100644 --- a/src/services/apps/transformations.ts +++ b/src/services/apps/transformations.ts @@ -1,4 +1,4 @@ -import { App, AppStatus, FormApp } from './types'; +import { App, AppStatus, AppForm } from './types'; const transformAppStatus = (status: string) => { switch (status) { @@ -24,14 +24,15 @@ export const transformApp = (response: any): App => { }; }; -export const transformAppRequest = (data: FormApp) => { +export const transformAppRequest = (data: AppForm) => { return { automatic_updates: data.automaticUpdates, }; }; -export const transformInstallAppRequest = (data: FormApp) => { +export const transformInstallAppRequest = (data: AppForm) => { return { subdomain: data.subdomain, + configuration: data.configuration, }; }; diff --git a/src/services/apps/types.ts b/src/services/apps/types.ts index d93e1ca..76a418f 100644 --- a/src/services/apps/types.ts +++ b/src/services/apps/types.ts @@ -7,7 +7,7 @@ export interface App { automaticUpdates: boolean; } -export interface FormApp extends App { +export interface AppForm extends App { configuration: string; }