implement code editor as a component
add notif on status 'Installing' click modify AppInstallModal
This commit is contained in:
parent
5b728d35d7
commit
3edc690075
13 changed files with 152 additions and 76 deletions
37
src/components/Form/CodeEditor/CodeEditor.tsx
Normal file
37
src/components/Form/CodeEditor/CodeEditor.tsx
Normal 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;
|
||||||
|
};
|
1
src/components/Form/CodeEditor/index.ts
Normal file
1
src/components/Form/CodeEditor/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { CodeEditor } from './CodeEditor';
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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')}`} />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -147,7 +157,9 @@ export const Apps: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} />
|
{installModalOpen && (
|
||||||
|
<AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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">
|
||||||
|
|
28
src/modules/apps/components/AppInstallModal/consts.ts
Normal file
28
src/modules/apps/components/AppInstallModal/consts.ts
Normal 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;
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue