From e409cbe9bc0b2b7c033d08b22e681f09b78e9dbd Mon Sep 17 00:00:00 2001 From: Sal Tijerina Date: Fri, 4 Oct 2024 12:02:27 -0500 Subject: [PATCH] task/WP-673: Interactive Modal Redesign (#1456) * interactive session modal context * use shared state * use shared context for interactive session modal * update existing modal context upon interactive session ready * new design * change launch button wording * linting * remove default message for interactive session * change resubmit to relaunch for interactive apps * linting * linting * formatting --- client/modules/_hooks/src/workspace/index.ts | 1 + .../workspace/useInteractiveModalContext.ts | 20 ++++ .../AppsSubmissionDetails.tsx | 4 +- .../AppsSubmissionForm/AppsSubmissionForm.tsx | 38 +++--- .../InteractiveSessionModal.module.css | 13 +-- .../InteractiveSessionModal.tsx | 99 +++++++++++----- .../src/JobsDetailModal/JobsDetailModal.tsx | 30 +++-- .../workspace/src/JobsListing/JobsListing.tsx | 32 +++-- client/modules/workspace/src/Toast/Toast.tsx | 109 ++++++++++-------- client/modules/workspace/src/index.ts | 1 + .../src/workspace/layouts/AppsViewLayout.tsx | 1 + .../workspace/layouts/WorkspaceBaseLayout.tsx | 14 ++- designsafe/apps/webhooks/views.py | 1 - 13 files changed, 225 insertions(+), 138 deletions(-) create mode 100644 client/modules/_hooks/src/workspace/useInteractiveModalContext.ts diff --git a/client/modules/_hooks/src/workspace/index.ts b/client/modules/_hooks/src/workspace/index.ts index d9b8661bcc..94e373286a 100644 --- a/client/modules/_hooks/src/workspace/index.ts +++ b/client/modules/_hooks/src/workspace/index.ts @@ -18,3 +18,4 @@ export * from './useGetJobs'; export * from './usePostJobs'; export * from './types'; export * from './useGetAllocations'; +export * from './useInteractiveModalContext'; diff --git a/client/modules/_hooks/src/workspace/useInteractiveModalContext.ts b/client/modules/_hooks/src/workspace/useInteractiveModalContext.ts new file mode 100644 index 0000000000..c28b542548 --- /dev/null +++ b/client/modules/_hooks/src/workspace/useInteractiveModalContext.ts @@ -0,0 +1,20 @@ +import React, { createContext, useContext } from 'react'; + +type TInteractiveModalDetails = { + show: boolean; + interactiveSessionLink?: string; + message?: string; + openedBySubmit?: boolean; +}; + +export type TInteractiveModalContext = [ + TInteractiveModalDetails, + React.Dispatch> +]; + +export const InteractiveModalContext = + createContext(null); + +export const useInteractiveModalContext = () => { + return useContext(InteractiveModalContext); +}; diff --git a/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx b/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx index f0a423b161..733bd601d7 100644 --- a/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx +++ b/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx @@ -259,9 +259,9 @@ export const AppsSubmissionDetails: React.FC<{ htmlType="submit" disabled={!isValid} loading={isSubmitting} - style={{ width: 120 }} + style={{ width: 130 }} > - Submit Job + {definition.notes.isInteractive ? 'Launch Session' : 'Submit Job'} } /> diff --git a/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx b/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx index a78684f15a..7ae8145bbb 100644 --- a/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx +++ b/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx @@ -19,6 +19,8 @@ import { TJobBody, useGetAllocationsSuspense, TTapisJob, + useInteractiveModalContext, + TInteractiveModalContext, } from '@client/hooks'; import { AppsSubmissionDetails } from '../AppsSubmissionDetails/AppsSubmissionDetails'; import { AppsWizard } from '../AppsWizard/AppsWizard'; @@ -73,6 +75,9 @@ export const AppsSubmissionForm: React.FC = () => { data: TTapisJob; }; + const [, setInteractiveModalDetails] = + useInteractiveModalContext() as TInteractiveModalContext; + const { definition, license, defaultSystemNeedsKeys } = app; const defaultStorageHost = defaultStorageSystem.host; @@ -368,6 +373,9 @@ export const AppsSubmissionForm: React.FC = () => { setPushKeysSystem(submitResult.execSys); } else if (isSuccess) { reset(initialValues); + if (definition.notes.isInteractive) { + setInteractiveModalDetails({ show: true, openedBySubmit: true }); + } } }, [submitResult]); @@ -516,20 +524,22 @@ export const AppsSubmissionForm: React.FC = () => { return ( <> - {submitResult && !submitResult.execSys && ( - - Job submitted successfully. Monitor its progress in{' '} - Job Status. - - } - type="success" - closable - showIcon - style={{ marginBottom: '1rem' }} - /> - )} + {submitResult && + !submitResult.execSys && + !definition.notes.isInteractive && ( + + Job submitted successfully. Monitor its progress in{' '} + Job Status. + + } + type="success" + closable + showIcon + style={{ marginBottom: '1rem' }} + /> + )} {missingAllocation && ( * { margin: 0.4rem; } @@ -17,3 +12,7 @@ color: grey; font-style: italic; } + +.icon { + margin-left: 10px; +} diff --git a/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx b/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx index d184d61a55..68e5cc4903 100644 --- a/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx +++ b/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx @@ -1,41 +1,82 @@ -import { Modal } from 'antd'; import React from 'react'; -import { PrimaryButton } from '@client/common-components'; +import { Modal } from 'antd'; +import { PrimaryButton, Icon } from '@client/common-components'; +import { + useInteractiveModalContext, + TInteractiveModalContext, +} from '@client/hooks'; import styles from './InteractiveSessionModal.module.css'; -export const InteractiveSessionModal: React.FC<{ - isOpen: boolean; - interactiveSessionLink: string; - message?: string; - onCancel: VoidFunction; -}> = ({ isOpen, interactiveSessionLink, message, onCancel }) => { +export const InteractiveSessionModal = () => { + const [interactiveModalDetails, setInteractiveModalDetails] = + useInteractiveModalContext() as TInteractiveModalContext; + + const { interactiveSessionLink, message, openedBySubmit, show } = + interactiveModalDetails; + return ( Open Session} - width="500px" - open={isOpen} - footer={ - - Connect - + title={ +

+ Interactive Session is {interactiveSessionLink ? 'Ready' : 'Queueing'} +

+ } + width="650px" + open={show} + footer={null} + onCancel={() => + setInteractiveModalDetails({ + show: false, + }) } - onCancel={onCancel} >
- - Click the button below to connect to the interactive session. - +
+ + Connect + {interactiveSessionLink && ( + + )} + +
+ {openedBySubmit && !interactiveSessionLink && ( + + While you wait, you can either: +
    +
  • Keep this modal open and wait to connect.
  • +
  • + Close this window and wait for a notification via{' '} + Job Status. +
  • +
+
+ )} {message && {message}} - To end the job, quit the application within the session. - - Files may take some time to appear in the output location after the - job has ended. - - - For security purposes, this is the URL that the connect button will - open: - - {interactiveSessionLink} + {interactiveSessionLink && ( + <> + + To end the job, quit the application within the session. + + + Files may take some time to appear in the output location after + the job has ended. + + + For security purposes, this is the URL that the connect button + will open: + + {interactiveSessionLink} + + )}
); diff --git a/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx b/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx index e4bc16be9a..dc29a3d517 100644 --- a/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx +++ b/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx @@ -235,7 +235,7 @@ export const JobsDetailModalBody: React.FC<{ (isInteractiveJob(jobData) ? ( @@ -296,21 +296,19 @@ export const JobsDetailModal: React.FC<{ uuid: string }> = ({ uuid }) => { -
- Job Detail: {uuid} - {jobData && ( -
-
Job UUID:
-
{jobData.uuid}
-
Application:
-
{JSON.parse(jobData.notes).label || jobData.appId}
-
System:
-
{jobData.execSystemId}
-
- )} -
- +
+ Job Detail: {uuid} + {jobData && ( +
+
Job UUID:
+
{jobData.uuid}
+
Application:
+
{JSON.parse(jobData.notes).label || jobData.appId}
+
System:
+
{jobData.execSystemId}
+
+ )} +
} width="60%" open={isModalOpen} diff --git a/client/modules/workspace/src/JobsListing/JobsListing.tsx b/client/modules/workspace/src/JobsListing/JobsListing.tsx index 6aaae7aa54..36b4d637db 100644 --- a/client/modules/workspace/src/JobsListing/JobsListing.tsx +++ b/client/modules/workspace/src/JobsListing/JobsListing.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useEffect } from 'react'; import useWebSocket from 'react-use-websocket'; import { TableProps, Row, Flex, Button as AntButton } from 'antd'; import type { ButtonSize } from 'antd/es/button'; @@ -13,6 +13,8 @@ import { TJobPostOperations, useReadNotifications, TGetNotificationsResponse, + useInteractiveModalContext, + TInteractiveModalContext, } from '@client/hooks'; import { JobsListingTable, @@ -26,7 +28,6 @@ import { isInteractiveJob, isTerminalState, } from '../utils'; -import { InteractiveSessionModal } from '../InteractiveSessionModal'; import styles from './JobsListing.module.css'; import { formatDateTimeFromValue } from '../utils/timeFormat'; import { JobsReuseInputsButton } from '../JobsReuseInputsButton/JobsReuseInputsButton'; @@ -59,16 +60,23 @@ export const JobActionButton: React.FC<{ const InteractiveSessionButtons: React.FC<{ uuid: string; - interactiveSessionLink: string; + interactiveSessionLink?: string; message?: string; }> = ({ uuid, interactiveSessionLink, message }) => { - const [interactiveModalState, setInteractiveModalState] = useState(false); + const [, setInteractiveModalDetails] = + useInteractiveModalContext() as TInteractiveModalContext; return ( <> setInteractiveModalState(true)} + onClick={() => + setInteractiveModalDetails({ + show: true, + interactiveSessionLink, + message, + }) + } > Open @@ -78,12 +86,6 @@ const InteractiveSessionButtons: React.FC<{ title="End" size="small" /> - setInteractiveModalState(false)} - /> ); }; @@ -175,7 +177,7 @@ export const JobsListing: React.FC> = ({ ) : ( @@ -247,9 +249,5 @@ export const JobsListing: React.FC> = ({ [interactiveSessionNotifs] ); - return ( - <> - - - ); + return ; }; diff --git a/client/modules/workspace/src/Toast/Toast.tsx b/client/modules/workspace/src/Toast/Toast.tsx index 576476be92..77544778a2 100644 --- a/client/modules/workspace/src/Toast/Toast.tsx +++ b/client/modules/workspace/src/Toast/Toast.tsx @@ -8,6 +8,8 @@ import { Icon } from '@client/common-components'; import { TJobStatusNotification, TGetNotificationsResponse, + useInteractiveModalContext, + TInteractiveModalContext, } from '@client/hooks'; import { getToastMessage } from '../utils'; import styles from './Notifications.module.css'; @@ -18,60 +20,69 @@ const Notifications = () => { ); const [api, contextHolder] = notification.useNotification({ maxCount: 1 }); + const [interactiveModalDetails, setInteractiveModalDetails] = + useInteractiveModalContext() as TInteractiveModalContext; const queryClient = useQueryClient(); const navigate = useNavigate(); const handleNotification = (notification: TJobStatusNotification) => { - if ( - notification.event_type === 'job' || - notification.event_type === 'interactive_session_ready' - ) { - queryClient.invalidateQueries({ - queryKey: ['workspace', 'notifications'], - }); - queryClient.invalidateQueries({ - queryKey: ['workspace', 'jobsListing'], - }); - api.open({ - message: ( - - {getToastMessage(notification)} - - - ), - placement: 'bottomLeft', - icon: , - className: `${ - notification.extra.status === 'FAILED' && styles['toast-is-error'] - } ${styles.root}`, - closeIcon: false, - duration: 5, - onClick: () => { - navigate('/history'); - }, - }); - } else if (notification.event_type === 'markAllNotificationsAsRead') { - // update unread count state - queryClient.setQueryData( - [ - 'workspace', - 'notifications', - { - eventTypes: ['interactive_session_ready', 'job'], - read: false, - markRead: false, + switch (notification.event_type) { + case 'interactive_session_ready': + setInteractiveModalDetails({ + ...interactiveModalDetails, + interactiveSessionLink: notification.action_link, + message: notification.message, + }); + /* falls through */ + case 'job': + queryClient.invalidateQueries({ + queryKey: ['workspace', 'notifications'], + }); + queryClient.invalidateQueries({ + queryKey: ['workspace', 'jobsListing'], + }); + api.open({ + message: ( + + {getToastMessage(notification)} + + + ), + placement: 'bottomLeft', + icon: , + className: `${ + notification.extra.status === 'FAILED' && styles['toast-is-error'] + } ${styles.root}`, + closeIcon: false, + duration: 5, + onClick: () => { + navigate('/history'); }, - ], - (oldData: TGetNotificationsResponse) => { - return { - ...oldData, - notifs: [], - unread: 0, - }; - } - ); + }); + break; + case 'markAllNotificationsAsRead': + // update unread count state + queryClient.setQueryData( + [ + 'workspace', + 'notifications', + { + eventTypes: ['interactive_session_ready', 'job'], + read: false, + markRead: false, + }, + ], + (oldData: TGetNotificationsResponse) => { + return { + ...oldData, + notifs: [], + unread: 0, + }; + } + ); + break; } }; @@ -79,9 +90,9 @@ const Notifications = () => { if (lastMessage !== null) { handleNotification(JSON.parse(lastMessage.data)); } - }, [lastMessage]); + }, [lastMessage, handleNotification]); - return <>{contextHolder}; + return contextHolder; }; export default Notifications; diff --git a/client/modules/workspace/src/index.ts b/client/modules/workspace/src/index.ts index 694e66c4f2..826f4ff469 100644 --- a/client/modules/workspace/src/index.ts +++ b/client/modules/workspace/src/index.ts @@ -9,3 +9,4 @@ export * from './SystemsPushKeysModal/SystemsPushKeysModal'; export * from './Toast'; export * from './utils'; export * from './constants'; +export * from './InteractiveSessionModal'; diff --git a/client/src/workspace/layouts/AppsViewLayout.tsx b/client/src/workspace/layouts/AppsViewLayout.tsx index b78563fd94..39f617613b 100644 --- a/client/src/workspace/layouts/AppsViewLayout.tsx +++ b/client/src/workspace/layouts/AppsViewLayout.tsx @@ -51,6 +51,7 @@ export const AppsViewLayout: React.FC = () => { View User Guide diff --git a/client/src/workspace/layouts/WorkspaceBaseLayout.tsx b/client/src/workspace/layouts/WorkspaceBaseLayout.tsx index e364998a03..b454d92deb 100644 --- a/client/src/workspace/layouts/WorkspaceBaseLayout.tsx +++ b/client/src/workspace/layouts/WorkspaceBaseLayout.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Outlet } from 'react-router-dom'; import { Flex, Layout } from 'antd'; import { @@ -7,6 +7,7 @@ import { useGetAppParams, AppsBreadcrumb, Toast, + InteractiveSessionModal, } from '@client/workspace'; import { Spinner } from '@client/common-components'; import { @@ -14,6 +15,7 @@ import { useAppsListing, usePrefetchGetSystems, usePrefetchGetAllocations, + InteractiveModalContext, } from '@client/hooks'; import styles from './layout.module.css'; @@ -25,6 +27,9 @@ const WorkspaceRoot: React.FC = () => { usePrefetchGetAllocations(); const { data, isLoading } = useAppsListing(); + const [interactiveModalDetails, setInteractiveModalDetails] = useState({ + show: false, + }); if (!data || isLoading) return ( @@ -41,7 +46,9 @@ const WorkspaceRoot: React.FC = () => { }; return ( - <> + { - + + ); }; diff --git a/designsafe/apps/webhooks/views.py b/designsafe/apps/webhooks/views.py index 389289e54c..47b6bfd694 100644 --- a/designsafe/apps/webhooks/views.py +++ b/designsafe/apps/webhooks/views.py @@ -173,7 +173,6 @@ def post(self, request, *args, **kwargs): Notification.EVENT_TYPE: event_type, Notification.STATUS: Notification.INFO, Notification.USER: job_owner, - Notification.MESSAGE: "Ready to view.", Notification.ACTION_LINK: address, }