diff --git a/src/components/Form/Switch/Switch.tsx b/src/components/Form/Switch/Switch.tsx new file mode 100644 index 0000000..4c10780 --- /dev/null +++ b/src/components/Form/Switch/Switch.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useController } from 'react-hook-form'; +import { Switch as HeadlessSwitch } from '@headlessui/react'; + +function classNames(...classes: any) { + return classes.filter(Boolean).join(' '); +} +/* eslint-disable react/react-in-jsx-scope */ +export const Switch = ({ control, name, label, required }: SwitchProps) => { + const { + field, + // fieldState: { invalid, isTouched, isDirty }, + // formState: { touchedFields, dirtyFields }, + } = useController({ + name, + control, + rules: { required }, + defaultValue: '', + }); + + return ( + <> + {label && ( + + )} + + + + ); +}; + +type SwitchProps = { + control: any; + name: string; + label?: string; + required?: boolean; +}; diff --git a/src/components/Form/Switch/index.ts b/src/components/Form/Switch/index.ts new file mode 100644 index 0000000..cee89a1 --- /dev/null +++ b/src/components/Form/Switch/index.ts @@ -0,0 +1 @@ +export { Switch } from './Switch'; diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index ad493dd..1fc764c 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -1,2 +1,3 @@ export { Input } from './Input'; export { Select } from './Select'; +export { Switch } from './Switch'; diff --git a/src/modules/apps/AppSingle.tsx b/src/modules/apps/AppSingle.tsx index a3602e9..79c0cc5 100644 --- a/src/modules/apps/AppSingle.tsx +++ b/src/modules/apps/AppSingle.tsx @@ -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: }, - { name: 'Advanced Configuration', component: }, -]; - 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: , + }, + { name: 'Advanced Configuration', component: }, + ]; return (
@@ -29,9 +43,9 @@ export const AppSingle: React.FC = () => { style={{ height: 'fit-content' }} >
- {appInfo?.label} + {app.name}
-

{appInfo?.label}

+

{app.name}

Installed on August 25, 2020
+
+ ); + }, + width: 'auto', + }, + ]; return (
@@ -51,12 +136,18 @@ export const Apps: React.FC = () => {
- +
app.slug !== 'dashboard') as any} + columns={columns} + loading={appTableLoading} + /> + + setInstallModalOpen(false)} open={installModalOpen} /> ); }; diff --git a/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx b/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx index a3b7c34..b3fb2f6 100644 --- a/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx +++ b/src/modules/apps/components/AdvancedTab/AdvancedTab.tsx @@ -22,6 +22,38 @@ export const AdvancedTab = () => {

Configuration

+
+
+
+ Current Configuration +
+
+
+                {`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`}
+              
+
+
+
@@ -102,38 +134,6 @@ export const AdvancedTab = () => {
-
-
-
- Current Configuration -
-
-
-                {`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`}
-              
-
-
-
-
- ); - }, - 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; -} diff --git a/src/redux/store.ts b/src/redux/store.ts index ff46366..8e1c58d 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -6,6 +6,7 @@ import storage from 'redux-persist/lib/storage'; import { reducer as authReducer } from 'src/services/auth'; import usersReducer from 'src/services/users/redux/reducers'; +import appsReducer from 'src/services/apps/redux/reducers'; import { State } from './types'; const persistConfig = { @@ -17,6 +18,7 @@ const persistConfig = { const appReducer = combineReducers({ auth: authReducer, users: usersReducer, + apps: appsReducer, }); const persistedReducer = persistReducer(persistConfig, appReducer); diff --git a/src/redux/types.ts b/src/redux/types.ts index 4d42236..8505796 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -2,10 +2,12 @@ import { Store } from 'redux'; import { AuthState } from 'src/services/auth/redux'; import { UsersState } from 'src/services/users/redux'; +import { AppsState } from 'src/services/apps/redux'; export interface AppStore extends Store, State {} export interface State { auth: AuthState; users: UsersState; + apps: AppsState; } diff --git a/src/services/apps/hooks/index.ts b/src/services/apps/hooks/index.ts new file mode 100644 index 0000000..f3648ce --- /dev/null +++ b/src/services/apps/hooks/index.ts @@ -0,0 +1 @@ +export { useApps } from './use-apps'; diff --git a/src/services/apps/hooks/use-apps.ts b/src/services/apps/hooks/use-apps.ts new file mode 100644 index 0000000..135fb04 --- /dev/null +++ b/src/services/apps/hooks/use-apps.ts @@ -0,0 +1,38 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { fetchApps, fetchAppBySlug, updateAppBySlug, installAppBySlug } from '../redux'; +import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors'; + +export function useApps() { + const dispatch = useDispatch(); + const apps = useSelector(getApps); + const app = useSelector(getCurrentApp); + const appLoading = useSelector(getAppLoading); + const appTableLoading = useSelector(getAppsLoading); + + function loadApps() { + return dispatch(fetchApps()); + } + + function loadApp(slug: string) { + return dispatch(fetchAppBySlug(slug)); + } + + function editAppBySlug(data: any) { + return dispatch(updateAppBySlug(data)); + } + + function installApp(data: any) { + return dispatch(installAppBySlug(data)); + } + + return { + apps, + app, + loadApp, + loadApps, + editAppBySlug, + appLoading, + appTableLoading, + installApp, + }; +} diff --git a/src/services/apps/index.ts b/src/services/apps/index.ts new file mode 100644 index 0000000..84e225b --- /dev/null +++ b/src/services/apps/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export { reducer } from './redux'; +export { useApps } from './hooks'; diff --git a/src/services/apps/redux/actions.ts b/src/services/apps/redux/actions.ts new file mode 100644 index 0000000..69c00ec --- /dev/null +++ b/src/services/apps/redux/actions.ts @@ -0,0 +1,119 @@ +import { Dispatch } from 'redux'; +import { showToast, ToastType } from 'src/common/util/show-toast'; +import { performApiCall } from 'src/services/api'; +import { transformAppRequest, transformApp, transformInstallAppRequest } from '../transformations'; + +export enum AppActionTypes { + FETCH_APPS = 'apps/fetch_apps', + FETCH_APP = 'apps/fetch_app', + UPDATE_APP = 'apps/update_app', + INSTALL_APP = 'apps/install_app', + SET_APP_LOADING = 'apps/app_loading', + SET_APPS_LOADING = 'apps/apps_loading', +} + +export const setAppsLoading = (isLoading: boolean) => (dispatch: Dispatch) => { + dispatch({ + type: AppActionTypes.SET_APPS_LOADING, + payload: isLoading, + }); +}; + +export const setAppLoading = (isLoading: boolean) => (dispatch: Dispatch) => { + dispatch({ + type: AppActionTypes.SET_APP_LOADING, + payload: isLoading, + }); +}; + +export const fetchApps = () => async (dispatch: Dispatch) => { + dispatch(setAppsLoading(true)); + + try { + const { data } = await performApiCall({ + path: '/apps', + method: 'GET', + }); + + dispatch({ + type: AppActionTypes.FETCH_APPS, + payload: data.map(transformApp), + }); + } catch (err) { + console.error(err); + } + + dispatch(setAppsLoading(false)); +}; + +export const fetchAppBySlug = (slug: string) => async (dispatch: Dispatch) => { + dispatch(setAppLoading(true)); + + try { + const { data } = await performApiCall({ + path: `/apps/${slug}`, + method: 'GET', + }); + + dispatch({ + type: AppActionTypes.FETCH_APP, + payload: transformApp(data), + }); + } catch (err) { + console.error(err); + } + + dispatch(setAppLoading(false)); +}; + +export const updateAppBySlug = (app: any) => async (dispatch: Dispatch) => { + dispatch(setAppLoading(true)); + + try { + const { data } = await performApiCall({ + path: `/apps/${app.slug}`, + method: 'PUT', + body: transformAppRequest(app), + }); + + dispatch({ + type: AppActionTypes.UPDATE_APP, + payload: transformApp(data), + }); + + showToast('App updated successfully.', ToastType.Success); + + dispatch(fetchApps()); + } catch (err) { + console.error(err); + } + + dispatch(setAppLoading(false)); +}; + +export const installAppBySlug = (app: any) => async (dispatch: Dispatch) => { + dispatch(setAppLoading(true)); + + try { + const { data } = await performApiCall({ + path: `/apps/${app.slug}/install`, + method: 'POST', + body: transformInstallAppRequest(app), + }); + + dispatch({ + type: AppActionTypes.INSTALL_APP, + payload: transformApp(data), + }); + + showToast('App installing...', ToastType.Success); + + dispatch(fetchApps()); + } catch (err: any) { + dispatch(setAppLoading(false)); + showToast(`${err}`, ToastType.Error); + throw err; + } + + dispatch(setAppLoading(false)); +}; diff --git a/src/services/apps/redux/index.ts b/src/services/apps/redux/index.ts new file mode 100644 index 0000000..88a7a71 --- /dev/null +++ b/src/services/apps/redux/index.ts @@ -0,0 +1,4 @@ +export * from './actions'; +export { default as reducer } from './reducers'; +export { getApps } from './selectors'; +export * from './types'; diff --git a/src/services/apps/redux/reducers.ts b/src/services/apps/redux/reducers.ts new file mode 100644 index 0000000..0258928 --- /dev/null +++ b/src/services/apps/redux/reducers.ts @@ -0,0 +1,39 @@ +import { AppActionTypes } from './actions'; + +const initialUsersState: any = { + users: [], + user: {}, + userModalLoading: false, + usersLoading: false, +}; + +const appsReducer = (state: any = initialUsersState, action: any) => { + switch (action.type) { + case AppActionTypes.FETCH_APPS: + return { + ...state, + apps: action.payload, + }; + case AppActionTypes.SET_APP_LOADING: + return { + ...state, + appLoading: action.payload, + }; + case AppActionTypes.SET_APPS_LOADING: + return { + ...state, + appsLoading: action.payload, + }; + case AppActionTypes.FETCH_APP: + case AppActionTypes.UPDATE_APP: + case AppActionTypes.INSTALL_APP: + return { + ...state, + currentApp: action.payload, + }; + default: + return state; + } +}; + +export default appsReducer; diff --git a/src/services/apps/redux/selectors.ts b/src/services/apps/redux/selectors.ts new file mode 100644 index 0000000..751f24c --- /dev/null +++ b/src/services/apps/redux/selectors.ts @@ -0,0 +1,6 @@ +import { State } from 'src/redux'; + +export const getApps = (state: State) => state.apps.apps; +export const getCurrentApp = (state: State) => state.apps.currentApp; +export const getAppLoading = (state: State) => state.apps.appLoading; +export const getAppsLoading = (state: State) => state.apps.appsLoading; diff --git a/src/services/apps/redux/types.ts b/src/services/apps/redux/types.ts new file mode 100644 index 0000000..82a6c26 --- /dev/null +++ b/src/services/apps/redux/types.ts @@ -0,0 +1,14 @@ +import { ApiStatus } from 'src/services/api/redux'; + +import { App } from '../types'; + +export interface CurrentUserState extends App { + _status: ApiStatus; +} + +export interface AppsState { + currentApp: CurrentUserState; + apps: App[]; + appLoading: boolean; + appsLoading: boolean; +} diff --git a/src/services/apps/transformations.ts b/src/services/apps/transformations.ts new file mode 100644 index 0000000..fd41caa --- /dev/null +++ b/src/services/apps/transformations.ts @@ -0,0 +1,37 @@ +import { App, AppStatus, FormApp } from './types'; + +const transformAppStatus = (status: string) => { + switch (status) { + case 'installed': + return AppStatus.Installed; + case 'installing': + return AppStatus.Installing; + case 'not_installed': + return AppStatus.NotInstalled; + default: + return AppStatus.NotInstalled; + } +}; + +export const transformApp = (response: any): App => { + return { + id: response.id ?? '', + name: response.name ?? '', + slug: response.slug ?? '', + status: transformAppStatus(response.status), + subdomain: response.subdomain, + automaticUpdates: response.automatic_updates, + }; +}; + +export const transformAppRequest = (data: FormApp) => { + return { + automatic_updates: data.automaticUpdates, + }; +}; + +export const transformInstallAppRequest = (data: FormApp) => { + return { + subdomain: data.subdomain, + }; +}; diff --git a/src/services/apps/types.ts b/src/services/apps/types.ts new file mode 100644 index 0000000..d93e1ca --- /dev/null +++ b/src/services/apps/types.ts @@ -0,0 +1,18 @@ +export interface App { + id: number; + name: string; + slug: string; + status?: AppStatus; + subdomain?: string; + automaticUpdates: boolean; +} + +export interface FormApp extends App { + configuration: string; +} + +export enum AppStatus { + NotInstalled = 'Not installed', + Installed = 'Installed', + Installing = 'Installing', +}