implement code editor as a component

add notif on status 'Installing' click
modify AppInstallModal
This commit is contained in:
Davor 2022-07-28 23:48:55 +02:00
parent 5b728d35d7
commit 3edc690075
13 changed files with 152 additions and 76 deletions

View file

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

View file

@ -0,0 +1 @@
export { CodeEditor } from './CodeEditor';

View file

@ -1,3 +1,4 @@
export { Input } from './Input'; export { Input } from './Input';
export { Select } from './Select'; export { Select } from './Select';
export { Switch } from './Switch'; export { Switch } from './Switch';
export { CodeEditor } from './CodeEditor';

View file

@ -166,7 +166,7 @@ export const Table = <T extends Record<string, unknown>>({
/> />
</svg> </svg>
</div> </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> </div>
</td> </td>
</tr> </tr>

View file

@ -5,21 +5,25 @@ export const appAccessList = [
name: 'wekan', name: 'wekan',
image: '/assets/wekan.svg', image: '/assets/wekan.svg',
label: 'Wekan', label: 'Wekan',
defaultSubdomain: 'wekan.{domain}',
}, },
{ {
name: 'wordpress', name: 'wordpress',
image: '/assets/wordpress.svg', image: '/assets/wordpress.svg',
label: 'Wordpress', label: 'Wordpress',
defaultSubdomain: 'www.{domain}',
}, },
{ {
name: 'nextcloud', name: 'nextcloud',
image: '/assets/nextcloud.svg', image: '/assets/nextcloud.svg',
label: 'Nextcloud', label: 'Nextcloud',
defaultSubdomain: 'files.{domain}',
}, },
{ {
name: 'zulip', name: 'zulip',
image: '/assets/zulip.svg', image: '/assets/zulip.svg',
label: 'Zulip', label: 'Zulip',
defaultSubdomain: 'zulip.{domain}',
}, },
]; ];

View file

@ -2,6 +2,7 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react'; import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { SearchIcon } from '@heroicons/react/solid'; import { SearchIcon } from '@heroicons/react/solid';
import { showToast, ToastType } from 'src/common/util/show-toast';
import _, { debounce } from 'lodash'; import _, { debounce } from 'lodash';
import { Table } from 'src/components'; import { Table } from 'src/components';
import { appAccessList } from 'src/components/UserModal/consts'; import { appAccessList } from 'src/components/UserModal/consts';
@ -61,7 +62,16 @@ export const Apps: React.FC = () => {
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} /> <div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} />
{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 className={`ml-2 text-sm text-${getConstForStatus(status, 'colorClass')}`}>{status}</div>
)}
</div> </div>
); );
}, },
@ -147,7 +157,9 @@ export const Apps: React.FC = () => {
</div> </div>
</div> </div>
{installModalOpen && (
<AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} /> <AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} />
)}
</div> </div>
); );
}; };

View file

