From 8f2c4c62a1fd10d1bfd638ae66d3f7bee6c40ffa Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Sun, 6 Apr 2025 19:12:17 +0530 Subject: [PATCH 001/157] feat: add ClustersAndEnvironments in Pages --- src/Assets/IconV2/ic-cluster.svg | 3 +++ .../ClustersAndEnvironments/index.ts | 1 + .../ClustersAndEnvironments/types.ts | 11 +++++++++++ src/Pages/GlobalConfigurations/index.ts | 1 + src/Shared/Components/Icon/Icon.tsx | 2 ++ 5 files changed, 18 insertions(+) create mode 100644 src/Assets/IconV2/ic-cluster.svg create mode 100644 src/Pages/GlobalConfigurations/ClustersAndEnvironments/index.ts create mode 100644 src/Pages/GlobalConfigurations/ClustersAndEnvironments/types.ts diff --git a/src/Assets/IconV2/ic-cluster.svg b/src/Assets/IconV2/ic-cluster.svg new file mode 100644 index 000000000..f6aea37fd --- /dev/null +++ b/src/Assets/IconV2/ic-cluster.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/index.ts b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/index.ts new file mode 100644 index 000000000..cb3c3c34c --- /dev/null +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/index.ts @@ -0,0 +1 @@ +export type { NewClusterFormProps, NewClusterFormFooterProps } from './types' diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/types.ts b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/types.ts new file mode 100644 index 000000000..a52837d0d --- /dev/null +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/types.ts @@ -0,0 +1,11 @@ +import { Dispatch, SetStateAction } from 'react' + +export interface NewClusterFormFooterProps { + apiCallInProgress: boolean + handleModalClose: () => void +} + +export interface NewClusterFormProps extends NewClusterFormFooterProps { + setApiCallInProgress: Dispatch> + FooterComponent: React.FunctionComponent +} diff --git a/src/Pages/GlobalConfigurations/index.ts b/src/Pages/GlobalConfigurations/index.ts index 69c70dff6..e57d047fa 100644 --- a/src/Pages/GlobalConfigurations/index.ts +++ b/src/Pages/GlobalConfigurations/index.ts @@ -18,3 +18,4 @@ export * from './BuildInfra' export * from './Authorization' export * from './ScopedVariables' export * from './DeploymentCharts' +export * from './ClustersAndEnvironments' diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index 53f04b867..7266c1d37 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -24,6 +24,7 @@ import { ReactComponent as ICCiWebhook } from '@IconsV2/ic-ci-webhook.svg' import { ReactComponent as ICCircleLoader } from '@IconsV2/ic-circle-loader.svg' import { ReactComponent as ICClock } from '@IconsV2/ic-clock.svg' import { ReactComponent as ICCloseSmall } from '@IconsV2/ic-close-small.svg' +import { ReactComponent as ICCluster } from '@IconsV2/ic-cluster.svg' import { ReactComponent as ICCode } from '@IconsV2/ic-code.svg' import { ReactComponent as ICContainer } from '@IconsV2/ic-container.svg' import { ReactComponent as ICCookr } from '@IconsV2/ic-cookr.svg' @@ -118,6 +119,7 @@ export const iconMap = { 'ic-circle-loader': ICCircleLoader, 'ic-clock': ICClock, 'ic-close-small': ICCloseSmall, + 'ic-cluster': ICCluster, 'ic-code': ICCode, 'ic-container': ICContainer, 'ic-cookr': ICCookr, From 5859e44bccc9b5dc7bff828afb7a861b073d7508 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Mon, 7 Apr 2025 22:08:39 +0530 Subject: [PATCH 002/157] feat: add Icons & installationId in cluster capacity type --- src/Assets/IconV2/ic-pencil.svg | 3 + src/Assets/IconV2/ic-sliders-vertical.svg | 3 + src/Pages/ResourceBrowser/types.ts | 1 + .../Components/Button/Button.component.tsx | 237 ++++++++++-------- src/Shared/Components/Icon/Icon.tsx | 4 + src/Shared/types.ts | 1 + 6 files changed, 149 insertions(+), 100 deletions(-) create mode 100644 src/Assets/IconV2/ic-pencil.svg create mode 100644 src/Assets/IconV2/ic-sliders-vertical.svg diff --git a/src/Assets/IconV2/ic-pencil.svg b/src/Assets/IconV2/ic-pencil.svg new file mode 100644 index 000000000..38e2207ed --- /dev/null +++ b/src/Assets/IconV2/ic-pencil.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-sliders-vertical.svg b/src/Assets/IconV2/ic-sliders-vertical.svg new file mode 100644 index 000000000..9d147df87 --- /dev/null +++ b/src/Assets/IconV2/ic-sliders-vertical.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Pages/ResourceBrowser/types.ts b/src/Pages/ResourceBrowser/types.ts index 935665f2f..764817022 100644 --- a/src/Pages/ResourceBrowser/types.ts +++ b/src/Pages/ResourceBrowser/types.ts @@ -64,6 +64,7 @@ export interface ClusterCapacityType { nodeErrors: Record[] status?: ClusterStatusType isProd: boolean + installationId?: number } export interface ClusterDetail extends ClusterCapacityType { diff --git a/src/Shared/Components/Button/Button.component.tsx b/src/Shared/Components/Button/Button.component.tsx index 118c118b6..b6bdb726b 100644 --- a/src/Shared/Components/Button/Button.component.tsx +++ b/src/Shared/Components/Button/Button.component.tsx @@ -14,7 +14,16 @@ * limitations under the License. */ -import { ButtonHTMLAttributes, MutableRefObject, PropsWithChildren, useEffect, useRef, useState } from 'react' +import { + ButtonHTMLAttributes, + forwardRef, + MutableRefObject, + PropsWithChildren, + RefCallback, + useEffect, + useRef, + useState, +} from 'react' import { Link, LinkProps } from 'react-router-dom' import { Progressing } from '@Common/Progressing' import { Tooltip } from '@Common/Tooltip' @@ -30,6 +39,7 @@ const ButtonElement = ({ buttonProps, onClick, elementRef, + forwardedRef, ...props }: PropsWithChildren< Omit< @@ -51,8 +61,28 @@ const ButtonElement = ({ 'data-testid': ButtonProps['dataTestId'] 'aria-label': ButtonProps['ariaLabel'] elementRef: MutableRefObject + forwardedRef: + | RefCallback + | MutableRefObject } >) => { + const refCallback = (node: HTMLButtonElement | HTMLAnchorElement) => { + // eslint-disable-next-line no-param-reassign + elementRef.current = node + + if (!forwardedRef) { + return + } + + if (typeof forwardedRef === 'function') { + forwardedRef(node) + return + } + + // eslint-disable-next-line no-param-reassign + forwardedRef.current = node + } + if (component === ButtonComponentType.link) { return ( } + ref={refCallback} /> ) } @@ -73,7 +103,7 @@ const ButtonElement = ({ // eslint-disable-next-line react/button-has-type type={buttonProps?.type || 'button'} onClick={onClick as ButtonHTMLAttributes['onClick']} - ref={elementRef as MutableRefObject} + ref={refCallback} /> ) } @@ -142,116 +172,123 @@ const ButtonElement = ({ * + ) + } + } + return (
  • {renderIcon(startIcon)} -
    - -
    - {label} -
    -
    - {description && - (typeof description === 'string' ? ( -

    - {description} -

    - ) : ( -
    {description}
    - ))} -
    + {renderComponent()} {renderIcon(endIcon)}
  • diff --git a/src/Shared/Components/ActionMenu/actionMenu.scss b/src/Shared/Components/ActionMenu/actionMenu.scss index 48d99f455..659f1ed47 100644 --- a/src/Shared/Components/ActionMenu/actionMenu.scss +++ b/src/Shared/Components/ActionMenu/actionMenu.scss @@ -3,14 +3,17 @@ margin: 0; &__group { - border-top: 1px solid var(--border-secondary-translucent); + border-top: 1px solid var(--border-secondary); &:first-child { border-top: none; - padding-top: 0; } } + &__group-label { + top: 0; + } + &__group-list { list-style: none; } @@ -23,11 +26,25 @@ &--focused-negative { background-color: var(--R50); } + + > button { + text-align: left; + } } - &__searchbox input { - border: none; - padding: 0; - background-color: transparent; + &__searchbox { + & + .action-menu__group { + border-top: none; + } + + & ~ .action-menu__group .action-menu__group-label { + top: 37.5px; + } + + input { + border: none; + padding: 0; + background-color: transparent; + } } } diff --git a/src/Shared/Components/ActionMenu/types.ts b/src/Shared/Components/ActionMenu/types.ts index c9787c109..e31e1fbf2 100644 --- a/src/Shared/Components/ActionMenu/types.ts +++ b/src/Shared/Components/ActionMenu/types.ts @@ -1,8 +1,32 @@ import { ReactElement } from 'react' +import { LinkProps } from 'react-router-dom' +import { ButtonProps } from '../Button' import { IconsProps } from '../Icon' import { SelectPickerOptionType, SelectPickerProps } from '../SelectPicker' +type ConditionalActionMenuComponentType = + | { + /** + * @default 'button' + */ + componentType?: 'button' + href?: never + to?: never + } + | { + componentType?: 'anchor' + /** Specifies the URL for the `` tag. */ + href: string + to?: never + } + | { + componentType?: 'link' + /** Specifies the `to` property for react-router `Link` */ + to: LinkProps['to'] + href?: never + } + export type ActionMenuItemType = Omit & { /** The text label for the menu item. */ label: string @@ -17,7 +41,7 @@ export type ActionMenuItemType = Omit /** Defines the icon to be displayed at the end of the menu item. */ endIcon?: Pick -} +} & ConditionalActionMenuComponentType export type ActionMenuOptionType = { /** @@ -70,13 +94,32 @@ export type UseActionMenuProps = { * @param item - The selected item. */ onClick: (item: ActionMenuItemType) => void + /** + * Callback function triggered when the action menu is opened or closed. + * @param open - A boolean indicating whether the menu is open. + */ + onOpen?: (open: boolean) => void } export type ActionMenuProps = UseActionMenuProps & - Pick & { - /** The React element to which the ActionMenu is attached. */ - children: ReactElement - } + Pick & + ( + | { + /** + * The React element to which the ActionMenu is attached. + * @note only use when children is not `Button` component otherwise use `buttonProps`. + */ + children: ReactElement + buttonProps?: never + } + | { + /** + * Properties for the button to which the ActionMenu is attached. + */ + buttonProps: ButtonProps + children?: never + } + ) export type ActionMenuItemProps = Pick & { item: ActionMenuItemType diff --git a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts index 78af32b16..58cb82615 100644 --- a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts +++ b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts @@ -1,4 +1,4 @@ -import { ChangeEvent, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { ChangeEvent, useLayoutEffect, useMemo, useRef, useState } from 'react' import { UseActionMenuProps } from './types' import { @@ -10,6 +10,8 @@ import { getActionMenuPositionStyle, } from './utils' +const ACTION_MENU_Z_INDEX_CLASS = 'dc__zi-20' + export const useActionMenu = ({ position = 'bottom', alignment = 'start', @@ -17,6 +19,7 @@ export const useActionMenu = ({ options, isSearchable, onClick, + onOpen, }: UseActionMenuProps) => { // STATES const [open, setOpen] = useState(false) @@ -41,11 +44,16 @@ export const useActionMenu = ({ const menuRef = useRef(null) // HANDLERS - const toggleMenu = () => setOpen(!open) + const updateOpenState = (openState: typeof open) => { + setOpen(openState) + onOpen?.(openState) + } + + const toggleMenu = () => updateOpenState(!open) const closeMenu = () => { setFocusedIndex(-1) - setOpen(false) + updateOpenState(false) } const getNextIndex = (start: number, arrowDirection: 1 | -1) => { @@ -96,7 +104,7 @@ export const useActionMenu = ({ const handleTriggerKeyDown = (e: React.KeyboardEvent) => { if (!open && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault() - setOpen(true) + updateOpenState(true) setFocusedIndex(0) } @@ -107,16 +115,6 @@ export const useActionMenu = ({ setSearchTerm(e.target.value) } - useEffect(() => { - const onClickOutside = (e: MouseEvent) => { - if (!menuRef.current?.contains(e.target as Node) && !triggerRef.current?.contains(e.target as Node)) { - closeMenu() - } - } - document.addEventListener('mousedown', onClickOutside) - return () => document.removeEventListener('mousedown', onClickOutside) - }, []) - useLayoutEffect(() => { if (!open || !triggerRef.current || !menuRef.current) { return @@ -134,6 +132,25 @@ export const useActionMenu = ({ setActualPosition(fallbackPosition) setActualAlignment(fallbackAlignment) + + // prevent scroll propagation unless scrollable + const handleWheel = (e: WheelEvent) => { + e.stopPropagation() + const atTop = menuRef.current.scrollTop === 0 && e.deltaY < 0 + const atBottom = + menuRef.current.scrollHeight - menuRef.current.clientHeight === menuRef.current.scrollTop && + e.deltaY > 0 + + if (atTop || atBottom) { + e.preventDefault() + } + } + + menuRef.current.addEventListener('wheel', handleWheel, { passive: false }) + // eslint-disable-next-line consistent-return + return () => { + menuRef.current.removeEventListener('wheel', handleWheel) + } }, [open, position, alignment]) return { @@ -150,10 +167,15 @@ export const useActionMenu = ({ 'aria-expanded': open, tabIndex: 0, }, + overlayProps: { + role: 'dialog', + onClick: closeMenu, + className: `dc__position-fixed dc__top-0 dc__right-0 dc__left-0 dc__bottom-0 ${ACTION_MENU_Z_INDEX_CLASS}`, + }, menuProps: { role: 'menu', ref: menuRef, - className: `action-menu dc__position-abs bg__menu--primary shadow__menu border__primary br-6 px-0 dc__zi-5 mxh-300 dc__overflow-auto ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, + className: `action-menu dc__position-abs bg__menu--primary shadow__menu border__primary br-6 px-0 mxh-300 dc__overflow-auto ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''} ${ACTION_MENU_Z_INDEX_CLASS}`, onKeyDown: handleMenuKeyDown, style: { width: !isAutoWidth ? `${width}px` : undefined, From 965db8b275e9d8ddf06238bc0ab0f9363232be73 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 9 May 2025 15:10:38 +0530 Subject: [PATCH 109/157] feat: revamp PageHeader Help button --- src/Assets/IconV2/ic-discord-fill.svg | 3 + src/Assets/IconV2/ic-file-edit.svg | 3 + src/Assets/IconV2/ic-file.svg | 3 + src/Assets/IconV2/ic-files.svg | 3 + src/Assets/IconV2/ic-megaphone-right.svg | 3 + src/Assets/IconV2/ic-path.svg | 3 + src/Shared/Components/Header/HelpButton.tsx | 125 +++++++++++++++ src/Shared/Components/Header/HelpNav.tsx | 160 -------------------- src/Shared/Components/Header/PageHeader.tsx | 44 ++---- src/Shared/Components/Header/constants.ts | 91 ++++++++--- src/Shared/Components/Header/types.ts | 23 +-- src/Shared/Components/Header/utils.ts | 60 ++++---- src/Shared/Components/Icon/Icon.tsx | 12 ++ 13 files changed, 283 insertions(+), 250 deletions(-) create mode 100644 src/Assets/IconV2/ic-discord-fill.svg create mode 100644 src/Assets/IconV2/ic-file-edit.svg create mode 100644 src/Assets/IconV2/ic-file.svg create mode 100644 src/Assets/IconV2/ic-files.svg create mode 100644 src/Assets/IconV2/ic-megaphone-right.svg create mode 100644 src/Assets/IconV2/ic-path.svg create mode 100644 src/Shared/Components/Header/HelpButton.tsx delete mode 100644 src/Shared/Components/Header/HelpNav.tsx diff --git a/src/Assets/IconV2/ic-discord-fill.svg b/src/Assets/IconV2/ic-discord-fill.svg new file mode 100644 index 000000000..2e3bc9aea --- /dev/null +++ b/src/Assets/IconV2/ic-discord-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-file-edit.svg b/src/Assets/IconV2/ic-file-edit.svg new file mode 100644 index 000000000..a4fa313ce --- /dev/null +++ b/src/Assets/IconV2/ic-file-edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-file.svg b/src/Assets/IconV2/ic-file.svg new file mode 100644 index 000000000..8aec25d57 --- /dev/null +++ b/src/Assets/IconV2/ic-file.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-files.svg b/src/Assets/IconV2/ic-files.svg new file mode 100644 index 000000000..b1ba9f862 --- /dev/null +++ b/src/Assets/IconV2/ic-files.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-megaphone-right.svg b/src/Assets/IconV2/ic-megaphone-right.svg new file mode 100644 index 000000000..7d9a151f4 --- /dev/null +++ b/src/Assets/IconV2/ic-megaphone-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-path.svg b/src/Assets/IconV2/ic-path.svg new file mode 100644 index 000000000..5bb3ef197 --- /dev/null +++ b/src/Assets/IconV2/ic-path.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Shared/Components/Header/HelpButton.tsx b/src/Shared/Components/Header/HelpButton.tsx new file mode 100644 index 000000000..114df2162 --- /dev/null +++ b/src/Shared/Components/Header/HelpButton.tsx @@ -0,0 +1,125 @@ +import { useRef, useState } from 'react' +import ReactGA from 'react-ga4' +import { SliderButton } from '@typeform/embed-react' + +import { ComponentSizeType } from '@Shared/constants' +import { useMainContext } from '@Shared/Providers' + +import { ActionMenu, ActionMenuItemType, ActionMenuProps } from '../ActionMenu' +import { ButtonStyleType, ButtonVariantType } from '../Button' +import { Icon } from '../Icon' +import { HelpButtonProps, HelpMenuItems, InstallationType } from './types' +import { getHelpActionMenuOptions } from './utils' + +export const HelpButton = ({ serverInfo, handleGettingStartedClick, onClick }: HelpButtonProps) => { + // STATES + const [isActionMenuOpen, setIsActionMenuOpen] = useState(false) + + // HOOKS + const { currentServerInfo, handleOpenLicenseInfoDialog, licenseData } = useMainContext() + + // REFS + const typeFormSliderButtonRef = useRef(null) + + // CONSTANTS + const FEEDBACK_FORM_ID = `UheGN3KJ#source=${window.location.hostname}` + const isEnterprise = currentServerInfo?.serverInfo?.installationType === InstallationType.ENTERPRISE + + // HANDLERS + const handleAnalytics = (option: ActionMenuItemType) => { + ReactGA.event({ + category: 'Help Nav', + action: `${option.label} Clicked`, + }) + } + + const handleOpenAboutDevtron = () => { + ReactGA.event({ + category: 'help-nav__about-devtron', + action: 'ABOUT_DEVTRON_CLICKED', + }) + handleOpenLicenseInfoDialog() + } + + const handleFeedbackClick = () => { + typeFormSliderButtonRef.current?.open() + } + + const handleActionMenuClick: ActionMenuProps['onClick'] = (item) => { + switch (item.value) { + case HelpMenuItems.GETTING_STARTED: + handleGettingStartedClick() + break + case HelpMenuItems.ABOUT_DEVTRON: + handleOpenAboutDevtron() + break + case HelpMenuItems.GIVE_FEEDBACK: + handleFeedbackClick() + break + case HelpMenuItems.JOIN_DISCORD_COMMUNITY: + case HelpMenuItems.VIEW_DOCUMENTATION: + case HelpMenuItems.OPEN_NEW_TICKET: + case HelpMenuItems.VIEW_ALL_TICKETS: + case HelpMenuItems.CHAT_WITH_SUPPORT: + case HelpMenuItems.RAISE_ISSUE_REQUEST: + handleAnalytics(item) + break + default: + } + } + + return ( + <> + , + endIcon: ( +
    + +
    + ), + onClick, + }} + /> + {isEnterprise && ( + + This button is hidden on UI (opening type-form via ref) + + )} + + ) +} + +// {serverInfo?.installationType === InstallationType.OSS_HELM && ( +//
    +// {fetchingServerInfo ? ( +// Checking current version +// ) : ( +// version {serverInfo?.currentVersion || ''} +// )} +//
    +// Check for Updates +//
    +// )} diff --git a/src/Shared/Components/Header/HelpNav.tsx b/src/Shared/Components/Header/HelpNav.tsx deleted file mode 100644 index 0916f53f9..000000000 --- a/src/Shared/Components/Header/HelpNav.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Fragment } from 'react' -import ReactGA from 'react-ga4' -import { NavLink } from 'react-router-dom' -import { SliderButton } from '@typeform/embed-react' - -import { ReactComponent as Feedback } from '../../../Assets/Icon/ic-feedback.svg' -import { ReactComponent as GettingStartedIcon } from '../../../Assets/Icon/ic-onboarding.svg' -import { stopPropagation, URLS } from '../../../Common' -import { useMainContext } from '../../Providers' -import { Icon } from '../Icon' -import { HelpNavType, HelpOptionType, InstallationType } from './types' -import { getHelpOptions } from './utils' - -const HelpNav = ({ - className, - setShowHelpCard, - serverInfo, - fetchingServerInfo, - setGettingStartedClicked, - showHelpCard, -}: HelpNavType) => { - const { currentServerInfo, handleOpenLicenseInfoDialog, licenseData } = useMainContext() - const isEnterprise = currentServerInfo?.serverInfo?.installationType === InstallationType.ENTERPRISE - const isTrial = licenseData?.isTrial ?? false - const FEEDBACK_FORM_ID = `UheGN3KJ#source=${window.location.hostname}` - - const CommonHelpOptions: HelpOptionType[] = getHelpOptions(isEnterprise, isTrial) - - const onClickGettingStarted = (): void => { - setGettingStartedClicked(true) - } - - const onClickHelpOptions = (option: HelpOptionType): void => { - ReactGA.event({ - category: 'Help Nav', - action: `${option.name} Clicked`, - }) - } - - const toggleHelpCard = (): void => { - setShowHelpCard(!showHelpCard) - } - - const renderHelpFeedback = (): JSX.Element => ( -
    - - - Give feedback - -
    - ) - - const handleHelpOptions = (e) => { - const option = CommonHelpOptions[e.currentTarget.dataset.index] - onClickHelpOptions(option) - } - - const handleOpenAboutDevtron = () => { - ReactGA.event({ - category: 'help-nav__about-devtron', - action: 'ABOUT_DEVTRON_CLICKED', - }) - handleOpenLicenseInfoDialog() - } - - const renderHelpOptions = (): JSX.Element => ( - <> - {CommonHelpOptions.map((option, index) => ( - -
    - -
    {option.name}
    -
    - {index === 1 && ( - <> - - {isEnterprise && ( -
    - Enterprise Support -
    - )} - - )} - - ))} - - ) - - return ( -
    -
    - {!window._env_.K8S_CLIENT && ( - - -
    - Getting started -
    -
    - )} - {renderHelpOptions()} - {isEnterprise && renderHelpFeedback()} - {serverInfo?.installationType === InstallationType.OSS_HELM && ( -
    - {fetchingServerInfo ? ( - Checking current version - ) : ( - version {serverInfo?.currentVersion || ''} - )} -
    - Check for Updates -
    - )} -
    -
    - ) -} - -export default HelpNav diff --git a/src/Shared/Components/Header/PageHeader.tsx b/src/Shared/Components/Header/PageHeader.tsx index 6ad4ca278..c3a15548d 100644 --- a/src/Shared/Components/Header/PageHeader.tsx +++ b/src/Shared/Components/Header/PageHeader.tsx @@ -20,7 +20,6 @@ import Tippy from '@tippyjs/react' import { ReactComponent as ICCaretDownSmall } from '@Icons/ic-caret-down-small.svg' import { ReactComponent as Close } from '@Icons/ic-close.svg' -import { ReactComponent as Question } from '@Icons/ic-help-outline.svg' import { ReactComponent as ICMediumPaintBucket } from '@IconsV2/ic-medium-paintbucket.svg' import { getAlphabetIcon, TippyCustomized, TippyTheme } from '../../../Common' @@ -29,7 +28,7 @@ import { useMainContext, useTheme, useUserEmail } from '../../Providers' import GettingStartedCard from '../GettingStartedCard/GettingStarted' import { InfoIconTippy } from '../InfoIconTippy' import LogoutCard from '../LogoutCard' -import HelpNav from './HelpNav' +import { HelpButton } from './HelpButton' import { IframePromoButton } from './IframePromoButton' import { getServerInfo } from './service' import { InstallationType, PageHeaderType, ServerInfo } from './types' @@ -62,7 +61,6 @@ const PageHeader = ({ const { isTippyCustomized, tippyRedirectLink, TippyIcon, tippyMessage, onClickTippyButton, additionalContent } = tippyProps || {} - const [showHelpCard, setShowHelpCard] = useState(false) const [showLogOutCard, setShowLogOutCard] = useState(false) const { email } = useUserEmail() const [currentServerInfo, setCurrentServerInfo] = useState<{ serverInfo: ServerInfo; fetchingServerInfo: boolean }>( @@ -94,6 +92,10 @@ const PageHeader = ({ setExpiryDate(+localStorage.getItem('clickedOkay')) }, []) + const handleGettingStartedClick = () => { + setGettingStartedClicked(true) + } + const hideGettingStartedCard = (count?: string) => { setShowGettingStartedCard(false) if (count) { @@ -108,24 +110,22 @@ const PageHeader = ({ const onClickLogoutButton = () => { handleCloseSwitchThemeLocationTippyChange() setShowLogOutCard(!showLogOutCard) - if (showHelpCard) { - setShowHelpCard(false) - } setActionWithExpiry('clickedOkay', 1) hideGettingStartedCard() } - const onClickHelp = async () => { + const onHelpButtonClick = async () => { if ( !window._env_.K8S_CLIENT && currentServerInfo.serverInfo?.installationType !== InstallationType.ENTERPRISE ) { await getCurrentServerInfo() } - setShowHelpCard(!showHelpCard) + if (showLogOutCard) { setShowLogOutCard(false) } + setActionWithExpiry('clickedOkay', 1) hideGettingStartedCard() await handlePostHogEventUpdate(POSTHOG_EVENT_ONBOARDING.HELP) @@ -144,18 +144,12 @@ const PageHeader = ({ const renderLogoutHelpSection = () => ( <> -
    - - - - - Help - - -
    + {!window._env_.K8S_CLIENT && ( {showTabs && renderHeaderTabs()} - {showHelpCard && ( - - )} {!window._env_.K8S_CLIENT && showGettingStartedCard && loginCount >= 0 && diff --git a/src/Shared/Components/Header/constants.ts b/src/Shared/Components/Header/constants.ts index e4b2ac646..392cb765b 100644 --- a/src/Shared/Components/Header/constants.ts +++ b/src/Shared/Components/Header/constants.ts @@ -14,45 +14,90 @@ * limitations under the License. */ +import { DISCORD_LINK, DOCUMENTATION_HOME_PAGE, URLS } from '@Common/Constants' import { CONTACT_SUPPORT_LINK, OPEN_NEW_TICKET, RAISE_ISSUE, VIEW_ALL_TICKETS } from '@Shared/constants' -import { ReactComponent as Chat } from '../../../Assets/Icon/ic-chat-circle-dots.svg' -import { ReactComponent as EditFile } from '../../../Assets/Icon/ic-edit-file.svg' -import { ReactComponent as Files } from '../../../Assets/Icon/ic-files.svg' -import { DISCORD_LINK } from '../../../Common' -import { HelpOptionType } from './types' +import { ActionMenuItemType } from '../ActionMenu' +import { HelpMenuItems } from './types' -export const EnterpriseHelpOptions: HelpOptionType[] = [ +export const COMMON_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ + ...((!window._env_?.K8S_CLIENT + ? [ + { + label: 'Getting started', + value: HelpMenuItems.GETTING_STARTED, + startIcon: { name: 'ic-path', color: 'N600' }, + componentType: 'link', + to: `/${URLS.GETTING_STARTED}`, + }, + ] + : []) satisfies ActionMenuItemType[]), { - name: 'Open new ticket', - link: OPEN_NEW_TICKET, - icon: EditFile, + label: 'View documentation', + value: HelpMenuItems.VIEW_DOCUMENTATION, + startIcon: { name: 'ic-file', color: 'N600' }, + componentType: 'anchor', + href: DOCUMENTATION_HOME_PAGE, }, { - name: 'View all tickets', - link: VIEW_ALL_TICKETS, - icon: Files, + label: 'Join discord community', + value: HelpMenuItems.JOIN_DISCORD_COMMUNITY, + startIcon: { name: 'ic-discord-fill', color: 'N600' }, + componentType: 'anchor', + href: DISCORD_LINK, + }, + { + label: 'About Devtron', + value: HelpMenuItems.ABOUT_DEVTRON, + startIcon: { name: 'ic-devtron', color: 'N600' }, }, ] -export const OSSHelpOptions: HelpOptionType[] = [ +export const OSS_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ { - name: 'Chat with support', - link: DISCORD_LINK, - icon: Chat, + label: 'Chat with support', + value: HelpMenuItems.CHAT_WITH_SUPPORT, + componentType: 'anchor', + href: CONTACT_SUPPORT_LINK, + startIcon: { name: 'ic-chat-circle-dots', color: 'N600' }, }, + { + label: 'Raise an issue/request', + value: HelpMenuItems.RAISE_ISSUE_REQUEST, + startIcon: { name: 'ic-file-edit', color: 'N600' }, + componentType: 'anchor', + href: RAISE_ISSUE, + }, +] +export const ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ { - name: 'Raise an issue/request', - link: RAISE_ISSUE, - icon: EditFile, + label: 'Request Support', + value: HelpMenuItems.REQUEST_SUPPORT, + startIcon: { name: 'ic-file-edit', color: 'N600' }, + componentType: 'anchor', + href: OPEN_NEW_TICKET, }, ] -export const TrialHelpOptions: HelpOptionType[] = [ +export const ENTERPRISE_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ + { + label: 'Open new ticket', + value: HelpMenuItems.OPEN_NEW_TICKET, + startIcon: { name: 'ic-file-edit', color: 'N600' }, + componentType: 'anchor', + href: OPEN_NEW_TICKET, + }, + { + label: 'View all tickets', + value: HelpMenuItems.VIEW_ALL_TICKETS, + startIcon: { name: 'ic-files', color: 'N600' }, + componentType: 'anchor', + href: VIEW_ALL_TICKETS, + }, { - name: 'Request Support', - link: CONTACT_SUPPORT_LINK, - icon: EditFile, + label: 'Give feedback', + value: HelpMenuItems.GIVE_FEEDBACK, + startIcon: { name: 'ic-megaphone-right', color: 'N600' }, }, ] diff --git a/src/Shared/Components/Header/types.ts b/src/Shared/Components/Header/types.ts index 51a12814e..160edfbf0 100644 --- a/src/Shared/Components/Header/types.ts +++ b/src/Shared/Components/Header/types.ts @@ -55,17 +55,22 @@ export interface ServerInfoResponse extends ResponseType { result?: ServerInfo } -export interface HelpNavType { - className: string - setShowHelpCard: React.Dispatch> +export interface HelpButtonProps { serverInfo: ServerInfo fetchingServerInfo: boolean - setGettingStartedClicked: (isClicked: boolean) => void - showHelpCard: boolean + handleGettingStartedClick: () => void + onClick: () => void } -export interface HelpOptionType { - name: string - link: string - icon: React.FunctionComponent> +export enum HelpMenuItems { + GETTING_STARTED = 'getting-started', + VIEW_DOCUMENTATION = 'view-documentation', + JOIN_DISCORD_COMMUNITY = 'join-discord-community', + ABOUT_DEVTRON = 'about-devtron', + REQUEST_SUPPORT = 'request-support', + OPEN_NEW_TICKET = 'open-new-ticket', + VIEW_ALL_TICKETS = 'view-all-tickets', + GIVE_FEEDBACK = 'give-feedback', + CHAT_WITH_SUPPORT = 'chat-with-support', + RAISE_ISSUE_REQUEST = 'raise-issue-request', } diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts index feebdc56a..99485b3a0 100644 --- a/src/Shared/Components/Header/utils.ts +++ b/src/Shared/Components/Header/utils.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import { ReactComponent as Discord } from '@Icons/ic-discord-fill.svg' -import { ReactComponent as File } from '@Icons/ic-file-text.svg' +import { LOGIN_COUNT } from '@Common/Constants' -import { DISCORD_LINK, DOCUMENTATION_HOME_PAGE, LOGIN_COUNT } from '../../../Common' +import { ActionMenuProps } from '../ActionMenu' import { DevtronLicenseInfo, LicenseStatus } from '../License' -import { EnterpriseHelpOptions, OSSHelpOptions, TrialHelpOptions } from './constants' +import { + COMMON_HELP_ACTION_MENU_ITEMS, + ENTERPRISE_HELP_ACTION_MENU_ITEMS, + ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS, + OSS_HELP_ACTION_MENU_ITEMS, +} from './constants' import { updatePostHogEvent } from './service' const millisecondsInDay = 86400000 @@ -42,27 +46,27 @@ export const setActionWithExpiry = (key: string, days: number): void => { export const getIsShowingLicenseData = (licenseData: DevtronLicenseInfo) => licenseData && (licenseData.licenseStatus !== LicenseStatus.ACTIVE || licenseData.isTrial) -const getInstallationSpecificHelpOptions = (isEnterprise: boolean, isTrial: boolean) => { - if (isEnterprise) { - return isTrial ? TrialHelpOptions : EnterpriseHelpOptions - } - return OSSHelpOptions -} - -export const getHelpOptions = (isEnterprise: boolean, isTrial: boolean) => { - const HelpOptions = getInstallationSpecificHelpOptions(isEnterprise, isTrial) - return [ - { - name: 'View documentation', - link: DOCUMENTATION_HOME_PAGE, - icon: File, - }, - - { - name: 'Join discord community', - link: DISCORD_LINK, - icon: Discord, - }, - ...HelpOptions, - ] -} +export const getHelpActionMenuOptions = ({ + isEnterprise, + isTrial, +}: { + isEnterprise: boolean + isTrial: boolean + isOSSHelm: boolean +}): ActionMenuProps['options'] => [ + { + items: COMMON_HELP_ACTION_MENU_ITEMS, + }, + ...(isEnterprise + ? [ + { + groupLabel: 'Enterprise Support', + items: isTrial ? ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS : ENTERPRISE_HELP_ACTION_MENU_ITEMS, + }, + ] + : [ + { + items: OSS_HELP_ACTION_MENU_ITEMS, + }, + ]), +] diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index caf41168e..b74c30265 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -49,6 +49,7 @@ import { ReactComponent as ICDelhivery } from '@IconsV2/ic-delhivery.svg' import { ReactComponent as ICDevtron } from '@IconsV2/ic-devtron.svg' import { ReactComponent as ICDevtronHeaderLogo } from '@IconsV2/ic-devtron-header-logo.svg' import { ReactComponent as ICDisconnect } from '@IconsV2/ic-disconnect.svg' +import { ReactComponent as ICDiscordFill } from '@IconsV2/ic-discord-fill.svg' import { ReactComponent as ICDockerhub } from '@IconsV2/ic-dockerhub.svg' import { ReactComponent as ICEcr } from '@IconsV2/ic-ecr.svg' import { ReactComponent as ICEnv } from '@IconsV2/ic-env.svg' @@ -56,7 +57,10 @@ import { ReactComponent as ICError } from '@IconsV2/ic-error.svg' import { ReactComponent as ICExpandRightSm } from '@IconsV2/ic-expand-right-sm.svg' import { ReactComponent as ICExpandSm } from '@IconsV2/ic-expand-sm.svg' import { ReactComponent as ICFailure } from '@IconsV2/ic-failure.svg' +import { ReactComponent as ICFile } from '@IconsV2/ic-file.svg' +import { ReactComponent as ICFileEdit } from '@IconsV2/ic-file-edit.svg' import { ReactComponent as ICFileKey } from '@IconsV2/ic-file-key.svg' +import { ReactComponent as ICFiles } from '@IconsV2/ic-files.svg' import { ReactComponent as ICFolderUser } from '@IconsV2/ic-folder-user.svg' import { ReactComponent as ICGear } from '@IconsV2/ic-gear.svg' import { ReactComponent as ICGift } from '@IconsV2/ic-gift.svg' @@ -96,6 +100,7 @@ import { ReactComponent as ICLogout } from '@IconsV2/ic-logout.svg' import { ReactComponent as ICMediumDelete } from '@IconsV2/ic-medium-delete.svg' import { ReactComponent as ICMediumPaintbucket } from '@IconsV2/ic-medium-paintbucket.svg' import { ReactComponent as ICMegaphoneLeft } from '@IconsV2/ic-megaphone-left.svg' +import { ReactComponent as ICMegaphoneRight } from '@IconsV2/ic-megaphone-right.svg' import { ReactComponent as ICMemory } from '@IconsV2/ic-memory.svg' import { ReactComponent as ICMicrosoft } from '@IconsV2/ic-microsoft.svg' import { ReactComponent as ICMinikube } from '@IconsV2/ic-minikube.svg' @@ -110,6 +115,7 @@ import { ReactComponent as ICOpenInNew } from '@IconsV2/ic-open-in-new.svg' import { ReactComponent as ICOpenshift } from '@IconsV2/ic-openshift.svg' import { ReactComponent as ICOutOfSync } from '@IconsV2/ic-out-of-sync.svg' import { ReactComponent as ICPaperPlaneColor } from '@IconsV2/ic-paper-plane-color.svg' +import { ReactComponent as ICPath } from '@IconsV2/ic-path.svg' import { ReactComponent as ICPencil } from '@IconsV2/ic-pencil.svg' import { ReactComponent as ICQuay } from '@IconsV2/ic-quay.svg' import { ReactComponent as ICQuote } from '@IconsV2/ic-quote.svg' @@ -195,6 +201,7 @@ export const iconMap = { 'ic-devtron-header-logo': ICDevtronHeaderLogo, 'ic-devtron': ICDevtron, 'ic-disconnect': ICDisconnect, + 'ic-discord-fill': ICDiscordFill, 'ic-dockerhub': ICDockerhub, 'ic-ecr': ICEcr, 'ic-env': ICEnv, @@ -202,7 +209,10 @@ export const iconMap = { 'ic-expand-right-sm': ICExpandRightSm, 'ic-expand-sm': ICExpandSm, 'ic-failure': ICFailure, + 'ic-file-edit': ICFileEdit, 'ic-file-key': ICFileKey, + 'ic-file': ICFile, + 'ic-files': ICFiles, 'ic-folder-user': ICFolderUser, 'ic-gear': ICGear, 'ic-gift-gradient': ICGiftGradient, @@ -242,6 +252,7 @@ export const iconMap = { 'ic-medium-delete': ICMediumDelete, 'ic-medium-paintbucket': ICMediumPaintbucket, 'ic-megaphone-left': ICMegaphoneLeft, + 'ic-megaphone-right': ICMegaphoneRight, 'ic-memory': ICMemory, 'ic-microsoft': ICMicrosoft, 'ic-minikube': ICMinikube, @@ -256,6 +267,7 @@ export const iconMap = { 'ic-openshift': ICOpenshift, 'ic-out-of-sync': ICOutOfSync, 'ic-paper-plane-color': ICPaperPlaneColor, + 'ic-path': ICPath, 'ic-pencil': ICPencil, 'ic-quay': ICQuay, 'ic-quote': ICQuote, From 9f74aad9a15e889f606416f457c72fe982bbcb78 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 9 May 2025 18:04:51 +0530 Subject: [PATCH 110/157] feat: implement Popover component & usePopover hook --- .../Components/Popover/Popover.component.tsx | 36 +++++ src/Shared/Components/Popover/index.ts | 3 + src/Shared/Components/Popover/types.ts | 80 +++++++++++ .../Components/Popover/usePopover.hook.ts | 125 +++++++++++++++++ src/Shared/Components/Popover/utils.ts | 128 ++++++++++++++++++ 5 files changed, 372 insertions(+) create mode 100644 src/Shared/Components/Popover/Popover.component.tsx create mode 100644 src/Shared/Components/Popover/index.ts create mode 100644 src/Shared/Components/Popover/types.ts create mode 100644 src/Shared/Components/Popover/usePopover.hook.ts create mode 100644 src/Shared/Components/Popover/utils.ts diff --git a/src/Shared/Components/Popover/Popover.component.tsx b/src/Shared/Components/Popover/Popover.component.tsx new file mode 100644 index 000000000..17e49e72c --- /dev/null +++ b/src/Shared/Components/Popover/Popover.component.tsx @@ -0,0 +1,36 @@ +import { AnimatePresence, motion } from 'framer-motion' + +import { Button } from '../Button' +import { PopoverProps } from './types' + +/** + * Popover Component \ + * This component serves as a base for creating popovers. It is not intended to be used directly. + * @note Use this component in conjunction with the `usePopover` hook to create a custom popover component. \ + * For example, see the `ActionMenu` component for reference. + */ +export const Popover = ({ + open, + popoverProps, + overlayProps, + triggerProps, + buttonProps, + triggerElement, + children, +}: PopoverProps) => ( +
    +
    {triggerElement ||
    + + + {open && ( + <> + {/* Overlay to block interactions with the background */} +
    + + {children} + + + )} + +
    +) diff --git a/src/Shared/Components/Popover/index.ts b/src/Shared/Components/Popover/index.ts new file mode 100644 index 000000000..76a40223d --- /dev/null +++ b/src/Shared/Components/Popover/index.ts @@ -0,0 +1,3 @@ +export * from './Popover.component' +export * from './types' +export * from './usePopover.hook' diff --git a/src/Shared/Components/Popover/types.ts b/src/Shared/Components/Popover/types.ts new file mode 100644 index 000000000..80a4cb929 --- /dev/null +++ b/src/Shared/Components/Popover/types.ts @@ -0,0 +1,80 @@ +import { DetailedHTMLProps, KeyboardEvent, MutableRefObject, ReactElement } from 'react' +import { HTMLMotionProps } from 'framer-motion' + +import { ButtonProps } from '../Button' + +export interface UsePopoverProps { + /** + * A unique identifier for the popover. + */ + id: string + /** + * The width of the popover. + * Can be a number representing the width in pixels or the string `'auto'` for automatic sizing (up to 250px). + * @default 'auto' + */ + width?: number | 'auto' + /** + * The position of the popover relative to its trigger element. + * Possible values are: + * - `'bottom'`: Positions the popover below the trigger. + * - `'top'`: Positions the popover above the trigger. + * - `'left'`: Positions the popover to the left of the trigger. + * - `'right'`: Positions the popover to the right of the trigger. + * @default 'bottom' + */ + position?: 'bottom' | 'top' | 'left' | 'right' + /** + * The alignment of the popover relative to its trigger element. + * Possible values are: + * - `'start'`: Aligns the popover to the start of the trigger. + * - `'middle'`: Aligns the popover to the center of the trigger. + * - `'end'`: Aligns the popover to the end of the trigger. + * @default 'start' + */ + alignment?: 'start' | 'middle' | 'end' + /** + * Callback function triggered when the popover is opened or closed. + * @param open - A boolean indicating whether the popover is open. + */ + onOpen?: (open: boolean) => void + /** + * Callback function triggered when a key is pressed while the trigger element is focused. + * @param e - The keyboard event. + * @param openState - A boolean indicating whether the popover is currently open. + * @param closePopover - A function to close the popover. + */ + onTriggerKeyDown?: (e: KeyboardEvent, openState: boolean, closePopover: () => void) => void + /** + * Callback function triggered when a key is pressed while the popover is focused. + * @param e - The keyboard event. + * @param openState - A boolean indicating whether the popover is currently open. + * @param closePopover - A function to close the popover. + */ + onPopoverKeyDown?: (e: KeyboardEvent, openState: boolean, closePopover: () => void) => void +} + +export interface UsePopoverReturnType { + open: boolean + triggerProps: DetailedHTMLProps, HTMLDivElement> + overlayProps: DetailedHTMLProps, HTMLDivElement> + popoverProps: HTMLMotionProps<'div'> & { ref: MutableRefObject } + closePopover: () => void +} + +export type PopoverProps = Pick & { + /** + * Popover contents. + * This can be any React element or JSX to render inside the popover. + */ + children: ReactElement + /** + * Properties for the button to which the Popover is attached. + */ + buttonProps: ButtonProps | null + /** + * The React element to which the Popover is attached. + * @note only use when `triggerElement` is not `Button` component otherwise use `buttonProps`. + */ + triggerElement: ReactElement | null +} diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts new file mode 100644 index 000000000..b7f191542 --- /dev/null +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -0,0 +1,125 @@ +import { useLayoutEffect, useRef, useState } from 'react' + +import { UsePopoverProps, UsePopoverReturnType } from './types' +import { + getPopoverActualPositionAlignment, + getPopoverAlignmentStyle, + getPopoverFramerProps, + getPopoverPositionStyle, +} from './utils' + +const POPOVER_Z_INDEX_CLASS = 'dc__zi-20' + +export const usePopover = ({ + id, + position = 'bottom', + alignment = 'start', + width = 'auto', + onOpen, + onPopoverKeyDown, + onTriggerKeyDown, +}: UsePopoverProps): UsePopoverReturnType => { + // STATES + const [open, setOpen] = useState(false) + const [actualPosition, setActualPosition] = useState(position) + const [actualAlignment, setActualAlignment] = useState(alignment) + + // CONSTANTS + const isAutoWidth = width === 'auto' + + // REFS + const triggerRef = useRef(null) + const popover = useRef(null) + + // HANDLERS + const updateOpenState = (openState: typeof open) => { + setOpen(openState) + onOpen?.(openState) + } + + const togglePopover = () => updateOpenState(!open) + + const closePopover = () => updateOpenState(false) + + const handleTriggerKeyDown = (e: React.KeyboardEvent) => { + if (!open && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault() + updateOpenState(true) + } + + onTriggerKeyDown?.(e, open, closePopover) + } + + const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown(e, open, closePopover) + + useLayoutEffect(() => { + if (!open || !triggerRef.current || !popover.current) { + return + } + + const triggerRect = triggerRef.current.getBoundingClientRect() + const popoverRect = popover.current.getBoundingClientRect() + + const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({ + position, + alignment, + triggerRect, + popoverRect, + }) + + setActualPosition(fallbackPosition) + setActualAlignment(fallbackAlignment) + + // prevent scroll propagation unless scrollable + const handleWheel = (e: WheelEvent) => { + e.stopPropagation() + const atTop = popover.current.scrollTop === 0 && e.deltaY < 0 + const atBottom = + popover.current.scrollHeight - popover.current.clientHeight === popover.current.scrollTop && + e.deltaY > 0 + + if (atTop || atBottom) { + e.preventDefault() + } + } + + popover.current.addEventListener('wheel', handleWheel, { passive: false }) + // eslint-disable-next-line consistent-return + return () => { + popover.current.removeEventListener('wheel', handleWheel) + } + }, [open, position, alignment]) + + return { + open, + triggerProps: { + role: 'button', + ref: triggerRef, + onClick: togglePopover, + onKeyDown: handleTriggerKeyDown, + 'aria-haspopup': 'listbox', + 'aria-expanded': open, + tabIndex: 0, + }, + overlayProps: { + role: 'dialog', + onClick: closePopover, + className: `dc__position-fixed dc__top-0 dc__right-0 dc__left-0 dc__bottom-0 ${POPOVER_Z_INDEX_CLASS}`, + }, + popoverProps: { + id, + ref: popover, + role: 'listbox', + className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 mxh-300 dc__overflow-auto ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''} ${POPOVER_Z_INDEX_CLASS}`, + onKeyDown: handlePopoverKeyDown, + style: { + width: !isAutoWidth ? `${width}px` : undefined, + ...getPopoverPositionStyle({ position: actualPosition }), + ...getPopoverAlignmentStyle({ position: actualPosition, alignment: actualAlignment }), + }, + ...getPopoverFramerProps({ position: actualPosition, alignment: actualAlignment }), + transition: { duration: 0.2 }, + }, + closePopover, + } +} diff --git a/src/Shared/Components/Popover/utils.ts b/src/Shared/Components/Popover/utils.ts new file mode 100644 index 000000000..04ebdf976 --- /dev/null +++ b/src/Shared/Components/Popover/utils.ts @@ -0,0 +1,128 @@ +import { HTMLMotionProps } from 'framer-motion' + +import { UsePopoverProps } from './types' + +export const getPopoverAlignmentStyle = ({ position, alignment }: Pick) => { + const isYDirection = position === 'top' || position === 'bottom' + + if (isYDirection) { + switch (alignment) { + case 'end': + return { right: 0 } + case 'middle': + return { left: '50%' } + case 'start': + default: + return { left: 0 } + } + } + + switch (alignment) { + case 'end': + return { bottom: 0 } + case 'middle': + return { top: '50%' } + case 'start': + default: + return { top: 0 } + } +} + +export const getPopoverPositionStyle = ({ position }: Pick) => { + switch (position) { + case 'top': + return { bottom: '100%', marginBottom: 6 } + case 'left': + return { right: '100%', marginRight: 6 } + case 'right': + return { left: '100%', marginLeft: 6 } + case 'bottom': + default: + return { top: '100%', marginTop: 6 } + } +} + +export const getPopoverFramerProps = ({ position, alignment }: Pick) => { + const isYDirection = position === 'top' || position === 'bottom' + const isMiddleAlignment = alignment === 'middle' + + if (isYDirection) { + const initialY = position === 'bottom' ? -12 : 12 + + return { + initial: { opacity: 0, y: initialY }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: initialY }, + transformTemplate: (isMiddleAlignment + ? ({ y }) => `translate(-50%, ${y})` + : undefined) as HTMLMotionProps<'div'>['transformTemplate'], + } satisfies HTMLMotionProps<'div'> + } + + const initialX = position === 'right' ? -12 : 12 + + return { + initial: { opacity: 0, x: initialX }, + animate: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: initialX }, + transformTemplate: (isMiddleAlignment + ? ({ x }) => `translate(${x}, -50%)` + : undefined) as HTMLMotionProps<'div'>['transformTemplate'], + } satisfies HTMLMotionProps<'div'> +} + +export const getPopoverActualPositionAlignment = ({ + position, + alignment, + triggerRect, + popoverRect, +}: Pick & { triggerRect: DOMRect; popoverRect: DOMRect }) => { + const space = { + top: triggerRect.top, + bottom: window.innerHeight - triggerRect.bottom, + left: triggerRect.left, + right: window.innerWidth - triggerRect.right, + } + + const fits = { + top: popoverRect.height <= space.top, + bottom: popoverRect.height <= space.bottom, + left: popoverRect.width <= space.left, + right: popoverRect.width <= space.right, + } + + const fallbackPosition = + (fits[position] && position) || + (fits.bottom && 'bottom') || + (fits.top && 'top') || + (fits.right && 'right') || + (fits.left && 'left') || + position + + const isYDirection = fallbackPosition === 'top' || fallbackPosition === 'bottom' + + const fitsAlign = isYDirection + ? { + start: triggerRect.left + popoverRect.width <= window.innerWidth, + middle: + triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2 >= 0 && + triggerRect.left + triggerRect.width / 2 + popoverRect.width / 2 <= window.innerWidth, + end: triggerRect.right - popoverRect.width >= 0, + } + : { + start: triggerRect.top + popoverRect.height <= window.innerHeight, + middle: + triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2 >= 0 && + triggerRect.top + triggerRect.height / 2 + popoverRect.height / 2 <= window.innerHeight, + end: triggerRect.bottom - popoverRect.height >= 0, + } + + const fallbackAlignment = + (fitsAlign[alignment] && alignment) || + (fitsAlign.start && 'start') || + (fitsAlign.middle && 'middle') || + (fitsAlign.end && 'end') || + alignment + + return { fallbackPosition, fallbackAlignment } +} From fc1f409f12f28f6c7ca3f1d9d2585601abb925b0 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 9 May 2025 18:38:44 +0530 Subject: [PATCH 111/157] feat: ActionMenu - integrate Popover with usePopover hook --- .../ActionMenu/ActionMenu.component.tsx | 123 +++++++------- .../Components/ActionMenu/ActionMenuItem.tsx | 19 ++- .../Components/ActionMenu/actionMenu.scss | 1 - src/Shared/Components/ActionMenu/types.ts | 59 ++----- .../ActionMenu/useActionMenu.hook.ts | 154 +++++------------- src/Shared/Components/ActionMenu/utils.ts | 133 --------------- 6 files changed, 133 insertions(+), 356 deletions(-) diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index c718cb93e..70e9b476c 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -1,8 +1,7 @@ import { Fragment } from 'react' -import { AnimatePresence, motion } from 'framer-motion' -import { Button } from '../Button' import { CustomInput } from '../CustomInput' +import { Popover } from '../Popover' import { ActionMenuItem } from './ActionMenuItem' import { ActionMenuItemType, ActionMenuProps } from './types' import { useActionMenu } from './useActionMenu.hook' @@ -10,6 +9,7 @@ import { useActionMenu } from './useActionMenu.hook' import './actionMenu.scss' export const ActionMenu = ({ + id, options, onClick, position, @@ -28,19 +28,20 @@ export const ActionMenu = ({ flatOptions, triggerProps, overlayProps, - menuProps, + popoverProps, focusedIndex, searchTerm, - setFocusedIndex, - closeMenu, handleSearch, + itemsRef, + setFocusedIndex, + closePopover, } = useActionMenu({ + id, options, position, alignment, width, isSearchable, - onClick, onOpen, }) @@ -49,7 +50,7 @@ export const ActionMenu = ({ const handleOptionOnClick = (item: ActionMenuItemType) => () => { onClick(item) - closeMenu() + closePopover() } // RENDERERS @@ -62,6 +63,7 @@ export const ActionMenu = ({ -
    {children ||
    - - - {open && ( - <> - {/* Overlay to block interactions with the background */} -
    - - {isSearchable && ( -
  • - -
  • + +
      + {isSearchable && ( +
    • + +
    • + )} + {filteredOptions.length > 0 ? ( + filteredOptions.map((option, sectionIndex) => ( +
    • + {option.groupLabel && ( +

      + {option.groupLabel} +

      )} - {filteredOptions.length > 0 ? ( - filteredOptions.map((option, sectionIndex) => ( -
    • - {option.groupLabel && ( -

      - {option.groupLabel} -

      - )} - {option.items.length > 0 ? ( -
        - {option.items.map((item, itemIndex) => ( - - {renderOption(item, sectionIndex, itemIndex)} - - ))} -
      - ) : ( -

      - No options in this group -

      - )} -
    • - )) + {option.items.length > 0 ? ( +
        + {option.items.map((item, itemIndex) => ( + + {renderOption(item, sectionIndex, itemIndex)} + + ))} +
      ) : ( -
    • -

      No options

      -
    • +

      No options in this group

      )} - - + + )) + ) : ( +
    • +

      No options

      +
    • )} - -
    + + ) } diff --git a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx index e45840505..6571b7f3b 100644 --- a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx @@ -1,4 +1,4 @@ -import { LegacyRef } from 'react' +import { LegacyRef, Ref } from 'react' import { Link } from 'react-router-dom' import { Tooltip } from '@Common/Tooltip' @@ -9,6 +9,7 @@ import { ActionMenuItemProps } from './types' export const ActionMenuItem = ({ item, + itemRef, isFocused, onClick, onMouseEnter, @@ -61,20 +62,30 @@ export const ActionMenuItem = ({ switch (item.componentType) { case 'anchor': return ( - + } + className="flex-grow-1" + href={item.href} + target="_blank" + rel="noreferrer" + > {renderContent()} ) case 'link': return ( - + } className="flex-grow-1" to={item.to}> {renderContent()} ) case 'button': default: return ( - ) diff --git a/src/Shared/Components/ActionMenu/actionMenu.scss b/src/Shared/Components/ActionMenu/actionMenu.scss index 659f1ed47..16794c543 100644 --- a/src/Shared/Components/ActionMenu/actionMenu.scss +++ b/src/Shared/Components/ActionMenu/actionMenu.scss @@ -1,6 +1,5 @@ .action-menu { list-style: none; - margin: 0; &__group { border-top: 1px solid var(--border-secondary); diff --git a/src/Shared/Components/ActionMenu/types.ts b/src/Shared/Components/ActionMenu/types.ts index e31e1fbf2..1b7e3d91d 100644 --- a/src/Shared/Components/ActionMenu/types.ts +++ b/src/Shared/Components/ActionMenu/types.ts @@ -1,8 +1,8 @@ -import { ReactElement } from 'react' +import { LegacyRef, Ref } from 'react' import { LinkProps } from 'react-router-dom' -import { ButtonProps } from '../Button' import { IconsProps } from '../Icon' +import { PopoverProps, UsePopoverProps } from '../Popover' import { SelectPickerOptionType, SelectPickerProps } from '../SelectPicker' type ConditionalActionMenuComponentType = @@ -55,32 +55,7 @@ export type ActionMenuOptionType = { items: ActionMenuItemType[] } -export type UseActionMenuProps = { - /** - * The width of the action menu. \ - * Can be a number representing the width in pixels or the string 'auto' for automatic sizing (upto 250px). - * @default 'auto' - */ - width?: number | 'auto' - /** - * The position of the action menu relative to its trigger element. \ - * Possible values are: - * - 'bottom': Positions the menu below the trigger. - * - 'top': Positions the menu above the trigger. - * - 'left': Positions the menu to the left of the trigger. - * - 'right': Positions the menu to the right of the trigger. - * @default 'bottom' - */ - position?: 'bottom' | 'top' | 'left' | 'right' - /** - * The alignment of the action menu relative to its trigger element. \ - * Possible values are: - * - 'start': Aligns the menu to the start of the trigger. - * - 'middle': Aligns the menu to the center of the trigger. - * - 'end': Aligns the menu to the end of the trigger. - * @default 'start' - */ - alignment?: 'start' | 'middle' | 'end' +export type UseActionMenuProps = Omit & { /** * The options to display in the action menu. */ @@ -89,40 +64,36 @@ export type UseActionMenuProps = { * Determines whether the action menu is searchable. */ isSearchable?: boolean - /** - * Callback function triggered when an item is clicked. - * @param item - The selected item. - */ - onClick: (item: ActionMenuItemType) => void - /** - * Callback function triggered when the action menu is opened or closed. - * @param open - A boolean indicating whether the menu is open. - */ - onOpen?: (open: boolean) => void } export type ActionMenuProps = UseActionMenuProps & - Pick & - ( + Pick & { + /** + * Callback function triggered when an item is clicked. + * @param item - The selected item. + */ + onClick: (item: ActionMenuItemType) => void + } & ( | { /** * The React element to which the ActionMenu is attached. * @note only use when children is not `Button` component otherwise use `buttonProps`. */ - children: ReactElement + children: NonNullable buttonProps?: never } | { + children?: never /** - * Properties for the button to which the ActionMenu is attached. + * Properties for the button to which the Popover is attached. */ - buttonProps: ButtonProps - children?: never + buttonProps: NonNullable } ) export type ActionMenuItemProps = Pick & { item: ActionMenuItemType + itemRef: Ref | LegacyRef isFocused?: boolean onMouseEnter?: () => void } diff --git a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts index 58cb82615..2b45541b3 100644 --- a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts +++ b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts @@ -1,35 +1,21 @@ -import { ChangeEvent, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { ChangeEvent, createRef, RefObject, useEffect, useMemo, useRef, useState } from 'react' +import { usePopover, UsePopoverProps } from '../Popover' import { UseActionMenuProps } from './types' -import { - filterActionMenuOptions, - getActionMenuActualPositionAlignment, - getActionMenuAlignmentStyle, - getActionMenuFlatOptions, - getActionMenuFramerProps, - getActionMenuPositionStyle, -} from './utils' - -const ACTION_MENU_Z_INDEX_CLASS = 'dc__zi-20' +import { filterActionMenuOptions, getActionMenuFlatOptions } from './utils' export const useActionMenu = ({ + id, position = 'bottom', alignment = 'start', width = 'auto', options, isSearchable, - onClick, onOpen, }: UseActionMenuProps) => { // STATES - const [open, setOpen] = useState(false) const [focusedIndex, setFocusedIndex] = useState(-1) - const [actualPosition, setActualPosition] = useState(position) - const [actualAlignment, setActualAlignment] = useState(alignment) - const [searchTerm, setSearchTerm] = useState('') - - // CONSTANTS - const isAutoWidth = width === 'auto' + const [searchTerm, setSearchTerm] = useState('') // MEMOIZED CONSTANTS const filteredOptions = useMemo( @@ -40,20 +26,17 @@ export const useActionMenu = ({ const flatOptions = useMemo(() => getActionMenuFlatOptions(filteredOptions), [filteredOptions]) // REFS - const triggerRef = useRef(null) - const menuRef = useRef(null) - - // HANDLERS - const updateOpenState = (openState: typeof open) => { - setOpen(openState) - onOpen?.(openState) - } + const itemsRef = useRef[]>( + flatOptions.map(() => createRef()), + ) - const toggleMenu = () => updateOpenState(!open) + useEffect(() => { + itemsRef.current = flatOptions.map(() => createRef()) + }, [flatOptions.length]) - const closeMenu = () => { - setFocusedIndex(-1) - updateOpenState(false) + // HANDLERS + const handleSearch = (e: ChangeEvent) => { + setSearchTerm(e.target.value) } const getNextIndex = (start: number, arrowDirection: 1 | -1) => { @@ -68,15 +51,14 @@ export const useActionMenu = ({ return start } - const handleMenuKeyDown = (e: React.KeyboardEvent) => { + const handlePopoverKeyDown: UsePopoverProps['onPopoverKeyDown'] = (e, openState, closePopover) => { e.stopPropagation() - if (open) { + if (openState) { switch (e.key) { case 'Escape': e.preventDefault() - closeMenu() - triggerRef.current?.focus() + closePopover() break case 'ArrowDown': e.preventDefault() @@ -90,9 +72,9 @@ export const useActionMenu = ({ case ' ': { e.preventDefault() const selectedItem = flatOptions[focusedIndex].option - if (!selectedItem.isDisabled) { - onClick(selectedItem) - closeMenu() + const selectedItemRef = itemsRef.current[focusedIndex].current + if (!selectedItem.isDisabled && selectedItemRef) { + selectedItemRef.click() } break } @@ -101,92 +83,42 @@ export const useActionMenu = ({ } } - const handleTriggerKeyDown = (e: React.KeyboardEvent) => { - if (!open && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault() - updateOpenState(true) + const handleTriggerKeyDown: UsePopoverProps['onTriggerKeyDown'] = (e, openState, closePopover) => { + if (!openState && (e.key === 'Enter' || e.key === ' ')) { setFocusedIndex(0) } - handleMenuKeyDown(e) - } - - const handleSearch = (e: ChangeEvent) => { - setSearchTerm(e.target.value) + handlePopoverKeyDown(e, openState, closePopover) } - useLayoutEffect(() => { - if (!open || !triggerRef.current || !menuRef.current) { - return - } - - const triggerRect = triggerRef.current.getBoundingClientRect() - const menuRect = menuRef.current.getBoundingClientRect() - - const { fallbackPosition, fallbackAlignment } = getActionMenuActualPositionAlignment({ - position, - alignment, - triggerRect, - menuRect, - }) - - setActualPosition(fallbackPosition) - setActualAlignment(fallbackAlignment) - - // prevent scroll propagation unless scrollable - const handleWheel = (e: WheelEvent) => { - e.stopPropagation() - const atTop = menuRef.current.scrollTop === 0 && e.deltaY < 0 - const atBottom = - menuRef.current.scrollHeight - menuRef.current.clientHeight === menuRef.current.scrollTop && - e.deltaY > 0 - - if (atTop || atBottom) { - e.preventDefault() - } - } - - menuRef.current.addEventListener('wheel', handleWheel, { passive: false }) - // eslint-disable-next-line consistent-return - return () => { - menuRef.current.removeEventListener('wheel', handleWheel) + // POPOVER HOOK + const { open, closePopover, overlayProps, popoverProps, triggerProps } = usePopover({ + id, + position, + alignment, + width, + onOpen, + onPopoverKeyDown: handlePopoverKeyDown, + onTriggerKeyDown: handleTriggerKeyDown, + }) + + useEffect(() => { + if (!open) { + setFocusedIndex(-1) } - }, [open, position, alignment]) + }, [open]) return { open, flatOptions, filteredOptions, focusedIndex, - triggerProps: { - role: 'button', - ref: triggerRef, - onClick: toggleMenu, - onKeyDown: handleTriggerKeyDown, - 'aria-haspopup': 'menu' as const, - 'aria-expanded': open, - tabIndex: 0, - }, - overlayProps: { - role: 'dialog', - onClick: closeMenu, - className: `dc__position-fixed dc__top-0 dc__right-0 dc__left-0 dc__bottom-0 ${ACTION_MENU_Z_INDEX_CLASS}`, - }, - menuProps: { - role: 'menu', - ref: menuRef, - className: `action-menu dc__position-abs bg__menu--primary shadow__menu border__primary br-6 px-0 mxh-300 dc__overflow-auto ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''} ${ACTION_MENU_Z_INDEX_CLASS}`, - onKeyDown: handleMenuKeyDown, - style: { - width: !isAutoWidth ? `${width}px` : undefined, - ...getActionMenuPositionStyle({ position: actualPosition }), - ...getActionMenuAlignmentStyle({ position: actualPosition, alignment: actualAlignment }), - }, - ...getActionMenuFramerProps({ position: actualPosition, alignment: actualAlignment }), - transition: { duration: 0.2 }, - }, + itemsRef, + triggerProps, + overlayProps, + popoverProps, setFocusedIndex, - closeMenu, + closePopover, searchTerm, handleSearch, } diff --git a/src/Shared/Components/ActionMenu/utils.ts b/src/Shared/Components/ActionMenu/utils.ts index 5db2a151c..bfd8be137 100644 --- a/src/Shared/Components/ActionMenu/utils.ts +++ b/src/Shared/Components/ActionMenu/utils.ts @@ -1,138 +1,5 @@ -import { HTMLMotionProps } from 'framer-motion' - import { UseActionMenuProps } from './types' -export const getActionMenuAlignmentStyle = ({ - position, - alignment, -}: Pick) => { - const isYDirection = position === 'top' || position === 'bottom' - - if (isYDirection) { - switch (alignment) { - case 'end': - return { right: 0 } - case 'middle': - return { left: '50%' } - case 'start': - default: - return { left: 0 } - } - } - - switch (alignment) { - case 'end': - return { bottom: 0 } - case 'middle': - return { top: '50%' } - case 'start': - default: - return { top: 0 } - } -} - -export const getActionMenuPositionStyle = ({ position }: Pick) => { - switch (position) { - case 'top': - return { bottom: '100%', marginBottom: 6 } - case 'left': - return { right: '100%', marginRight: 6 } - case 'right': - return { left: '100%', marginLeft: 6 } - case 'bottom': - default: - return { top: '100%', marginTop: 6 } - } -} - -export const getActionMenuFramerProps = ({ - position, - alignment, -}: Pick) => { - const isYDirection = position === 'top' || position === 'bottom' - const isMiddleAlignment = alignment === 'middle' - - if (isYDirection) { - const initialY = position === 'bottom' ? -12 : 12 - - return { - initial: { opacity: 0, y: initialY }, - animate: { opacity: 1, y: 0 }, - exit: { opacity: 0, y: initialY }, - transformTemplate: (isMiddleAlignment - ? ({ y }) => `translate(-50%, ${y})` - : undefined) as HTMLMotionProps<'ul'>['transformTemplate'], - } satisfies HTMLMotionProps<'ul'> - } - - const initialX = position === 'right' ? -12 : 12 - - return { - initial: { opacity: 0, x: initialX }, - animate: { opacity: 1, x: 0 }, - exit: { opacity: 0, x: initialX }, - transformTemplate: (isMiddleAlignment - ? ({ x }) => `translate(${x}, -50%)` - : undefined) as HTMLMotionProps<'ul'>['transformTemplate'], - } satisfies HTMLMotionProps<'ul'> -} - -export const getActionMenuActualPositionAlignment = ({ - position, - alignment, - triggerRect, - menuRect, -}: Pick & { triggerRect: DOMRect; menuRect: DOMRect }) => { - const space = { - top: triggerRect.top, - bottom: window.innerHeight - triggerRect.bottom, - left: triggerRect.left, - right: window.innerWidth - triggerRect.right, - } - - const fits = { - top: menuRect.height <= space.top, - bottom: menuRect.height <= space.bottom, - left: menuRect.width <= space.left, - right: menuRect.width <= space.right, - } - - const fallbackPosition = - (fits[position] && position) || - (fits.bottom && 'bottom') || - (fits.top && 'top') || - (fits.right && 'right') || - (fits.left && 'left') || - position - - const isYDirection = fallbackPosition === 'top' || fallbackPosition === 'bottom' - - const fitsAlign = isYDirection - ? { - start: triggerRect.left + menuRect.width <= window.innerWidth, - middle: - triggerRect.left + triggerRect.width / 2 - menuRect.width / 2 >= 0 && - triggerRect.left + triggerRect.width / 2 + menuRect.width / 2 <= window.innerWidth, - end: triggerRect.right - menuRect.width >= 0, - } - : { - start: triggerRect.top + menuRect.height <= window.innerHeight, - middle: - triggerRect.top + triggerRect.height / 2 - menuRect.height / 2 >= 0 && - triggerRect.top + triggerRect.height / 2 + menuRect.height / 2 <= window.innerHeight, - end: triggerRect.bottom - menuRect.height >= 0, - } - - const fallbackAlignment = - (fitsAlign[alignment] && alignment) || - (fitsAlign.start && 'start') || - (fitsAlign.middle && 'middle') || - (fitsAlign.end && 'end') || - alignment - - return { fallbackPosition, fallbackAlignment } -} - export const getActionMenuFlatOptions = (options: UseActionMenuProps['options']) => options.flatMap( (option, sectionIndex) => From 2b6940d87adc93d37d3c25ca916003a67a370d8c Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 12 May 2025 23:35:32 +0530 Subject: [PATCH 112/157] feat: ActionMenu - add default menu item icon color --- .../Components/ActionMenu/ActionMenu.component.tsx | 2 +- src/Shared/Components/ActionMenu/ActionMenuItem.tsx | 2 +- src/Shared/Components/ActionMenu/types.ts | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index 70e9b476c..4090aa084 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -99,7 +99,7 @@ export const ActionMenu = ({ {filteredOptions.length > 0 ? ( filteredOptions.map((option, sectionIndex) => (
  • diff --git a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx index 6571b7f3b..031b95910 100644 --- a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx @@ -36,7 +36,7 @@ export const ActionMenuItem = ({ const renderIcon = (iconProps: typeof startIcon) => iconProps && (
    - +
    ) diff --git a/src/Shared/Components/ActionMenu/types.ts b/src/Shared/Components/ActionMenu/types.ts index 1b7e3d91d..b9da77900 100644 --- a/src/Shared/Components/ActionMenu/types.ts +++ b/src/Shared/Components/ActionMenu/types.ts @@ -27,6 +27,11 @@ type ConditionalActionMenuComponentType = href?: never } +type ActionMenuItemIconType = Pick & { + /** @default 'N800' */ + color?: IconsProps['color'] +} + export type ActionMenuItemType = Omit & { /** The text label for the menu item. */ label: string @@ -38,9 +43,9 @@ export type ActionMenuItemType = Omit + startIcon?: ActionMenuItemIconType /** Defines the icon to be displayed at the end of the menu item. */ - endIcon?: Pick + endIcon?: ActionMenuItemIconType } & ConditionalActionMenuComponentType export type ActionMenuOptionType = { From 65171bc1b543c9c56921ac16d89cd66ca67cbd68 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 15:35:29 +0530 Subject: [PATCH 113/157] feat: ActionMenu - add support for footerConfig, code optimization --- .../ActionMenu/ActionMenu.component.tsx | 131 ++++++++++-------- .../Components/ActionMenu/ActionMenuItem.tsx | 18 ++- src/Shared/Components/ActionMenu/types.ts | 8 +- .../ActionMenu/useActionMenu.hook.ts | 3 +- src/Shared/Components/Popover/types.ts | 27 ++++ .../Components/Popover/usePopover.hook.ts | 17 ++- src/Shared/Components/SelectPicker/common.tsx | 8 +- src/Shared/Components/SelectPicker/type.ts | 5 + 8 files changed, 142 insertions(+), 75 deletions(-) diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index 4090aa084..aa7a3a23d 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -1,7 +1,8 @@ -import { Fragment } from 'react' +import { MutableRefObject } from 'react' import { CustomInput } from '../CustomInput' import { Popover } from '../Popover' +import { SelectPickerMenuListFooter } from '../SelectPicker/common' import { ActionMenuItem } from './ActionMenuItem' import { ActionMenuItemType, ActionMenuProps } from './types' import { useActionMenu } from './useActionMenu.hook' @@ -20,6 +21,7 @@ export const ActionMenu = ({ buttonProps, children, onOpen, + footerConfig, }: ActionMenuProps) => { // HOOKS const { @@ -35,6 +37,7 @@ export const ActionMenu = ({ itemsRef, setFocusedIndex, closePopover, + scrollableRef, } = useActionMenu({ id, options, @@ -53,25 +56,6 @@ export const ActionMenu = ({ closePopover() } - // RENDERERS - const renderOption = (item: ActionMenuItemType, sectionIndex: number, itemIndex: number) => { - const index = flatOptions.findIndex( - (flatOption) => flatOption.sectionIndex === sectionIndex && flatOption.itemIndex === itemIndex, - ) - - return ( - - ) - } - return ( -
      - {isSearchable && ( -
    • - -
    • - )} - {filteredOptions.length > 0 ? ( - filteredOptions.map((option, sectionIndex) => ( +
      +
        } + role="menu" + className="action-menu m-0 p-0 flex-grow-1 dc__overflow-auto dc__overscroll-none" + > + {isSearchable && (
      • - {option.groupLabel && ( -

        - {option.groupLabel} -

        - )} - {option.items.length > 0 ? ( -
          - {option.items.map((item, itemIndex) => ( - - {renderOption(item, sectionIndex, itemIndex)} - - ))} -
        - ) : ( -

        No options in this group

        - )} + +
      • + )} + {filteredOptions.length > 0 ? ( + filteredOptions.map((option, sectionIndex) => ( +
      • + {option.groupLabel && ( +

        + {option.groupLabel} +

        + )} + {option.items.length > 0 ? ( +
          + {option.items.map((item, itemIndex) => { + const index = flatOptions.findIndex( + (flatOption) => + flatOption.sectionIndex === sectionIndex && + flatOption.itemIndex === itemIndex, + ) + + return ( + + ) + })} +
        + ) : ( +

        No options in this group

        + )} +
      • + )) + ) : ( +
      • +

        No options

      • - )) - ) : ( -
      • -

        No options

        -
      • + )} +
      + {footerConfig && ( +
      + +
      )} -
    +
  • ) } diff --git a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx index 031b95910..20d76e6b1 100644 --- a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx @@ -15,7 +15,17 @@ export const ActionMenuItem = ({ onMouseEnter, disableDescriptionEllipsis = false, }: ActionMenuItemProps) => { - const { description, label, startIcon, endIcon, tooltipProps, type = 'neutral', isDisabled } = item + const { + id, + description, + label, + startIcon, + endIcon, + tooltipProps, + type = 'neutral', + isDisabled, + componentType = 'button', + } = item // REFS const ref: LegacyRef = (el) => { @@ -36,7 +46,7 @@ export const ActionMenuItem = ({ const renderIcon = (iconProps: typeof startIcon) => iconProps && (
    - +
    ) @@ -59,7 +69,7 @@ export const ActionMenuItem = ({ ) const renderComponent = () => { - switch (item.componentType) { + switch (componentType) { case 'anchor': return ( & { color?: IconsProps['color'] } -export type ActionMenuItemType = Omit & { +export type ActionMenuItemType = Omit & { + /** A unique identifier for the action menu item. */ + id: string | number /** The text label for the menu item. */ label: string /** Indicates whether the menu item is disabled. */ @@ -78,6 +80,10 @@ export type ActionMenuProps = UseActionMenuProps & * @param item - The selected item. */ onClick: (item: ActionMenuItemType) => void + /** + * Config for the footer at the bottom of action menu list. It is sticky by default + */ + footerConfig?: SelectPickerProps['menuListFooterConfig'] } & ( | { /** diff --git a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts index 2b45541b3..cceca59e2 100644 --- a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts +++ b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts @@ -92,7 +92,7 @@ export const useActionMenu = ({ } // POPOVER HOOK - const { open, closePopover, overlayProps, popoverProps, triggerProps } = usePopover({ + const { open, closePopover, overlayProps, popoverProps, triggerProps, scrollableRef } = usePopover({ id, position, alignment, @@ -121,5 +121,6 @@ export const useActionMenu = ({ closePopover, searchTerm, handleSearch, + scrollableRef, } } diff --git a/src/Shared/Components/Popover/types.ts b/src/Shared/Components/Popover/types.ts index 80a4cb929..5b1fdaac8 100644 --- a/src/Shared/Components/Popover/types.ts +++ b/src/Shared/Components/Popover/types.ts @@ -54,11 +54,38 @@ export interface UsePopoverProps { onPopoverKeyDown?: (e: KeyboardEvent, openState: boolean, closePopover: () => void) => void } +/** + * Represents the return type of the `usePopover` hook, providing properties and methods + * to manage and interact with a popover component. + */ export interface UsePopoverReturnType { + /** + * Indicates whether the popover is currently open. + */ open: boolean + /** + * Props to be spread onto the trigger element that opens the popover. + * These props include standard HTML attributes for a `div` element. + */ triggerProps: DetailedHTMLProps, HTMLDivElement> + /** + * Props to be spread onto the overlay element of the popover. + * These props include standard HTML attributes for a `div` element. + */ overlayProps: DetailedHTMLProps, HTMLDivElement> + /** + * Props to be spread onto the popover element itself. + * Includes motion-related props for animations and a `ref` to the popover's `div` element. + */ popoverProps: HTMLMotionProps<'div'> & { ref: MutableRefObject } + /** + * A mutable reference to the scrollable element inside the popover. \ + * This reference should be assigned to the element that is scrollable. + */ + scrollableRef: MutableRefObject + /** + * A function to close the popover. + */ closePopover: () => void } diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts index b7f191542..3ee7ac10b 100644 --- a/src/Shared/Components/Popover/usePopover.hook.ts +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -30,6 +30,7 @@ export const usePopover = ({ // REFS const triggerRef = useRef(null) const popover = useRef(null) + const scrollableRef = useRef(null) // HANDLERS const updateOpenState = (openState: typeof open) => { @@ -53,7 +54,7 @@ export const usePopover = ({ const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown(e, open, closePopover) useLayoutEffect(() => { - if (!open || !triggerRef.current || !popover.current) { + if (!open || !triggerRef.current || !popover.current || !scrollableRef.current) { return } @@ -73,20 +74,21 @@ export const usePopover = ({ // prevent scroll propagation unless scrollable const handleWheel = (e: WheelEvent) => { e.stopPropagation() - const atTop = popover.current.scrollTop === 0 && e.deltaY < 0 + + const atTop = scrollableRef.current.scrollTop === 0 && e.deltaY < 0 const atBottom = - popover.current.scrollHeight - popover.current.clientHeight === popover.current.scrollTop && - e.deltaY > 0 + scrollableRef.current.scrollHeight - scrollableRef.current.clientHeight === + scrollableRef.current.scrollTop && e.deltaY > 0 if (atTop || atBottom) { e.preventDefault() } } - popover.current.addEventListener('wheel', handleWheel, { passive: false }) + scrollableRef.current.addEventListener('wheel', handleWheel, { passive: false }) // eslint-disable-next-line consistent-return return () => { - popover.current.removeEventListener('wheel', handleWheel) + scrollableRef.current.removeEventListener('wheel', handleWheel) } }, [open, position, alignment]) @@ -110,7 +112,7 @@ export const usePopover = ({ id, ref: popover, role: 'listbox', - className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 mxh-300 dc__overflow-auto ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''} ${POPOVER_Z_INDEX_CLASS}`, + className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''} ${POPOVER_Z_INDEX_CLASS}`, onKeyDown: handlePopoverKeyDown, style: { width: !isAutoWidth ? `${width}px` : undefined, @@ -120,6 +122,7 @@ export const usePopover = ({ ...getPopoverFramerProps({ position: actualPosition, alignment: actualAlignment }), transition: { duration: 0.2 }, }, + scrollableRef, closePopover, } } diff --git a/src/Shared/Components/SelectPicker/common.tsx b/src/Shared/Components/SelectPicker/common.tsx index 6e960cb35..6abf2a98d 100644 --- a/src/Shared/Components/SelectPicker/common.tsx +++ b/src/Shared/Components/SelectPicker/common.tsx @@ -252,7 +252,7 @@ export const SelectPickerOption = ({ ) } -const SelectPickerMenuListFooter = ({ +export const SelectPickerMenuListFooter = ({ menuListFooterConfig, }: Required>) => { if (!menuListFooterConfig) { @@ -287,6 +287,12 @@ const SelectPickerMenuListFooter = ({ ) } + if (type === 'customNode') { + const { value } = menuListFooterConfig + + return value + } + return null } diff --git a/src/Shared/Components/SelectPicker/type.ts b/src/Shared/Components/SelectPicker/type.ts index 3ef481f64..2ef42c294 100644 --- a/src/Shared/Components/SelectPicker/type.ts +++ b/src/Shared/Components/SelectPicker/type.ts @@ -73,6 +73,11 @@ type MenuListFooterConfigType = variant: ButtonVariantType.primary | ButtonVariantType.borderLess } & Omit, 'size' | 'fullWidth' | 'icon' | 'endIcon' | 'variant' | 'style'> } + | { + type: 'customNode' + value: ReactElement + buttonProps?: never + } declare module 'react-select/base' { // eslint-disable-next-line @typescript-eslint/no-unused-vars From 64e518610f077fce98462cf0e592b09ebbdeb6d7 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 15:47:00 +0530 Subject: [PATCH 114/157] feat: HelpButton - add CheckForUpdates option for OSS --- src/Assets/IconV2/ic-chat-circle-online.svg | 4 + src/Assets/IconV2/ic-edit.svg | 3 + src/Shared/Components/Header/HelpButton.tsx | 89 +++++++++++++-------- src/Shared/Components/Header/constants.ts | 44 +++++----- src/Shared/Components/Header/utils.ts | 1 - src/Shared/Components/Icon/Icon.tsx | 4 + 6 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 src/Assets/IconV2/ic-chat-circle-online.svg create mode 100644 src/Assets/IconV2/ic-edit.svg diff --git a/src/Assets/IconV2/ic-chat-circle-online.svg b/src/Assets/IconV2/ic-chat-circle-online.svg new file mode 100644 index 000000000..77560f3b4 --- /dev/null +++ b/src/Assets/IconV2/ic-chat-circle-online.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/IconV2/ic-edit.svg b/src/Assets/IconV2/ic-edit.svg new file mode 100644 index 000000000..2f3fcf52e --- /dev/null +++ b/src/Assets/IconV2/ic-edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Shared/Components/Header/HelpButton.tsx b/src/Shared/Components/Header/HelpButton.tsx index 114df2162..93ca203b9 100644 --- a/src/Shared/Components/Header/HelpButton.tsx +++ b/src/Shared/Components/Header/HelpButton.tsx @@ -2,16 +2,40 @@ import { useRef, useState } from 'react' import ReactGA from 'react-ga4' import { SliderButton } from '@typeform/embed-react' +import { URLS } from '@Common/Constants' import { ComponentSizeType } from '@Shared/constants' import { useMainContext } from '@Shared/Providers' import { ActionMenu, ActionMenuItemType, ActionMenuProps } from '../ActionMenu' -import { ButtonStyleType, ButtonVariantType } from '../Button' +import { Button, ButtonComponentType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' import { HelpButtonProps, HelpMenuItems, InstallationType } from './types' import { getHelpActionMenuOptions } from './utils' -export const HelpButton = ({ serverInfo, handleGettingStartedClick, onClick }: HelpButtonProps) => { +const CheckForUpdates = ({ + serverInfo, + fetchingServerInfo, +}: Pick) => ( +
    + {fetchingServerInfo ? ( +

    Checking version

    + ) : ( +

    Version {serverInfo?.currentVersion || ''}

    + )} +
    +) + +export const HelpButton = ({ serverInfo, fetchingServerInfo, handleGettingStartedClick, onClick }: HelpButtonProps) => { // STATES const [isActionMenuOpen, setIsActionMenuOpen] = useState(false) @@ -46,7 +70,7 @@ export const HelpButton = ({ serverInfo, handleGettingStartedClick, onClick }: H } const handleActionMenuClick: ActionMenuProps['onClick'] = (item) => { - switch (item.value) { + switch (item.id) { case HelpMenuItems.GETTING_STARTED: handleGettingStartedClick() break @@ -71,33 +95,42 @@ export const HelpButton = ({ serverInfo, handleGettingStartedClick, onClick }: H return ( <> , - endIcon: ( -
    - -
    - ), - onClick, - }} - /> + {...(serverInfo?.installationType === InstallationType.OSS_HELM + ? { + menuListFooterConfig: { + type: 'customNode', + value: ( + + ), + }, + } + : {})} + > + +
    {isEnterprise && ( ) } - -// {serverInfo?.installationType === InstallationType.OSS_HELM && ( -//
    -// {fetchingServerInfo ? ( -// Checking current version -// ) : ( -// version {serverInfo?.currentVersion || ''} -// )} -//
    -// Check for Updates -//
    -// )} diff --git a/src/Shared/Components/Header/constants.ts b/src/Shared/Components/Header/constants.ts index 392cb765b..cf16317d1 100644 --- a/src/Shared/Components/Header/constants.ts +++ b/src/Shared/Components/Header/constants.ts @@ -24,47 +24,47 @@ export const COMMON_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ ...((!window._env_?.K8S_CLIENT ? [ { + id: HelpMenuItems.GETTING_STARTED, label: 'Getting started', - value: HelpMenuItems.GETTING_STARTED, - startIcon: { name: 'ic-path', color: 'N600' }, + startIcon: { name: 'ic-path' }, componentType: 'link', to: `/${URLS.GETTING_STARTED}`, }, ] : []) satisfies ActionMenuItemType[]), { + id: HelpMenuItems.VIEW_DOCUMENTATION, label: 'View documentation', - value: HelpMenuItems.VIEW_DOCUMENTATION, - startIcon: { name: 'ic-file', color: 'N600' }, + startIcon: { name: 'ic-book-open' }, componentType: 'anchor', href: DOCUMENTATION_HOME_PAGE, }, { + id: HelpMenuItems.JOIN_DISCORD_COMMUNITY, label: 'Join discord community', - value: HelpMenuItems.JOIN_DISCORD_COMMUNITY, - startIcon: { name: 'ic-discord-fill', color: 'N600' }, + startIcon: { name: 'ic-discord-fill' }, componentType: 'anchor', href: DISCORD_LINK, }, { + id: HelpMenuItems.ABOUT_DEVTRON, label: 'About Devtron', - value: HelpMenuItems.ABOUT_DEVTRON, - startIcon: { name: 'ic-devtron', color: 'N600' }, + startIcon: { name: 'ic-devtron' }, }, ] export const OSS_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ { + id: HelpMenuItems.CHAT_WITH_SUPPORT, label: 'Chat with support', - value: HelpMenuItems.CHAT_WITH_SUPPORT, componentType: 'anchor', - href: CONTACT_SUPPORT_LINK, - startIcon: { name: 'ic-chat-circle-dots', color: 'N600' }, + href: DISCORD_LINK, + startIcon: { name: 'ic-chat-circle-online' }, }, { + id: HelpMenuItems.RAISE_ISSUE_REQUEST, label: 'Raise an issue/request', - value: HelpMenuItems.RAISE_ISSUE_REQUEST, - startIcon: { name: 'ic-file-edit', color: 'N600' }, + startIcon: { name: 'ic-file-edit' }, componentType: 'anchor', href: RAISE_ISSUE, }, @@ -72,32 +72,32 @@ export const OSS_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ export const ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ { + id: HelpMenuItems.REQUEST_SUPPORT, label: 'Request Support', - value: HelpMenuItems.REQUEST_SUPPORT, - startIcon: { name: 'ic-file-edit', color: 'N600' }, + startIcon: { name: 'ic-file-edit' }, componentType: 'anchor', - href: OPEN_NEW_TICKET, + href: CONTACT_SUPPORT_LINK, }, ] export const ENTERPRISE_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ { + id: HelpMenuItems.OPEN_NEW_TICKET, label: 'Open new ticket', - value: HelpMenuItems.OPEN_NEW_TICKET, - startIcon: { name: 'ic-file-edit', color: 'N600' }, + startIcon: { name: 'ic-edit' }, componentType: 'anchor', href: OPEN_NEW_TICKET, }, { + id: HelpMenuItems.VIEW_ALL_TICKETS, label: 'View all tickets', - value: HelpMenuItems.VIEW_ALL_TICKETS, - startIcon: { name: 'ic-files', color: 'N600' }, + startIcon: { name: 'ic-files' }, componentType: 'anchor', href: VIEW_ALL_TICKETS, }, { + id: HelpMenuItems.GIVE_FEEDBACK, label: 'Give feedback', - value: HelpMenuItems.GIVE_FEEDBACK, - startIcon: { name: 'ic-megaphone-right', color: 'N600' }, + startIcon: { name: 'ic-megaphone-right' }, }, ] diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts index 99485b3a0..35f762665 100644 --- a/src/Shared/Components/Header/utils.ts +++ b/src/Shared/Components/Header/utils.ts @@ -52,7 +52,6 @@ export const getHelpActionMenuOptions = ({ }: { isEnterprise: boolean isTrial: boolean - isOSSHelm: boolean }): ActionMenuProps['options'] => [ { items: COMMON_HELP_ACTION_MENU_ITEMS, diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index b74c30265..6667e4c72 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -27,6 +27,7 @@ import { ReactComponent as ICCaretLeft } from '@IconsV2/ic-caret-left.svg' import { ReactComponent as ICCaretRight } from '@IconsV2/ic-caret-right.svg' import { ReactComponent as ICCd } from '@IconsV2/ic-cd.svg' import { ReactComponent as ICChatCircleDots } from '@IconsV2/ic-chat-circle-dots.svg' +import { ReactComponent as ICChatCircleOnline } from '@IconsV2/ic-chat-circle-online.svg' import { ReactComponent as ICCheck } from '@IconsV2/ic-check.svg' import { ReactComponent as ICChecks } from '@IconsV2/ic-checks.svg' import { ReactComponent as ICCiLinked } from '@IconsV2/ic-ci-linked.svg' @@ -52,6 +53,7 @@ import { ReactComponent as ICDisconnect } from '@IconsV2/ic-disconnect.svg' import { ReactComponent as ICDiscordFill } from '@IconsV2/ic-discord-fill.svg' import { ReactComponent as ICDockerhub } from '@IconsV2/ic-dockerhub.svg' import { ReactComponent as ICEcr } from '@IconsV2/ic-ecr.svg' +import { ReactComponent as ICEdit } from '@IconsV2/ic-edit.svg' import { ReactComponent as ICEnv } from '@IconsV2/ic-env.svg' import { ReactComponent as ICError } from '@IconsV2/ic-error.svg' import { ReactComponent as ICExpandRightSm } from '@IconsV2/ic-expand-right-sm.svg' @@ -179,6 +181,7 @@ export const iconMap = { 'ic-caret-right': ICCaretRight, 'ic-cd': ICCd, 'ic-chat-circle-dots': ICChatCircleDots, + 'ic-chat-circle-online': ICChatCircleOnline, 'ic-check': ICCheck, 'ic-checks': ICChecks, 'ic-ci-linked': ICCiLinked, @@ -204,6 +207,7 @@ export const iconMap = { 'ic-discord-fill': ICDiscordFill, 'ic-dockerhub': ICDockerhub, 'ic-ecr': ICEcr, + 'ic-edit': ICEdit, 'ic-env': ICEnv, 'ic-error': ICError, 'ic-expand-right-sm': ICExpandRightSm, From d06ffdbf7a91a1dc305d0ec972ab40d9e7e62602 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 15:55:04 +0530 Subject: [PATCH 115/157] refactor: Popover - move zIndex from class to scss variable --- src/Shared/Components/Popover/Popover.component.tsx | 2 ++ src/Shared/Components/Popover/popover.scss | 12 ++++++++++++ src/Shared/Components/Popover/usePopover.hook.ts | 6 ++---- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/Shared/Components/Popover/popover.scss diff --git a/src/Shared/Components/Popover/Popover.component.tsx b/src/Shared/Components/Popover/Popover.component.tsx index 17e49e72c..68640c840 100644 --- a/src/Shared/Components/Popover/Popover.component.tsx +++ b/src/Shared/Components/Popover/Popover.component.tsx @@ -3,6 +3,8 @@ import { AnimatePresence, motion } from 'framer-motion' import { Button } from '../Button' import { PopoverProps } from './types' +import './popover.scss' + /** * Popover Component \ * This component serves as a base for creating popovers. It is not intended to be used directly. diff --git a/src/Shared/Components/Popover/popover.scss b/src/Shared/Components/Popover/popover.scss new file mode 100644 index 000000000..f241ad180 --- /dev/null +++ b/src/Shared/Components/Popover/popover.scss @@ -0,0 +1,12 @@ +.popover-overlay { + position: fixed; + top:0; + right:0; + bottom:0; + left:0; + z-index: var(--modal-index); +} + +.popover-content { + z-index: var(--modal-index); +} diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts index 3ee7ac10b..d95ff93b2 100644 --- a/src/Shared/Components/Popover/usePopover.hook.ts +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -8,8 +8,6 @@ import { getPopoverPositionStyle, } from './utils' -const POPOVER_Z_INDEX_CLASS = 'dc__zi-20' - export const usePopover = ({ id, position = 'bottom', @@ -106,13 +104,13 @@ export const usePopover = ({ overlayProps: { role: 'dialog', onClick: closePopover, - className: `dc__position-fixed dc__top-0 dc__right-0 dc__left-0 dc__bottom-0 ${POPOVER_Z_INDEX_CLASS}`, + className: 'popover-overlay', }, popoverProps: { id, ref: popover, role: 'listbox', - className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''} ${POPOVER_Z_INDEX_CLASS}`, + className: `popover-content dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, onKeyDown: handlePopoverKeyDown, style: { width: !isAutoWidth ? `${width}px` : undefined, From 4121e146244ce8e1770bd67aea8c837d27cb6acf Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 15:59:28 +0530 Subject: [PATCH 116/157] fix: update codemirror chunk --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index e62fa6155..bbf0c0665 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -88,7 +88,7 @@ export default defineConfig({ return '@vendor' } - if (id.includes('codemirror') || id.includes('src/Common/CodeMirror')) { + if (id.match('codemirror') || id.includes('src/Shared/Components/CodeEditor')) { return '@code-editor' } From 6f41bb6a2ced36902d308b848e5e12678b4d7f21 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 16:06:16 +0530 Subject: [PATCH 117/157] fix: ActionMenu - add footer background color --- src/Shared/Components/ActionMenu/ActionMenu.component.tsx | 2 +- src/Shared/Components/Header/HelpButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index aa7a3a23d..4b5d8be5e 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -131,7 +131,7 @@ export const ActionMenu = ({ )} {footerConfig && ( -
    +
    )} diff --git a/src/Shared/Components/Header/HelpButton.tsx b/src/Shared/Components/Header/HelpButton.tsx index 93ca203b9..49e9c1804 100644 --- a/src/Shared/Components/Header/HelpButton.tsx +++ b/src/Shared/Components/Header/HelpButton.tsx @@ -16,7 +16,7 @@ const CheckForUpdates = ({ serverInfo, fetchingServerInfo, }: Pick) => ( -
    +
    {fetchingServerInfo ? (

    Checking version

    ) : ( From 4b0abc5117a4994b3c99366573bf4437751dcbdc Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 16:18:27 +0530 Subject: [PATCH 118/157] feat: ActionMenu - improve search box rendering and clean up styles --- .../ActionMenu/ActionMenu.component.tsx | 30 +++++++++---------- .../Components/ActionMenu/ActionMenuItem.tsx | 2 +- .../Components/ActionMenu/actionMenu.scss | 22 +++----------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index 4b5d8be5e..a31ab5387 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -66,25 +66,25 @@ export const ActionMenu = ({ triggerElement={children} >
    + {isSearchable && ( +
    + +
    + )}
      } role="menu" className="action-menu m-0 p-0 flex-grow-1 dc__overflow-auto dc__overscroll-none" > - {isSearchable && ( -
    • - -
    • - )} {filteredOptions.length > 0 ? ( filteredOptions.map((option, sectionIndex) => (
    • {option.groupLabel && ( -

      +

      {option.groupLabel}

      )} diff --git a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx index 20d76e6b1..f4ed739dc 100644 --- a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx @@ -46,7 +46,7 @@ export const ActionMenuItem = ({ const renderIcon = (iconProps: typeof startIcon) => iconProps && (
      - +
      ) diff --git a/src/Shared/Components/ActionMenu/actionMenu.scss b/src/Shared/Components/ActionMenu/actionMenu.scss index 16794c543..8768b44ec 100644 --- a/src/Shared/Components/ActionMenu/actionMenu.scss +++ b/src/Shared/Components/ActionMenu/actionMenu.scss @@ -9,10 +9,6 @@ } } - &__group-label { - top: 0; - } - &__group-list { list-style: none; } @@ -31,19 +27,9 @@ } } - &__searchbox { - & + .action-menu__group { - border-top: none; - } - - & ~ .action-menu__group .action-menu__group-label { - top: 37.5px; - } - - input { - border: none; - padding: 0; - background-color: transparent; - } + &__searchbox input { + border: none; + padding: 0; + background-color: transparent; } } From 4597a758154a0b443ef22574cc71462cd0af2128 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 13 May 2025 16:24:28 +0530 Subject: [PATCH 119/157] refactor: code optimization --- src/Shared/Components/Popover/utils.ts | 40 ++++++++++++-------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Shared/Components/Popover/utils.ts b/src/Shared/Components/Popover/utils.ts index 04ebdf976..cc74909ff 100644 --- a/src/Shared/Components/Popover/utils.ts +++ b/src/Shared/Components/Popover/utils.ts @@ -3,9 +3,7 @@ import { HTMLMotionProps } from 'framer-motion' import { UsePopoverProps } from './types' export const getPopoverAlignmentStyle = ({ position, alignment }: Pick) => { - const isYDirection = position === 'top' || position === 'bottom' - - if (isYDirection) { + if (position === 'top' || position === 'bottom') { switch (alignment) { case 'end': return { right: 0 } @@ -43,10 +41,9 @@ export const getPopoverPositionStyle = ({ position }: Pick) => { - const isYDirection = position === 'top' || position === 'bottom' const isMiddleAlignment = alignment === 'middle' - if (isYDirection) { + if (position === 'top' || position === 'bottom') { const initialY = position === 'bottom' ? -12 : 12 return { @@ -99,23 +96,22 @@ export const getPopoverActualPositionAlignment = ({ (fits.left && 'left') || position - const isYDirection = fallbackPosition === 'top' || fallbackPosition === 'bottom' - - const fitsAlign = isYDirection - ? { - start: triggerRect.left + popoverRect.width <= window.innerWidth, - middle: - triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2 >= 0 && - triggerRect.left + triggerRect.width / 2 + popoverRect.width / 2 <= window.innerWidth, - end: triggerRect.right - popoverRect.width >= 0, - } - : { - start: triggerRect.top + popoverRect.height <= window.innerHeight, - middle: - triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2 >= 0 && - triggerRect.top + triggerRect.height / 2 + popoverRect.height / 2 <= window.innerHeight, - end: triggerRect.bottom - popoverRect.height >= 0, - } + const fitsAlign = + fallbackPosition === 'top' || fallbackPosition === 'bottom' + ? { + start: triggerRect.left + popoverRect.width <= window.innerWidth, + middle: + triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2 >= 0 && + triggerRect.left + triggerRect.width / 2 + popoverRect.width / 2 <= window.innerWidth, + end: triggerRect.right - popoverRect.width >= 0, + } + : { + start: triggerRect.top + popoverRect.height <= window.innerHeight, + middle: + triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2 >= 0 && + triggerRect.top + triggerRect.height / 2 + popoverRect.height / 2 <= window.innerHeight, + end: triggerRect.bottom - popoverRect.height >= 0, + } const fallbackAlignment = (fitsAlign[alignment] && alignment) || From 68aba96c9b87ea857598433d3eef1ea29724b22c Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 13 May 2025 17:30:00 +0530 Subject: [PATCH 120/157] refactor: Update timelineStatus type to ReactNode in DeploymentStatusBreakdownItemType --- .../CICDHistory/DeploymentStatusDetailRow.tsx | 25 ++----------------- src/Shared/types.ts | 2 +- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx index 4146b88be..e656fcbda 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx @@ -24,7 +24,7 @@ import { AppType, TIMELINE_STATUS } from '@Shared/types' import { ReactComponent as DropDownIcon } from '../../../Assets/Icon/ic-chevron-down.svg' import { DATE_TIME_FORMATS, showError } from '../../../Common' -import { ComponentSizeType, DEPLOYMENT_STATUS, statusIcon } from '../../constants' +import { ComponentSizeType, DEPLOYMENT_STATUS } from '../../constants' import { AppStatusContent } from '../AppStatusModal' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { APP_HEALTH_DROP_DOWN_LIST, MANIFEST_STATUS_HEADERS, TERMINAL_STATUS_MAP } from './constants' @@ -47,10 +47,6 @@ export const DeploymentStatusDetailRow = ({ const statusBreakDownType = deploymentDetailedData.deploymentStatusBreakdown[type] const [isCollapsed, setIsCollapsed] = useState(statusBreakDownType.isCollapsed) - const isHelmManifestPushFailed = - type === TIMELINE_STATUS.HELM_MANIFEST_PUSHED_TO_HELM_REPO && - deploymentDetailedData.deploymentStatus === statusIcon.failed - useEffect(() => { setIsCollapsed(statusBreakDownType.isCollapsed) }, [statusBreakDownType.isCollapsed]) @@ -132,22 +128,6 @@ export const DeploymentStatusDetailRow = ({ ) } - const renderErrorInfoBar = () => { - if (deploymentDetailedData.lastFailedStatusType !== TIMELINE_STATUS.HELM_MANIFEST_PUSHED_TO_HELM_REPO) { - return null - } - - return ( -
      - {deploymentDetailedData.deploymentError} -
        -
      1. Ensure provided repository path is valid
      2. -
      3. Check if credentials provided for OCI registry are valid and have PUSH permission
      4. -
      -
      - ) - } - const isAccordion = statusBreakDownType.subSteps?.length || (type === TIMELINE_STATUS.APP_HEALTH && APP_HEALTH_DROP_DOWN_LIST.includes(statusBreakDownType.icon)) || @@ -198,7 +178,7 @@ export const DeploymentStatusDetailRow = ({ <>
      {renderDeploymentTimelineIcon(statusBreakDownType.icon)} @@ -247,7 +227,6 @@ export const DeploymentStatusDetailRow = ({ /> )}
      - {isHelmManifestPushFailed && renderErrorInfoBar()}
      {renderAccordionDetails()} diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 67e8f5527..7e559e365 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -1265,7 +1265,7 @@ export interface DeploymentStatusBreakdownItemType { /** * To be shown in accordion details below heading tile */ - timelineStatus?: string + timelineStatus?: ReactNode showHelmManifest?: boolean } From f97f6ca3a51cc20b5cc82aec7005201340200642 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 13 May 2025 17:30:59 +0530 Subject: [PATCH 121/157] refactor: Remove unused deploymentError field from DeploymentStatusDetailsBreakdownDataType --- src/Shared/Components/DeploymentStatusBreakdown/utils.tsx | 1 - src/Shared/types.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index 5b4219c97..1695ba5a5 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -39,7 +39,6 @@ const getDefaultDeploymentStatusTimeline = ( deploymentStatus: WFR_STATUS_DTO_TO_DEPLOYMENT_STATUS_MAP[data?.wfrStatus] || DEPLOYMENT_STATUS.INPROGRESS, deploymentTriggerTime: data?.deploymentStartedOn || '', deploymentEndTime: data?.deploymentFinishedOn || '', - deploymentError: '', triggeredBy: data?.triggeredBy || '', lastFailedStatusType: '', deploymentStatusBreakdown: { diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 7e559e365..ea4741980 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -1273,10 +1273,6 @@ export interface DeploymentStatusDetailsBreakdownDataType { deploymentStatus: (typeof DEPLOYMENT_STATUS)[keyof typeof DEPLOYMENT_STATUS] deploymentTriggerTime: string deploymentEndTime: string - /** - * Only required - isHelmManifestPushFailed === true then in error bar below heading tile - */ - deploymentError?: string triggeredBy: string /** * Only required - isHelmManifestPushFailed === true then in error bar below heading tile From 755c127c9b063e6af071601f8870165c9f10fb75 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 13 May 2025 17:51:10 +0530 Subject: [PATCH 122/157] refactor: Remove unused lastFailedStatusType from DeploymentStatusDetailsBreakdownDataType and update related types --- src/Shared/Components/CICDHistory/types.tsx | 8 -------- .../Components/DeploymentStatusBreakdown/utils.tsx | 1 - src/Shared/types.ts | 13 ++++--------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index 9f419f3bb..b5e9dff11 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -527,14 +527,6 @@ export interface DeploymentStatusDetailRowType extends Pick { status: string statusDetail: string statusTime: string @@ -1191,7 +1190,7 @@ export interface DeploymentStatusDetailsType { statusFetchCount: number statusLastFetchedAt: string timelines: DeploymentStatusDetailsTimelineType[] - wfrStatus?: string + wfrStatus?: WorkflowRunnerStatusDTO isDeploymentWithoutApproval: boolean } @@ -1274,10 +1273,6 @@ export interface DeploymentStatusDetailsBreakdownDataType { deploymentTriggerTime: string deploymentEndTime: string triggeredBy: string - /** - * Only required - isHelmManifestPushFailed === true then in error bar below heading tile - */ - lastFailedStatusType?: DeploymentStatusTimelineType | '' deploymentStatusBreakdown: Partial> } From 50e18013dcf84f2b346d27b57eb449302fbdfdd5 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 13 May 2025 18:50:30 +0530 Subject: [PATCH 123/157] refactor: Simplify component structure and improve state management in DeploymentStatus components --- .../CICDHistory/DeploymentStatusBreakdown.tsx | 10 ++-- .../CICDHistory/DeploymentStatusDetailRow.tsx | 7 ++- .../DeploymentStatusBreakdown/constants.ts | 12 ++--- .../DeploymentStatusBreakdown/types.ts | 2 +- .../DeploymentStatusBreakdown/utils.tsx | 47 +++++++------------ .../Components/StatusComponent/utils.ts | 3 ++ src/Shared/types.ts | 38 +++++++-------- 7 files changed, 54 insertions(+), 65 deletions(-) diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx index cce419847..019b98e72 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import { Fragment } from 'react' - import { TIMELINE_STATUS } from '@Shared/types' import { DeploymentStatusDetailRow } from './DeploymentStatusDetailRow' @@ -57,9 +55,11 @@ const DeploymentStatusDetailBreakdown = ({ TIMELINE_STATUS.KUBECTL_APPLY, ] as DeploymentStatusDetailRowType['type'][] ).map((timelineStatus) => ( - - - + ))} { - setIsCollapsed(!isCollapsed) + setIsCollapsed((prevState) => !prevState) } const renderDetailedData = () => { @@ -79,7 +79,6 @@ export const DeploymentStatusDetailRow = ({ return (
      - {/* TODO: Can be statusBreakDownType */} {statusBreakDownType.subSteps?.map((items, index) => ( // eslint-disable-next-line react/no-array-index-key
      @@ -101,7 +100,7 @@ export const DeploymentStatusDetailRow = ({
      {statusBreakDownType.resourceDetails.map((nodeDetails) => (
      {nodeDetails.resourceKind}
      @@ -129,7 +128,7 @@ export const DeploymentStatusDetailRow = ({ } const isAccordion = - statusBreakDownType.subSteps?.length || + !!statusBreakDownType.subSteps?.length || (type === TIMELINE_STATUS.APP_HEALTH && APP_HEALTH_DROP_DOWN_LIST.includes(statusBreakDownType.icon)) || ((type === TIMELINE_STATUS.GIT_COMMIT || type === TIMELINE_STATUS.ARGOCD_SYNC) && statusBreakDownType.icon === 'failed') diff --git a/src/Shared/Components/DeploymentStatusBreakdown/constants.ts b/src/Shared/Components/DeploymentStatusBreakdown/constants.ts index 737065370..3b961cb7c 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/constants.ts +++ b/src/Shared/Components/DeploymentStatusBreakdown/constants.ts @@ -3,12 +3,13 @@ import { DeploymentPhaseType, DeploymentStatusTimelineType, TIMELINE_STATUS } fr import { WorkflowRunnerStatusDTO } from './types' -export const DEPLOYMENT_STATUS_TEXT_MAP: Record<(typeof DEPLOYMENT_STATUS)[keyof typeof DEPLOYMENT_STATUS], string> = { +export const DEPLOYMENT_STATUS_TEXT_MAP: Readonly< + Record<(typeof DEPLOYMENT_STATUS)[keyof typeof DEPLOYMENT_STATUS], string> +> = { [DEPLOYMENT_STATUS.SUCCEEDED]: 'Succeeded', [DEPLOYMENT_STATUS.HEALTHY]: 'Healthy', [DEPLOYMENT_STATUS.FAILED]: 'Failed', [DEPLOYMENT_STATUS.TIMED_OUT]: 'Timed out', - // TODO: Add icons [DEPLOYMENT_STATUS.UNABLE_TO_FETCH]: 'Unable to fetch status', [DEPLOYMENT_STATUS.INPROGRESS]: 'In progress', [DEPLOYMENT_STATUS.PROGRESSING]: 'Progressing', @@ -21,9 +22,8 @@ export const DEPLOYMENT_STATUS_TEXT_MAP: Record<(typeof DEPLOYMENT_STATUS)[keyof } // Might be more but as per BE its only these for now -export const WFR_STATUS_DTO_TO_DEPLOYMENT_STATUS_MAP: Record< - WorkflowRunnerStatusDTO, - (typeof DEPLOYMENT_STATUS)[keyof typeof DEPLOYMENT_STATUS] +export const WFR_STATUS_DTO_TO_DEPLOYMENT_STATUS_MAP: Readonly< + Record > = { [WorkflowRunnerStatusDTO.ABORTED]: DEPLOYMENT_STATUS.FAILED, [WorkflowRunnerStatusDTO.FAILED]: DEPLOYMENT_STATUS.FAILED, @@ -43,7 +43,7 @@ export const WFR_STATUS_DTO_TO_DEPLOYMENT_STATUS_MAP: Record< [WorkflowRunnerStatusDTO.QUEUED]: DEPLOYMENT_STATUS.QUEUED, } -export const PROGRESSING_DEPLOYMENT_STATUS: (typeof DEPLOYMENT_STATUS)[keyof typeof DEPLOYMENT_STATUS][] = [ +export const PROGRESSING_DEPLOYMENT_STATUS: Readonly<(typeof DEPLOYMENT_STATUS)[keyof typeof DEPLOYMENT_STATUS][]> = [ DEPLOYMENT_STATUS.INPROGRESS, DEPLOYMENT_STATUS.PROGRESSING, DEPLOYMENT_STATUS.STARTING, diff --git a/src/Shared/Components/DeploymentStatusBreakdown/types.ts b/src/Shared/Components/DeploymentStatusBreakdown/types.ts index 8176a4a0e..811f83c19 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/types.ts +++ b/src/Shared/Components/DeploymentStatusBreakdown/types.ts @@ -15,7 +15,7 @@ export enum WorkflowRunnerStatusDTO { STARTING = 'Starting', QUEUED = 'Queued', INITIATING = 'Initiating', - // Not found on BE but for Backward compatibility + // Not found on BE but added for Backward compatibility HEALTHY = 'Healthy', DEGRADED = 'Degraded', } diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index 0206efcdb..b2e211b1e 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -86,7 +86,7 @@ const getPredicate = case TIMELINE_STATUS.APP_HEALTH: return [TIMELINE_STATUS.HEALTHY, TIMELINE_STATUS.DEGRADED, TIMELINE_STATUS.DEPLOYMENT_FAILED].includes( - timelineItem.status as TIMELINE_STATUS, + timelineItem.status, ) default: @@ -136,22 +136,15 @@ const processKubeCTLApply = ( if (resourceDetails) { // Used to parse resource details base struct with current phase as last phase DEPLOYMENT_PHASES.forEach((phase) => { - let breakPhase = false - resourceDetails.forEach((item) => { - if (breakPhase) { - return - } - - if (phase === item.resourcePhase) { - tableData.currentPhase = phase - tableData.currentTableData.push({ - icon: 'success', - phase, - message: `${phase}: Create and update resources based on manifest`, - }) - breakPhase = true - } - }) + const resourceWithSamePhase = resourceDetails.find((item) => item.resourcePhase === phase) + if (resourceWithSamePhase) { + tableData.currentPhase = phase + tableData.currentTableData.push({ + icon: 'success', + phase, + message: `${phase}: Create and update resources based on manifest`, + }) + } }) } @@ -231,16 +224,15 @@ export const processDeploymentStatusDetailsData = ( return deploymentData } - // Would move for each timeline iteratively and if timeline is in terminal state then early return - // If timeline is in non-terminal state then we mark it as waiting - if (!data?.timelines?.length) { + if (!data.timelines.length) { return deploymentData } const isProgressing = PROGRESSING_DEPLOYMENT_STATUS.includes(deploymentStatus) - const isArgoCDAvailable = data.timelines.some((timeline) => timeline.status.includes(TIMELINE_STATUS.ARGOCD_SYNC)) + const isArgoCDSyncAvailable = data.timelines.some((timeline) => + timeline.status.includes(TIMELINE_STATUS.ARGOCD_SYNC), + ) - // After initial processing will mark all unavailable timelines [present before last invalid state] as success PHYSICAL_ENV_DEPLOYMENT_TIMELINE_ORDER.forEach((timelineStatusType, index) => { const element = findRight(data.timelines, getPredicate(timelineStatusType)) @@ -292,11 +284,7 @@ export const processDeploymentStatusDetailsData = ( timelineData.displaySubText = '' // These are singular events so either their success will come or failure - if ( - [TIMELINE_STATUS.GIT_COMMIT_FAILED, TIMELINE_STATUS.ARGOCD_SYNC_FAILED].includes( - element.status as TIMELINE_STATUS, - ) - ) { + if ([TIMELINE_STATUS.GIT_COMMIT_FAILED, TIMELINE_STATUS.ARGOCD_SYNC_FAILED].includes(element.status)) { timelineData.displaySubText = 'Failed' timelineData.icon = 'failed' timelineData.isCollapsed = false @@ -307,7 +295,7 @@ export const processDeploymentStatusDetailsData = ( break case TIMELINE_STATUS.KUBECTL_APPLY: { - if (!isArgoCDAvailable) { + if (!isArgoCDSyncAvailable) { deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.icon = 'success' deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.displaySubText = '' deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.time = element.statusTime @@ -337,8 +325,8 @@ export const processDeploymentStatusDetailsData = ( break } + // Moving the next timeline to inprogress if (timelineData.icon === 'success' && index !== PHYSICAL_ENV_DEPLOYMENT_TIMELINE_ORDER.length - 1) { - // Moving the next timeline to inprogress const nextTimelineStatus = PHYSICAL_ENV_DEPLOYMENT_TIMELINE_ORDER[index + 1] const nextTimeline = deploymentData.deploymentStatusBreakdown[nextTimelineStatus] @@ -353,7 +341,6 @@ export const processDeploymentStatusDetailsData = ( const timelineData = deploymentData.deploymentStatusBreakdown[timelineStatusType] if (timelineData.icon === 'inprogress' || timelineData.icon === 'success') { - // If the timeline is in progress or success then we will mark all the previous steps as success for (let j = i - 1; j >= 0; j -= 1) { const prevTimelineStatusType = PHYSICAL_ENV_DEPLOYMENT_TIMELINE_ORDER[j] const prevTimelineData = deploymentData.deploymentStatusBreakdown[prevTimelineStatusType] diff --git a/src/Shared/Components/StatusComponent/utils.ts b/src/Shared/Components/StatusComponent/utils.ts index 29773051b..20972b952 100644 --- a/src/Shared/Components/StatusComponent/utils.ts +++ b/src/Shared/Components/StatusComponent/utils.ts @@ -70,6 +70,8 @@ export const getIconName = (status: string, showAnimatedIcon: boolean): IconName case 'timedout': case 'timed_out': return 'ic-timeout-dash' + case 'unable_to_fetch': + return 'ic-disconnect' default: return null } @@ -90,6 +92,7 @@ export const getIconColor = (status: string): IconsProps['color'] => { case 'request_accepted': case 'starting': return 'O500' + case 'unable_to_fetch': case 'timedout': case 'timed_out': return 'R500' diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 07e0527cf..a0fed9ca1 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -1175,25 +1175,6 @@ export interface SyncStageResourceDetail { statusMessage: string } -export interface DeploymentStatusDetailsTimelineType - extends Pick { - status: string - statusDetail: string - statusTime: string - resourceDetails?: SyncStageResourceDetail[] -} - -export interface DeploymentStatusDetailsType { - deploymentFinishedOn: string - deploymentStartedOn: string - triggeredBy: string - statusFetchCount: number - statusLastFetchedAt: string - timelines: DeploymentStatusDetailsTimelineType[] - wfrStatus?: WorkflowRunnerStatusDTO - isDeploymentWithoutApproval: boolean -} - export enum TIMELINE_STATUS { DEPLOYMENT_INITIATED = 'DEPLOYMENT_INITIATED', GIT_COMMIT = 'GIT_COMMIT', @@ -1218,6 +1199,25 @@ export enum TIMELINE_STATUS { HELM_MANIFEST_PUSHED_TO_HELM_REPO_FAILED = 'HELM_MANIFEST_PUSHED_TO_HELM_REPO_FAILED', } +export interface DeploymentStatusDetailsTimelineType + extends Pick { + status: TIMELINE_STATUS + statusDetail: string + statusTime: string + resourceDetails?: SyncStageResourceDetail[] +} + +export interface DeploymentStatusDetailsType { + deploymentFinishedOn: string + deploymentStartedOn: string + triggeredBy: string + statusFetchCount: number + statusLastFetchedAt: string + timelines: DeploymentStatusDetailsTimelineType[] + wfrStatus?: WorkflowRunnerStatusDTO + isDeploymentWithoutApproval: boolean +} + export type DeploymentStatusTimelineType = | TIMELINE_STATUS.DEPLOYMENT_INITIATED | TIMELINE_STATUS.GIT_COMMIT From 071fff1450e9b553a60635f9879868aa1b7e7fa1 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 13 May 2025 18:58:59 +0530 Subject: [PATCH 124/157] refactor: Rename actionButton to actionItem in StatusHeadingContainer and update related IDs for consistency --- .../Components/AppStatusModal/AppStatusBody.tsx | 16 ++++++++-------- .../AppStatusModal/AppStatusModalTabList.tsx | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 7e392385c..82b41a30e 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -43,17 +43,17 @@ const StatusHeadingContainer = ({ type, appId, envId, - actionButton, + actionItem, }: PropsWithChildren> & { appId: number envId?: number - actionButton?: ReactNode + actionItem?: ReactNode }) => (
      {children}
      - {actionButton} + {actionItem} {type === 'release' ? (
      ) -export const HelpButton = ({ serverInfo, fetchingServerInfo, handleGettingStartedClick, onClick }: HelpButtonProps) => { +export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButtonProps) => { // STATES const [isActionMenuOpen, setIsActionMenuOpen] = useState(false) // HOOKS - const { currentServerInfo, handleOpenLicenseInfoDialog, licenseData } = useMainContext() + const { currentServerInfo, handleOpenLicenseInfoDialog, licenseData, setGettingStartedClicked } = useMainContext() // REFS const typeFormSliderButtonRef = useRef(null) @@ -69,7 +69,11 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, handleGettingStarte typeFormSliderButtonRef.current?.open() } - const handleActionMenuClick: ActionMenuProps['onClick'] = (item) => { + const handleGettingStartedClick = () => { + setGettingStartedClicked(true) + } + + const handleActionMenuClick: HelpButtonActionMenuProps['onClick'] = (item) => { switch (item.id) { case HelpMenuItems.GETTING_STARTED: handleGettingStartedClick() @@ -94,7 +98,7 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, handleGettingStarte return ( <> - id="page-header-help-action-menu" alignment="end" width={220} diff --git a/src/Shared/Components/Header/PageHeader.tsx b/src/Shared/Components/Header/PageHeader.tsx index c3a15548d..e30dfdedb 100644 --- a/src/Shared/Components/Header/PageHeader.tsx +++ b/src/Shared/Components/Header/PageHeader.tsx @@ -49,14 +49,8 @@ const PageHeader = ({ markAsBeta, tippyProps, }: PageHeaderType) => { - const { - loginCount, - setLoginCount, - showGettingStartedCard, - setShowGettingStartedCard, - setGettingStartedClicked, - licenseData, - } = useMainContext() + const { loginCount, setLoginCount, showGettingStartedCard, setShowGettingStartedCard, licenseData } = + useMainContext() const { showSwitchThemeLocationTippy, handleShowSwitchThemeLocationTippyChange } = useTheme() const { isTippyCustomized, tippyRedirectLink, TippyIcon, tippyMessage, onClickTippyButton, additionalContent } = @@ -92,10 +86,6 @@ const PageHeader = ({ setExpiryDate(+localStorage.getItem('clickedOkay')) }, []) - const handleGettingStartedClick = () => { - setGettingStartedClicked(true) - } - const hideGettingStartedCard = (count?: string) => { setShowGettingStartedCard(false) if (count) { @@ -147,7 +137,6 @@ const PageHeader = ({ {!window._env_.K8S_CLIENT && ( diff --git a/src/Shared/Components/Header/constants.ts b/src/Shared/Components/Header/constants.ts index cf16317d1..0d0263c34 100644 --- a/src/Shared/Components/Header/constants.ts +++ b/src/Shared/Components/Header/constants.ts @@ -17,10 +17,9 @@ import { DISCORD_LINK, DOCUMENTATION_HOME_PAGE, URLS } from '@Common/Constants' import { CONTACT_SUPPORT_LINK, OPEN_NEW_TICKET, RAISE_ISSUE, VIEW_ALL_TICKETS } from '@Shared/constants' -import { ActionMenuItemType } from '../ActionMenu' -import { HelpMenuItems } from './types' +import { HelpButtonActionMenuProps, HelpMenuItems } from './types' -export const COMMON_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ +export const COMMON_HELP_ACTION_MENU_ITEMS: HelpButtonActionMenuProps['options'][number]['items'] = [ ...((!window._env_?.K8S_CLIENT ? [ { @@ -31,7 +30,7 @@ export const COMMON_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ to: `/${URLS.GETTING_STARTED}`, }, ] - : []) satisfies ActionMenuItemType[]), + : []) satisfies HelpButtonActionMenuProps['options'][number]['items']), { id: HelpMenuItems.VIEW_DOCUMENTATION, label: 'View documentation', @@ -53,7 +52,7 @@ export const COMMON_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ }, ] -export const OSS_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ +export const OSS_HELP_ACTION_MENU_ITEMS: HelpButtonActionMenuProps['options'][number]['items'] = [ { id: HelpMenuItems.CHAT_WITH_SUPPORT, label: 'Chat with support', @@ -70,7 +69,7 @@ export const OSS_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ }, ] -export const ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ +export const ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS: HelpButtonActionMenuProps['options'][number]['items'] = [ { id: HelpMenuItems.REQUEST_SUPPORT, label: 'Request Support', @@ -80,7 +79,7 @@ export const ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ }, ] -export const ENTERPRISE_HELP_ACTION_MENU_ITEMS: ActionMenuItemType[] = [ +export const ENTERPRISE_HELP_ACTION_MENU_ITEMS: HelpButtonActionMenuProps['options'][number]['items'] = [ { id: HelpMenuItems.OPEN_NEW_TICKET, label: 'Open new ticket', diff --git a/src/Shared/Components/Header/types.ts b/src/Shared/Components/Header/types.ts index 160edfbf0..0dc15f387 100644 --- a/src/Shared/Components/Header/types.ts +++ b/src/Shared/Components/Header/types.ts @@ -17,6 +17,7 @@ import { ModuleStatus } from '@Shared/types' import { ResponseType, TippyCustomizedProps } from '../../../Common' +import { ActionMenuProps } from '../ActionMenu' export enum InstallationType { OSS_KUBECTL = 'oss_kubectl', @@ -58,7 +59,6 @@ export interface ServerInfoResponse extends ResponseType { export interface HelpButtonProps { serverInfo: ServerInfo fetchingServerInfo: boolean - handleGettingStartedClick: () => void onClick: () => void } @@ -74,3 +74,5 @@ export enum HelpMenuItems { CHAT_WITH_SUPPORT = 'chat-with-support', RAISE_ISSUE_REQUEST = 'raise-issue-request', } + +export type HelpButtonActionMenuProps = ActionMenuProps diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts index 35f762665..769041b14 100644 --- a/src/Shared/Components/Header/utils.ts +++ b/src/Shared/Components/Header/utils.ts @@ -16,7 +16,6 @@ import { LOGIN_COUNT } from '@Common/Constants' -import { ActionMenuProps } from '../ActionMenu' import { DevtronLicenseInfo, LicenseStatus } from '../License' import { COMMON_HELP_ACTION_MENU_ITEMS, @@ -25,6 +24,7 @@ import { OSS_HELP_ACTION_MENU_ITEMS, } from './constants' import { updatePostHogEvent } from './service' +import { HelpButtonActionMenuProps } from './types' const millisecondsInDay = 86400000 export const getDateInMilliseconds = (days) => 1 + new Date().valueOf() + (days ?? 0) * millisecondsInDay @@ -52,7 +52,7 @@ export const getHelpActionMenuOptions = ({ }: { isEnterprise: boolean isTrial: boolean -}): ActionMenuProps['options'] => [ +}): HelpButtonActionMenuProps['options'] => [ { items: COMMON_HELP_ACTION_MENU_ITEMS, }, From 10f20243bab5d0e2c69dc3e7f22fc0a15da45e8c Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Wed, 14 May 2025 14:31:10 +0530 Subject: [PATCH 129/157] feat: HeaderWithCreateButton - update Create button with ActionMenu --- .../HeaderWithCreateButon.tsx | 119 ++++-------------- .../HeaderWithCreateButton.scss | 44 ------- .../Header/HeaderWithCreateButton/types.ts | 13 ++ .../Header/HeaderWithCreateButton/utils.ts | 38 ++++++ 4 files changed, 78 insertions(+), 136 deletions(-) delete mode 100644 src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButton.scss create mode 100644 src/Shared/Components/Header/HeaderWithCreateButton/types.ts create mode 100644 src/Shared/Components/Header/HeaderWithCreateButton/utils.ts diff --git a/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx b/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx index 811226972..0b503f3d2 100644 --- a/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx +++ b/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx @@ -14,122 +14,57 @@ * limitations under the License. */ -import { useState } from 'react' -import { useHistory, useLocation, useParams } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' -import { ReactComponent as AddIcon } from '@Icons/ic-add.svg' -import { ReactComponent as DropDown } from '@Icons/ic-caret-down-small.svg' -import { ReactComponent as ChartIcon } from '@Icons/ic-charts.svg' -import { ReactComponent as JobIcon } from '@Icons/ic-k8s-job.svg' +import { SERVER_MODE, URLS } from '@Common/Constants' +import { noop } from '@Common/Helper' +import { ActionMenu } from '@Shared/Components/ActionMenu' +import { ButtonComponentType } from '@Shared/Components/Button' import Button from '@Shared/Components/Button/Button.component' +import { Icon } from '@Shared/Components/Icon' +import { AppListConstants, ComponentSizeType } from '@Shared/constants' +import { useMainContext } from '@Shared/Providers' -import { Modal, SERVER_MODE, URLS } from '../../../../Common' -import { AppListConstants, ComponentSizeType } from '../../../constants' -import { useMainContext } from '../../../Providers' import PageHeader from '../PageHeader' -import { getIsShowingLicenseData } from '../utils' - -import './HeaderWithCreateButton.scss' - -export interface HeaderWithCreateButtonProps { - headerName: string -} +import { HeaderWithCreateButtonProps } from './types' +import { getCreateActionMenuOptions } from './utils' export const HeaderWithCreateButton = ({ headerName }: HeaderWithCreateButtonProps) => { + // HOOKS + const { serverMode } = useMainContext() const params = useParams<{ appType: string }>() - const history = useHistory() const location = useLocation() - const { serverMode, licenseData } = useMainContext() - const [showCreateSelectionModal, setShowCreateSelectionModal] = useState(false) - - const showingLicenseBar = getIsShowingLicenseData(licenseData) - - const handleCreateButton = () => { - setShowCreateSelectionModal((prevState) => !prevState) - } - const redirectToHelmAppDiscover = () => { - history.push(URLS.CHARTS_DISCOVER) - } - - const openCreateDevtronAppModel = () => { - const _urlPrefix = `${URLS.APP}/${URLS.APP_LIST}/${params.appType ?? AppListConstants.AppType.DEVTRON_APPS}` - history.push(`${_urlPrefix}/${AppListConstants.CREATE_DEVTRON_APP_URL}${location.search}`) - } - - const openCreateJobModel = () => { - history.push(`${URLS.JOB}/${URLS.APP_LIST}/${URLS.CREATE_JOB}`) - } + // CONSTANTS + const createCustomAppURL = `${URLS.APP}/${URLS.APP_LIST}/${params.appType ?? AppListConstants.AppType.DEVTRON_APPS}/${AppListConstants.CREATE_DEVTRON_APP_URL}${location.search}` const renderActionButtons = () => serverMode === SERVER_MODE.FULL ? ( - {isEnterprise && ( diff --git a/src/Shared/Components/Icon/IconBase.tsx b/src/Shared/Components/Icon/IconBase.tsx index 8e6803b90..a42bdce0b 100644 --- a/src/Shared/Components/Icon/IconBase.tsx +++ b/src/Shared/Components/Icon/IconBase.tsx @@ -16,6 +16,7 @@ import { ConditionalWrap } from '@Common/Helper' import { Tooltip } from '@Common/Tooltip' +import { isNullOrUndefined } from '@Shared/Helpers' import { ICON_STROKE_WIDTH_MAP } from './constants' import { IconBaseProps } from './types' @@ -48,13 +49,13 @@ export const IconBase = ({ From 61d30a7a97d358b2bb7a3ccfe68c8915f35e6d2b Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Wed, 14 May 2025 17:26:49 +0530 Subject: [PATCH 132/157] refactor: ActionMenu - update styling for searchbox and menu items, improve padding and border styles --- src/Shared/Components/ActionMenu/ActionMenu.component.tsx | 7 ++++--- src/Shared/Components/ActionMenu/actionMenu.scss | 4 ++-- .../Components/Header/HeaderWithCreateButton/utils.ts | 4 ---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index 480baa070..938301e16 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -69,7 +69,7 @@ export const ActionMenu = ({ {isSearchable && (
      ({ placeholder="Search" onChange={handleSearch} fullWidth + autoFocus />
      )} @@ -125,13 +126,13 @@ export const ActionMenu = ({
    • )) ) : ( -
    • +
    • No options

    • )}
    {footerConfig && ( -
    +
    )} diff --git a/src/Shared/Components/ActionMenu/actionMenu.scss b/src/Shared/Components/ActionMenu/actionMenu.scss index 8768b44ec..127e110d6 100644 --- a/src/Shared/Components/ActionMenu/actionMenu.scss +++ b/src/Shared/Components/ActionMenu/actionMenu.scss @@ -2,7 +2,7 @@ list-style: none; &__group { - border-top: 1px solid var(--border-secondary); + border-top: 1px solid var(--border-secondary-translucent); &:first-child { border-top: none; @@ -29,7 +29,7 @@ &__searchbox input { border: none; - padding: 0; + padding: 8px 12px; background-color: transparent; } } diff --git a/src/Shared/Components/Header/HeaderWithCreateButton/utils.ts b/src/Shared/Components/Header/HeaderWithCreateButton/utils.ts index 4850ba48e..829e6b159 100644 --- a/src/Shared/Components/Header/HeaderWithCreateButton/utils.ts +++ b/src/Shared/Components/Header/HeaderWithCreateButton/utils.ts @@ -13,10 +13,6 @@ export const getCreateActionMenuOptions = (createCustomAppURL: string): CreateAc componentType: 'link', to: createCustomAppURL, }, - ], - }, - { - items: [ { id: CreateActionMenuItems.CHART_STORE, label: 'From Chart store', From c02b976414394d9e94da086a36bbf71add83cbf7 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 14 May 2025 17:44:03 +0530 Subject: [PATCH 133/157] fix: use appStatus for debug button, rename to updateDeploymentStatusDetailsBreakdownData, minor css enhancement --- .../Components/AppStatusModal/AppStatusBody.tsx | 10 +++------- .../AppStatusModal/AppStatusModal.component.tsx | 4 ++-- src/Shared/Components/AppStatusModal/types.ts | 4 ++-- .../CICDHistory/DeploymentDetailSteps.tsx | 13 ++++++------- .../CICDHistory/DeploymentStatusBreakdown.scss | 6 +++++- .../CICDHistory/DeploymentStatusBreakdown.tsx | 6 +++++- .../CICDHistory/DeploymentStatusDetailRow.tsx | 4 +++- src/Shared/Components/CICDHistory/types.tsx | 1 + 8 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 47c574d00..8375ad57d 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -4,7 +4,6 @@ import { getAIAnalyticsEvents } from '@Common/Helper' import { Tooltip } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' import { getAppDetailsURL } from '@Shared/Helpers' -import { AppType } from '@Shared/types' import { Button, ButtonComponentType, ButtonVariantType } from '../Button' import { DeploymentStatusDetailBreakdown } from '../CICDHistory' @@ -106,7 +105,7 @@ export const AppStatusBody = ({ envId={appDetails.environmentId} actionItem={ ExplainWithAIButton && - appDetails.appStatus?.toLowerCase() !== StatusType.HEALTHY.toLowerCase() && + appStatus?.toLowerCase() !== StatusType.HEALTHY.toLowerCase() && (debugNode || message) ? ( ) : null diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx index 228359fc7..24e3dbbdd 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -29,7 +29,7 @@ const AppStatusModal = ({ type, appDetails: appDetailsProp, processVirtualEnvironmentDeploymentData, - handleUpdateDeploymentStatusDetailsBreakdownData, + updateDeploymentStatusDetailsBreakdownData, isConfigDriftEnabled, configDriftModal: ConfigDriftModal, appId, @@ -95,7 +95,7 @@ const AppStatusModal = ({ deploymentStatusAbortControllerRef, ) - handleUpdateDeploymentStatusDetailsBreakdownData?.(response) + updateDeploymentStatusDetailsBreakdownData?.(response) return response } diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index 04a7b2a07..eca8a6c77 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -30,13 +30,13 @@ export type AppStatusModalProps = { envId: number appDetails?: never initialTab?: never - handleUpdateDeploymentStatusDetailsBreakdownData?: never + updateDeploymentStatusDetailsBreakdownData?: never } | { type: 'devtron-app' | 'other-apps' | 'stack-manager' appDetails: AppDetails initialTab: AppStatusModalTabType - handleUpdateDeploymentStatusDetailsBreakdownData: (data: DeploymentStatusDetailsBreakdownDataType) => void + updateDeploymentStatusDetailsBreakdownData: (data: DeploymentStatusDetailsBreakdownDataType) => void appId?: never envId?: never } diff --git a/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx b/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx index 4aa862694..bce3f7053 100644 --- a/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx @@ -163,13 +163,12 @@ const DeploymentDetailSteps = ({ {renderDeploymentApprovalInfo && getIsApprovalPolicyConfigured(userApprovalMetadata?.approvalConfigData) && renderDeploymentApprovalInfo(userApprovalMetadata)} -
    - -
    +
    ) diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss index f4f1158bf..0c33b2fca 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss +++ b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss @@ -56,4 +56,8 @@ border-radius: 0 0 4px 4px; border-top: 0; } -} \ No newline at end of file +} + +.deployment-approval-container + .deployment-status-breakdown-container { + padding-top: 0px; +} diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx index 019b98e72..4a26a54c9 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx @@ -25,6 +25,7 @@ const DeploymentStatusDetailBreakdown = ({ deploymentStatusDetailsBreakdownData, isVirtualEnvironment, appDetails, + rootClassName = '', }: DeploymentStatusDetailBreakdownType) => { const isHelmManifestPushed = deploymentStatusDetailsBreakdownData.deploymentStatusBreakdown[ @@ -38,7 +39,10 @@ const DeploymentStatusDetailBreakdown = ({ } return ( -
    +
    {statusBreakDownType.displaySubText && ( - + {statusBreakDownType.displaySubText} )} diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index b5e9dff11..cc102c0a0 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -519,6 +519,7 @@ export interface DeploymentStatusDetailBreakdownType { * Won't be available if coming directly to deployment history from url */ appDetails: AppDetails | null + rootClassName?: string } export interface DeploymentStatusDetailRowType extends Pick { From fd34959936d94f6f040512b6ac959e5e3b44ed0f Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 14 May 2025 19:00:42 +0530 Subject: [PATCH 134/157] fix: review comments --- src/Common/Helper.tsx | 2 +- .../AppStatusModal/AppStatusBody.tsx | 24 ++++---------- .../AppStatusModal.component.tsx | 22 ++++++++++--- .../AppStatusModal/AppStatusModalTabList.tsx | 2 +- src/Shared/Components/AppStatusModal/types.ts | 8 ++++- .../Components/AppStatusModal/utils.tsx | 25 ++++----------- .../CICDHistory/DeploymentStatusDetailRow.tsx | 14 ++++---- .../DeploymentStatusBreakdown/types.ts | 2 +- .../DeploymentStatusBreakdown/utils.tsx | 32 +++++++++++++++---- .../Components/TabGroup/TabGroup.types.ts | 26 +++++++++++---- src/Shared/constants.tsx | 3 ++ 11 files changed, 97 insertions(+), 63 deletions(-) diff --git a/src/Common/Helper.tsx b/src/Common/Helper.tsx index f81054c9d..d060a1042 100644 --- a/src/Common/Helper.tsx +++ b/src/Common/Helper.tsx @@ -1124,7 +1124,7 @@ const getAppTypeCategory = (appType: AppType) => { export const getAIAnalyticsEvents = (context: string, appType?: AppType) => `AI_${appType ? `${getAppTypeCategory(appType)}_` : ''}${context}` -export const findRight = (arr: T[], predicate: (item: T) => boolean): T => { +export const findRight = (arr: T[], predicate: (item: T) => boolean): T | null => { for (let i = arr.length - 1; i >= 0; i--) { if (predicate(arr[i])) { return arr[i] diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 8375ad57d..09dba91d2 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, PropsWithChildren, ReactNode } from 'react' +import { ComponentProps, ReactNode } from 'react' import { getAIAnalyticsEvents } from '@Common/Helper' import { Tooltip } from '@Common/Tooltip' @@ -13,7 +13,7 @@ import { ShowMoreText } from '../ShowMoreText' import { AppStatus, DeploymentStatus, StatusType } from '../StatusComponent' import AppStatusContent from './AppStatusContent' import { APP_STATUS_CUSTOM_MESSAGES } from './constants' -import { AppStatusBodyProps, AppStatusModalTabType } from './types' +import { AppStatusBodyProps, AppStatusModalTabType, StatusHeadingContainerProps } from './types' import { getAppStatusMessageFromAppDetails } from './utils' const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; value: ReactNode; isLast?: boolean }) => ( @@ -37,17 +37,7 @@ const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; val
    ) -const StatusHeadingContainer = ({ - children, - type, - appId, - envId, - actionItem, -}: PropsWithChildren> & { - appId: number - envId?: number - actionItem?: ReactNode -}) => ( +const StatusHeadingContainer = ({ children, type, appId, envId, actionItem }: StatusHeadingContainerProps) => (
    {children} @@ -96,7 +86,7 @@ export const AppStatusBody = ({ return [ { - id: `app-status-block${1}`, + id: 'app-status-row', heading: type !== 'stack-manager' ? 'Application Status' : 'Status', value: ( diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx index 24e3dbbdd..c7cd462ac 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -7,7 +7,11 @@ import { Drawer } from '@Common/Drawer' import { GenericEmptyState } from '@Common/EmptyState' import { handleUTCTime, stopPropagation, useAsync } from '@Common/Helper' import { DeploymentAppTypes, ImageType } from '@Common/Types' -import { ComponentSizeType } from '@Shared/constants' +import { + APP_DETAILS_FALLBACK_POLLING_INTERVAL, + ComponentSizeType, + PROGRESSING_DEPLOYMENT_STATUS_POLLING_INTERVAL, +} from '@Shared/constants' import { AppType } from '@Shared/types' import { APIResponseHandler } from '../APIResponseHandler' @@ -56,6 +60,10 @@ const AppStatusModal = ({ return response } + /** + * Fetching logic for app details is we initially call from useAsync then through useEffect initiate polling + * Since the dependency of useAsync is empty array, it will only be called once and then we will call the polling method is triggered based on the polling interval set in the environment variables. + */ const [ areInitialAppDetailsLoading, fetchedAppDetails, @@ -100,6 +108,12 @@ const AppStatusModal = ({ return response } + /** + * Fetching logic for deployment status is we initially call from useAsync then through useEffect initiate polling + * Now on tab switch we need to clear the previous timeout and set a new one reason being tab would have changed and in polling method that would not be reflected since closure is created + * So we re-trigger useAsync to get the new data and set a new timeout + * resetOnChange is there so that user don't see the change in icon in tabs + */ const [ isDeploymentTimelineLoading, deploymentStatusDetailsBreakdownData, @@ -122,7 +136,7 @@ const AppStatusModal = ({ // eslint-disable-next-line @typescript-eslint/no-floating-promises handleAppDetailsExternalSync() }, - Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || 30000, + Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || APP_DETAILS_FALLBACK_POLLING_INTERVAL, ) } @@ -136,7 +150,7 @@ const AppStatusModal = ({ appDetails.appType !== AppType.DEVTRON_HELM_CHART ? window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL : window._env_.HELM_APP_DETAILS_POLLING_INTERVAL, - ) || 30000 + ) || APP_DETAILS_FALLBACK_POLLING_INTERVAL deploymentStatusPollingTimeoutRef.current = setTimeout( async () => { @@ -149,7 +163,7 @@ const AppStatusModal = ({ // eslint-disable-next-line @typescript-eslint/no-floating-promises handleDeploymentStatusExternalSync() }, - isDeploymentInProgress ? 10000 : pollingIntervalFromFlag, + isDeploymentInProgress ? PROGRESSING_DEPLOYMENT_STATUS_POLLING_INTERVAL : pollingIntervalFromFlag, ) } diff --git a/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx b/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx index a75ebe7c7..f50950417 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx @@ -79,7 +79,7 @@ const AppStatusModalTabList = ({ // Could have achieved via onDataLoad but, have done this through useEffect to avoid abrupt shift in case some tabs went missing after polling useEffect(() => { if (tabGroups.length && !selectedTab) { - handleSelectTab(tabGroups[0]?.id as AppStatusModalTabType) + handleSelectTab(tabGroups[0].id as AppStatusModalTabType) } }, []) diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index eca8a6c77..9ac3c4b0a 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -1,4 +1,4 @@ -import { FunctionComponent } from 'react' +import { FunctionComponent, PropsWithChildren, ReactNode } from 'react' import { APIOptions } from '@Common/Types' import { @@ -92,3 +92,9 @@ export interface AppStatusModalTabListProps extends Pick> { + appId: number + envId?: number + actionItem?: ReactNode +} diff --git a/src/Shared/Components/AppStatusModal/utils.tsx b/src/Shared/Components/AppStatusModal/utils.tsx index d02794500..f42b2bd8f 100644 --- a/src/Shared/Components/AppStatusModal/utils.tsx +++ b/src/Shared/Components/AppStatusModal/utils.tsx @@ -84,30 +84,19 @@ export const getShowDeploymentStatusModal = ({ type, appDetails, }: Pick): boolean => { - if (!appDetails) { - return false - } - - const isHelmOrDevtronApp = - appDetails.appType === AppType.DEVTRON_APP || appDetails.appType === AppType.DEVTRON_HELM_CHART - - if (type === 'stack-manager' || !isHelmOrDevtronApp) { + if ( + !appDetails || + type === 'stack-manager' || + (appDetails.appType !== AppType.DEVTRON_APP && appDetails.appType !== AppType.DEVTRON_HELM_CHART) + ) { return false } if (appDetails.appType === AppType.DEVTRON_HELM_CHART) { - if (!appDetails.lastDeployedTime || appDetails.deploymentAppType === DeploymentAppTypes.HELM) { - return false - } - - return true - } - - if (appDetails.releaseMode === ReleaseMode.MIGRATE_EXTERNAL_APPS && !appDetails.isPipelineTriggered) { - return false + return !!appDetails.lastDeployedTime && appDetails.deploymentAppType !== DeploymentAppTypes.HELM } - return true + return appDetails.releaseMode !== ReleaseMode.MIGRATE_EXTERNAL_APPS || appDetails.isPipelineTriggered } export const getEmptyViewImageFromHelmDeploymentStatus = ( diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx index 68a8c7210..0b23c111d 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx @@ -78,18 +78,18 @@ export const DeploymentStatusDetailRow = ({ return (
    -
    - {statusBreakDownType.subSteps?.map((items, index) => ( - // eslint-disable-next-line react/no-array-index-key -
    + {statusBreakDownType.subSteps?.map((items, index) => ( + // eslint-disable-next-line react/no-array-index-key +
    +
    {renderDeploymentTimelineIcon(items.icon)} {items.message}
    - ))} -
    +
    + ))} {statusBreakDownType.resourceDetails?.length ? (
    -
    +
    {MANIFEST_STATUS_HEADERS.map((headerKey, index) => ( // eslint-disable-next-line react/no-array-index-key
    diff --git a/src/Shared/Components/DeploymentStatusBreakdown/types.ts b/src/Shared/Components/DeploymentStatusBreakdown/types.ts index 811f83c19..6c0cc185e 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/types.ts +++ b/src/Shared/Components/DeploymentStatusBreakdown/types.ts @@ -20,7 +20,7 @@ export enum WorkflowRunnerStatusDTO { DEGRADED = 'Degraded', } -export interface HandleUpdateTimelineDataForTimedOutOrUnableToFetchStatusParamsType { +export interface ProcessUnableToFetchOrTimedOutStatusType { timelineData: DeploymentStatusBreakdownItemType timelineStatusType: DeploymentStatusTimelineType deploymentStatus: typeof DEPLOYMENT_STATUS.UNABLE_TO_FETCH | typeof DEPLOYMENT_STATUS.TIMED_OUT diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index b2e211b1e..a2576e95a 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -19,7 +19,7 @@ import { SUCCESSFUL_DEPLOYMENT_STATUS, WFR_STATUS_DTO_TO_DEPLOYMENT_STATUS_MAP, } from './constants' -import { HandleUpdateTimelineDataForTimedOutOrUnableToFetchStatusParamsType } from './types' +import { ProcessUnableToFetchOrTimedOutStatusType } from './types' const getDefaultDeploymentStatusTimeline = ( data?: DeploymentStatusDetailsType, @@ -94,13 +94,13 @@ const getPredicate = } } -const handleUpdateTimelineDataForTimedOutOrUnableToFetchStatus = ({ +const processUnableToFetchOrTimedOutStatus = ({ timelineData, timelineStatusType, deploymentStatus, statusLastFetchedAt, statusFetchCount, -}: HandleUpdateTimelineDataForTimedOutOrUnableToFetchStatusParamsType) => { +}: ProcessUnableToFetchOrTimedOutStatusType) => { timelineData.icon = deploymentStatus === DEPLOYMENT_STATUS.UNABLE_TO_FETCH ? 'disconnect' : 'timed_out' timelineData.displaySubText = 'Unknown' timelineData.isCollapsed = false @@ -149,7 +149,7 @@ const processKubeCTLApply = ( } if (element.status === TIMELINE_STATUS.KUBECTL_APPLY_STARTED) { - timelineData.resourceDetails = element.resourceDetails?.filter( + timelineData.resourceDetails = (element.resourceDetails || []).filter( (item) => item.resourcePhase === tableData.currentPhase, ) @@ -176,7 +176,7 @@ const processKubeCTLApply = ( deploymentStatus === DEPLOYMENT_STATUS.TIMED_OUT || deploymentStatus === DEPLOYMENT_STATUS.UNABLE_TO_FETCH ) { - handleUpdateTimelineDataForTimedOutOrUnableToFetchStatus({ + processUnableToFetchOrTimedOutStatus({ timelineData, timelineStatusType: TIMELINE_STATUS.KUBECTL_APPLY, deploymentStatus, @@ -202,6 +202,24 @@ const processKubeCTLApply = ( } } +/** + * @description + * This function processes the deployment status details data and returns a breakdown of the deployment status. + * Cases it handles: + * 1. If timelines are not present, say the case of helm deployment, we will parse the wfrStatus and put the status and basic deployment info [triggeredBy, deploymentStartedOn, deploymentFinishedOn] into the breakdown data and return it. + * 2. In case of gitops: + * - There are five timelines in chronological order: + * - Deployment Initiated + * - Git commit + * - ArgoCD Sync + * - Kubectl Apply + * - App Health + * - Basic flow is we traverse the timelines in order, if find the last status for that specific timeline from response by traversing the timelines in reverse order. + * - If element is found, we will parse the status and set the icon, display text, time, etc. for that timeline and set the next timeline to inprogress. + * - If element is not found, we will parse on basis of factors like: + * - If this timeline is not inprogress and deploymentStatus is progressing, we will set the current timeline to waiting. + * - In similar fashion based on the deploymentStatus we will set the icon and display text for the timeline. + */ export const processDeploymentStatusDetailsData = ( data?: DeploymentStatusDetailsType, ): DeploymentStatusDetailsBreakdownDataType => { @@ -229,6 +247,8 @@ export const processDeploymentStatusDetailsData = ( } const isProgressing = PROGRESSING_DEPLOYMENT_STATUS.includes(deploymentStatus) + // This key will be used since argocd sync is manual or auto based on flag on BE. + // And in old data as well this timeline won't be present so in KUBECTL_APPLY timeline we will set the icon to success const isArgoCDSyncAvailable = data.timelines.some((timeline) => timeline.status.includes(TIMELINE_STATUS.ARGOCD_SYNC), ) @@ -263,7 +283,7 @@ export const processDeploymentStatusDetailsData = ( deploymentStatus === DEPLOYMENT_STATUS.TIMED_OUT) && timelineData.icon === 'inprogress' ) { - handleUpdateTimelineDataForTimedOutOrUnableToFetchStatus({ + processUnableToFetchOrTimedOutStatus({ timelineData, timelineStatusType, deploymentStatus, diff --git a/src/Shared/Components/TabGroup/TabGroup.types.ts b/src/Shared/Components/TabGroup/TabGroup.types.ts index c4a6cabfc..619b9a064 100644 --- a/src/Shared/Components/TabGroup/TabGroup.types.ts +++ b/src/Shared/Components/TabGroup/TabGroup.types.ts @@ -93,23 +93,35 @@ type TabTooltipProps = tooltipProps?: never } +/** + * Represents the properties for defining an icon in a tab group. + * This type allows for three configurations: + * + * 1. **Icon as a functional component or string**: + * - Use the `icon` property to specify either a functional component that renders an SVG or a string representing the name of the icon. + * - The `iconElement` property must not be provided in this case. + * + * 2. **Icon as a JSX element**: + * - Use the `iconElement` property to specify a JSX element representing the icon. + * - The `icon` property must not be provided in this case. + * + * 3. **No icon**: + * - Neither `icon` nor `iconElement` is provided, resulting in no icon being displayed. + * + */ type TabGroupIconProp = | { /** - * Icon to be displayed in the tab. - * This can either be a functional component that renders a SVG - * or a string representing the name of the icon to be rendered by the Icon component. + * A functional component rendering an SVG or a string representing the icon name. Mutually exclusive with `iconElement`. */ icon: React.FunctionComponent> | IconName iconElement?: never } | { + icon?: never /** - * Icon to be displayed in the tab. - * This can either be a functional component that renders a SVG - * or a string representing the name of the icon to be rendered by the Icon component. + * A JSX element representing the icon. Mutually exclusive with `icon`. */ - icon?: never iconElement: JSX.Element } | { diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx index e60ec0945..501c21c70 100644 --- a/src/Shared/constants.tsx +++ b/src/Shared/constants.tsx @@ -577,3 +577,6 @@ export const DEPLOYMENT_STAGE_TO_NODE_MAP: Readonly Date: Wed, 14 May 2025 19:14:50 +0530 Subject: [PATCH 135/157] refactor: change currentPhase type from string to null for better type safety in processKubeCTLApply --- src/Shared/Components/DeploymentStatusBreakdown/utils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index a2576e95a..f08f71dd6 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -121,10 +121,10 @@ const processKubeCTLApply = ( data: DeploymentStatusDetailsType, ) => { const tableData: { - currentPhase: DeploymentPhaseType | '' + currentPhase: DeploymentPhaseType | null currentTableData: DeploymentStatusBreakdownItemType['subSteps'] } = { - currentPhase: '', + currentPhase: null, currentTableData: [{ icon: 'success', message: 'Started by Argo CD' }], } From 3d00862364b61dd7ac6d343e33736b9d21d81488 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 14 May 2025 19:53:21 +0530 Subject: [PATCH 136/157] chore: update version from 1.13.0-pre-3 to 1.13.0-beta-1 in package.json and package-lock.json --- 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 2f8afd806..a87cde2e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-3", + "version": "1.13.0-beta-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-3", + "version": "1.13.0-beta-1", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 47fbe8eeb..25f5867cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-3", + "version": "1.13.0-beta-1", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", From 3efb36b0a0cc5b58d9908e9d78ebda9887d8a061 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 14 May 2025 23:36:04 +0530 Subject: [PATCH 137/157] chore: update version to 1.13.0-beta-2 in package.json and package-lock.json; add error handling in deployment status processing --- package-lock.json | 4 +- package.json | 2 +- .../AppStatusModal/AppStatusBody.tsx | 1 + .../CICDHistory/DeploymentStatusBreakdown.tsx | 23 ++++++-- .../DeploymentStatusBreakdown/utils.tsx | 52 +++++++++++-------- src/Shared/types.ts | 4 ++ 6 files changed, 57 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index a87cde2e5..c0e680605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-1", + "version": "1.13.0-beta-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-1", + "version": "1.13.0-beta-2", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 25f5867cb..8c8914749 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-1", + "version": "1.13.0-beta-2", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 09dba91d2..4ac6ca1b8 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -184,6 +184,7 @@ export const AppStatusBody = ({ deploymentStatusDetailsBreakdownData={deploymentStatusDetailsBreakdownData} isVirtualEnvironment={appDetails.isVirtualEnvironment} appDetails={appDetails} + rootClassName="pb-20" /> )}
    diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx index 4a26a54c9..5ba7e5b59 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx @@ -14,8 +14,11 @@ * limitations under the License. */ +import { Fragment } from 'react' + import { TIMELINE_STATUS } from '@Shared/types' +import { InfoBlock } from '../InfoBlock' import { DeploymentStatusDetailRow } from './DeploymentStatusDetailRow' import { DeploymentStatusDetailBreakdownType, DeploymentStatusDetailRowType } from './types' @@ -59,11 +62,21 @@ const DeploymentStatusDetailBreakdown = ({ TIMELINE_STATUS.KUBECTL_APPLY, ] as DeploymentStatusDetailRowType['type'][] ).map((timelineStatus) => ( - + + {deploymentStatusDetailsBreakdownData.errorBarConfig?.nextTimelineToProcess === + timelineStatus && ( + <> + +
    + + )} + + ))} timeline.status === TIMELINE_STATUS.DEPLOYMENT_FAILED) + ?.statusDetail || '' + : '' + return { - deploymentStatus: WFR_STATUS_DTO_TO_DEPLOYMENT_STATUS_MAP[data?.wfrStatus] || DEPLOYMENT_STATUS.INPROGRESS, + deploymentStatus, deploymentTriggerTime: data?.deploymentStartedOn || '', deploymentEndTime: data?.deploymentFinishedOn || '', triggeredBy: data?.triggeredBy || '', @@ -65,6 +73,12 @@ const getDefaultDeploymentStatusTimeline = ( displayText: 'Propagate manifest to Kubernetes resources', }, }, + errorBarConfig: deploymentErrorMessage + ? { + deploymentErrorMessage, + nextTimelineToProcess: TIMELINE_STATUS.GIT_COMMIT, + } + : null, } } @@ -85,9 +99,7 @@ const getPredicate = return timelineItem.status.includes(TIMELINE_STATUS.KUBECTL_APPLY) case TIMELINE_STATUS.APP_HEALTH: - return [TIMELINE_STATUS.HEALTHY, TIMELINE_STATUS.DEGRADED, TIMELINE_STATUS.DEPLOYMENT_FAILED].includes( - timelineItem.status, - ) + return [TIMELINE_STATUS.HEALTHY, TIMELINE_STATUS.DEGRADED].includes(timelineItem.status) default: return false @@ -247,6 +259,7 @@ export const processDeploymentStatusDetailsData = ( } const isProgressing = PROGRESSING_DEPLOYMENT_STATUS.includes(deploymentStatus) + const hasDeploymentFailed = deploymentStatus === DEPLOYMENT_STATUS.FAILED // This key will be used since argocd sync is manual or auto based on flag on BE. // And in old data as well this timeline won't be present so in KUBECTL_APPLY timeline we will set the icon to success const isArgoCDSyncAvailable = data.timelines.some((timeline) => @@ -255,7 +268,6 @@ export const processDeploymentStatusDetailsData = ( PHYSICAL_ENV_DEPLOYMENT_TIMELINE_ORDER.forEach((timelineStatusType, index) => { const element = findRight(data.timelines, getPredicate(timelineStatusType)) - const timelineData = deploymentData.deploymentStatusBreakdown[timelineStatusType] if (!element) { @@ -265,6 +277,7 @@ export const processDeploymentStatusDetailsData = ( timelineData.displaySubText = 'Waiting' } + // We don't even need to clean this in final loop since deployment status won't be in progress if next timeline is progressing if (isProgressing && timelineStatusType === TIMELINE_STATUS.KUBECTL_APPLY) { timelineData.subSteps = [ { icon: '', message: 'Waiting to be started by Argo CD' }, @@ -273,9 +286,13 @@ export const processDeploymentStatusDetailsData = ( timelineData.isCollapsed = false } - if (deploymentStatus === DEPLOYMENT_STATUS.FAILED) { - timelineData.displaySubText = '' - timelineData.icon = 'unreachable' + if (hasDeploymentFailed) { + const hasCurrentTimelineFailed = + timelineStatusType === TIMELINE_STATUS.APP_HEALTH && + deploymentData.deploymentStatusBreakdown.KUBECTL_APPLY.icon === 'success' + + timelineData.displaySubText = hasCurrentTimelineFailed ? 'Failed' : '' + timelineData.icon = hasCurrentTimelineFailed ? 'failed' : 'unreachable' } if ( @@ -291,7 +308,6 @@ export const processDeploymentStatusDetailsData = ( statusFetchCount: data?.statusFetchCount, }) } - return } @@ -327,18 +343,8 @@ export const processDeploymentStatusDetailsData = ( case TIMELINE_STATUS.APP_HEALTH: timelineData.time = element.statusTime - if (element.status === TIMELINE_STATUS.DEPLOYMENT_FAILED) { - // TODO: Check why its icon is not failed in earlier implementation - timelineData.icon = 'failed' - timelineData.displaySubText = 'Failed' - timelineData.timelineStatus = element.statusDetail - break - } - - if (element.status === TIMELINE_STATUS.HEALTHY || element.status === TIMELINE_STATUS.DEGRADED) { - timelineData.icon = 'success' - timelineData.displaySubText = element.status === TIMELINE_STATUS.HEALTHY ? '' : 'Degraded' - } + timelineData.icon = 'success' + timelineData.displaySubText = element.status === TIMELINE_STATUS.HEALTHY ? '' : 'Degraded' break default: @@ -350,6 +356,10 @@ export const processDeploymentStatusDetailsData = ( const nextTimelineStatus = PHYSICAL_ENV_DEPLOYMENT_TIMELINE_ORDER[index + 1] const nextTimeline = deploymentData.deploymentStatusBreakdown[nextTimelineStatus] + if (deploymentData.errorBarConfig) { + deploymentData.errorBarConfig.nextTimelineToProcess = nextTimelineStatus + } + nextTimeline.icon = 'inprogress' nextTimeline.displaySubText = 'In progress' } diff --git a/src/Shared/types.ts b/src/Shared/types.ts index a0fed9ca1..f5efd13e8 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -1274,6 +1274,10 @@ export interface DeploymentStatusDetailsBreakdownDataType { deploymentEndTime: string triggeredBy: string deploymentStatusBreakdown: Partial> + errorBarConfig?: { + deploymentErrorMessage: string + nextTimelineToProcess: DeploymentStatusTimelineType + } | null } export interface IntelligenceConfig { From 115493759e4c6df1c366b1bbc7dc8b756e323de7 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Thu, 15 May 2025 00:13:24 +0530 Subject: [PATCH 138/157] chore: update version to 1.13.0-beta-3 in package.json and package-lock.json; enhance deployment status details processing --- package-lock.json | 4 ++-- package.json | 2 +- src/Shared/Components/DeploymentStatusBreakdown/utils.tsx | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0e680605..4b9aa5289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-2", + "version": "1.13.0-beta-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-2", + "version": "1.13.0-beta-3", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 8c8914749..92ebdd115 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-2", + "version": "1.13.0-beta-3", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index 1bcbfb2b7..9b29f1d5e 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -376,6 +376,8 @@ export const processDeploymentStatusDetailsData = ( const prevTimelineData = deploymentData.deploymentStatusBreakdown[prevTimelineStatusType] prevTimelineData.icon = 'success' prevTimelineData.displaySubText = '' + prevTimelineData.isCollapsed = false + prevTimelineData.timelineStatus = '' } break } From 6eaf709dd94c209317dbb3f4a5dd969c29fe0e47 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Thu, 15 May 2025 00:28:39 +0530 Subject: [PATCH 139/157] chore: update version to 1.13.0-beta-4 in package.json and package-lock.json; modify deployment status collapse behavior --- package-lock.json | 4 ++-- package.json | 2 +- src/Shared/Components/DeploymentStatusBreakdown/utils.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b9aa5289..648fad28c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-3", + "version": "1.13.0-beta-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-3", + "version": "1.13.0-beta-4", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 92ebdd115..ffbc7f943 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-3", + "version": "1.13.0-beta-4", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index 9b29f1d5e..df0ce16fc 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -376,7 +376,7 @@ export const processDeploymentStatusDetailsData = ( const prevTimelineData = deploymentData.deploymentStatusBreakdown[prevTimelineStatusType] prevTimelineData.icon = 'success' prevTimelineData.displaySubText = '' - prevTimelineData.isCollapsed = false + prevTimelineData.isCollapsed = true prevTimelineData.timelineStatus = '' } break From 7868287f0d30d47ac3b93d3fe39b11e21b33af62 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Thu, 15 May 2025 07:37:18 +0530 Subject: [PATCH 140/157] chore: update version to 1.13.0-beta-5 in package.json and package-lock.json; set ARGOCD_SYNC collapse state to true --- package-lock.json | 4 ++-- package.json | 2 +- src/Shared/Components/DeploymentStatusBreakdown/utils.tsx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 648fad28c..1ddf4e19e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-4", + "version": "1.13.0-beta-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-4", + "version": "1.13.0-beta-5", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index ffbc7f943..0d86976fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-beta-4", + "version": "1.13.0-beta-5", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx index df0ce16fc..89a232fe0 100644 --- a/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx +++ b/src/Shared/Components/DeploymentStatusBreakdown/utils.tsx @@ -335,6 +335,7 @@ export const processDeploymentStatusDetailsData = ( deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.icon = 'success' deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.displaySubText = '' deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.time = element.statusTime + deploymentData.deploymentStatusBreakdown.ARGOCD_SYNC.isCollapsed = true } processKubeCTLApply(timelineData, element, deploymentStatus, data) From 05c985c3144a9c848915482097864aa52ddc3d9c Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Thu, 15 May 2025 16:46:29 +0530 Subject: [PATCH 141/157] feat: add logsRendererRef to LogStageAccordion and LogsRenderer components --- src/Shared/Components/CICDHistory/LogStageAccordion.tsx | 8 +++++++- src/Shared/Components/CICDHistory/LogsRenderer.tsx | 8 +++++++- src/Shared/Components/CICDHistory/types.tsx | 3 ++- .../TargetPlatforms/TargetPlatformListTooltip.tsx | 3 ++- src/Shared/Components/TargetPlatforms/types.ts | 6 +++--- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Shared/Components/CICDHistory/LogStageAccordion.tsx b/src/Shared/Components/CICDHistory/LogStageAccordion.tsx index aa21f4c34..cfb9577d9 100644 --- a/src/Shared/Components/CICDHistory/LogStageAccordion.tsx +++ b/src/Shared/Components/CICDHistory/LogStageAccordion.tsx @@ -43,6 +43,7 @@ const LogStageAccordion = ({ fullScreenView, searchIndex, targetPlatforms, + logsRendererRef, }: LogStageAccordionProps) => { const handleAccordionToggle = () => { if (isOpen) { @@ -78,6 +79,8 @@ const LogStageAccordion = ({ } } + const getLogsRendererReference = () => logsRendererRef.current + return (