diff --git a/src/components/Form/CodeEditor/CodeEditor.tsx b/src/components/Form/CodeEditor/CodeEditor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e0ad33d42c6ab8305d69c99c2e39fd112dd55867 --- /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 ( + <> + <Editor + value={field.value} + onValueChange={field.onChange} + 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" + /> + </> + ); +}; + +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 0000000000000000000000000000000000000000..9f6648589e9fda8625b1ae948da0337d71bb8efa --- /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 1fc764c889bf826e01b8169210b45fe9f51429cb..5276586281294c676e305d9e61c3f560a0574392 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 157b72896aea57e98df4683667a6346fa7dcbe1c..95f22567a3a0760f239c38ab0f8a03b6f1e8840c 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -166,7 +166,7 @@ export const Table = <T extends Record<string, unknown>>({ /> </svg> </div> - <p className="text-sm text-primary-600 mt-2">Loading users</p> + <p className="text-sm text-primary-600 mt-2">Loading...</p> </div> </td> </tr> diff --git a/src/components/UserModal/consts.ts b/src/components/UserModal/consts.ts index 7304a419c1f98dd2c6ebced01760f86fda475b81..3d36fc70744ef1f460f8484cabab9df95be2eee9 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 74039a0f38cced7b3e0a7cc92aa5b465ad1c6cda..945146ab22b137330da67347a43642702c9161e3 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 ( <div className="flex items-center"> <div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} /> - <div className={`ml-2 text-sm text-${getConstForStatus(status, 'colorClass')}`}>{status}</div> + {status === AppStatus.Installing ? ( + <div + className={`ml-2 cursor-pointer text-sm text-${getConstForStatus(status, 'colorClass')}`} + onClick={() => showToast('Installing an app can take up to 10 minutes.', ToastType.Success)} + > + {status} + </div> + ) : ( + <div className={`ml-2 text-sm text-${getConstForStatus(status, 'colorClass')}`}>{status}</div> + )} </div> ); }, @@ -147,7 +157,9 @@ export const Apps: React.FC = () => { </div> </div> - <AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} /> + {installModalOpen && ( + <AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} /> + )} </div> ); }; diff --git a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx index e69937ede9c7f6133f4e4c2a07415ca07ccdf199..4a2b925df9314c6269489f50f32ea5f8c942e227 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<AppForm>({ + 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 ( <div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6"> <div className="sm:col-span-3"> - <input name="subdomain" placeholder="Subdomain" onKeyPress={handleKeyPress} required={false} /> + <Input control={control} name="subdomain" label="Subdomain" onKeyPress={handleKeyPress} required={false} /> </div> </div> ); @@ -65,40 +73,10 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps <div> <div className="bg-gray-100 overflow-hidden rounded-lg"> <div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center"> - <span className="text-gray-600 text-lg leading-6 font-medium">Current Configuration</span> + <span className="text-gray-600 text-lg leading-6 font-medium">App Configuration</span> </div> <div className="px-4 py-5 sm:p-6 overflow-x-auto"> - <Editor - value={code} - onValueChange={(value) => 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" - /> - <pre 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`} - </pre> + <CodeEditor control={control} name="configuration" /> </div> </div> </div> @@ -110,10 +88,6 @@ search: { name: 'Advanced Configuration', component: renderConfiguration() }, ]; - const handleClose = () => { - onClose(); - }; - return ( <> <Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={appLoading} useCancelButton> @@ -121,7 +95,7 @@ search: <div className="space-y-10 divide-y divide-gray-200"> <div> <div> - <h3 className="text-lg leading-6 font-medium text-gray-900">Install app {_.get(app, 'name')}</h3> + <h3 className="text-lg leading-6 font-medium text-gray-900">Install app {appName}</h3> </div> <div className="px-4 py-5 sm:p-6"> diff --git a/src/modules/apps/components/AppInstallModal/consts.ts b/src/modules/apps/components/AppInstallModal/consts.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee4b0b4682866a8763eb8dd5cfec1e8694824692 --- /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 135fb045217c541d65c184638b49de1d800186c2..9137f36d0cd3dc3a99ac3a2c6d7a26ae65049d59 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 69c00ec3e550968bbdf3bd77d7b186d9abbdfaf0..47c365f2864cbfb510f781ae80c8973f64481dba 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<any>) => 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<any>) => dispatch(setAppLoading(false)); }; + +export const clearCurrentApp = () => (dispatch: Dispatch<any>) => { + dispatch({ + type: AppActionTypes.CLEAR_APP, + payload: {}, + }); +}; diff --git a/src/services/apps/redux/reducers.ts b/src/services/apps/redux/reducers.ts index 0258928d44d5cb88903974dd2067192e25189912..c978377777caba46f3e5528d1841c4c374011259 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 fd41caab382cf8c860addb29b5195fb2c3726181..0d66b28da62cabb628ce275d592569e52efd66e4 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 d93e1ca135a15d0bbc5eaade13b0944d89b8c53a..76a418f4374d9f5d086fd1c247cc365aef7f7eb3 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; }