@ -1,36 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Editor from 'react-simple-code-editor'; import { useForm } from 'react-hook-form';
import { highlight, languages } from 'prismjs';
import _ from 'lodash'; import _ from 'lodash';
import { useApps } from 'src/services/apps'; import { AppForm, useApps } from 'src/services/apps';
import { Modal, Tabs } from 'src/components'; import { Modal, Tabs } from 'src/components';
import { CodeEditor, Input } from 'src/components/Form';
import { appAccessList } from 'src/components/UserModal/consts';
import { AppInstallModalProps } from './types'; import { AppInstallModalProps } from './types';
import { initialAppForm, initialCode } from './consts';
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 AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps) => { export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps) => {
const [code, setCode] = useState(initialCode); const [appName, setAppName] = useState('');
const { app, appLoading, loadApp } = useApps(); const { app, appLoading, installApp, loadApp, clearSelectedApp } = useApps();
const { control, reset, handleSubmit } = useForm<AppForm>({
defaultValues: initialAppForm,
});
const getDefaultSubdomain = () => _.get(_.find(appAccessList, ['name', appSlug]), 'defaultSubdomain', '');
useEffect(() => { useEffect(() => {
if (appSlug) { if (appSlug) {
@ -39,9 +25,31 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [appSlug, open]); }, [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 () => { const handleSave = async () => {
_.noop(); try {
// todo: implement await handleSubmit((data) => installApp(data))();
} catch (e: any) {
// Continue
}
handleClose();
}; };
const handleKeyPress = (e: any) => { const handleKeyPress = (e: any) => {
@ -54,7 +62,7 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps
return ( return (
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6"> <div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3"> <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>
</div> </div>
); );
@ -65,40 +73,10 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps
<div> <div>
<div className="bg-gray-100 overflow-hidden rounded-lg"> <div className="bg-gray-100 overflow-hidden rounded-lg">
<div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center"> <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>
<div className="px-4 py-5 sm:p-6 overflow-x-auto"> <div className="px-4 py-5 sm:p-6 overflow-x-auto">
<Editor <CodeEditor control={control} name="configuration" />
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>
</div> </div>
</div> </div>
</div> </div>
@ -110,10 +88,6 @@ search:
{ name: 'Advanced Configuration', component: renderConfiguration() }, { name: 'Advanced Configuration', component: renderConfiguration() },
]; ];
const handleClose = () => {
onClose();
};
return ( return (
<> <>
<Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={appLoading} useCancelButton> <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 className="space-y-10 divide-y divide-gray-200">
<div> <div>
<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>
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">

View file

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

View file

@ -1,5 +1,5 @@
import { useDispatch, useSelector } from 'react-redux'; 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'; import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors';
export function useApps() { export function useApps() {
@ -25,6 +25,10 @@ export function useApps() {
return dispatch(installAppBySlug(data)); return dispatch(installAppBySlug(data));
} }
function clearSelectedApp() {
return dispatch(clearCurrentApp());
}
return { return {
apps, apps,
app, app,
@ -34,5 +38,6 @@ export function useApps() {
appLoading, appLoading,
appTableLoading, appTableLoading,
installApp, installApp,
clearSelectedApp,
}; };
} }

View file

@ -8,6 +8,7 @@ export enum AppActionTypes {
FETCH_APP = 'apps/fetch_app', FETCH_APP = 'apps/fetch_app',
UPDATE_APP = 'apps/update_app', UPDATE_APP = 'apps/update_app',
INSTALL_APP = 'apps/install_app', INSTALL_APP = 'apps/install_app',
CLEAR_APP = 'apps/clear_app',
SET_APP_LOADING = 'apps/app_loading', SET_APP_LOADING = 'apps/app_loading',
SET_APPS_LOADING = 'apps/apps_loading', SET_APPS_LOADING = 'apps/apps_loading',
} }
@ -97,7 +98,7 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch<any>) =>
try { try {
const { data } = await performApiCall({ const { data } = await performApiCall({
path: `/apps/${app.slug}/install`, path: `/apps/${app.slug}/install`,
method: 'POST', method: 'PATCH',
body: transformInstallAppRequest(app), body: transformInstallAppRequest(app),
}); });
@ -117,3 +118,10 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch<any>) =>
dispatch(setAppLoading(false)); dispatch(setAppLoading(false));
}; };
export const clearCurrentApp = () => (dispatch: Dispatch<any>) => {
dispatch({
type: AppActionTypes.CLEAR_APP,
payload: {},
});
};

View file

@ -31,6 +31,11 @@ const appsReducer = (state: any = initialUsersState, action: any) => {
...state, ...state,
currentApp: action.payload, currentApp: action.payload,
}; };
case AppActionTypes.CLEAR_APP:
return {
...state,
currentApp: {},
};
default: default:
return state; return state;
} }

View file

@ -1,4 +1,4 @@
import { App, AppStatus, FormApp } from './types'; import { App, AppStatus, AppForm } from './types';
const transformAppStatus = (status: string) => { const transformAppStatus = (status: string) => {
switch (status) { switch (status) {
@ -24,14 +24,15 @@ export const transformApp = (response: any): App => {
}; };
}; };
export const transformAppRequest = (data: FormApp) => { export const transformAppRequest = (data: AppForm) => {
return { return {
automatic_updates: data.automaticUpdates, automatic_updates: data.automaticUpdates,
}; };
}; };
export const transformInstallAppRequest = (data: FormApp) => { export const transformInstallAppRequest = (data: AppForm) => {
return { return {
subdomain: data.subdomain, subdomain: data.subdomain,
configuration: data.configuration,
}; };
}; };

View file

@ -7,7 +7,7 @@ export interface App {
automaticUpdates: boolean; automaticUpdates: boolean;
} }
export interface FormApp extends App { export interface AppForm extends App {
configuration: string; configuration: string;
} }