diff --git a/package-lock.json b/package-lock.json index 20a8ff5ee..5773b038a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "ansi_up": "^5.2.1", "dayjs": "^1.11.13", "fast-json-patch": "^3.1.1", + "framer-motion": "^6.5.1", "jsonpath-plus": "^10.0.0", "react-dates": "^21.8.0", "react-monaco-editor": "^0.54.0", @@ -652,6 +653,23 @@ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "peer": true }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT", + "optional": true + }, "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", @@ -1666,6 +1684,70 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@motionone/animation": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "license": "MIT", + "dependencies": { + "@motionone/easing": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/dom": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", + "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", + "license": "MIT", + "dependencies": { + "@motionone/animation": "^10.12.0", + "@motionone/generators": "^10.12.0", + "@motionone/types": "^10.12.0", + "@motionone/utils": "^10.12.0", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/easing": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "license": "MIT", + "dependencies": { + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/generators": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "license": "MIT", + "dependencies": { + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/types": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==", + "license": "MIT" + }, + "node_modules/@motionone/utils": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "license": "MIT", + "dependencies": { + "@motionone/types": "^10.17.1", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5620,6 +5702,36 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/framer-motion": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", + "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "license": "MIT", + "dependencies": { + "@motionone/dom": "10.12.0", + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "popmotion": "11.0.3", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0 || ^18.0.0", + "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/framesync": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", + "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -5934,6 +6046,12 @@ "he": "bin/he" } }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", + "license": "MIT" + }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -7939,6 +8057,18 @@ "pathe": "^1.1.2" } }, + "node_modules/popmotion": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", + "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", + "license": "MIT", + "dependencies": { + "framesync": "6.0.1", + "hey-listen": "^1.0.8", + "style-value-types": "5.0.0", + "tslib": "^2.1.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -9187,6 +9317,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-value-types": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", + "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", + "license": "MIT", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", diff --git a/package.json b/package.json index 5aa81ab82..3c1d6e423 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "ansi_up": "^5.2.1", "dayjs": "^1.11.13", "fast-json-patch": "^3.1.1", + "framer-motion": "^6.5.1", "jsonpath-plus": "^10.0.0", "react-dates": "^21.8.0", "react-monaco-editor": "^0.54.0", diff --git a/src/Common/Hooks/UseRegisterShortcut/types.ts b/src/Common/Hooks/UseRegisterShortcut/types.ts index 7239058c2..ea44dd32e 100644 --- a/src/Common/Hooks/UseRegisterShortcut/types.ts +++ b/src/Common/Hooks/UseRegisterShortcut/types.ts @@ -25,6 +25,8 @@ export const KEYBOARD_KEYS_MAP = { E: 'E', R: 'R', K: 'K', + Escape: 'Escape', + Enter: 'Enter', } as const export type SupportedKeyboardKeysType = keyof typeof KEYBOARD_KEYS_MAP diff --git a/src/Shared/Components/Backdrop/Backdrop.tsx b/src/Shared/Components/Backdrop/Backdrop.tsx new file mode 100644 index 000000000..92b62524a --- /dev/null +++ b/src/Shared/Components/Backdrop/Backdrop.tsx @@ -0,0 +1,46 @@ +import { ReactNode, useEffect } from 'react' +import { motion } from 'framer-motion' +import { useRegisterShortcut } from '@Common/Hooks' +import { preventBodyScroll, toggleOutsideFocus } from '@Shared/Helpers' +import './backdrop.scss' +import { createPortal } from 'react-dom' +import { ToggleFocusType } from '@Shared/types' +import { DEVTRON_BASE_MAIN_ID } from '@Shared/constants' + +const Backdrop = ({ children, onEscape }: { children: ReactNode; onEscape: () => void }) => { + const { registerShortcut, unregisterShortcut } = useRegisterShortcut() + + // useEffect on onEscape since onEscape might change based on conditions + useEffect(() => { + registerShortcut({ keys: ['Escape'], callback: onEscape }) + + return () => { + unregisterShortcut(['Escape']) + } + }, [onEscape]) + + useEffect(() => { + preventBodyScroll(true) + // Setting main as inert to that focus is trapped inside the new portal + toggleOutsideFocus({ identifier: DEVTRON_BASE_MAIN_ID, toggleFocus: ToggleFocusType.Disable }) + + return () => { + preventBodyScroll(false) + toggleOutsideFocus({ identifier: DEVTRON_BASE_MAIN_ID, toggleFocus: ToggleFocusType.Enable }) + } + }, []) + + return createPortal( + + {children} + , + document.getElementById('backdrop'), + ) +} + +export default Backdrop diff --git a/src/Shared/Components/Backdrop/backdrop.scss b/src/Shared/Components/Backdrop/backdrop.scss new file mode 100644 index 000000000..b937d8e25 --- /dev/null +++ b/src/Shared/Components/Backdrop/backdrop.scss @@ -0,0 +1,5 @@ +.backdrop { + background: #000000bf; + z-index: var(--modal-index); + backdrop-filter: blur(1px); +} diff --git a/src/Shared/Components/Backdrop/index.tsx b/src/Shared/Components/Backdrop/index.tsx new file mode 100644 index 000000000..81f132b47 --- /dev/null +++ b/src/Shared/Components/Backdrop/index.tsx @@ -0,0 +1 @@ +export { default as Backdrop } from './Backdrop' diff --git a/src/Shared/Components/CICDHistory/TriggerOutput.tsx b/src/Shared/Components/CICDHistory/TriggerOutput.tsx index a06cc1a5d..e60b91b07 100644 --- a/src/Shared/Components/CICDHistory/TriggerOutput.tsx +++ b/src/Shared/Components/CICDHistory/TriggerOutput.tsx @@ -34,7 +34,6 @@ import { Reload, createGitCommitUrl, useAsync, - not, ZERO_TIME_STRING, useInterval, URLS, @@ -187,7 +186,7 @@ const ProgressingStatus = React.memo(({ status, stage, type }: ProgressingStatus } const toggleAbortConfiguration = (): void => { - setAbortConfirmation(not) + setAbortConfirmation(!abortConfirmation) } const closeForceAbortModal = (): void => { setAbortError({ @@ -215,64 +214,55 @@ const ProgressingStatus = React.memo(({ status, stage, type }: ProgressingStatus )} - - {abortConfirmation && ( - - )} - {abortError.status && ( - -
- Please try to force abort -
-
- Some resource might get orphaned which will be cleaned up with Job-lifecycle -
-
- )} + + +
+ Please try to force abort +
+
+ Some resource might get orphaned which will be cleaned up with Job-lifecycle +
+
) }) diff --git a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx index cd46a83e9..a67eab561 100644 --- a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx +++ b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx @@ -1,10 +1,12 @@ -import { ButtonHTMLAttributes, ChangeEvent, useState } from 'react' -import { CustomInput, VisibleModal } from '@Common/index' +import { ButtonHTMLAttributes, ChangeEvent, useCallback, useEffect, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' +import { CustomInput, useRegisterShortcut, UseRegisterShortcutProvider } from '@Common/index' import { ComponentSizeType } from '@Shared/constants' import { ConfirmationModalProps } from './types' import { getPrimaryButtonStyleFromVariant, getConfirmationLabel, getIconFromVariant } from './utils' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import './confirmationModal.scss' +import { Backdrop } from '../Backdrop' const ConfirmationModal = ({ title, @@ -14,92 +16,152 @@ const ConfirmationModal = ({ buttonConfig, customInputConfig, children, + showConfirmationModal, handleClose, }: ConfirmationModalProps) => { + const { registerShortcut, unregisterShortcut } = useRegisterShortcut() const customInputIdentifier = customInputConfig?.identifier const confirmationKeyword = customInputConfig?.confirmationKeyword const { primaryButtonConfig, secondaryButtonConfig } = buttonConfig const [confirmationText, setConfirmationText] = useState('') const RenderIcon = Icon ?? getIconFromVariant(variant) + const disablePrimaryButton: boolean = confirmationKeyword && confirmationText.trim() !== confirmationKeyword + + const handleEnterKeyPress = () => { + if (primaryButtonConfig && !disablePrimaryButton) { + primaryButtonConfig.onClick() + } + } + + const handleCustomInputKeyDown = (e) => { + if (e.key === 'Enter') { + handleEnterKeyPress() + } + } + + const handleCloseWrapper = useCallback(() => { + if (!primaryButtonConfig?.isLoading && !secondaryButtonConfig?.disabled) { + handleClose() + } + }, [primaryButtonConfig, secondaryButtonConfig]) + + useEffect(() => { + if (showConfirmationModal) { + // Timeout so that if modal is opened on enter press, it does not trigger onClick + setTimeout(() => { + registerShortcut({ keys: ['Enter'], callback: handleEnterKeyPress }) + }, 100) + } + + return () => { + if (showConfirmationModal) { + unregisterShortcut(['Enter']) + } + } + }, [showConfirmationModal, primaryButtonConfig, disablePrimaryButton]) + const handleCustomInputChange = (e: ChangeEvent) => { setConfirmationText(e.target.value) } - const disablePrimaryButton: boolean = confirmationKeyword && confirmationText.trim() !== confirmationKeyword - return ( - -
-
- - {typeof title === 'string' ? ( -
{title}
- ) : ( - title - )} - {typeof subtitle === 'string' ? ( -
{subtitle}
- ) : ( - subtitle - )} - {customInputConfig && ( - - )} - {children} -
-
- {secondaryButtonConfig && ( -
-
-
+ + {showConfirmationModal ? ( + + +
+ + + {typeof title === 'string' ? ( + {title} + ) : ( + title + )} + + {typeof subtitle === 'string' ? ( + {subtitle} + ) : ( + subtitle + )} + + {customInputConfig && ( + + )} + + {children} +
+
+ {secondaryButtonConfig && ( +
+
+
+ ) : null} +
) } -export default ConfirmationModal +const WrapWithShortcutProvider = (props: ConfirmationModalProps) => ( + + + +) + +export default WrapWithShortcutProvider diff --git a/src/Shared/Components/ConfirmationModal/types.tsx b/src/Shared/Components/ConfirmationModal/types.tsx index 72c0b5f59..4a236a0dd 100644 --- a/src/Shared/Components/ConfirmationModal/types.tsx +++ b/src/Shared/Components/ConfirmationModal/types.tsx @@ -8,9 +8,9 @@ export enum ConfirmationModalVariantType { custom = 'custom', } -interface CommonButtonProps - extends Pick, - Partial> {} +interface CommonButtonProps extends Pick, Partial> { + onClick: (e?: any) => void +} interface CustomInputConfig { identifier: string @@ -49,6 +49,7 @@ export type ConfirmationModalProps = { title: ReactNode subtitle: ReactNode handleClose: (e?: any) => void + showConfirmationModal: boolean } & ( | { variant: Exclude diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx index 8f5e8af10..69654e821 100644 --- a/src/Shared/Helpers.tsx +++ b/src/Shared/Helpers.tsx @@ -40,6 +40,8 @@ import { IntersectionChangeHandler, IntersectionOptions, Nodes, + ToggleFocusType, + ToggleOutsideFocus, WebhookEventNameType, } from './types' import { ReactComponent as ICPullRequest } from '../Assets/Icon/ic-pull-request.svg' @@ -96,6 +98,14 @@ export const preventBodyScroll = (lock: boolean): void => { } } +export const toggleOutsideFocus = ({ identifier, toggleFocus }: ToggleOutsideFocus) => { + if (toggleFocus === ToggleFocusType.Disable) { + document.getElementById(identifier).setAttribute('inert', 'true') + } else { + document.getElementById(identifier).removeAttribute('inert') + } +} + const getIsMaterialInfoValid = (materialInfo: MaterialInfo): boolean => !!( materialInfo.webhookData || diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx index 65d383eb4..870558fe6 100644 --- a/src/Shared/constants.tsx +++ b/src/Shared/constants.tsx @@ -503,3 +503,5 @@ export const ALL_RESOURCE_KIND_FILTER = 'all' export const OPEN_NEW_TICKET = 'https://enterprise.devtron.ai/portal/en/newticket' export const VIEW_ALL_TICKETS = 'https://enterprise.devtron.ai/portal/en/myarea' export const RAISE_ISSUE = 'https://github.com/devtron-labs/devtron/issues/new/choose' + +export const DEVTRON_BASE_MAIN_ID = 'devtron-base-main-identifier' diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 7a15c7ef0..80206f4b0 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -853,3 +853,13 @@ export interface DynamicTabType extends CommonTabArgsType { */ lastActiveTabId: string | null } + +export enum ToggleFocusType { + Enable = 'Enable', + Disable = 'Disable', +} + +export interface ToggleOutsideFocus { + identifier: string + toggleFocus: ToggleFocusType +}