implement fetching apps from BE

impl install modal
This commit is contained in:
Davor 2022-07-27 22:39:40 +02:00
parent a3adcfc3da
commit 5b728d35d7
24 changed files with 649 additions and 163 deletions

View file

@ -1,26 +1,40 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import _ from 'lodash';
import { XCircleIcon } from '@heroicons/react/outline';
import { useApps } from 'src/services/apps';
import { Tabs } from 'src/components';
import { appAccessList } from 'src/components/UserModal/consts';
import { AdvancedTab, GeneralTab } from './components';
const pages = [
{ name: 'Apps', to: '/apps', current: true },
{ name: 'Nextcloud', to: '', current: false },
];
const tabs = [
{ name: 'General', component: <GeneralTab /> },
{ name: 'Advanced Configuration', component: <AdvancedTab /> },
];
export const AppSingle: React.FC = () => {
const params = useParams();
const appSlug = params.slug;
const { app, loadApp } = useApps();
const appInfo = _.find(appAccessList, { name: appSlug });
useEffect(() => {
if (appSlug) {
loadApp(appSlug);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appSlug]);
if (!app) {
return null;
}
const appImageSrc = _.find(appAccessList, { name: appSlug })?.image;
const handleAutomaticUpdatesChange = () => {
app.automaticUpdates = !app.automaticUpdates;
};
const tabs = [
{
name: 'General',
component: <GeneralTab automaticUpdates={app.automaticUpdates} onChange={handleAutomaticUpdatesChange} />,
},
{ name: 'Advanced Configuration', component: <AdvancedTab /> },
];
return (
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden lg:flex lg:flex-row">
@ -29,9 +43,9 @@ export const AppSingle: React.FC = () => {
style={{ height: 'fit-content' }}
>
<div className="px-4 py-5 sm:p-6 flex flex-col">
<img className="h-24 w-24 rounded-md overflow-hidden mr-4 mb-3" src={appInfo?.image} alt={appInfo?.label} />
<img className="h-24 w-24 rounded-md overflow-hidden mr-4 mb-3" src={appImageSrc} alt={app.name} />
<div className="mb-3">
<h2 className="text-2xl leading-8 font-bold">{appInfo?.label}</h2>
<h2 className="text-2xl leading-8 font-bold">{app.name}</h2>
<div className="text-sm leading-5 font-medium text-gray-500">Installed on August 25, 2020</div>
</div>
<button

View file

@ -1,19 +1,104 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { SearchIcon } from '@heroicons/react/solid';
import _, { debounce } from 'lodash';
import { Table } from 'src/components';
import { columns, data } from './consts';
import { appAccessList } from 'src/components/UserModal/consts';
import { App, AppStatus, useApps } from 'src/services/apps';
import { AppInstallModal } from './components';
import { getConstForStatus } from './consts';
export const Apps: React.FC = () => {
const [search, setSearch] = useState('');
const [installModalOpen, setInstallModalOpen] = useState(false);
const [appSlug, setAppSlug] = useState(null);
const { apps, appTableLoading, loadApps } = useApps();
const handleSearch = useCallback((event: any) => {
setSearch(event.target.value);
}, []);
const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
useEffect(() => {
loadApps();
return () => {
debouncedSearch.cancel();
};
}, []);
const filterSearch = useMemo(() => {
return data.filter((item: any) => item.name?.toLowerCase().includes(search.toLowerCase()));
}, [search]);
return _.filter(apps, (item) => item.name?.toLowerCase().includes(search.toLowerCase()));
}, [apps, search]);
const columns: any = [
{
Header: 'Name',
accessor: 'name',
Cell: (e: any) => {
const app = e.cell.row.original as App;
const imageSrc = _.find(appAccessList, { name: app.slug })?.image;
return (
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<img className="h-10 w-10 rounded-md overflow-hidden" src={imageSrc} alt={app.name} />
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{app.name}</div>
</div>
</div>
);
},
width: 'auto',
},
{
Header: 'Status',
accessor: 'status',
Cell: (e: any) => {
const status = e.cell.row.original.status as AppStatus;
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>
</div>
);
},
width: 'auto',
},
{
Header: ' ',
Cell: (e: any) => {
const navigate = useNavigate();
const appStatus = e.cell.row.original.status as AppStatus;
if (appStatus === AppStatus.Installing) {
return null;
}
const { slug } = e.cell.row.original;
let buttonFuntion = () => navigate(`/apps/${slug}`);
if (appStatus === AppStatus.NotInstalled) {
buttonFuntion = () => {
setAppSlug(slug);
setInstallModalOpen(true);
};
}
return (
<div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={buttonFuntion}
type="button"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{getConstForStatus(appStatus, 'buttonIcon')}
{getConstForStatus(appStatus, 'buttonTitle')}
</button>
</div>
);
},
width: 'auto',
},
];
return (
<div className="relative">
@ -51,12 +136,18 @@ export const Apps: React.FC = () => {
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow border-b border-gray-200 sm:rounded-lg">
<Table data={filterSearch} columns={columns} />
<Table
data={_.filter(filterSearch, (app) => app.slug !== 'dashboard') as any}
columns={columns}
loading={appTableLoading}
/>
</div>
</div>
</div>
</div>
</div>
<AppInstallModal appSlug={appSlug} onClose={() => setInstallModalOpen(false)} open={installModalOpen} />
</div>
);
};

View file

@ -22,6 +22,38 @@ export const AdvancedTab = () => {
<h1 className="text-2xl leading-6 font-medium text-gray-900">Configuration</h1>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-8">
<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>
</div>
<div className="px-4 py-5 sm:p-6 overflow-x-auto">
<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 className="px-4 h-16 sm:px-6 bg-gray-200 flex justify-between items-center rounded-t-lg">
@ -102,38 +134,6 @@ export const AdvancedTab = () => {
</div>
</div>
</div>
<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>
</div>
<div className="px-4 py-5 sm:p-6 overflow-x-auto">
<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 className="flex justify-end mt-10">
<button

View file

@ -0,0 +1,136 @@
import React, { useEffect, useState } from 'react';
import Editor from 'react-simple-code-editor';
import { highlight, languages } from 'prismjs';
import _ from 'lodash';
import { useApps } from 'src/services/apps';
import { Modal, Tabs } from 'src/components';
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`;
export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps) => {
const [code, setCode] = useState(initialCode);
const { app, appLoading, loadApp } = useApps();
useEffect(() => {
if (appSlug) {
loadApp(appSlug);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appSlug, open]);
const handleSave = async () => {
_.noop();
// todo: implement
};
const handleKeyPress = (e: any) => {
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
handleSave();
}
};
const renderSubdomain = () => {
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} />
</div>
</div>
);
};
const renderConfiguration = () => {
return (
<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>
</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>
</div>
</div>
</div>
);
};
const tabs = [
{ name: 'Subdomain', component: renderSubdomain() },
{ name: 'Advanced Configuration', component: renderConfiguration() },
];
const handleClose = () => {
onClose();
};
return (
<>
<Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={appLoading} useCancelButton>
<div className="bg-white px-4">
<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>
</div>
<div className="px-4 py-5 sm:p-6">
<Tabs tabs={tabs} />
</div>
</div>
</div>
</div>
</Modal>
</>
);
};

View file

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

View file

@ -0,0 +1,5 @@
export type AppInstallModalProps = {
open: boolean;
onClose: () => void;
appSlug: string | null;
};

View file

@ -1,13 +1,16 @@
import React, { useState } from 'react';
import React from 'react';
import { Switch } from '@headlessui/react';
function classNames(...classes: any) {
return classes.filter(Boolean).join(' ');
}
export const GeneralTab = () => {
const [enabled, setEnabled] = useState(false);
interface GeneralTabProps {
automaticUpdates: boolean;
onChange: () => void;
}
export const GeneralTab = ({ automaticUpdates, onChange }: GeneralTabProps) => {
return (
<div className="py-4">
<div className="md:grid md:grid-cols-3 md:gap-6">
@ -20,17 +23,17 @@ export const GeneralTab = () => {
</div>
<div className="my-auto ml-auto">
<Switch
checked={enabled}
onChange={setEnabled}
checked={automaticUpdates}
onChange={onChange}
className={classNames(
enabled ? 'bg-primary-600' : 'bg-gray-200',
automaticUpdates ? 'bg-primary-600' : 'bg-gray-200',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none',
)}
>
<span
aria-hidden="true"
className={classNames(
enabled ? 'translate-x-5' : 'translate-x-0',
automaticUpdates ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
)}
/>

View file

@ -1,2 +1,3 @@
export { GeneralTab } from './GeneralTab';
export { AdvancedTab } from './AdvancedTab';
export { AppInstallModal } from './AppInstallModal';

View file

@ -1,7 +1,7 @@
import React from 'react';
import { CogIcon, PlusCircleIcon } from '@heroicons/react/outline';
import _ from 'lodash';
import { useNavigate } from 'react-router';
import { AppStatus } from 'src/services/apps';
export const initialEditorYaml = () => {
return `luck: except
@ -27,12 +27,6 @@ search:
great: stomach`;
};
export enum AppStatus {
NotInstalled = 'Not installed',
Installed = 'Installed',
Installing = 'Installing',
}
const tableConsts = [
{
status: AppStatus.Installed,
@ -54,104 +48,7 @@ const tableConsts = [
},
];
const getConstForStatus = (appStatus: AppStatus, paramName: string) => {
export const getConstForStatus = (appStatus: AppStatus, paramName: string) => {
const tableConst = _.find(tableConsts, { status: appStatus });
return _.get(tableConst, paramName);
};
export const columns: any = [
{
Header: 'Name',
accessor: 'name',
Cell: (e: any) => {
return (
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<img className="h-10 w-10 rounded-md overflow-hidden" src={e.cell.row.original.assetSrc} alt="" />
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{e.cell.row.original.name}</div>
</div>
</div>
);
},
width: 'auto',
},
{
Header: 'Status',
accessor: 'status',
Cell: (e: any) => {
const appStatus = e.cell.row.original.status as AppStatus;
return (
<div className="flex items-center">
<div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(appStatus, 'colorClass')}`} />
<div className={`ml-2 text-sm text-${getConstForStatus(appStatus, 'colorClass')}`}>{appStatus}</div>
</div>
);
},
width: 'auto',
},
{
Header: ' ',
Cell: (e: any) => {
const navigate = useNavigate();
const appStatus = e.cell.row.original.status as AppStatus;
const appSlug = e.cell.row.original.slug;
if (appStatus === AppStatus.Installing) {
return null;
}
return (
<div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => navigate(`/apps/${appSlug}`)}
type="button"
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{getConstForStatus(appStatus, 'buttonIcon')}
{getConstForStatus(appStatus, 'buttonTitle')}
</button>
</div>
);
},
width: 'auto',
},
];
export const data: any[] = [
{
id: 1,
name: 'Nextcloud',
slug: 'nextcloud',
status: AppStatus.Installed,
assetSrc: './assets/nextcloud.svg',
},
{
id: 2,
name: 'Wekan',
slug: 'wekan',
status: AppStatus.Installing,
assetSrc: './assets/wekan.svg',
},
{
id: 3,
name: 'Zulip',
slug: 'zulip',
status: AppStatus.NotInstalled,
assetSrc: './assets/zulip.svg',
},
{
id: 4,
name: 'Wordpress',
slug: 'wordpress',
status: AppStatus.Installed,
assetSrc: './assets/wordpress.svg',
},
];
export interface AppInfo {
id: number;
name: string;
slug: string;
status: AppStatus;
assetSrc: string;
}