diff --git a/package-lock.json b/package-lock.json index e207b28dd..1c60a47ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.16.0-pre-4", + "version": "1.16.0-beta-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.16.0-pre-4", + "version": "1.16.0-beta-6", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index ccccb9598..d7ac2bf8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.16.0-pre-4", + "version": "1.16.0-beta-6", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Common/Checkbox/Checkbox.tsx b/src/Common/Checkbox/Checkbox.tsx index d95747cf4..d7a65dfaf 100644 --- a/src/Common/Checkbox/Checkbox.tsx +++ b/src/Common/Checkbox/Checkbox.tsx @@ -14,6 +14,10 @@ * limitations under the License. */ +import { forwardRef } from 'react' + +import { stopPropagation } from '@Common/Helper' + import { CheckboxProps } from '../Types' import './Checkbox.scss' @@ -28,38 +32,32 @@ Valid States of Checkbox: 6. disabled: false, checked: true, value: CHECKED */ // TODO: Associate label with input element -export const Checkbox = ({ - rootClassName, - onClick, - name, - disabled, - value, - onChange, - tabIndex, - isChecked, - id, - dataTestId, - children, -}: CheckboxProps) => { - const rootClass = `${rootClassName || ''}` +export const Checkbox = forwardRef( + ( + { rootClassName, onClick, name, disabled, value, onChange, tabIndex, isChecked, id, dataTestId, children }, + forwardedRef, + ) => { + const rootClass = `${rootClassName || ''}` - return ( - // eslint-disable-next-line jsx-a11y/label-has-associated-control - - ) -} + return ( + // eslint-disable-next-line jsx-a11y/label-has-associated-control + + ) + }, +) diff --git a/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx b/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx index 36c349812..e93934753 100644 --- a/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx +++ b/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { KeyboardEvent, useCallback, useEffect, useMemo, useRef } from 'react' import { deepEquals } from '@rjsf/utils' +import { noop } from '@Common/Helper' + import { ShortcutType, UseRegisterShortcutContextType, UseRegisterShortcutProviderType } from './types' import { UseRegisterShortcutContext } from './UseRegisterShortcutContext' import { preprocessKeys, verifyCallbackStack } from './utils' @@ -25,7 +27,7 @@ const IGNORE_TAGS_FALLBACK = ['input', 'textarea', 'select'] const DEFAULT_TIMEOUT = 300 const UseRegisterShortcutProvider = ({ - containerRef, + shouldHookOntoWindow = true, ignoreTags, preventDefault = false, shortcutTimeout, @@ -117,13 +119,12 @@ const UseRegisterShortcutProvider = ({ } }, []) - const handleKeydownEvent = useCallback((event: KeyboardEvent) => { + const handleKeydownEvent = useCallback((event: KeyboardEvent) => { if ( // NOTE: in case of custom events generated by password managers autofill, the event.key is not set !event.key || - (ignoredTags.map((tag) => tag.toUpperCase()).indexOf((event.target as HTMLElement).tagName?.toUpperCase()) > - -1 && - (!containerRef || containerRef.current?.contains(event.target as HTMLElement))) || + ignoredTags.map((tag) => tag.toUpperCase()).indexOf((event.target as HTMLElement).tagName?.toUpperCase()) > + -1 || (event.target as HTMLElement)?.role === 'textbox' || disableShortcutsRef.current ) { @@ -161,12 +162,19 @@ const UseRegisterShortcutProvider = ({ }, []) useEffect(() => { - window.addEventListener('keydown', handleKeydownEvent) + if (!shouldHookOntoWindow) { + return noop + } + + window.addEventListener('keydown', handleKeydownEvent as unknown as (event: globalThis.KeyboardEvent) => void) window.addEventListener('keyup', handleKeyupEvent) window.addEventListener('blur', handleBlur) return () => { - window.removeEventListener('keydown', handleKeydownEvent) + window.removeEventListener( + 'keydown', + handleKeydownEvent as unknown as (event: globalThis.KeyboardEvent) => void, + ) window.removeEventListener('keyup', handleKeyupEvent) window.removeEventListener('blur', handleBlur) @@ -174,7 +182,7 @@ const UseRegisterShortcutProvider = ({ clearTimeout(keyDownTimeoutRef.current) } } - }, [handleKeyupEvent, handleKeydownEvent, handleBlur]) + }, [handleKeyupEvent, handleKeydownEvent, handleBlur, shouldHookOntoWindow]) const providerValue: UseRegisterShortcutContextType = useMemo( () => ({ @@ -182,8 +190,20 @@ const UseRegisterShortcutProvider = ({ unregisterShortcut, setDisableShortcuts, triggerShortcut, + ...(!shouldHookOntoWindow + ? { targetProps: { onKeyDown: handleKeydownEvent, onKeyUp: handleKeyupEvent, onBlur: handleBlur } } + : {}), }), - [registerShortcut, unregisterShortcut, setDisableShortcuts, triggerShortcut], + [ + registerShortcut, + unregisterShortcut, + setDisableShortcuts, + triggerShortcut, + shouldHookOntoWindow, + handleKeydownEvent, + handleKeyupEvent, + handleBlur, + ], ) return {children} diff --git a/src/Common/Hooks/UseRegisterShortcut/types.ts b/src/Common/Hooks/UseRegisterShortcut/types.ts index 13171cf9d..f6831c59d 100644 --- a/src/Common/Hooks/UseRegisterShortcut/types.ts +++ b/src/Common/Hooks/UseRegisterShortcut/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RefObject } from 'react' +import { FocusEvent, KeyboardEvent } from 'react' import { IS_PLATFORM_MAC_OS } from '@Common/Constants' @@ -80,10 +80,26 @@ export interface UseRegisterShortcutContextType { * Programmatically trigger a shortcut if already registered */ triggerShortcut: (keys: ShortcutType['keys']) => void + /** + * If shouldHookOntoWindow is false, these props need to be hooked onto + * the component that needs to listen to the shortcuts + */ + targetProps?: { + onKeyDown: (event: KeyboardEvent) => void + onKeyUp: (event: KeyboardEvent) => void + onBlur: (event: FocusEvent) => void + } } export interface UseRegisterShortcutProviderType { - containerRef?: RefObject + /** + * If false, the shortcuts will not be registered to the window object + * instead onKeyDown, onKeyUp and onBlur will be exposed as context methods + * which need to be hooked onto the component that needs to listen to the shortcuts + * + * defaults to true + */ + shouldHookOntoWindow?: boolean children: React.ReactNode /** * Defines how long after holding the keys down do we trigger the callback in milliseconds diff --git a/src/Common/Modals/VisibleModal.tsx b/src/Common/Modals/VisibleModal.tsx index a57e72ab6..84dbad2ae 100644 --- a/src/Common/Modals/VisibleModal.tsx +++ b/src/Common/Modals/VisibleModal.tsx @@ -27,6 +27,7 @@ export class VisibleModal extends React.Component<{ onEscape?: (e) => void }> { modalRef = document.getElementById('visible-modal') + previousActiveElement: HTMLElement | null = null constructor(props) { super(props) @@ -50,6 +51,8 @@ export class VisibleModal extends React.Component<{ this.modalRef.classList.add(this.props.noBackground ? 'show' : 'show-with-bg') preventBodyScroll(true) + this.previousActiveElement = document.activeElement as HTMLElement + if (this.props.parentClassName) { this.modalRef.classList.add(this.props.parentClassName) } @@ -61,6 +64,8 @@ export class VisibleModal extends React.Component<{ this.modalRef.classList.remove('show-with-bg') preventBodyScroll(false) + this.previousActiveElement?.focus({ preventScroll: true }) + if (this.props.parentClassName) { this.modalRef.classList.remove(this.props.parentClassName) } diff --git a/src/Common/Modals/VisibleModal2.tsx b/src/Common/Modals/VisibleModal2.tsx index d0806666d..c8ee73232 100644 --- a/src/Common/Modals/VisibleModal2.tsx +++ b/src/Common/Modals/VisibleModal2.tsx @@ -21,6 +21,7 @@ import { stopPropagation } from '../Helper' export class VisibleModal2 extends React.Component<{ className?: string; close?: (e) => void }> { modalRef = document.getElementById('visible-modal-2') + previousActiveElement: HTMLElement | null = null constructor(props) { super(props) @@ -38,12 +39,14 @@ export class VisibleModal2 extends React.Component<{ className?: string; close?: document.addEventListener('keydown', this.escFunction) this.modalRef.classList.add('show-with-bg') preventBodyScroll(true) + this.previousActiveElement = document.activeElement as HTMLElement } componentWillUnmount() { document.removeEventListener('keydown', this.escFunction) this.modalRef.classList.remove('show-with-bg') preventBodyScroll(false) + this.previousActiveElement?.focus({ preventScroll: true }) } handleBodyClick = (e: SyntheticEvent) => { diff --git a/src/Pages/ResourceBrowser/constants.tsx b/src/Pages/ResourceBrowser/constants.tsx index 598939977..8a776aa1f 100644 --- a/src/Pages/ResourceBrowser/constants.tsx +++ b/src/Pages/ResourceBrowser/constants.tsx @@ -17,6 +17,7 @@ import { ReactComponent as ICCleanBrush } from '@Icons/ic-medium-clean-brush.svg' import { ReactComponent as ICMediumPause } from '@Icons/ic-medium-pause.svg' import { ReactComponent as ICMediumPlay } from '@Icons/ic-medium-play.svg' +import { URLS } from '@Common/Constants' import { SelectPickerOptionType } from '@Shared/Components' import { NodeDrainRequest } from './types' @@ -114,3 +115,18 @@ export const NODE_DRAIN_OPTIONS_CHECKBOX_CONFIG: { export const GVK_FILTER_KIND_QUERY_PARAM_KEY = 'gvkFilterKind' export const GVK_FILTER_API_VERSION_QUERY_PARAM_KEY = 'gvkFilterApiVersion' + +export const DUMMY_RESOURCE_GVK_VERSION = 'v1' + +export const RESOURCE_BROWSER_ROUTES = { + OVERVIEW: `${URLS.RESOURCE_BROWSER}/:clusterId/overview`, + MONITORING_DASHBOARD: `${URLS.RESOURCE_BROWSER}/:clusterId/monitoring-dashboard`, + TERMINAL: `${URLS.RESOURCE_BROWSER}/:clusterId/terminal`, + CLUSTER_UPGRADE: `${URLS.RESOURCE_BROWSER}/:clusterId/cluster-upgrade`, + NODE_DETAIL: `${URLS.RESOURCE_BROWSER}/:clusterId/node/:name`, + K8S_RESOURCE_DETAIL: `${URLS.RESOURCE_BROWSER}/:clusterId/:namespace/:kind/:group/:version/:name`, + K8S_RESOURCE_LIST: `${URLS.RESOURCE_BROWSER}/:clusterId/:kind/:group/:version`, + RESOURCE_RECOMMENDER: `${URLS.RESOURCE_BROWSER}/:clusterId/resource-recommender`, +} as const + +export const K8S_EMPTY_GROUP = 'k8sEmptyGroup' diff --git a/src/Pages/ResourceBrowser/types.ts b/src/Pages/ResourceBrowser/types.ts index 425a28acd..c33fc3c23 100644 --- a/src/Pages/ResourceBrowser/types.ts +++ b/src/Pages/ResourceBrowser/types.ts @@ -150,3 +150,22 @@ export interface InstallationClusterConfigType status: InstallationClusterStatus correspondingClusterId: number | 0 } + +export enum NodeActionMenuOptionIdEnum { + terminal = 'terminal', + cordon = 'cordon', + uncordon = 'uncordon', + drain = 'drain', + editTaints = 'edit-taints', + editYaml = 'edit-yaml', + delete = 'delete', +} + +export enum ResourceBrowserActionMenuEnum { + manifest = 'manifest', + events = 'events', + logs = 'logs', + terminal = 'terminal', + delete = 'delete', + vulnerability = 'vulnerability', +} diff --git a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts index 6d78b2f5a..3395246aa 100644 --- a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts +++ b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts @@ -79,12 +79,10 @@ export const useActionMenu = ({ } } - const handleTriggerKeyDown: UsePopoverProps['onTriggerKeyDown'] = (e, openState, closePopover) => { + const handleTriggerKeyDown: UsePopoverProps['onTriggerKeyDown'] = (e, openState) => { if (!openState && (e.key === 'Enter' || e.key === ' ')) { setFocusedIndex(0) } - - handlePopoverKeyDown(e, openState, closePopover) } // POPOVER HOOK diff --git a/src/Shared/Components/Backdrop/Backdrop.tsx b/src/Shared/Components/Backdrop/Backdrop.tsx index d396e8a53..c130efd5e 100644 --- a/src/Shared/Components/Backdrop/Backdrop.tsx +++ b/src/Shared/Components/Backdrop/Backdrop.tsx @@ -45,6 +45,8 @@ const Backdrop = ({ children, onEscape, onClick, hasClearBackground = false, onB }, [onEscape]) useEffect(() => { + const previousActiveElement = document.activeElement as HTMLElement + preventBodyScroll(true) // Setting main as inert to that focus is trapped inside the new portal preventOutsideFocus({ identifier: DEVTRON_BASE_MAIN_ID, preventFocus: true }) @@ -56,6 +58,8 @@ const Backdrop = ({ children, onEscape, onClick, hasClearBackground = false, onB preventOutsideFocus({ identifier: DEVTRON_BASE_MAIN_ID, preventFocus: false }) preventOutsideFocus({ identifier: 'visible-modal', preventFocus: false }) preventOutsideFocus({ identifier: 'visible-modal-2', preventFocus: false }) + + previousActiveElement?.focus({ preventScroll: true }) } }, []) diff --git a/src/Shared/Components/BulkSelection/BulkSelection.tsx b/src/Shared/Components/BulkSelection/BulkSelection.tsx index 349b484c8..128ad5ece 100644 --- a/src/Shared/Components/BulkSelection/BulkSelection.tsx +++ b/src/Shared/Components/BulkSelection/BulkSelection.tsx @@ -17,13 +17,13 @@ import { forwardRef, MouseEvent } from 'react' import { ReactComponent as ICChevronDown } from '../../../Assets/Icon/ic-chevron-down.svg' -import { Checkbox, noop } from '../../../Common' +import { Checkbox, ConditionalWrap, noop } from '../../../Common' import { ActionMenu, ActionMenuItemType, ActionMenuProps } from '../ActionMenu' import { useBulkSelection } from './BulkSelectionProvider' import { BULK_DROPDOWN_TEST_ID, BulkSelectionOptionsLabels } from './constants' import { BulkSelectionEvents, BulkSelectionProps } from './types' -const BulkSelection = forwardRef( +const BulkSelection = forwardRef( ( { showPagination, disabled = false, showChevronDownIcon = true, selectAllIfNotPaginated = false }, forwardedRef, @@ -73,7 +73,9 @@ const BulkSelection = forwardRef( }) } - return ( + const shouldWrapActionMenu = !selectAllIfNotPaginated || showPagination + + const wrapWithActionMenu = (children: React.ReactElement) => ( ( }, ]} > -
-
- - - {showChevronDownIcon && } -
+ {children} + + ) -