From c69f010959a2d854bac7a694baf5e5a635aeae8b Mon Sep 17 00:00:00 2001 From: Frank Flitton Date: Fri, 19 May 2023 15:24:53 -0700 Subject: [PATCH] Feature: Fullview Flyte Deck modal (#764) * chore: full viewport modal Signed-off-by: Frank Flitton * chore: close modal on escape Signed-off-by: Frank Flitton * chore: key events as hook Signed-off-by: Frank Flitton * chore: add close on esc key to other modals Signed-off-by: Frank Flitton --------- Signed-off-by: Frank Flitton --- .../Entities/EntityDetailsHeader.tsx | 8 ++- .../ExecutionDetailsActions.tsx | 63 ++++++++++++++----- .../ExecutionDetailsAppBarContent.tsx | 7 ++- .../ExecutionInputsOutputsModal.tsx | 2 + .../Launch/LaunchForm/LaunchFormDialog.tsx | 6 +- .../components/common/ClosableDialogTitle.tsx | 5 ++ .../hooks/test/useKeyListener.test.tsx | 23 +++++++ .../src/components/hooks/useKeyListener.ts | 29 +++++++++ 8 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 packages/console/src/components/hooks/test/useKeyListener.test.tsx create mode 100644 packages/console/src/components/hooks/useKeyListener.ts diff --git a/packages/console/src/components/Entities/EntityDetailsHeader.tsx b/packages/console/src/components/Entities/EntityDetailsHeader.tsx index db0c5c0e1..562c46b46 100644 --- a/packages/console/src/components/Entities/EntityDetailsHeader.tsx +++ b/packages/console/src/components/Entities/EntityDetailsHeader.tsx @@ -9,6 +9,7 @@ import { getProjectDomain } from 'models/Project/utils'; import * as React from 'react'; import { Link } from 'react-router-dom'; import { LaunchForm } from 'components/Launch/LaunchForm/LaunchForm'; +import { useEscapeKey } from 'components/hooks/useKeyListener'; import { backUrlGenerator, backToDetailUrlGenerator } from './generators'; import { entityStrings } from './constants'; import t, { patternKey } from './strings'; @@ -64,7 +65,12 @@ export const EntityDetailsHeader: React.FC = ({ const commonStyles = useCommonStyles(); const [showLaunchForm, setShowLaunchForm] = React.useState(false); - const onCancelLaunch = () => setShowLaunchForm(false); + const onCancelLaunch = (_?: KeyboardEvent) => { + setShowLaunchForm(false); + }; + + // Close modal on escape key press + useEscapeKey(onCancelLaunch); const domain = getProjectDomain(project, id.domain); const headerText = `${domain.name} / ${id.name}`; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx index ea5202b99..8fd46e07d 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Button, Dialog, IconButton } from '@material-ui/core'; +import { Button, Dialog, Grid, IconButton } from '@material-ui/core'; import { ResourceIdentifier, Identifier } from 'models/Common/types'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { getTask } from 'models/Task/api'; @@ -15,6 +15,8 @@ import { NodeExecutionPhase } from 'models/Execution/enums'; import { extractCompiledNodes } from 'components/hooks/utils'; import Close from '@material-ui/icons/Close'; import classnames from 'classnames'; +import { Fullscreen, FullscreenExit } from '@material-ui/icons'; +import { useEscapeKey } from 'components/hooks/useKeyListener'; import { NodeExecutionDetails } from '../types'; import t from './strings'; import { ExecutionNodeDeck } from './ExecutionNodeDeck'; @@ -38,10 +40,18 @@ const useStyles = makeStyles((theme: Theme) => { maxHeight: `calc(100% - ${theme.spacing(12)}px)`, height: theme.spacing(90), width: theme.spacing(110), + transition: 'all 0.3s ease', }, - dialogTitle: { - display: 'flex', - alignItems: 'center', + fullscreenDialog: { + maxWidth: '100vw', + width: '100vw', + maxHeight: '100svh', + height: '100svh', + margin: 0, + transition: 'all 0.3s ease', + borderRadius: 0, + }, + dialogHeader: { padding: theme.spacing(2), paddingBottom: theme.spacing(0), fontFamily: 'Open sans', @@ -56,8 +66,7 @@ const useStyles = makeStyles((theme: Theme) => { paddingBottom: theme.spacing(2), }, close: { - position: 'absolute', - right: theme.spacing(2), + paddingRight: theme.spacing(2), }, }; }); @@ -128,6 +137,14 @@ export const ExecutionDetailsActions = ({ const [showDeck, setShowDeck] = React.useState(false); const onCloseDeck = () => setShowDeck(false); + // Close deck modal on escape key press + useEscapeKey(onCloseDeck); + + const [fullScreen, setSetFullScreen] = React.useState(false); + const toggleFullScreen = () => { + setSetFullScreen(!fullScreen); + }; + const rerunOnClick = (e: React.MouseEvent) => { e.stopPropagation(); setShowLaunchForm(true); @@ -181,20 +198,32 @@ export const ExecutionDetailsActions = ({ )} {execution?.value?.closure?.deckUri && ( -
-

{t('flyteDeck')}

- - - -
+ + + + {fullScreen ? : } + + + +

{t('flyteDeck')}

+
+ + + + + +
)} diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx index c5fcc052a..ea4257b6b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx @@ -13,6 +13,7 @@ import { history } from 'routes/history'; import { Routes } from 'routes/routes'; import { WorkflowExecutionPhase } from 'models/Execution/enums'; import { SubNavBarContent } from 'components/Navigation/SubNavBarContent'; +import { useEscapeKey } from 'components/hooks/useKeyListener'; import { ExecutionInputsOutputsModal } from '../ExecutionInputsOutputsModal'; import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; import { TerminateExecutionButton } from '../TerminateExecution/TerminateExecutionButton'; @@ -90,7 +91,11 @@ export const ExecutionDetailsAppBarContentInner: React.FC<{}> = () => { const isTerminal = executionIsTerminal(execution); const onClickShowInputsOutputs = () => setShowInputsOutputs(true); const onClickRelaunch = () => setShowRelaunchForm(true); - const onCloseRelaunch = () => setShowRelaunchForm(false); + const onCloseRelaunch = (_?: any) => setShowRelaunchForm(false); + + // Close modal on escape key press + useEscapeKey(onCloseRelaunch); + const fromExecutionNav = new URLSearchParams(history.location.search).get( 'fromExecutionNav', ); diff --git a/packages/console/src/components/Executions/ExecutionInputsOutputsModal.tsx b/packages/console/src/components/Executions/ExecutionInputsOutputsModal.tsx index d2dd93a26..e67d24ede 100644 --- a/packages/console/src/components/Executions/ExecutionInputsOutputsModal.tsx +++ b/packages/console/src/components/Executions/ExecutionInputsOutputsModal.tsx @@ -6,6 +6,7 @@ import { LiteralMapViewer } from 'components/Literals/LiteralMapViewer'; import { emptyLiteralMapBlob } from 'models/Common/constants'; import { Execution } from 'models/Execution/types'; import * as React from 'react'; +import { useEscapeKey } from 'components/hooks/useKeyListener'; import { useWorkflowExecutionData } from './useWorkflowExecution'; const useStyles = makeStyles((theme: Theme) => ({ @@ -126,6 +127,7 @@ export const ExecutionInputsOutputsModal: React.FC< ExecutionInputsOutputsModalProps > = ({ execution, onClose }) => { const styles = useStyles(); + useEscapeKey(onClose); return ( { - const onCancelLaunch = () => setShowLaunchForm(false); + const onCancelLaunch = (_?: any) => setShowLaunchForm(false); + + // Close modal on escape key press + useEscapeKey(onCancelLaunch); // prevent child onclick event in the dialog triggers parent onclick event const dialogOnClick = (e: React.MouseEvent) => { diff --git a/packages/console/src/components/common/ClosableDialogTitle.tsx b/packages/console/src/components/common/ClosableDialogTitle.tsx index 9e2aa95a2..3f5e74a8c 100644 --- a/packages/console/src/components/common/ClosableDialogTitle.tsx +++ b/packages/console/src/components/common/ClosableDialogTitle.tsx @@ -1,6 +1,7 @@ import { DialogTitle, IconButton, Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import Close from '@material-ui/icons/Close'; +import { useEscapeKey } from 'components/hooks/useKeyListener'; import * as React from 'react'; const useStyles = makeStyles((theme: Theme) => ({ @@ -27,6 +28,10 @@ export const ClosableDialogTitle: React.FC = ({ onClose, }) => { const styles = useStyles(); + + // Close modal on escape key press + useEscapeKey(onClose); + return ( {children} diff --git a/packages/console/src/components/hooks/test/useKeyListener.test.tsx b/packages/console/src/components/hooks/test/useKeyListener.test.tsx new file mode 100644 index 000000000..7b52c5daa --- /dev/null +++ b/packages/console/src/components/hooks/test/useKeyListener.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { fireEvent, getByText, render } from '@testing-library/react'; +import { useEscapeKey } from '../useKeyListener'; + +it('calls the callback on pressing the ESC key', () => { + const callbackSpy = jest.fn(); + const callback = (_?: KeyboardEvent) => { + callbackSpy(); + }; + + const TestComponent = () => { + useEscapeKey(callback); + return
test
; + }; + + render(); + + fireEvent.keyDown(getByText(global.document.body, 'test'), { + key: 'Escape', + }); + + expect(callbackSpy).toHaveBeenCalled(); +}); diff --git a/packages/console/src/components/hooks/useKeyListener.ts b/packages/console/src/components/hooks/useKeyListener.ts new file mode 100644 index 000000000..0a181b43c --- /dev/null +++ b/packages/console/src/components/hooks/useKeyListener.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; + +/** + * Safely register and unregister a key listener on the document. + * @param onKeyPress + * @param keycode + */ +export const useKeyListener = ( + onKeyPress: (e: KeyboardEvent) => void, + keycode = 'Escape', +) => { + useEffect(() => { + const eventCallback = e => { + if (e.key === keycode) { + onKeyPress(e); + } + }; + document.addEventListener('keydown', eventCallback); + return () => document.removeEventListener('keydown', eventCallback); + }, []); +}; + +/** + * Register a key listener for the Escape key. + * @param onKeyPress + */ +export const useEscapeKey = (onKeyPress: (e: KeyboardEvent) => void) => { + useKeyListener(onKeyPress, 'Escape'); +};