From 2bf02c3139984948eadd62d64ddb58fe4aa51551 Mon Sep 17 00:00:00 2001 From: "Christian R. Garcia" Date: Fri, 2 Aug 2024 13:32:00 -0700 Subject: [PATCH] Sidebar changes. FM Formik MUI stuff. Pods update wip. --- .../icicle-tapisui-extension/tsconfig.json | 2 +- packages/tapisui-api/src/pods/index.ts | 1 + packages/tapisui-api/src/pods/updatePod.ts | 18 + packages/tapisui-common/src/index.ts | 2 + .../src/ui-formik-mui/FMTextField.tsx | 44 + .../src/ui-formik-mui/TextField.tsx | 7 + .../tapisui-common/src/ui-formik-mui/index.ts | 3 + .../ui/Breadcrumbs/breadcrumbsFromPathname.ts | 2 +- .../src/wrappers/Navbar/Navbar.module.scss | 5 +- .../src/wrappers/Navbar/Navbar.tsx | 8 +- packages/tapisui-hooks/src/pods/index.ts | 1 + packages/tapisui-hooks/src/pods/queryKeys.ts | 1 + packages/tapisui-hooks/src/pods/useDetails.ts | 11 + .../src/pods/useGetPodSecrets.ts | 11 + packages/tapisui-hooks/src/pods/useLogs.ts | 9 + .../tapisui-hooks/src/pods/useUpdatePod.ts | 48 ++ public/tapisicon.png | Bin 0 -> 45081 bytes .../_components/NavSnapshots/NavSnapshots.tsx | 2 +- .../Pods/_components/PagePods/PagePods.tsx | 15 + .../CreatePodModal/CreatePodBase.tsx | 653 ++++++++++++++ .../CreatePodModal/UpdatePodBase.tsx | 808 ++++++++++++++++++ .../PodToolbar/CreatePodModal/index.ts | 3 + .../PodsCodeMirror/PodsCodeMirror.tsx | 98 ++- src/app/_Layout/Layout.scss | 2 +- src/app/_Layout/Layout.tsx | 23 +- src/app/_components/Sidebar/Sidebar.tsx | 117 ++- src/index.tsx | 3 + src/theme.tsx | 19 + 28 files changed, 1862 insertions(+), 54 deletions(-) create mode 100644 packages/tapisui-api/src/pods/updatePod.ts create mode 100644 packages/tapisui-common/src/ui-formik-mui/FMTextField.tsx create mode 100644 packages/tapisui-common/src/ui-formik-mui/TextField.tsx create mode 100644 packages/tapisui-common/src/ui-formik-mui/index.ts create mode 100644 packages/tapisui-hooks/src/pods/useUpdatePod.ts create mode 100644 public/tapisicon.png create mode 100644 src/app/Pods/_components/PodToolbar/CreatePodModal/CreatePodBase.tsx create mode 100644 src/app/Pods/_components/PodToolbar/CreatePodModal/UpdatePodBase.tsx create mode 100644 src/theme.tsx diff --git a/packages/icicle-tapisui-extension/tsconfig.json b/packages/icicle-tapisui-extension/tsconfig.json index d7b5b6928..2f6b7fc11 100644 --- a/packages/icicle-tapisui-extension/tsconfig.json +++ b/packages/icicle-tapisui-extension/tsconfig.json @@ -12,7 +12,7 @@ "types": ["node"], "typeRoots": ["./types", "node_modules/@types"], "incremental": true, - "esModuleInterop": true, // required or pages won't import + "esModuleInterop": true // required or pages won't import // "baseUrl": "src", // "allowJs": true, // // "allowSyntheticDefaultImports": true, diff --git a/packages/tapisui-api/src/pods/index.ts b/packages/tapisui-api/src/pods/index.ts index 8b9dc17be..810891ec4 100644 --- a/packages/tapisui-api/src/pods/index.ts +++ b/packages/tapisui-api/src/pods/index.ts @@ -13,6 +13,7 @@ export { default as detailsTemplates } from './detailsTemplates'; export { default as logs } from './logs'; export { default as makeNewPod } from './makeNewPod'; +export { default as updatePod } from './updatePod'; export { default as deletePod } from './deletePod'; export { default as startPod } from './startPod'; export { default as restartPod } from './restartPod'; diff --git a/packages/tapisui-api/src/pods/updatePod.ts b/packages/tapisui-api/src/pods/updatePod.ts new file mode 100644 index 000000000..11348e945 --- /dev/null +++ b/packages/tapisui-api/src/pods/updatePod.ts @@ -0,0 +1,18 @@ +import { Pods } from '@tapis/tapis-typescript'; +import { apiGenerator, errorDecoder } from '../utils'; + +const updatePod = ( + reqUpdatePod: Pods.UpdatePodRequest, + basePath: string, + jwt: string +): Promise => { + const api: Pods.PodsApi = apiGenerator( + Pods, + Pods.PodsApi, + basePath, + jwt + ); + return errorDecoder(() => api.updatePod(reqUpdatePod)); +}; + +export default updatePod; diff --git a/packages/tapisui-common/src/index.ts b/packages/tapisui-common/src/index.ts index 92fa8daca..b0aedd134 100644 --- a/packages/tapisui-common/src/index.ts +++ b/packages/tapisui-common/src/index.ts @@ -47,6 +47,7 @@ import { NavItem, } from './wrappers'; import { FieldWrapperFormik } from './ui-formik'; +import { FMTextField } from './ui-formik-mui'; import { FormikInput, FormikSelect, @@ -127,6 +128,7 @@ export { NavItem, // Formik UI FieldWrapperFormik, + FMTextField, FormikInput, FormikSelect, FormikCheck, diff --git a/packages/tapisui-common/src/ui-formik-mui/FMTextField.tsx b/packages/tapisui-common/src/ui-formik-mui/FMTextField.tsx new file mode 100644 index 000000000..b3cc974fe --- /dev/null +++ b/packages/tapisui-common/src/ui-formik-mui/FMTextField.tsx @@ -0,0 +1,44 @@ +// FMTextField.tsx +import React from 'react'; +import { TextField } from '@mui/material'; +import { FormikProps } from 'formik'; + +interface FMTextFieldProps { + formik: FormikProps; + name: string; + label: string; + description: string; + type?: string; + size?: 'small' | 'medium'; +} + +const FMTextField: React.FC = ({ + formik, + name, + label, + description, + type = 'text', + size = 'small', +}) => { + return ( + + ); +}; + +export default FMTextField; diff --git a/packages/tapisui-common/src/ui-formik-mui/TextField.tsx b/packages/tapisui-common/src/ui-formik-mui/TextField.tsx new file mode 100644 index 000000000..5546e1bcd --- /dev/null +++ b/packages/tapisui-common/src/ui-formik-mui/TextField.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { TextField } from '@mui/material'; +import { useField } from 'formik'; + +const FMTextField = ({}) => {}; + +export default FMTextField; diff --git a/packages/tapisui-common/src/ui-formik-mui/index.ts b/packages/tapisui-common/src/ui-formik-mui/index.ts new file mode 100644 index 000000000..a16c3b89a --- /dev/null +++ b/packages/tapisui-common/src/ui-formik-mui/index.ts @@ -0,0 +1,3 @@ +import FMTextField from './FMTextField'; + +export { FMTextField }; diff --git a/packages/tapisui-common/src/ui/Breadcrumbs/breadcrumbsFromPathname.ts b/packages/tapisui-common/src/ui/Breadcrumbs/breadcrumbsFromPathname.ts index c33ae3a1d..7fa0a7e2b 100644 --- a/packages/tapisui-common/src/ui/Breadcrumbs/breadcrumbsFromPathname.ts +++ b/packages/tapisui-common/src/ui/Breadcrumbs/breadcrumbsFromPathname.ts @@ -3,7 +3,7 @@ import normalize from 'normalize-path'; const breadcrumbsFromPathname = (pathname: string) => { const items: Array = []; - if (pathname.startsWith('/pods/images')) { + if (pathname.startsWith('/pods/images/')) { // special case - images urls can have slashes in them const image = pathname.replace('/pods/images/', ''); console.log(image); diff --git a/packages/tapisui-common/src/wrappers/Navbar/Navbar.module.scss b/packages/tapisui-common/src/wrappers/Navbar/Navbar.module.scss index 68d60401c..e4a9cbe7a 100644 --- a/packages/tapisui-common/src/wrappers/Navbar/Navbar.module.scss +++ b/packages/tapisui-common/src/wrappers/Navbar/Navbar.module.scss @@ -6,12 +6,13 @@ $core-portal-color-primary: #9d85ef; --min-width: 100%; list-style: none; flex-direction: column; + flex: 1; } .nav-link { min-width: var(--min-width); border: none; - padding: 0.5rem 1rem; + padding: 0.5rem 1.5rem; padding-left: var(--horizontal-buffer); color: #707070; cursor: pointer; @@ -27,7 +28,7 @@ $core-portal-color-primary: #9d85ef; } .nav-text { - padding-left: 16px; + padding-left: 1rem; font-size: 0.75rem; font-weight: 500; } diff --git a/packages/tapisui-common/src/wrappers/Navbar/Navbar.tsx b/packages/tapisui-common/src/wrappers/Navbar/Navbar.tsx index b60ab3f91..b12a58171 100644 --- a/packages/tapisui-common/src/wrappers/Navbar/Navbar.tsx +++ b/packages/tapisui-common/src/wrappers/Navbar/Navbar.tsx @@ -16,7 +16,9 @@ export const NavItem: React.FC< >
{icon && } - {children} + {children ? ( + {children} + ) : undefined}
); @@ -25,7 +27,9 @@ export const NavItem: React.FC<
{icon && } - {children} + {children ? ( + {children} + ) : undefined}
); diff --git a/packages/tapisui-hooks/src/pods/index.ts b/packages/tapisui-hooks/src/pods/index.ts index 3f9c5c427..d5267edd9 100644 --- a/packages/tapisui-hooks/src/pods/index.ts +++ b/packages/tapisui-hooks/src/pods/index.ts @@ -14,6 +14,7 @@ export { default as useDetailsTemplates } from './useDetailsTemplates'; export { default as useLogs } from './useLogs'; export { default as useGetPodSecrets } from './useGetPodSecrets'; export { default as useMakeNewPod } from './useMakeNewPod'; +export { default as useUpdatePod } from './useUpdatePod'; export { default as useDeletePod } from './useDeletePod'; export { default as useStartPod } from './useStartPod'; export { default as useRestartPod } from './useRestartPod'; diff --git a/packages/tapisui-hooks/src/pods/queryKeys.ts b/packages/tapisui-hooks/src/pods/queryKeys.ts index 291e17b37..1a41f0378 100644 --- a/packages/tapisui-hooks/src/pods/queryKeys.ts +++ b/packages/tapisui-hooks/src/pods/queryKeys.ts @@ -11,6 +11,7 @@ const QueryKeys = { detailsImages: 'pods/detailsImages', detailsTemplates: 'pods/detailsTemplates', makeNewPod: 'pods/makeNewPod', + updatePod: 'pods/updatePod', deletePod: 'pods/deletePod', getLogs: 'pods/getPodLogs', startPod: 'pods/startPod', diff --git a/packages/tapisui-hooks/src/pods/useDetails.ts b/packages/tapisui-hooks/src/pods/useDetails.ts index fd72c7e6b..fae72ef86 100644 --- a/packages/tapisui-hooks/src/pods/useDetails.ts +++ b/packages/tapisui-hooks/src/pods/useDetails.ts @@ -16,7 +16,18 @@ const useDetails = ( // which is expected behavior for not having a token () => API.details(params, basePath, accessToken?.access_token ?? ''), { + // Disable automatic refetching on window focus, mount, and reconnect + refetchIntervalInBackground: false, + refetchInterval: false, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + // staleTime: 100000, // 30 seconds + // cacheTime: 10000, + // Keep the enabled option to control query activation based on accessToken presence enabled: !!accessToken, + // Spread any user-provided options to allow for customization + ...options, } ); const invalidate = () => { diff --git a/packages/tapisui-hooks/src/pods/useGetPodSecrets.ts b/packages/tapisui-hooks/src/pods/useGetPodSecrets.ts index 3a2ffa2a9..c330e6956 100644 --- a/packages/tapisui-hooks/src/pods/useGetPodSecrets.ts +++ b/packages/tapisui-hooks/src/pods/useGetPodSecrets.ts @@ -16,7 +16,18 @@ const useGetPodSecrets = ( // which is expected behavior for not having a token () => API.getPodSecrets(params, basePath, accessToken?.access_token ?? ''), { + // Disable automatic refetching on window focus, mount, and reconnect + refetchIntervalInBackground: false, + refetchInterval: false, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + staleTime: 20000, // 20 seconds + cacheTime: 20000, // 20 seconds + // Keep the enabled option to control query activation based on accessToken presence enabled: !!accessToken, + // Spread any user-provided options to allow for customization + ...options, } ); diff --git a/packages/tapisui-hooks/src/pods/useLogs.ts b/packages/tapisui-hooks/src/pods/useLogs.ts index abe788871..da41b624e 100644 --- a/packages/tapisui-hooks/src/pods/useLogs.ts +++ b/packages/tapisui-hooks/src/pods/useLogs.ts @@ -16,6 +16,15 @@ const useLogs = ( // which is expected behavior for not having a token () => API.logs(params, basePath, accessToken?.access_token ?? ''), { + // Disable automatic refetching on window focus, mount, and reconnect + refetchIntervalInBackground: false, + refetchInterval: false, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + staleTime: 20000, + cacheTime: 20000, + // Keep the enabled option to control query activation based on accessToken presence enabled: !!accessToken, } ); diff --git a/packages/tapisui-hooks/src/pods/useUpdatePod.ts b/packages/tapisui-hooks/src/pods/useUpdatePod.ts new file mode 100644 index 000000000..a895662bb --- /dev/null +++ b/packages/tapisui-hooks/src/pods/useUpdatePod.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import { useMutation, MutateOptions } from 'react-query'; +import { Pods } from '@tapis/tapis-typescript'; +import { Pods as API } from '@tapis/tapisui-api'; +import { useTapisConfig } from '../context'; +import QueryKeys from './queryKeys'; +import { updatePod } from '@tapis/tapisui-api/dist/pods'; + +type UpdatePodHookParams = { + updatePod: Pods.UpdatePod; +}; + +const useUpdatePod = (podId: string) => { + const { basePath, accessToken } = useTapisConfig(); + const jwt = accessToken?.access_token || ''; + + // The useMutation react-query hook is used to call operations that make server-side changes + // (Other hooks would be used for data retrieval) + // + // In this case, mkdir helper is called to perform the operation + const { mutate, isLoading, isError, isSuccess, data, error, reset } = + useMutation( + [QueryKeys.updatePod, podId, basePath, jwt], + (updatePod) => API.updatePod(updatePod, basePath, jwt) + ); + + useEffect(() => reset(), [reset, podId]); + + // Return hook object with loading states and login function + return { + isLoading, + isError, + isSuccess, + data, + error, + reset, + updatePod: ( + updatePod: Pods.UpdatePod, + // react-query options to allow callbacks such as onSuccess + options?: MutateOptions + ) => { + // Call mutate to trigger a single post-like API operation + return mutate({ updatePod, podId }, options); + }, + }; +}; + +export default useUpdatePod; diff --git a/public/tapisicon.png b/public/tapisicon.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2e2198c267b985da813df3d58907f455674994 GIT binary patch literal 45081 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 { .sort((a, b) => a.snapshot_id.localeCompare(b.snapshot_id)) //sort by `snapshot_id` property .map((snapshot) => ( diff --git a/src/app/Pods/_components/PagePods/PagePods.tsx b/src/app/Pods/_components/PagePods/PagePods.tsx index 669ffc412..a287c56b8 100644 --- a/src/app/Pods/_components/PagePods/PagePods.tsx +++ b/src/app/Pods/_components/PagePods/PagePods.tsx @@ -64,6 +64,8 @@ const PagePods: React.FC<{ objId: string | undefined }> = ({ objId }) => { }; const [podBarTab, setPodBarTab] = useState('details'); + const [editValue, setEditValue] = useState(''); + const loadingText = PodsLoadingText(); const tooltipConfigs: { @@ -105,6 +107,8 @@ const PagePods: React.FC<{ objId: string | undefined }> = ({ objId }) => { return null; }; + const [sharedData, setSharedData] = useState(); + const getCodeMirrorValue = () => { switch (podBarTab) { case 'details': @@ -131,6 +135,13 @@ const PagePods: React.FC<{ objId: string | undefined }> = ({ objId }) => { : isFetchingSecrets ? loadingText : JSON.stringify(podSecrets, null, 2); + case 'edit': + return error + ? `error: ${error}` + : isFetching + ? loadingText + : JSON.stringify(sharedData, null, 2); + default: return ''; // Default or placeholder value } @@ -167,6 +178,7 @@ const PagePods: React.FC<{ objId: string | undefined }> = ({ objId }) => { } }, }, + { id: 'edit', label: 'Edit', tabValue: 'edit' }, { id: 'details', label: 'Details', tabValue: 'details' }, { id: 'logs', label: 'Logs', tabValue: 'logs' }, { id: 'actionlogs', label: 'Action Logs', tabValue: 'actionlogs' }, @@ -265,6 +277,9 @@ const PagePods: React.FC<{ objId: string | undefined }> = ({ objId }) => { diff --git a/src/app/Pods/_components/PodToolbar/CreatePodModal/CreatePodBase.tsx b/src/app/Pods/_components/PodToolbar/CreatePodModal/CreatePodBase.tsx new file mode 100644 index 000000000..0e7e0ee0e --- /dev/null +++ b/src/app/Pods/_components/PodToolbar/CreatePodModal/CreatePodBase.tsx @@ -0,0 +1,653 @@ +import { Button } from 'reactstrap'; +import { GenericModal } from '@tapis/tapisui-common'; +import { SubmitWrapper } from '@tapis/tapisui-common'; +import { ToolbarModalProps } from '../PodToolbar'; +import { Form, Formik, FieldArray, useFormikContext } from 'formik'; +import { FormikInput, Collapse, Icon } from '@tapis/tapisui-common'; +import { FormikSelect } from '@tapis/tapisui-common'; +import { Pods as Hooks } from '@tapis/tapisui-hooks'; +import { useEffect, useCallback } from 'react'; //useState +import styles from './CreatePodModal.module.scss'; +import * as Yup from 'yup'; +import { useQueryClient } from 'react-query'; +//import { Pods } from '@tapis/tapis-typescript'; + +export enum PodProtocolEnum { + http = 'http', + tcp = 'tcp', + postgres = 'postgres', + local_only = 'local_only', +} + +export enum PodVolumeEnum { + tapisvolume = 'tapisvolume', + tapissnapshot = 'tapissnapshot', + pvc = 'pvc', +} + +export type CodeEditProps = { + sharedData: any; + setSharedData: any; +}; + +//Arrays that are used in the drop-down menus +const podProtocols = Object.values(PodProtocolEnum); +const podVolumeTypes = Object.values(PodVolumeEnum); + +const EnvVarValueSource: React.FC<{ index: number }> = ({ index }) => { + return ( +
+
+ +
+
+ +
+
+ ); +}; + +const NetworkingValueSource: React.FC<{ index: number }> = ({ index }) => { + return ( +
+
+ +
+
+ + + {podProtocols.map((values) => { + return ; + })} + +
+
+ +
+
+ ); +}; + +const VolumeMountsValueSource: React.FC<{ index: number }> = ({ index }) => { + return ( +
+
+ +
+
+ + + {podVolumeTypes.map((values) => { + return ; + })} + +
+
+ +
+
+ +
+
+ ); +}; + +const CreatePodBase: React.FC = ({ + sharedData, + setSharedData, +}) => { + //Allows the pod list to update without the user having to refresh the page + const queryClient = useQueryClient(); + const onSuccess = useCallback(() => { + queryClient.invalidateQueries(Hooks.queryKeys.list); + }, [queryClient]); + + const { makeNewPod, isLoading, error, isSuccess, reset } = + Hooks.useMakeNewPod(); + + useEffect(() => { + reset(); + }, [reset]); + + //used for the advanced checkbox + // const [simplified, setSimplified] = useState(false); + // const onChange = useCallback(() => { + // setSimplified(!simplified); + // }, [setSimplified, simplified]); + + const validationSchema = Yup.object({ + pod_id: Yup.string() + .min(1) + .max(80, 'Pod id should not be longer than 80 characters') + .matches( + /^[a-z0-9]+$/, + 'Must contain only lowercase alphanumeric characters' + ) + .required('Pod ID is a required field'), + image: Yup.string() + .min(1) + .max(128, 'Pod Image should not be longer than 128 characters'), + template: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + description: Yup.string() + .min(1) + .max(2048, 'Description should not be longer than 2048 characters'), + command: Yup.array() + .of(Yup.string()) + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + environment_variables: Yup.array().of( + Yup.object({ + id: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('id is a required field'), + value: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('value is a required field'), + }) + ), + volume_mounts: Yup.array().of( + Yup.object({ + id: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('id is a required field'), + type: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('type is a required field'), + mount_path: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('mount_path is a required field'), + sub_path: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + }) + ), + networking: Yup.array().of( + Yup.object({ + id: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + protocol: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + port: Yup.number() + .min(1000) + .max(99999, 'Port must be between 1000 and 99999'), + }) + ), + resources: Yup.object({ + cpu_request: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + cpu_limit: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + mem_request: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + mem_limit: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + gpus: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + }), + }); + + const initialValues = { + pod_id: '', + description: undefined, + command: undefined, + image: '', + template: '', + time_to_stop_default: 43200, + time_to_stop_instance: undefined, + environment_variables: [], + networking: [], + volume_mounts: [], + resources: { + cpu_request: 250, + cpu_limit: 2000, + mem_request: 256, + mem_limit: 3072, + gpus: 0, + }, + }; + + /// Environment Variables area + ////////////////////////////// + type EnvVarType = { + id: string; + value: string; + }; + + type EnvVarsTransformFn = (envVars: Array) => { + [key: string]: string; + }; + + const envVarsArrayToInputObject: EnvVarsTransformFn = (envVars) => { + const env: { [key: string]: string } = {}; + envVars.forEach((envVar) => { + env[envVar.id] = envVar.value; + }); + console.log(env); + return env; + }; + + /// Networking area + /////////////////// + type NetworkingType = { + id: string; + protocol: PodProtocolEnum; + port: string; + }; + + type NetworkingTransformFn = (envVars: Array) => { + [key: string]: object; + }; + + const networkingArrayToInputObject: NetworkingTransformFn = (envVars) => { + const env: { [key: string]: object } = {}; + envVars.forEach((envVar) => { + env[envVar.id] = { + protocol: envVar.protocol, + port: envVar.port, + }; + }); + console.log(env); + return env; + }; + + /// Volume Mounts area + /////////////////// + type VolumeMountsType = { + id: string; + type: PodProtocolEnum; + mount_path: string; + sub_path: string; + }; + + type volumeMountsTransformFn = (volumes: Array) => { + [key: string]: object; + }; + + const volume_mountsArrayToInputObject: volumeMountsTransformFn = ( + volumes + ) => { + const formatted_volumes: { [key: string]: object } = {}; + volumes.forEach((volume) => { + formatted_volumes[volume.id] = { + type: volume.type, + mount_path: volume.mount_path, + sub_path: volume.sub_path, + }; + }); + console.log(formatted_volumes); + return formatted_volumes; + }; + + /// onSubmit is made up of an object X properties. I destructure the object, allowing me to select properties from the object in my function, after =>. + const onSubmit = ({ + pod_id, + description, + command, + image, + template, + time_to_stop_default, + time_to_stop_instance, + environment_variables, + networking, + volume_mounts, + resources, + }: { + pod_id: string; + description: string | undefined; + image: string | undefined; + command: string | undefined; + template: string; + time_to_stop_default: number | undefined; + time_to_stop_instance: number | undefined; + environment_variables: Array; + networking: Array; + volume_mounts: Array; + resources: { + cpu_request: number; + cpu_limit: number; + mem_request: number; + mem_limit: number; + gpus: number; + }; + }) => { + makeNewPod( + { + pod_id: pod_id, + description, + command: command ? JSON.parse(command) : undefined, + image: image, + template: template, + time_to_stop_default: time_to_stop_default, + time_to_stop_instance: time_to_stop_instance, + environment_variables: envVarsArrayToInputObject(environment_variables), + networking: networkingArrayToInputObject(networking), + volume_mounts: volume_mountsArrayToInputObject(volume_mounts), + resources: resources, + }, + { onSuccess } + ); + }; + + return ( +
+ ( +
+ + + + + + + + + + + + ( +
+
+ {values.environment_variables && + values.environment_variables.length > 0 && + values.environment_variables.map((_, i) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+ + + ( +
+
+ {values.networking && + values.networking.length > 0 && + values.networking.map((_, i) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+ + + ( +
+
+ {values.volume_mounts && + values.volume_mounts.length > 0 && + values.volume_mounts.map((_, i) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+ + +
+ + + + + +
+
+ + )} + >
+
+ ); +}; + +export default CreatePodBase; diff --git a/src/app/Pods/_components/PodToolbar/CreatePodModal/UpdatePodBase.tsx b/src/app/Pods/_components/PodToolbar/CreatePodModal/UpdatePodBase.tsx new file mode 100644 index 000000000..d37b116cc --- /dev/null +++ b/src/app/Pods/_components/PodToolbar/CreatePodModal/UpdatePodBase.tsx @@ -0,0 +1,808 @@ +////import { Button } from 'reactstrap'; +import { GenericModal } from '@tapis/tapisui-common'; +import { SubmitWrapper } from '@tapis/tapisui-common'; +import { ToolbarModalProps } from '../PodToolbar'; +import { Form, Formik, FieldArray, useFormikContext, useFormik } from 'formik'; +import { FormikInput, Collapse, Icon } from '@tapis/tapisui-common'; +import { FormikSelect, FMTextField } from '@tapis/tapisui-common'; +import { Pods as Hooks } from '@tapis/tapisui-hooks'; +import { useEffect, useCallback } from 'react'; //useState +import styles from './CreatePodModal.module.scss'; +import * as Yup from 'yup'; +import { useQueryClient } from 'react-query'; +//import { Pods } from '@tapis/tapis-typescript'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; + +import { useLocation } from 'react-router-dom'; + +export enum PodProtocolEnum { + http = 'http', + tcp = 'tcp', + postgres = 'postgres', + local_only = 'local_only', +} + +export enum PodVolumeEnum { + tapisvolume = 'tapisvolume', + tapissnapshot = 'tapissnapshot', + pvc = 'pvc', +} + +export type CodeEditProps = { + sharedData: any; + setSharedData: any; +}; + +//Arrays that are used in the drop-down menus +const podProtocols = Object.values(PodProtocolEnum); +const podVolumeTypes = Object.values(PodVolumeEnum); + +const EnvVarValueSource: React.FC<{ index: number }> = ({ index }) => { + return ( +
+
+ + + +
+
+ +
+
+ ); +}; + +const NetworkingValueSource: React.FC<{ index: number }> = ({ index }) => { + return ( +
+
+ +
+
+ + + {podProtocols.map((values) => { + return ; + })} + +
+
+ +
+
+ ); +}; + +const VolumeMountsValueSource: React.FC<{ index: number }> = ({ index }) => { + return ( +
+
+ +
+
+ + + {podVolumeTypes.map((values) => { + return ; + })} + +
+
+ +
+
+ +
+
+ ); +}; + +const initialValues = {}; + +const UpdatePodBase: React.FC = ({ + sharedData, + setSharedData, +}) => { + //Allows the pod list to update without the user having to refresh the page + const queryClient = useQueryClient(); + const onSuccess = useCallback(() => { + queryClient.invalidateQueries(Hooks.queryKeys.list); + }, [queryClient]); + + const podId = useLocation().pathname.split('/')[2]; + + const { updatePod, isLoading, error, isSuccess, reset } = + Hooks.useUpdatePod(podId); + + useEffect(() => { + reset(); + }, [reset]); + + //used for the advanced checkbox + // const [simplified, setSimplified] = useState(false); + // const onChange = useCallback(() => { + // setSimplified(!simplified); + // }, [setSimplified, simplified]); + + const validationSchema = Yup.object({ + image: Yup.string() + .min(1) + .max(128, 'Pod Image should not be longer than 128 characters'), + template: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + description: Yup.string() + .min(22) + .max(2048, 'Description should not be longer than 2048 characters'), + command: Yup.array() + .of(Yup.string()) + .min(0) + .max(128, 'Pod Template should not be longer than 128 characters'), + environment_variables: Yup.array().of( + Yup.object({ + id: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('id is a required field'), + value: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('value is a required field'), + }) + ), + volume_mounts: Yup.array().of( + Yup.object({ + id: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('id is a required field'), + type: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('type is a required field'), + mount_path: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters') + .required('mount_path is a required field'), + sub_path: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + }) + ), + networking: Yup.array().of( + Yup.object({ + id: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + protocol: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + port: Yup.number() + .min(1000) + .max(99999, 'Port must be between 1000 and 99999'), + }) + ), + resources: Yup.object({ + cpu_request: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + cpu_limit: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + mem_request: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + mem_limit: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + gpus: Yup.string() + .min(1) + .max(128, 'Pod Template should not be longer than 128 characters'), + }), + }); + + /// Environment Variables area + ////////////////////////////// + type EnvVarType = { + id: string; + value: string; + }; + + type EnvVarsTransformFn = (envVars: Array) => { + [key: string]: string; + }; + + const envVarsArrayToInputObject: EnvVarsTransformFn = (envVars) => { + if (!envVars) { + console.warn('envVars is undefined'); + return {}; + } + + const env: { [key: string]: string } = {}; + envVars.forEach((envVar) => { + env[envVar.id] = envVar.value; + }); + console.log(env); + return env; + }; + /// Networking area + /////////////////// + type NetworkingType = { + id: string; + protocol: PodProtocolEnum; + port: string; + }; + + type NetworkingTransformFn = (envVars: Array) => { + [key: string]: object; + }; + + const networkingArrayToInputObject: NetworkingTransformFn = ( + networkingArray + ) => { + if (!networkingArray) { + console.warn('networkingArray is undefined'); + return {}; + } + + const env: { [key: string]: object } = {}; + networkingArray.forEach((networkingArray) => { + env[networkingArray.id] = { + protocol: networkingArray.protocol, + port: networkingArray.port, + }; + }); + console.log(env); + return env; + }; + + /// Volume Mounts area + /////////////////// + type VolumeMountsType = { + id: string; + type: PodProtocolEnum; + mount_path: string; + sub_path: string; + }; + + type volumeMountsTransformFn = (volumes: Array) => { + [key: string]: object; + }; + + const volume_mountsArrayToInputObject: volumeMountsTransformFn = ( + volumes + ) => { + if (!volumes) { + console.warn('volumes is undefined'); + return {}; + } + + const formatted_volumes: { [key: string]: object } = {}; + volumes.forEach((volume) => { + formatted_volumes[volume.id] = { + type: volume.type, + mount_path: volume.mount_path, + sub_path: volume.sub_path, + }; + }); + console.log(formatted_volumes); + return formatted_volumes; + }; + + type UpdatePodBaseProps = { + description: string | undefined; + image: string | undefined; + command: string | undefined; + template: string | undefined; + time_to_stop_default: number | undefined; + time_to_stop_instance: number | undefined; + environment_variables: Array; + networking: Array; + volume_mounts: Array; + resources: + | { + cpu_request: number; + cpu_limit: number; + mem_request: number; + mem_limit: number; + gpus: number; + } + | undefined; + }; + + /// onSubmit is made up of an object X properties. I destructure the object, allowing me to select properties from the object in my function, after =>. + const onSubmit = ({ + description, + command, + time_to_stop_default, + time_to_stop_instance, + environment_variables, + networking, + volume_mounts, + resources, + }: UpdatePodBaseProps) => { + updatePod( + filterUndefinedValues({ + description, + command: command ? JSON.parse(command) : undefined, + time_to_stop_default, + time_to_stop_instance, + environment_variables: envVarsArrayToInputObject(environment_variables), + networking: networkingArrayToInputObject(networking), + volume_mounts: volume_mountsArrayToInputObject(volume_mounts), + resources, + }), + { onSuccess } + ); + }; + + // Have to filter info to updatePod to remove undefined values + const filterUndefinedValues = (obj) => { + return Object.keys(obj).reduce((acc, key) => { + const value = obj[key]; + const isEmptyArray = Array.isArray(value) && value.length === 0; + const isEmptyObject = + value && typeof value === 'object' && Object.keys(value).length === 0; + + if (value !== undefined && !isEmptyArray && !isEmptyObject) { + acc[key] = value; + } + return acc; + }, {}); + }; + + const AutoPruneEmptyFields: React.FC = () => { + const { values, setFieldValue } = useFormikContext(); + + useEffect(() => { + const pruneEmptyFields = (obj, parentKey = '') => { + Object.keys(obj).forEach((key) => { + const value = obj[key]; + const path = parentKey ? `${parentKey}.${key}` : key; + const isEmptyString = value === ''; + const isEmptyArray = Array.isArray(value) && value.length === 0; + const isEmptyObject = + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + Object.keys(value).length === 0; + + if (isEmptyString || isEmptyArray || isEmptyObject) { + setFieldValue(path, undefined); + } else if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) + ) { + pruneEmptyFields(value, path); + } + }); + }; + + pruneEmptyFields(values); + }, [values, setFieldValue]); + + return null; // This component does not render anything + }; + + function handleFormChange(values: any) { + let transformedValues = { ...values }; + + if (values.environment_variables) { + transformedValues.environment_variables = envVarsArrayToInputObject( + values.environment_variables + ); + } + + if (values.networking) { + transformedValues.networking = networkingArrayToInputObject( + values.networking + ); + } + + if (values.volume_mounts) { + transformedValues.volume_mounts = volume_mountsArrayToInputObject( + values.volume_mounts + ); + } + console.warn(transformedValues); + } + + const formik = useFormik({ + initialValues: { + description: '', + command: '', + time_to_stop_default: 0, + time_to_stop_instance: 0, + environment_variables: [], + networking: [], + volume_mounts: [], + resources: { + cpu_request: 0, + cpu_limit: 0, + mem_request: 0, + mem_limit: 0, + gpus: 0, + }, + }, + validationSchema: validationSchema, + onSubmit: (values) => { + alert(JSON.stringify(values, null, 2)); + }, + }); + + return ( +
+ + + + + {/*
+ + + */} +
+ {/* */} + {/* */} + {/* */} + + + + + + + {/* + */} + + + ( +
+ + {/* + + + */} + + ( +
+
+ {values.environment_variables && + values.environment_variables.length > 0 && + values.environment_variables.map((_, i) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+ + + ( +
+
+ {values.networking && + values.networking.length > 0 && + values.networking.map((_, i) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+ + + ( +
+
+ {values.volume_mounts && + values.volume_mounts.length > 0 && + values.volume_mounts.map((_, i) => ( +
+ + +
+ ))} +
+ +
+ )} + /> +
+ + +
+ + + + + +
+
+ + )} + >
+
+ ); +}; + +export default UpdatePodBase; diff --git a/src/app/Pods/_components/PodToolbar/CreatePodModal/index.ts b/src/app/Pods/_components/PodToolbar/CreatePodModal/index.ts index 07ff4cdf8..57089262c 100644 --- a/src/app/Pods/_components/PodToolbar/CreatePodModal/index.ts +++ b/src/app/Pods/_components/PodToolbar/CreatePodModal/index.ts @@ -1,3 +1,6 @@ import CreatePodModal from './CreatePodModal'; +import CreatePodBase from './CreatePodBase'; +import UpdatePodBase from './UpdatePodBase'; export default CreatePodModal; +export { CreatePodBase, UpdatePodBase }; diff --git a/src/app/Pods/_components/PodsCodeMirror/PodsCodeMirror.tsx b/src/app/Pods/_components/PodsCodeMirror/PodsCodeMirror.tsx index 40781c8a6..f204ec468 100644 --- a/src/app/Pods/_components/PodsCodeMirror/PodsCodeMirror.tsx +++ b/src/app/Pods/_components/PodsCodeMirror/PodsCodeMirror.tsx @@ -1,49 +1,105 @@ -import React from 'react'; +import React, { useState } from 'react'; import CodeMirror from '@uiw/react-codemirror'; +import { SubmitWrapper } from '@tapis/tapisui-common'; + import { json } from '@codemirror/lang-json'; import { vscodeDarkInit } from '@uiw/codemirror-theme-vscode'; import styles from '../Pages.module.scss'; // Adjust the import path as necessary +import { UpdatePodBase } from '../PodToolbar/CreatePodModal'; +import { Button } from 'reactstrap'; interface PodsCodeMirrorProps { value: string; + editValue?: string; height?: string; width?: string; + minWidth?: string; isVisible: boolean; + editable?: boolean; + onChange?: (newValue: string) => void; + isEditorVisible?: boolean; + sharedData: any; + setSharedData: any; } const PodsCodeMirror: React.FC = ({ value, + editValue = '', height = '800px', // Default height width = '100%', // Default width isVisible, + editable = false, + onChange, + isEditorVisible = false, + sharedData, + setSharedData, }) => { if (!isVisible) { return null; } return ( -
- + {isEditorVisible && ( +
+ +
+ )} +
+ > + +
); }; diff --git a/src/app/_Layout/Layout.scss b/src/app/_Layout/Layout.scss index bd4eb5305..2a199b843 100644 --- a/src/app/_Layout/Layout.scss +++ b/src/app/_Layout/Layout.scss @@ -104,7 +104,7 @@ } .logo { - margin-right: 16px; + //margin-right: 1rem; } .dropdown-button { diff --git a/src/app/_Layout/Layout.tsx b/src/app/_Layout/Layout.tsx index 9f0e4f23c..1558bb103 100644 --- a/src/app/_Layout/Layout.tsx +++ b/src/app/_Layout/Layout.tsx @@ -34,17 +34,7 @@ const Layout: React.FC = () => { const header = (
-
- - - - {extension?.logo?.logoText || 'TapisUI'} -
-
+
@@ -87,18 +77,21 @@ const Layout: React.FC = () => { )}
+
); return ( -
+
} right={ -
- +
+
{header}
+
+ +
} /> diff --git a/src/app/_components/Sidebar/Sidebar.tsx b/src/app/_components/Sidebar/Sidebar.tsx index 4befd54a4..c2488a19d 100644 --- a/src/app/_components/Sidebar/Sidebar.tsx +++ b/src/app/_components/Sidebar/Sidebar.tsx @@ -5,8 +5,23 @@ import styles from './Sidebar.module.scss'; import { Navbar, NavItem } from '@tapis/tapisui-common'; import { useExtension } from 'extensions'; import { Menu, ExpandLess, ExpandMore } from '@mui/icons-material'; -import { Collapse } from '@mui/material'; +import { Collapse, Button, Chip } from '@mui/material'; import { v4 as uuidv4 } from 'uuid'; +import { Tenants as Hooks } from '@tapis/tapisui-hooks'; +import { Link, useHistory, useLocation } from 'react-router-dom'; + +import { + ButtonDropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, +} from 'reactstrap'; +import { + QueryWrapper, + PageLayout, + Breadcrumbs, + breadcrumbsFromPathname, +} from '@tapis/tapisui-common'; type SidebarItems = { [key: string]: any; @@ -17,6 +32,15 @@ const Sidebar: React.FC = () => { const { extension } = useExtension(); const [expanded, setExpanded] = useState(true); const [openSecondary, setOpenSecondary] = useState(false); //Added openSecondary state to manage the visibility of the secondary sidebar items. + const [isOpen, setIsOpen] = useState(false); + + const { data, isLoading, error } = Hooks.useList(); + const result = data?.result ?? []; + const tenants = result; + const history = useHistory(); + + const { claims } = useTapisConfig(); + const renderSidebarItem = ( to: string, @@ -26,7 +50,7 @@ const Sidebar: React.FC = () => { return ( {expanded ? ( - + {text} ) : ( @@ -79,16 +103,46 @@ const Sidebar: React.FC = () => { setOpenSecondary(!openSecondary); }; + + + const chipLabel = expanded ? '<<' : '>>'; + return ( -
-
- { - setExpanded(!expanded); - }} - /> +
+
+ + + + + {expanded ? extension?.logo?.logoText : undefined}
+ + { + setExpanded(!expanded); + }} + /> + {renderSidebarItem('/', 'dashboard', 'Dashboard')} {!accessToken && renderSidebarItem('/login', 'user', 'Login')} @@ -129,6 +183,49 @@ const Sidebar: React.FC = () => { )} + + {/*
+ {claims['sub'] && ( + setIsOpen(!isOpen)} + className="dropdown-button" + > + {claims['sub']} + + {((extension !== undefined && extension.allowMutiTenant) || + extension === undefined || + (extension !== undefined && extension.allowMutiTenant)) && ( + <> + Tenants + + + {tenants.map((tenant) => { + return ( + { + window.location.href = + tenant.base_url + '/tapis-ui/'; + }} + > + {tenant.tenant_id} + + ); + })} + + + + )} + history.push('/logout')}> + Logout + + + + )} +
+ */} +
); }; diff --git a/src/index.tsx b/src/index.tsx index c8ae60907..99250cc90 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,6 +9,7 @@ import reportWebVitals from './reportWebVitals'; import { ExtensionsProvider } from './extensions'; import { Extension } from '@tapis/tapisui-extensions-core'; import { extension as icicleExtension } from '@icicle/tapisui-extension'; +import Theme from './theme'; // Import the Theme component const initializedExtensions: { [key: string]: Extension } = { '@icicle/tapisui-extension': icicleExtension, @@ -22,7 +23,9 @@ root.render( + + diff --git a/src/theme.tsx b/src/theme.tsx new file mode 100644 index 000000000..81f56ce00 --- /dev/null +++ b/src/theme.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +const theme = createTheme({ + palette: { + // primary: { + // main: '#2a9461', + // }, + // secondary: { + // main: '#494c7d', + // }, + }, +}); + +const Theme: React.FC = ({ children }: React.PropsWithChildren<{}>) => { + return {children}; +}; + +export default Theme; \ No newline at end of file