From 07ee634c78e6ca18f867fd07c8a5b9345ca914dd Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Tue, 1 Jul 2025 11:11:26 +0530 Subject: [PATCH 1/6] fix: retain previous active element on backdrop open/close --- src/Common/Checkbox/Checkbox.tsx | 66 ++++++------ .../UseRegisterShortcutProvider.tsx | 27 +++-- src/Common/Hooks/UseRegisterShortcut/types.ts | 20 +++- src/Common/Modals/VisibleModal.tsx | 5 + src/Common/Modals/VisibleModal2.tsx | 3 + .../ActionMenu/useActionMenu.hook.ts | 4 +- src/Shared/Components/Backdrop/Backdrop.tsx | 4 + .../BulkSelection/BulkSelection.tsx | 32 +++--- src/Shared/Components/CICDHistory/utils.tsx | 2 +- .../Components/Popover/usePopover.hook.ts | 5 +- .../Table/BulkSelectionActionWidget.tsx | 13 +-- src/Shared/Components/Table/InternalTable.tsx | 101 +++++++++++------- .../Components/Table/Table.component.tsx | 39 ++----- src/Shared/Components/Table/TableContent.tsx | 11 +- .../Table/useTableWithKeyboardShortcuts.ts | 13 ++- 15 files changed, 186 insertions(+), 159 deletions(-) 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..40d65894b 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) @@ -182,6 +190,9 @@ const UseRegisterShortcutProvider = ({ unregisterShortcut, setDisableShortcuts, triggerShortcut, + ...(!shouldHookOntoWindow + ? { targetProps: { onKeyDown: handleKeydownEvent, onKeyUp: handleKeyupEvent, onBlur: handleBlur } } + : {}), }), [registerShortcut, unregisterShortcut, setDisableShortcuts, triggerShortcut], ) 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/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..48de5355f 100644 --- a/src/Shared/Components/BulkSelection/BulkSelection.tsx +++ b/src/Shared/Components/BulkSelection/BulkSelection.tsx @@ -23,7 +23,7 @@ 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, @@ -84,27 +84,19 @@ const BulkSelection = forwardRef( }, ]} > -
-
- - - {showChevronDownIcon && } -
- -
) diff --git a/src/Shared/Components/CICDHistory/utils.tsx b/src/Shared/Components/CICDHistory/utils.tsx index c039d5e26..f8b71f141 100644 --- a/src/Shared/Components/CICDHistory/utils.tsx +++ b/src/Shared/Components/CICDHistory/utils.tsx @@ -233,7 +233,7 @@ export const getHistoryItemStatusIconFromWorkflowStages = ( } export const getWorkerPodBaseUrl = (clusterId: number = DEFAULT_CLUSTER_ID, podNamespace: string = DEFAULT_NAMESPACE) => - `/resource-browser/${clusterId}/${podNamespace}/pod/k8sEmptyGroup` + `/resource-browser/${clusterId}/${podNamespace}/pod/k8sEmptyGroup/v1` export const getWorkflowNodeStatusTitle = (status: string) => { if (!status) { diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts index 278b1f7c4..b19fa217b 100644 --- a/src/Shared/Components/Popover/usePopover.hook.ts +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -1,4 +1,4 @@ -import { MouseEvent, useLayoutEffect, useRef, useState } from 'react' +import { useLayoutEffect, useRef, useState } from 'react' import { UsePopoverProps, UsePopoverReturnType } from './types' import { @@ -39,8 +39,7 @@ export const usePopover = ({ onOpen?.(openState) } - const togglePopover = (e: MouseEvent) => { - e.stopPropagation() + const togglePopover = () => { updateOpenState(!open) } diff --git a/src/Shared/Components/Table/BulkSelectionActionWidget.tsx b/src/Shared/Components/Table/BulkSelectionActionWidget.tsx index 7cf71e4c1..04b4e0b15 100644 --- a/src/Shared/Components/Table/BulkSelectionActionWidget.tsx +++ b/src/Shared/Components/Table/BulkSelectionActionWidget.tsx @@ -2,11 +2,10 @@ * Copyright (c) 2024. Devtron Inc. */ -import { MouseEvent, useEffect } from 'react' +import { MouseEvent } from 'react' import { ReactComponent as ICClose } from '@Icons/ic-close.svg' import { DraggableButton, DraggablePositionVariant, DraggableWrapper } from '@Common/DraggableWrapper' -import { useRegisterShortcut } from '@Common/Hooks' import { ComponentSizeType } from '@Shared/constants' import { Button, ButtonComponentType, ButtonStyleType, ButtonVariantType } from '../Button' @@ -21,16 +20,6 @@ const BulkSelectionActionWidget = ({ bulkActionsData, setBulkActionState, }: BulkSelectionActionWidgetProps) => { - const { registerShortcut, unregisterShortcut } = useRegisterShortcut() - - useEffect(() => { - registerShortcut({ keys: ['Escape'], callback: handleClearBulkSelection }) - - return () => { - unregisterShortcut(['Escape']) - } - }, []) - const onActionClick = (event: MouseEvent) => { const { dataset: { key }, diff --git a/src/Shared/Components/Table/InternalTable.tsx b/src/Shared/Components/Table/InternalTable.tsx index eb6721390..59bead88b 100644 --- a/src/Shared/Components/Table/InternalTable.tsx +++ b/src/Shared/Components/Table/InternalTable.tsx @@ -1,7 +1,7 @@ import { Fragment, useEffect, useMemo, useRef } from 'react' import ErrorScreenManager from '@Common/ErrorScreenManager' -import { GenericEmptyState, GenericFilterEmptyState, useAsync } from '@Common/index' +import { GenericEmptyState, GenericFilterEmptyState, useAsync, UseRegisterShortcutProvider } from '@Common/index' import { NO_ROWS_OR_GET_ROWS_ERROR } from './constants' import TableContent from './TableContent' @@ -53,16 +53,31 @@ const InternalTable = ({ ...otherFilters } = filterData ?? {} + const wrapperDivRef = useRef(null) + const { setIdentifiers } = bulkSelectionReturnValue ?? {} const searchSortTimeoutRef = useRef(-1) - useEffect( - () => () => { + useEffect(() => { + wrapperDivRef.current?.addEventListener('focusout', (e: FocusEvent) => { + const container = e.currentTarget as HTMLElement + const related = e.relatedTarget as HTMLElement | null + + if (container && (!related || related.tagName === 'BODY')) { + const tableElement = wrapperDivRef.current.getElementsByClassName('generic-table')[0] as HTMLDivElement + tableElement?.focus() + } + }) + + return () => { clearTimeout(searchSortTimeoutRef.current) - }, - [], - ) + } + }, []) + + useEffect(() => { + handleClearBulkSelection() + }, [rows]) const [_areFilteredRowsLoading, filteredRows, filteredRowsError, reloadFilteredRows] = useAsync(async () => { if (!rows && !getRows) { @@ -121,45 +136,49 @@ const InternalTable = ({ } return ( - + + + ) } return ( - - {renderContent()} - +
+ + {renderContent()} + +
) } diff --git a/src/Shared/Components/Table/Table.component.tsx b/src/Shared/Components/Table/Table.component.tsx index 8bf897e25..501209513 100644 --- a/src/Shared/Components/Table/Table.component.tsx +++ b/src/Shared/Components/Table/Table.component.tsx @@ -1,12 +1,6 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' -import { - noop, - UseRegisterShortcutProvider, - useResizableTableConfig, - useStateFilters, - useUrlFilters, -} from '@Common/index' +import { noop, useResizableTableConfig, useStateFilters, useUrlFilters } from '@Common/index' import { BulkSelectionEvents, BulkSelectionProvider, useBulkSelection } from '../BulkSelection' import { BULK_ACTION_GUTTER_LABEL } from './constants' @@ -92,7 +86,7 @@ const TableWithUseBulkSelectionReturnValue = (tableProps: TableWithBulkSelection (row: RowType) => { const isRowSelected = selectedIdentifiers[row.id] - if (!isRowSelected) { + if (!isRowSelected && !isBulkSelectionApplied) { /** * !FIXME: handleBulkSelection does not handle multiple updates in a single call * can be done by using callbacks when setting setIdentifiers in BulkSelectionProvider @@ -190,31 +184,16 @@ const UseUrlFilterWrapper = (props: FilterWrapperProps) => { const TableWrapper = (tableProps: TableProps) => { const { filtersVariant } = tableProps - const tableContainerRef = useRef(null) - - const renderContent = () => { - if (filtersVariant === FiltersTypeEnum.STATE) { - return - } - if (filtersVariant === FiltersTypeEnum.URL) { - return - } + if (filtersVariant === FiltersTypeEnum.STATE) { + return + } - return + if (filtersVariant === FiltersTypeEnum.URL) { + return } - return ( - -
- {renderContent()} -
-
- ) + return } export default TableWrapper diff --git a/src/Shared/Components/Table/TableContent.tsx b/src/Shared/Components/Table/TableContent.tsx index 38462cc0a..707ad208a 100644 --- a/src/Shared/Components/Table/TableContent.tsx +++ b/src/Shared/Components/Table/TableContent.tsx @@ -34,7 +34,7 @@ const TableContent = ({ }: TableContentProps) => { const rowsContainerRef = useRef(null) const parentRef = useRef(null) - const bulkSelectionButtonRef = useRef(null) + const bulkSelectionButtonRef = useRef(null) const headerRef = useRef(null) const [bulkActionState, setBulkActionState] = useState(null) @@ -99,7 +99,7 @@ const TableContent = ({ paginationVariant === PaginationEnum.PAGINATED && filteredRows?.length > (pageSizeOptions?.[0]?.value ?? DEFAULT_BASE_PAGE_SIZE) - const { activeRowIndex, setActiveRowIndex } = useTableWithKeyboardShortcuts( + const { activeRowIndex, setActiveRowIndex, shortcutContainerProps } = useTableWithKeyboardShortcuts( { bulkSelectionConfig, bulkSelectionReturnValue, handleToggleBulkSelectionOnRow }, visibleRows, showPagination, @@ -266,7 +266,12 @@ const TableContent = ({ } return ( -
+
, visibleRows: RowsType, showPagination: boolean, - bulkSelectionButtonRef: React.RefObject, + bulkSelectionButtonRef: React.RefObject, ) => { const isBulkSelectionConfigured = !!bulkSelectionConfig - const { registerShortcut, unregisterShortcut } = useRegisterShortcut() + const { registerShortcut, unregisterShortcut, targetProps } = useRegisterShortcut() const { handleBulkSelection } = bulkSelectionReturnValue ?? {} @@ -127,6 +127,13 @@ const useTableWithKeyboardShortcuts = ( return noop } + registerShortcut({ + keys: ['Escape'], + callback: () => { + handleBulkSelection({ action: BulkSelectionEvents.CLEAR_ALL_SELECTIONS }) + }, + }) + registerShortcut({ keys: ['Shift', 'ArrowDown'], callback: () => { @@ -204,6 +211,7 @@ const useTableWithKeyboardShortcuts = ( unregisterShortcut(['Shift', 'ArrowDown']) unregisterShortcut(['X']) unregisterShortcut(['Control', 'A']) + unregisterShortcut(['Escape']) } }, [ getMoveFocusToNextRowHandler, @@ -218,6 +226,7 @@ const useTableWithKeyboardShortcuts = ( return { activeRowIndex, setActiveRowIndex, + shortcutContainerProps: targetProps, } } From f545d666055f2d1321660b306a0e0015a2144f18 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Tue, 1 Jul 2025 11:34:20 +0530 Subject: [PATCH 2/6] chore: bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e207b28dd..6ca2aa805 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-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.16.0-pre-4", + "version": "1.16.0-beta-5", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index ccccb9598..cd9482f5c 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-5", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", From 44d758fb67daf5196580f995f05cec6e038a4e42 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Wed, 2 Jul 2025 03:14:41 +0530 Subject: [PATCH 3/6] fix: review comments --- .../UseRegisterShortcutProvider.tsx | 13 +++++++++++-- src/Pages/ResourceBrowser/constants.tsx | 16 ++++++++++++++++ src/Pages/ResourceBrowser/types.ts | 19 +++++++++++++++++++ src/Shared/Components/CICDHistory/utils.tsx | 5 +++-- src/Shared/Components/Table/InternalTable.tsx | 7 +++++-- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx b/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx index 40d65894b..e93934753 100644 --- a/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx +++ b/src/Common/Hooks/UseRegisterShortcut/UseRegisterShortcutProvider.tsx @@ -182,7 +182,7 @@ const UseRegisterShortcutProvider = ({ clearTimeout(keyDownTimeoutRef.current) } } - }, [handleKeyupEvent, handleKeydownEvent, handleBlur]) + }, [handleKeyupEvent, handleKeydownEvent, handleBlur, shouldHookOntoWindow]) const providerValue: UseRegisterShortcutContextType = useMemo( () => ({ @@ -194,7 +194,16 @@ const UseRegisterShortcutProvider = ({ ? { 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/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/CICDHistory/utils.tsx b/src/Shared/Components/CICDHistory/utils.tsx index f8b71f141..eb7511aa7 100644 --- a/src/Shared/Components/CICDHistory/utils.tsx +++ b/src/Shared/Components/CICDHistory/utils.tsx @@ -19,10 +19,11 @@ import moment from 'moment' import { ReactComponent as ICCheck } from '@Icons/ic-check.svg' import { ReactComponent as Close } from '@Icons/ic-close.svg' import { ReactComponent as ICInProgress } from '@Icons/ic-in-progress.svg' -import { DATE_TIME_FORMATS } from '@Common/Constants' +import { DATE_TIME_FORMATS, URLS } from '@Common/Constants' import { DeploymentAppTypes } from '@Common/Types' import { ALL_RESOURCE_KIND_FILTER } from '@Shared/constants' import { isTimeStringAvailable } from '@Shared/Helpers' +import { DUMMY_RESOURCE_GVK_VERSION, K8S_EMPTY_GROUP } from '@Pages/ResourceBrowser' import { DeploymentStatusBreakdownItemType, Node, ResourceKindType, WorkflowStatusEnum } from '../../types' import { Icon } from '../Icon' @@ -233,7 +234,7 @@ export const getHistoryItemStatusIconFromWorkflowStages = ( } export const getWorkerPodBaseUrl = (clusterId: number = DEFAULT_CLUSTER_ID, podNamespace: string = DEFAULT_NAMESPACE) => - `/resource-browser/${clusterId}/${podNamespace}/pod/k8sEmptyGroup/v1` + `${URLS.RESOURCE_BROWSER}/${clusterId}/${podNamespace}/pod/${K8S_EMPTY_GROUP}/${DUMMY_RESOURCE_GVK_VERSION}` export const getWorkflowNodeStatusTitle = (status: string) => { if (!status) { diff --git a/src/Shared/Components/Table/InternalTable.tsx b/src/Shared/Components/Table/InternalTable.tsx index 59bead88b..480b892e2 100644 --- a/src/Shared/Components/Table/InternalTable.tsx +++ b/src/Shared/Components/Table/InternalTable.tsx @@ -60,7 +60,7 @@ const InternalTable = ({ const searchSortTimeoutRef = useRef(-1) useEffect(() => { - wrapperDivRef.current?.addEventListener('focusout', (e: FocusEvent) => { + const handleFocusOutEvent = (e: FocusEvent) => { const container = e.currentTarget as HTMLElement const related = e.relatedTarget as HTMLElement | null @@ -68,10 +68,13 @@ const InternalTable = ({ const tableElement = wrapperDivRef.current.getElementsByClassName('generic-table')[0] as HTMLDivElement tableElement?.focus() } - }) + } + + wrapperDivRef.current?.addEventListener('focusout', handleFocusOutEvent) return () => { clearTimeout(searchSortTimeoutRef.current) + wrapperDivRef.current?.removeEventListener('focusout', handleFocusOutEvent) } }, []) From 1cb7a9c24ebe5ef9bf49b49a054fd851d4dc4cf9 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Wed, 2 Jul 2025 03:20:48 +0530 Subject: [PATCH 4/6] fix: dont scroll to view on horizontal scroll --- src/Shared/Components/Table/TableContent.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Shared/Components/Table/TableContent.tsx b/src/Shared/Components/Table/TableContent.tsx index 707ad208a..ffba0e4d1 100644 --- a/src/Shared/Components/Table/TableContent.tsx +++ b/src/Shared/Components/Table/TableContent.tsx @@ -124,9 +124,15 @@ const TableContent = ({ } const focusActiveRow = (node: HTMLDivElement) => { - if (node && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName.toUpperCase())) { + if ( + node && + !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName.toUpperCase()) && + node.dataset.active === 'true' + ) { node.focus({ preventScroll: true }) scrollToShowActiveElementIfNeeded(node, rowsContainerRef.current, headerRef.current?.offsetHeight) + // eslint-disable-next-line no-param-reassign + node.dataset.active = 'false' } } From 1f18d07b1cc79c55face9c0e230a31493b14cc2c Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Wed, 2 Jul 2025 03:21:23 +0530 Subject: [PATCH 5/6] chore: bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ca2aa805..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-beta-5", + "version": "1.16.0-beta-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.16.0-beta-5", + "version": "1.16.0-beta-6", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index cd9482f5c..d7ac2bf8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.16.0-beta-5", + "version": "1.16.0-beta-6", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", From d4dc889966d5073857cfcf7df55b2b444e7cfd7c Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Wed, 2 Jul 2025 11:32:41 +0530 Subject: [PATCH 6/6] chore: conditional wrap in bulk selection --- .../Components/BulkSelection/BulkSelection.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Shared/Components/BulkSelection/BulkSelection.tsx b/src/Shared/Components/BulkSelection/BulkSelection.tsx index 48de5355f..128ad5ece 100644 --- a/src/Shared/Components/BulkSelection/BulkSelection.tsx +++ b/src/Shared/Components/BulkSelection/BulkSelection.tsx @@ -17,7 +17,7 @@ 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' @@ -73,7 +73,9 @@ const BulkSelection = forwardRef( }) } - return ( + const shouldWrapActionMenu = !selectAllIfNotPaginated || showPagination + + const wrapWithActionMenu = (children: React.ReactElement) => ( ( }, ]} > + {children} + + ) + + return ( +
( rootClassName="icon-dim-20 m-0" value={checkboxValue} disabled={disabled} - onClick={selectAllIfNotPaginated && !showPagination ? onSinglePageSelectAll : null} + onClick={!shouldWrapActionMenu ? onSinglePageSelectAll : null} // Ideally should be disabled but was giving issue with cursor /> {showChevronDownIcon && }
- +
) }, )