)}
diff --git a/src/Common/GenericDescription/types.ts b/src/Common/GenericDescription/types.ts
index ead6ad9db..802b5c9f7 100644
--- a/src/Common/GenericDescription/types.ts
+++ b/src/Common/GenericDescription/types.ts
@@ -18,11 +18,14 @@ export interface GenericDescriptionProps {
text?: string
updatedBy?: string
updatedOn?: string
- isDescriptionPreview: boolean
updateDescription: (string) => Promise
title: string
tabIndex?: number
minEditorHeight?: number
+ emptyStateConfig?: {
+ img: string
+ subtitle: JSX.Element
+ }
}
export enum MDEditorSelectedTabType {
diff --git a/src/Common/GenericDescription/utils.ts b/src/Common/GenericDescription/utils.ts
deleted file mode 100644
index 0f0ccd4ee..000000000
--- a/src/Common/GenericDescription/utils.ts
+++ /dev/null
@@ -1,29 +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 moment from 'moment'
-
-import { DATE_TIME_FORMATS, ZERO_TIME_STRING } from '@Common/Constants'
-
-export const getParsedUpdatedOnDate = (updatedOn: string) => {
- if (!updatedOn || updatedOn === ZERO_TIME_STRING) {
- return ''
- }
-
- const _moment = moment(updatedOn)
-
- return _moment.isValid() ? _moment.format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT) : updatedOn
-}
diff --git a/src/Common/GenericDescription/utils.tsx b/src/Common/GenericDescription/utils.tsx
new file mode 100644
index 000000000..b4e7f8c7e
--- /dev/null
+++ b/src/Common/GenericDescription/utils.tsx
@@ -0,0 +1,194 @@
+/*
+ * 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 Tippy from '@tippyjs/react'
+import moment from 'moment'
+
+import { ReactComponent as BoldIcon } from '@Icons/ic-bold.svg'
+import { ReactComponent as CheckedListIcon } from '@Icons/ic-checked-list.svg'
+import { ReactComponent as CodeIcon } from '@Icons/ic-code.svg'
+import { ReactComponent as HeaderIcon } from '@Icons/ic-header.svg'
+import { ReactComponent as ImageIcon } from '@Icons/ic-image.svg'
+import { ReactComponent as ItalicIcon } from '@Icons/ic-italic.svg'
+import { ReactComponent as LinkIcon } from '@Icons/ic-link.svg'
+import { ReactComponent as OrderedListIcon } from '@Icons/ic-ordered-list.svg'
+import { ReactComponent as QuoteIcon } from '@Icons/ic-quote.svg'
+import { ReactComponent as StrikethroughIcon } from '@Icons/ic-strikethrough.svg'
+import { ReactComponent as UnorderedListIcon } from '@Icons/ic-unordered-list.svg'
+import { DATE_TIME_FORMATS, ZERO_TIME_STRING } from '@Common/Constants'
+import { logExceptionToSentry } from '@Common/Helper'
+import { MARKDOWN_EDITOR_COMMAND_ICON_TIPPY_CONTENT, MARKDOWN_EDITOR_COMMAND_TITLE } from '@Common/Markdown/constant'
+
+export const getParsedUpdatedOnDate = (updatedOn: string) => {
+ if (!updatedOn || updatedOn === ZERO_TIME_STRING) {
+ return ''
+ }
+
+ const _moment = moment(updatedOn)
+
+ return _moment.isValid() ? _moment.format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT) : updatedOn
+}
+
+export const getEditorCustomIcon = (commandName: string): JSX.Element => {
+ switch (commandName) {
+ case MARKDOWN_EDITOR_COMMAND_TITLE.HEADER:
+ return (
+
+
diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx
index a2cd53503..daf66cdea 100644
--- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx
+++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx
@@ -99,6 +99,7 @@ const AppStatusModal = ({
}
: null,
isHelmApp: appDetails.appType === AppType.DEVTRON_HELM_CHART,
+ deploymentAppType: appDetails.deploymentAppType,
}),
deploymentStatusAbortControllerRef,
)
diff --git a/src/Shared/Components/AppStatusModal/service.ts b/src/Shared/Components/AppStatusModal/service.ts
index e862199d3..93bf49caf 100644
--- a/src/Shared/Components/AppStatusModal/service.ts
+++ b/src/Shared/Components/AppStatusModal/service.ts
@@ -51,6 +51,7 @@ export const getDeploymentStatusWithTimeline = async ({
showTimeline,
virtualEnvironmentConfig,
isHelmApp,
+ deploymentAppType,
}: GetDeploymentStatusWithTimelineParamsType): Promise => {
const baseURL = isHelmApp ? ROUTES.HELM_DEPLOYMENT_STATUS_TIMELINE_INSTALLED_APP : ROUTES.DEPLOYMENT_STATUS
@@ -68,5 +69,8 @@ export const getDeploymentStatusWithTimeline = async ({
return virtualEnvironmentConfig
? virtualEnvironmentConfig.processVirtualEnvironmentDeploymentData(deploymentStatusDetailsResponse.result)
- : processDeploymentStatusDetailsData(deploymentStatusDetailsResponse.result)
+ : processDeploymentStatusDetailsData(
+ deploymentStatusDetailsResponse.result.deploymentAppType ?? deploymentAppType,
+ deploymentStatusDetailsResponse.result,
+ )
}
diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts
index 6b86ada85..9671e06a7 100644
--- a/src/Shared/Components/AppStatusModal/types.ts
+++ b/src/Shared/Components/AppStatusModal/types.ts
@@ -1,6 +1,6 @@
import { FunctionComponent, PropsWithChildren, ReactNode } from 'react'
-import { APIOptions } from '@Common/Types'
+import { APIOptions, DeploymentAppTypes } from '@Common/Types'
import {
AppDetails,
ConfigDriftModalProps,
@@ -85,6 +85,7 @@ export type GetDeploymentStatusWithTimelineParamsType = Pick {
diff --git a/src/Shared/Components/Backdrop/Backdrop.tsx b/src/Shared/Components/Backdrop/Backdrop.tsx
index 7c0fb5e72..c130efd5e 100644
--- a/src/Shared/Components/Backdrop/Backdrop.tsx
+++ b/src/Shared/Components/Backdrop/Backdrop.tsx
@@ -14,19 +14,27 @@
* limitations under the License.
*/
-import { useEffect } from 'react'
+import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { motion } from 'framer-motion'
import { useRegisterShortcut } from '@Common/Hooks'
import { DEVTRON_BASE_MAIN_ID } from '@Shared/constants'
-import { preventBodyScroll, preventOutsideFocus } from '@Shared/Helpers'
+import { getUniqueId, preventBodyScroll, preventOutsideFocus } from '@Shared/Helpers'
import { BackdropProps } from './types'
+import { createPortalContainerAndAppendToDOM } from './utils'
-const Backdrop = ({ children, onEscape }: BackdropProps) => {
+const Backdrop = ({ children, onEscape, onClick, hasClearBackground = false, onBackdropMount }: BackdropProps) => {
+ // STATES
+ const [portalContainer, setPortalContainer] = useState(null)
+
+ // HOOKS
const { registerShortcut, unregisterShortcut } = useRegisterShortcut()
+ // REFS
+ const portalContainerIdRef = useRef(`backdrop-${getUniqueId()}`)
+
// useEffect on onEscape since onEscape might change based on conditions
useEffect(() => {
registerShortcut({ keys: ['Escape'], callback: onEscape })
@@ -37,6 +45,8 @@ const Backdrop = ({ children, onEscape }: BackdropProps) => {
}, [onEscape])
useEffect(() => {
+ const previousActiveElement = document.activeElement as HTMLElement
+
preventBodyScroll(true)
// Setting main as inert to that focus is trapped inside the new portal
preventOutsideFocus({ identifier: DEVTRON_BASE_MAIN_ID, preventFocus: true })
@@ -48,19 +58,68 @@ const Backdrop = ({ children, onEscape }: BackdropProps) => {
preventOutsideFocus({ identifier: DEVTRON_BASE_MAIN_ID, preventFocus: false })
preventOutsideFocus({ identifier: 'visible-modal', preventFocus: false })
preventOutsideFocus({ identifier: 'visible-modal-2', preventFocus: false })
+
+ previousActiveElement?.focus({ preventScroll: true })
}
}, [])
+ useEffect(() => {
+ onBackdropMount?.(!!portalContainer)
+ }, [portalContainer])
+
+ /**
+ * Manages a dedicated DOM node for rendering a portal backdrop.
+ *
+ * On mount:
+ * - Looks for an existing element with the specified `portalContainerId`
+ * - If not found, creates and appends a new wrapper element to the DOM
+ * - Sets the found or created element as the target for rendering the portal
+ *
+ * On unmount:
+ * - Removes the portal container element from the DOM *only* if this component created it
+ *
+ * Why this is needed:
+ * - Ensures each portal instance has an isolated container
+ * - Avoids duplicate DOM nodes or conflicts with other portals
+ * - Prevents memory leaks by cleaning up unused elements
+ * - Allows rendering outside the parent DOM hierarchy to avoid layout/z-index issues
+ */
+ useLayoutEffect(() => {
+ let element = document.getElementById(portalContainerIdRef.current)
+ let systemCreated = false
+
+ // If the portal container doesn't exist, create and append it to the DOM
+ if (!element) {
+ systemCreated = true
+ element = createPortalContainerAndAppendToDOM(portalContainerIdRef.current)
+ }
+
+ // Set the container element as the portal's render target
+ setPortalContainer(element)
+
+ return () => {
+ // Clean up only if we created the element
+ if (systemCreated && element?.parentNode) {
+ element.parentNode.removeChild(element)
+ }
+ }
+ }, [])
+
+ if (portalContainer === null) {
+ return null
+ }
+
return createPortal(
{children}
,
- document.getElementById('animated-dialog-backdrop'),
+ portalContainer,
)
}
diff --git a/src/Shared/Components/Backdrop/index.tsx b/src/Shared/Components/Backdrop/index.ts
similarity index 96%
rename from src/Shared/Components/Backdrop/index.tsx
rename to src/Shared/Components/Backdrop/index.ts
index 1eb94274c..7d972dae0 100644
--- a/src/Shared/Components/Backdrop/index.tsx
+++ b/src/Shared/Components/Backdrop/index.ts
@@ -15,3 +15,4 @@
*/
export { default as Backdrop } from './Backdrop'
+export * from './types'
diff --git a/src/Shared/Components/Backdrop/types.ts b/src/Shared/Components/Backdrop/types.ts
new file mode 100644
index 000000000..a2939c4c5
--- /dev/null
+++ b/src/Shared/Components/Backdrop/types.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { MouseEvent, ReactNode } from 'react'
+
+export interface BackdropProps {
+ /**
+ * The content to be rendered within the backdrop component.
+ */
+ children: ReactNode
+ /**
+ * Callback function that gets triggered when the Escape key is pressed. \
+ * Should be wrapped in useCallback to prevent unnecessary re-renders.
+ * @example
+ * const handleEscape = useCallback(() => {
+ * // Handle escape key press
+ * }, []);
+ */
+ onEscape: () => void
+ /**
+ * Callback function that gets triggered when the backdrop is clicked.
+ * Useful for dismissing modals or other overlay content.
+ * @param e - The mouse event object from the click interaction
+ */
+ onClick?: (e: MouseEvent) => void
+ /**
+ * Determines if the backdrop should be transparent.
+ * When true, the backdrop will not have any background color or blur filter.
+ * @default false
+ */
+ hasClearBackground?: boolean
+ /**
+ * Callback function that gets triggered when the backdrop component mounts or unmounts.
+ * This can be used to perform side effects or state updates when the backdrop's visibility changes.
+ * @param isMounted - A boolean indicating whether the backdrop is currently mounted (true) or not (false)
+ */
+ onBackdropMount?: (isMounted: boolean) => void
+}
diff --git a/src/Shared/Components/Backdrop/types.tsx b/src/Shared/Components/Backdrop/types.tsx
deleted file mode 100644
index 0a5d956f9..000000000
--- a/src/Shared/Components/Backdrop/types.tsx
+++ /dev/null
@@ -1,25 +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 { ReactNode } from 'react'
-
-export interface BackdropProps {
- children: ReactNode
- /**
- * @param onEscape: please wrap in a useCallback, with respective dependencies or []
- */
- onEscape: () => void
-}
diff --git a/src/Shared/Components/Backdrop/utils.ts b/src/Shared/Components/Backdrop/utils.ts
new file mode 100644
index 000000000..033984d0c
--- /dev/null
+++ b/src/Shared/Components/Backdrop/utils.ts
@@ -0,0 +1,14 @@
+import { DEVTRON_BASE_MAIN_ID } from '@Shared/constants'
+
+export const createPortalContainerAndAppendToDOM = (portalContainerId: string) => {
+ const devtronBaseMainElement = document.getElementById(DEVTRON_BASE_MAIN_ID)
+ if (!devtronBaseMainElement) {
+ return null
+ }
+
+ const portalContainer = document.createElement('div')
+ portalContainer.setAttribute('id', portalContainerId)
+ devtronBaseMainElement.parentElement.appendChild(portalContainer)
+
+ return portalContainer
+}
diff --git a/src/Shared/Components/Badge/Badge.tsx b/src/Shared/Components/Badge/Badge.tsx
new file mode 100644
index 000000000..f3128c865
--- /dev/null
+++ b/src/Shared/Components/Badge/Badge.tsx
@@ -0,0 +1,38 @@
+import { ComponentSizeType } from '@Shared/constants'
+
+import { Icon } from '../Icon'
+import { BadgeProps } from './types'
+import { getClassNameAccToSize, getClassNameAccToVariant } from './utils'
+
+const Badge = ({
+ label,
+ bgColor,
+ fontColor,
+ endIconProps,
+ startIconProps,
+ variant = 'default',
+ size = ComponentSizeType.xs,
+}: BadgeProps) => {
+ const { styles, iconColor } = getClassNameAccToVariant(variant)
+ const iconSize = size === ComponentSizeType.xs ? 20 : 16
+
+ return (
+