Repository: devtron-labs/dashboard
Pull request #2680: feat: banner incompatibility support added
Author: login: shivani170
Labels: name: PR:Issue-verification-failed
description: PR:Issue-verification-failed
Created: 2025-04-29T13:14:52Z | Merged: 2025-05-05T13:42:23Z

# Description
Incompatibility support added

Fixes # (issue)

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# Checklist:

* [x] The title of the PR states what changed and the related issues number (used for the release note).
* [ ] Does this PR require documentation updates?
* [ ] I've updated documentation as required by this PR.
* [x] I have performed a self-review of my own code
* [x] I have commented my code, particularly in hard-to-understand areas





Diff:
diff --git a/src/App.tsx b/src/App.tsx
index aa23573437..5fe028d80b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -28,6 +28,7 @@ import {
     useUserEmail,
 } from '@devtron-labs/devtron-fe-common-lib'
 
+import { useVersionUpdateReload } from '@Components/common/hooks/useVersionUpdate'
 import ActivateLicense from '@Pages/License/ActivateLicense'
 
 import { ErrorBoundary, getApprovalModalTypeFromURL, importComponentFromFELibrary } from './components/common'
@@ -45,12 +46,18 @@ const App = () => {
     const [validating, setValidating] = useState(true)
     const [approvalToken, setApprovalToken] = useState<string>('')
     const [approvalType, setApprovalType] = useState<APPROVAL_MODAL_TYPE>(APPROVAL_MODAL_TYPE.CONFIG)
+    const [showVersionUpdateToast, setShowVersionUpdateToast] = useState(true)
 
     const { setEmail } = useUserEmail()
 
     const location = useLocation()
     const { push } = useHistory()
 
+    const { bgUpdated, handleAppUpdate, doesNeedRefresh, updateServiceWorker, handleControllerChange, updateToastRef } =
+        useVersionUpdateReload({
+            showVersionUpdateToast,
+        })
+
     const isDirectApprovalNotification =
         location.pathname &&
         location.pathname.includes('approve') &&
@@ -78,6 +85,10 @@ const App = () => {
         }
     }
 
+    const hideVersionUpdateToast = () => {
+        setShowVersionUpdateToast(false)
+    }
+
     async function validation() {
         try {
             const {
@@ -119,6 +130,15 @@ const App = () => {
         }
     }, [])
 
+    const reloadVersionConfig = {
+        handleAppUpdate,
+        doesNeedRefresh,
+        updateServiceWorker,
+        handleControllerChange,
+        bgUpdated,
+        updateToastRef,
+    }
+
     const renderRoutesWithErrorBoundary = () =>
         errorPage ? (
             <div className="full-height-width bg__tertiary">
@@ -137,7 +157,15 @@ const App = () => {
                             <ActivateLicense />
                         </Route>
                         {!window._env_.K8S_CLIENT && <Route path={URLS.LOGIN} component={Login} />}
-                        <Route path="/" render={() => <NavigationRoutes />} />
+                        <Route
+                            path="/"
+                            render={() => (
+                                <NavigationRoutes
+                                    reloadVersionConfig={reloadVersionConfig}
+                                    hideVersionUpdateToast={hideVersionUpdateToast}
+                                />
+                            )}
+                        />
                         <Redirect to={window._env_.K8S_CLIENT ? '/' : `${URLS.LOGIN_SSO}${location.search}`} />
                     </Switch>
                     <div id="visible-modal" />
diff --git a/src/components/app/details/triggerView/cdMaterial.tsx b/src/components/app/details/triggerView/cdMaterial.tsx
index d222f65260..d9df03f71b 100644
--- a/src/components/app/details/triggerView/cdMaterial.tsx
+++ b/src/components/app/details/triggerView/cdMaterial.tsx
@@ -124,7 +124,6 @@ import { URLS } from '../../../../config'
 import { PipelineConfigDiff } from './PipelineConfigDiff'
 import { usePipelineDeploymentConfig } from './PipelineConfigDiff/usePipelineDeploymentConfig'
 import { PipelineConfigDiffStatusTile } from './PipelineConfigDiff/PipelineConfigDiffStatusTile'
-import { Banner } from '@Components/common/Banner/Banner'
 
 const ApprovalInfoTippy = importComponentFromFELibrary('ApprovalInfoTippy')
 const ExpireApproval = importComponentFromFELibrary('ExpireApproval')
diff --git a/src/components/common/Banner/Banner.tsx b/src/components/common/Banner/Banner.tsx
index 4f456c89ed..970f6fca3d 100644
--- a/src/components/common/Banner/Banner.tsx
+++ b/src/components/common/Banner/Banner.tsx
@@ -14,23 +14,30 @@
  * limitations under the License.
  */
 
-import { useEffect, useRef, useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
 
 import {
+    AnimatePresence,
     Button,
     ButtonStyleType,
     ButtonVariantType,
     ComponentSizeType,
     Icon,
     InfoBlockVariant,
+    MotionDiv,
+    noop,
     setActionWithExpiry,
     useMainContext,
 } from '@devtron-labs/devtron-fe-common-lib'
 
-import { importComponentFromFELibrary, useOnline } from '../helpers/Helpers'
+import { InstallationType } from '@Components/v2/devtronStackManager/DevtronStackManager.type'
+
+import { importComponentFromFELibrary } from '../helpers/Helpers'
 import { InteractiveCellText } from '../helpers/InteractiveCellText/InteractiveCellText'
-import { ANNOUNCEMENT_CONFIG, BannerVariant, ONLINE_BANNER_TIMEOUT } from './constants'
-import { useVersionUpdateReload } from './useVersionUpdateReload'
+import { useOnline } from '../hooks'
+import { ONLINE_BANNER_TIMEOUT } from '../hooks/constants'
+import { ANNOUNCEMENT_CONFIG, BannerVariant } from './constants'
+import { BannerTypes } from './types'
 import {
     getBannerConfig,
     getBannerIcon,
@@ -41,45 +48,78 @@ import {
 
 import './banner.scss'
 
-const useEnterpriseLicenseConfig = importComponentFromFELibrary('useEnterpriseLicenseConfig', null, 'function')
+const isFELibAvailable = importComponentFromFELibrary('isFELibAvailable', null, 'function')
+const useEnterpriseLicenseConfig = importComponentFromFELibrary('useEnterpriseLicenseConfig', noop, 'function')
+
+const bannerVariants = {
+    enter: {
+        y: -40,
+        opacity: 0,
+        height: 0,
+    },
+    center: {
+        y: 0,
+        opacity: 1,
+        height: 'auto',
+    },
+    exitUp: (isBannerReplaced: boolean) =>
+        !isBannerReplaced
+            ? {
+                  y: -40,
+                  opacity: 0,
+                  height: 0,
+              }
+            : {
+                  y: 40,
+                  opacity: 0,
+                  height: 0,
+              },
+}
 
-export const Banner = () => {
-    const didMountRef = useRef(false)
-    const isOnline = useOnline()
-    const { isAirgapped } = useMainContext()
+export const Banner = ({ hideVersionUpdateToast }: BannerTypes) => {
+    const { isAirgapped, currentServerInfo, reloadVersionConfig } = useMainContext()
+    const { doesNeedRefresh, handleAppUpdate, bgUpdated } = reloadVersionConfig
+    const licenseConfig = useEnterpriseLicenseConfig()
 
     const [showOnlineBanner, setShowOnlineBanner] = useState(false)
     const [showAnnouncementBanner, setShowAnnouncementBanner] = useState(
         ANNOUNCEMENT_CONFIG.message ? shouldShowAnnouncementBanner() : false,
     )
-    const { bgUpdated, doesNeedRefresh, handleAppUpdate } = useVersionUpdateReload()
 
-    const enterpriseLicenseConfig = useEnterpriseLicenseConfig
+    const onlineTimer = useRef<ReturnType<typeof setTimeout>>(null)
 
-    const licenseConfig = enterpriseLicenseConfig ? enterpriseLicenseConfig() : null
+    const onOnline = useCallback(() => {
+        setShowOnlineBanner(true)
+
+        // Clear any existing timer before setting a new one
+        if (onlineTimer.current) {
+            clearTimeout(onlineTimer.current)
+        }
+
+        onlineTimer.current = setTimeout(() => setShowOnlineBanner(false), ONLINE_BANNER_TIMEOUT)
+    }, [])
+
+    const isOnline = useOnline({ onOnline })
 
     useEffect(() => {
-        let timer: NodeJS.Timeout
-        if (didMountRef.current) {
-            if (isOnline) {
-                setShowOnlineBanner(true)
-                // Auto-hide online banner after timeout
-                timer = setTimeout(() => setShowOnlineBanner(false), ONLINE_BANNER_TIMEOUT)
+        hideVersionUpdateToast()
+        return () => {
+            if (onlineTimer.current) {
+                clearTimeout(onlineTimer.current)
             }
-        } else {
-            didMountRef.current = true
-            setShowOnlineBanner(false)
         }
-        return () => clearTimeout(timer)
-    }, [isOnline])
+    }, [])
 
-    const onClickCloseAnnouncementBanner = () => {
-        setShowAnnouncementBanner(false)
-        if (typeof Storage !== 'undefined') {
-            setActionWithExpiry('expiryDateOfHidingAnnouncementBanner', 1)
+    const getIncompatibleMicroserviceName = (): 'frontend' | 'backend' | null => {
+        const { serverInfo } = currentServerInfo
+        if (serverInfo) {
+            if (serverInfo.installationType !== InstallationType.ENTERPRISE && isFELibAvailable) return 'backend'
+            if (serverInfo.installationType === InstallationType.ENTERPRISE && !isFELibAvailable) return 'frontend'
         }
+        return null
     }
 
+    const incompatibleService = getIncompatibleMicroserviceName()
     const {
         message: enterpriseLicenseBarMessage = '',
         type: licenseType = InfoBlockVariant.HELP,
@@ -87,71 +127,95 @@ export const Banner = () => {
         handleOpenLicenseDialog,
     } = licenseConfig ?? {}
 
-    const getCurrentBanner = (): BannerVariant => {
-        if (!isOnline) return BannerVariant.INTERNET_CONNECTIVITY
-        if (showOnlineBanner) return BannerVariant.INTERNET_CONNECTIVITY
+    const getCurrentBanner = (): BannerVariant | null => {
+        if ((!isOnline || showOnlineBanner) && !isAirgapped) {
+            if (!isOnline) {
+                return BannerVariant.OFFLINE
+            }
+
+            if (showOnlineBanner) {
+                return BannerVariant.ONLINE
+            }
+        }
         if (doesNeedRefresh || bgUpdated) return BannerVariant.VERSION_UPDATE
+        if (incompatibleService) return BannerVariant.INCOMPATIBLE_MICROSERVICES
         if (showAnnouncementBanner) return BannerVariant.ANNOUNCEMENT
         if (licenseConfig?.message) return BannerVariant.LICENSE
         return null
     }
-
-    const bannerVariant: BannerVariant = getCurrentBanner()
-
-    if (!bannerVariant) return null
+    const bannerVariant = getCurrentBanner()
 
     const config = getBannerConfig({
         bannerVariant,
-        isOnline,
         licenseType,
         enterpriseLicenseBarMessage,
-        hideInternetConnectivityBar: isAirgapped,
+        microservice: incompatibleService,
     })
 
-    if (!config) return null
-
     const actionButtons = getButtonConfig(bannerVariant, handleOpenLicenseDialog, handleAppUpdate)
-    const baseClassName = `w-100 ${config.rootClassName || ''} ${getBannerTextColor(bannerVariant)} ${config.isDismissible ? 'dc__grid banner-row' : 'flex'}`
-    const isOffline = !(isOnline && bannerVariant === BannerVariant.INTERNET_CONNECTIVITY)
+    const baseClassName = `w-100 ${config?.rootClassName || ''} ${getBannerTextColor(bannerVariant)} ${config?.isDismissible ? 'dc__grid banner--row dc__column-gap-16' : 'flex'}`
 
     const shouldShowActionButton = () => {
-        if (bannerVariant === BannerVariant.INTERNET_CONNECTIVITY) {
-            return isOffline
-        }
-        if (bannerVariant === BannerVariant.ANNOUNCEMENT) {
-            return !!ANNOUNCEMENT_CONFIG.buttonLink
-        }
-        if (bannerVariant === BannerVariant.VERSION_UPDATE) {
+        if (bannerVariant === BannerVariant.OFFLINE) return true
+        if (bannerVariant === BannerVariant.ANNOUNCEMENT) return !!ANNOUNCEMENT_CONFIG.buttonLink
+        if (
+            bannerVariant === BannerVariant.VERSION_UPDATE ||
+            bannerVariant === BannerVariant.INCOMPATIBLE_MICROSERVICES
+        )
             return true
-        }
         return false
     }
 
+    const onClickCloseAnnouncementBanner = () => {
+        if (typeof Storage !== 'undefined' && bannerVariant === BannerVariant.ANNOUNCEMENT) {
+            setActionWithExpiry('expiryDateOfHidingAnnouncementBanner', 1)
+        }
+        setShowAnnouncementBanner(false)
+    }
+
     return (
-        <div className={baseClassName}>
-            {config.isDismissible && <div className="icon-dim-28" />}
-            <div className="py-4 flex dc__gap-8 dc__align-center pr-16">
-                {isOffline && getBannerIcon(bannerVariant, ANNOUNCEMENT_CONFIG.type, iconName)}
-                <InteractiveCellText text={config.text} rootClassName="fs-12 fw-5" />
-
-                {shouldShowActionButton() && (
-                    <div className="dc__no-shrink">
-                        <Button {...actionButtons} />
+        <AnimatePresence custom={!!bannerVariant}>
+            {bannerVariant && config && (
+                <MotionDiv
+                    layout
+                    key={bannerVariant}
+                    variants={bannerVariants}
+                    initial="enter"
+                    animate="center"
+                    exit="exitUp"
+                    custom={!!bannerVariant}
+                    transition={{
+                        duration: 0.3,
+                        ease: 'easeOut',
+                    }}
+                    className={baseClassName}
+                >
+                    {config.isDismissible && <div className="icon-dim-28 dc__no-shrink" />}
+                    <div className="py-4 flex dc__gap-12 dc__align-items-center">
+                        <div className="flex dc__gap-8">
+                            {bannerVariant !== BannerVariant.ONLINE && getBannerIcon(bannerVariant, iconName)}
+                            <InteractiveCellText text={config.text} rootClassName="fw-5" fontSize={12} interactive />
+                        </div>
+                        {shouldShowActionButton() && (
+                            <div className="dc__no-shrink">
+                                <Button {...actionButtons} />
+                            </div>
+                        )}
                     </div>
-                )}
-            </div>
-            {config.isDismissible && (
-                <Button
-                    icon={<Icon name="ic-close-small" color="N700" />}
-                    variant={ButtonVariantType.borderLess}
-                    style={ButtonStyleType.negativeGrey}
-                    size={ComponentSizeType.small}
-                    onClick={onClickCloseAnnouncementBanner}
-                    dataTestId="banner-dismiss-button"
-                    ariaLabel="Close banner"
-                    showAriaLabelInTippy={false}
-                />
+                    {config.isDismissible && (
+                        <Button
+                            icon={<Icon name="ic-close-small" color="N700" />}
+                            variant={ButtonVariantType.borderLess}
+                            style={ButtonStyleType.negativeGrey}
+                            size={ComponentSizeType.small}
+                            onClick={onClickCloseAnnouncementBanner}
+                            dataTestId="banner-dismiss-button"
+                            ariaLabel="Close banner"
+                            showAriaLabelInTippy={false}
+                        />
+                    )}
+                </MotionDiv>
             )}
-        </div>
+        </AnimatePresence>
     )
 }
diff --git a/src/components/common/Banner/banner.scss b/src/components/common/Banner/banner.scss
index bea3670bc8..1fda42439a 100644
--- a/src/components/common/Banner/banner.scss
+++ b/src/components/common/Banner/banner.scss
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
- .banner-row {
-    grid-template-columns: 28px 1fr 28px;
-}
 
-.banner__version-update {
-    background: linear-gradient(92deg, #4C40BF 0%, #4084BF 72.45%, #8529A3 96.63%);
+.banner {
+    &--row {
+        grid-template-columns: 28px 1fr 28px;
+    }
+
+    &__version-update {
+        background: linear-gradient(92deg, #4C40BF 0%, #4084BF 72.45%, #8529A3 96.63%);
+    }
 }
\ No newline at end of file
diff --git a/src/components/common/Banner/constants.ts b/src/components/common/Banner/constants.ts
index 9d40090c71..71ca67c3fc 100644
--- a/src/components/common/Banner/constants.ts
+++ b/src/components/common/Banner/constants.ts
@@ -20,10 +20,12 @@ import { AnnouncementConfigTypes } from './types'
 import { getValidAnnouncementType } from './utils'
 
 export enum BannerVariant {
-    INTERNET_CONNECTIVITY = 'INTERNET_CONNECTIVITY',
     VERSION_UPDATE = 'VERSION_UPDATE',
     ANNOUNCEMENT = 'ANNOUNCEMENT',
     LICENSE = 'LICENSE',
+    INCOMPATIBLE_MICROSERVICES = 'INCOMPATIBLE_MICROSERVICES',
+    OFFLINE = 'OFFLINE',
+    ONLINE = 'ONLINE',
 }
 
 export const ANNOUNCEMENT_CONFIG: AnnouncementConfigTypes = {
@@ -34,5 +36,3 @@ export const ANNOUNCEMENT_CONFIG: AnnouncementConfigTypes = {
     buttonText: window._env_.ANNOUNCEMENT_BANNER_BUTTON_TEXT,
     buttonLink: window._env_.ANNOUNCEMENT_BANNER_BUTTON_LINK,
 }
-
-export const ONLINE_BANNER_TIMEOUT = 3000 // online banner timeout
diff --git a/src/components/common/Banner/types.ts b/src/components/common/Banner/types.ts
index 940702bd1d..71d7ec21d2 100644
--- a/src/components/common/Banner/types.ts
+++ b/src/components/common/Banner/types.ts
@@ -16,27 +16,22 @@
 
 import { InfoBlockProps } from '@devtron-labs/devtron-fe-common-lib'
 
+import { NavigationRoutesTypes } from '../navigation/types'
 import { BannerVariant } from './constants'
 
-export interface BannerType {
-    isDismissible?: boolean
-    showAnnouncementBannerOnly?: boolean
-}
 export interface BannerConfigProps {
     bannerVariant: Partial<BannerVariant>
-    isOnline: boolean
     licenseType?: InfoBlockProps['variant']
     enterpriseLicenseBarMessage?: string
     hideInternetConnectivityBar?: boolean
+    microservice: 'frontend' | 'backend' | null
     isDismissible?: boolean
-    showAnnouncementBannerOnly?: boolean
 }
 
-export interface BannerConfigType {
+export interface BannerConfigType extends Pick<BannerConfigProps, 'isDismissible'> {
     text: string
     rootClassName: string
     icon?: string
-    isDismissible?: boolean
 }
 
 export interface AnnouncementConfigTypes {
@@ -45,3 +40,5 @@ export interface AnnouncementConfigTypes {
     buttonText?: string
     buttonLink?: string
 }
+
+export interface BannerTypes extends Pick<NavigationRoutesTypes, 'hideVersionUpdateToast'> {}
diff --git a/src/components/common/Banner/utils.tsx b/src/components/common/Banner/utils.tsx
index 9af492c0e8..3302be6cdc 100644
--- a/src/components/common/Banner/utils.tsx
+++ b/src/components/common/Banner/utils.tsx
@@ -20,10 +20,11 @@ import {
     ButtonStyleType,
     ButtonVariantType,
     ComponentSizeType,
+    CONTACT_SUPPORT_LINK,
     getDateInMilliseconds,
     Icon,
+    IconBaseColorType,
     IconsProps,
-    InfoBlockProps,
     InfoBlockVariant,
     InfoBlockVariantType,
     refresh,
@@ -34,71 +35,90 @@ import {
 import { ANNOUNCEMENT_CONFIG, BannerVariant } from './constants'
 import { BannerConfigProps, BannerConfigType } from './types'
 
-export const getBannerIcon = (
-    bannerVariant: BannerVariant,
-    type: InfoBlockProps['variant'],
-    iconName: IconsProps['name'],
-) => {
-    const variantWithIconMap: Record<BannerVariant, IconsProps['name'] | null> = {
-        [BannerVariant.INTERNET_CONNECTIVITY]: 'ic-disconnect',
-        [BannerVariant.VERSION_UPDATE]: 'ic-sparkle-color',
-        [BannerVariant.LICENSE]: iconName,
-        [BannerVariant.ANNOUNCEMENT]: 'ic-megaphone-left',
-    }
+export const getValidAnnouncementType = (type): type is InfoBlockVariantType =>
+    Object.values(InfoBlockVariant).includes(type)
 
-    return (
-        <Icon
-            name={variantWithIconMap[bannerVariant]}
-            color={bannerVariant === BannerVariant.ANNOUNCEMENT ? VARIANT_TO_ICON_COLOR_MAP[type] : null}
-            size={16}
-        />
-    )
-}
+export const AnnouncementBannerType = getValidAnnouncementType(window._env_.ANNOUNCEMENT_BANNER_TYPE)
+    ? window._env_.ANNOUNCEMENT_BANNER_TYPE
+    : InfoBlockVariant.HELP
+
+const getVariantWithIconMap = (iconName: IconsProps['name']): Partial<Record<BannerVariant, IconsProps['name']>> => ({
+    [BannerVariant.OFFLINE]: 'ic-disconnect',
+    [BannerVariant.VERSION_UPDATE]: 'ic-sparkle-color',
+    [BannerVariant.INCOMPATIBLE_MICROSERVICES]: 'ic-info-outline',
+    [BannerVariant.LICENSE]: iconName,
+    [BannerVariant.ANNOUNCEMENT]: 'ic-megaphone-left',
+})
+
+const getVariantWithIconColorMap = (): Record<BannerVariant, IconBaseColorType | null> => ({
+    [BannerVariant.OFFLINE]: null,
+    [BannerVariant.ONLINE]: null,
+    [BannerVariant.VERSION_UPDATE]: null,
+    [BannerVariant.INCOMPATIBLE_MICROSERVICES]: 'N0',
+    [BannerVariant.LICENSE]: null,
+    [BannerVariant.ANNOUNCEMENT]: VARIANT_TO_ICON_COLOR_MAP[AnnouncementBannerType],
+})
+export const getBannerIcon = (bannerVariant: BannerVariant, iconName: IconsProps['name']) => (
+    <Icon
+        name={getVariantWithIconMap(iconName)[bannerVariant]}
+        color={getVariantWithIconColorMap()[bannerVariant]}
+        size={16}
+    />
+)
 
 export const getBannerConfig = ({
     bannerVariant,
-    isOnline,
     licenseType,
     enterpriseLicenseBarMessage = '',
-    hideInternetConnectivityBar = false,
-}: BannerConfigProps): BannerConfigType => {
-    const bannerConfigMap: Partial<Record<BannerVariant, BannerConfigType>> = {
-        ...(!hideInternetConnectivityBar
-            ? {
-                  [BannerVariant.INTERNET_CONNECTIVITY]: isOnline
-                      ? {
-                            text: 'You’re back online!',
-                            rootClassName: 'bcg-5',
-                        }
-                      : {
-                            text: 'You’re offline! Please check your internet connection.',
-                            icon: 'ic-disconnected',
-                            rootClassName: 'bcr-5',
-                        },
-              }
-            : {}),
-
-        [BannerVariant.VERSION_UPDATE]: {
-            text: 'A new version is available. Please refresh the page to get the latest features.',
-            icon: 'ic-sparkle-color',
-            rootClassName: 'banner__version-update',
-        },
-
-        [BannerVariant.ANNOUNCEMENT]: {
-            text: ANNOUNCEMENT_CONFIG.message,
-            icon: 'ic-megaphone-left',
-            rootClassName: VARIANT_TO_BG_MAP[ANNOUNCEMENT_CONFIG.type],
-            isDismissible: true,
-        },
-
-        [BannerVariant.LICENSE]: {
-            text: enterpriseLicenseBarMessage,
-            icon: licenseType,
-            rootClassName: VARIANT_TO_BG_MAP[licenseType],
-        },
-    }
+    microservice,
+}: BannerConfigProps): BannerConfigType | null => {
+    switch (bannerVariant) {
+        case BannerVariant.ONLINE:
+            return {
+                text: 'You’re back online!',
+                icon: null,
+                rootClassName: 'bcg-5',
+            }
+
+        case BannerVariant.OFFLINE:
+            return {
+                text: 'You’re offline! Please check your internet connection.',
+                icon: 'ic-disconnected',
+                rootClassName: 'bcr-5',
+            }
+
+        case BannerVariant.VERSION_UPDATE:
+            return {
+                text: 'A new version is available.',
+                icon: 'ic-sparkle-color',
+                rootClassName: 'banner__version-update',
+            }
+
+        case BannerVariant.INCOMPATIBLE_MICROSERVICES:
+            return {
+                text: `Incompatible ${microservice} service detected.`,
+                icon: 'ic-info-outline',
+                rootClassName: 'bcr-5',
+            }
+
+        case BannerVariant.ANNOUNCEMENT:
+            return {
+                text: ANNOUNCEMENT_CONFIG.message,
+                icon: 'ic-megaphone-left',
+                rootClassName: VARIANT_TO_BG_MAP[ANNOUNCEMENT_CONFIG.type],
+                isDismissible: true,
+            }
+
+        case BannerVariant.LICENSE:
+            return {
+                text: enterpriseLicenseBarMessage,
+                icon: licenseType,
+                rootClassName: VARIANT_TO_BG_MAP[licenseType],
+            }
 
-    return bannerConfigMap[bannerVariant]
+        default:
+            return null
+    }
 }
 
 export const getButtonConfig = (
@@ -107,7 +127,7 @@ export const getButtonConfig = (
     handleAppUpdate: () => void,
 ): ButtonProps<ButtonComponentType> | null => {
     switch (bannerView) {
-        case BannerVariant.INTERNET_CONNECTIVITY:
+        case BannerVariant.OFFLINE:
             return {
                 text: 'Retry',
                 startIcon: <Icon name="ic-arrow-clockwise" color={null} />,
@@ -127,6 +147,21 @@ export const getButtonConfig = (
                 dataTestId: 'banner-version-update-reload-button',
                 style: ButtonStyleType.neutralWhite,
             }
+        case BannerVariant.INCOMPATIBLE_MICROSERVICES:
+            return {
+                startIcon: <Icon name="ic-chat-circle-dots" color={null} />,
+                text: 'Contact Support',
+                variant: ButtonVariantType.text,
+                size: ComponentSizeType.xxs,
+                dataTestId: 'banner-incompatible-reload-button',
+                style: ButtonStyleType.neutralN0,
+                component: ButtonComponentType.anchor,
+                anchorProps: {
+                    href: CONTACT_SUPPORT_LINK,
+                    target: '_blank',
+                    rel: 'noreferrer noopener',
+                },
+            }
         case BannerVariant.ANNOUNCEMENT:
             return {
                 text: ANNOUNCEMENT_CONFIG.buttonText || 'Learn more',
@@ -156,7 +191,9 @@ export const getButtonConfig = (
 
 export const getBannerTextColor = (bannerVariant: BannerVariant) => {
     switch (bannerVariant) {
-        case BannerVariant.INTERNET_CONNECTIVITY:
+        case BannerVariant.ONLINE:
+        case BannerVariant.OFFLINE:
+        case BannerVariant.INCOMPATIBLE_MICROSERVICES:
             return 'cn-0'
         case BannerVariant.VERSION_UPDATE:
             return 'text__white'
@@ -165,18 +202,8 @@ export const getBannerTextColor = (bannerVariant: BannerVariant) => {
     }
 }
 
-export const getValidAnnouncementType = (type): type is InfoBlockVariantType =>
-    [
-        InfoBlockVariant.SUCCESS,
-        InfoBlockVariant.WARNING,
-        InfoBlockVariant.ERROR,
-        InfoBlockVariant.INFORMATION,
-        InfoBlockVariant.HELP,
-    ].includes(type)
-
 export const shouldShowAnnouncementBanner = (): boolean => {
     const expiry = localStorage.getItem('expiryDateOfHidingAnnouncementBanner')
     const loginTime = localStorage.getItem('dashboardLoginTime')
-    if (!expiry) return true
-    return getDateInMilliseconds(loginTime) > getDateInMilliseconds(expiry)
+    return !expiry || getDateInMilliseconds(loginTime) > getDateInMilliseconds(expiry)
 }
diff --git a/src/components/common/helpers/Helpers.tsx b/src/components/common/helpers/Helpers.tsx
index 60ef268088..5d7e79c5c5 100644
--- a/src/components/common/helpers/Helpers.tsx
+++ b/src/components/common/helpers/Helpers.tsx
@@ -392,27 +392,6 @@ export function deepEqual(configA: any, configB: any): boolean {
     }
 }
 
-export function useOnline() {
-    const [online, setOnline] = useState(navigator.onLine)
-    useEffect(() => {
-        function handleOnline(e) {
-            setOnline(true)
-        }
-
-        function handleOffline(e) {
-            setOnline(false)
-        }
-        window.addEventListener('online', handleOnline)
-        window.addEventListener('offline', handleOffline)
-        return () => {
-            window.removeEventListener('online', handleOnline)
-            window.removeEventListener('offline', handleOffline)
-        }
-    }, [])
-
-    return online
-}
-
 /**
  * @deprecated
  */
@@ -1032,10 +1011,6 @@ export const parsePassword = (password: string): string => {
     return password === DEFAULT_SECRET_PLACEHOLDER ? '' : password.trim()
 }
 
-export const reloadLocation = () => {
-    window.location.reload()
-}
-
 /**
  * @deprecated
  */
diff --git a/src/components/common/helpers/InteractiveCellText/InteractiveCellText.tsx b/src/components/common/helpers/InteractiveCellText/InteractiveCellText.tsx
index 4c8d991950..e9e736f9ff 100644
--- a/src/components/common/helpers/InteractiveCellText/InteractiveCellText.tsx
+++ b/src/components/common/helpers/InteractiveCellText/InteractiveCellText.tsx
@@ -18,19 +18,29 @@ import { Tooltip } from '@devtron-labs/devtron-fe-common-lib'
 
 import { InteractiveCellTextProps } from './types'
 
-export const InteractiveCellText = ({ text, onClickHandler, dataTestId, rootClassName }: InteractiveCellTextProps) => (
-    <Tooltip content={text} placement="top" showOnTruncate={!!text} className="mxh-210 dc__overflow-auto">
+export const InteractiveCellText = ({
+    text,
+    onClickHandler,
+    dataTestId,
+    rootClassName,
+    interactive = false,
+    fontSize = 13,
+}: InteractiveCellTextProps) => (
+    <Tooltip content={text} placement="bottom" className="mxh-210 dc__overflow-auto" interactive={interactive}>
         {typeof onClickHandler === 'function' ? (
             <button
                 type="button"
                 onClick={onClickHandler}
-                className={`flex left dc__unset-button-styles lh-20 dc__truncate fs-13 cb-5 dc__no-decor cursor ${rootClassName}`}
+                className={`flex left dc__unset-button-styles lh-20 dc__truncate fs-13 cb-5 dc__no-decor cursor ${rootClassName} fs-${fontSize}`}
                 data-testid={dataTestId}
             >
                 {text || '-'}
             </button>
         ) : (
-            <p className={`lh-20 dc__truncate m-0 fs-13 ${rootClassName}`} data-testid={dataTestId}>
+            <p
+                className={`lh-20 dc__truncate m-0 dc__align-item-left ${rootClassName} fs-${fontSize}`}
+                data-testid={dataTestId}
+            >
                 {text || '-'}
             </p>
         )}
diff --git a/src/components/common/helpers/InteractiveCellText/types.ts b/src/components/common/helpers/InteractiveCellText/types.ts
index 65b67efdc1..5be06f3aa9 100644
--- a/src/components/common/helpers/InteractiveCellText/types.ts
+++ b/src/components/common/helpers/InteractiveCellText/types.ts
@@ -19,4 +19,6 @@ export interface InteractiveCellTextProps {
     onClickHandler?: () => void
     dataTestId?: string
     rootClassName?: string
+    interactive?: boolean
+    fontSize?: number
 }
diff --git a/src/components/common/hooks/constants.ts b/src/components/common/hooks/constants.ts
index 05740640f2..8c9776946d 100644
--- a/src/components/common/hooks/constants.ts
+++ b/src/components/common/hooks/constants.ts
@@ -31,3 +31,6 @@ export const FILE_READING_FAILED_STATUS = {
     },
     status: FileReaderStatus.FAILED,
 }
+
+export const ONLINE_BANNER_TIMEOUT = 3000
+export const INTERNET_CONNECTIVITY_INTERVAL = 30000
diff --git a/src/components/common/hooks/index.ts b/src/components/common/hooks/index.ts
new file mode 100644
index 0000000000..277d65d096
--- /dev/null
+++ b/src/components/common/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useOnline'
diff --git a/src/components/common/hooks/useOnline/index.ts b/src/components/common/hooks/useOnline/index.ts
new file mode 100644
index 0000000000..a6888c1acb
--- /dev/null
+++ b/src/components/common/hooks/useOnline/index.ts
@@ -0,0 +1 @@
+export { useOnline } from './useOnline'
diff --git a/src/components/common/hooks/useOnline/useOnline.ts b/src/components/common/hooks/useOnline/useOnline.ts
new file mode 100644
index 0000000000..d20b1287dc
--- /dev/null
+++ b/src/components/common/hooks/useOnline/useOnline.ts
@@ -0,0 +1,66 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>(new AbortController())
+    const timeoutRef = useRef<NodeJS.Timeout>(null)
+    const { isAirgapped } = useMainContext()
+
+    const checkConnectivity = async () => {
+        if (isAirgapped) return
+        // Cancel any pending request
+        if (abortControllerRef.current) {
+            abortControllerRef.current.abort()
+        }
+
+        const controller = new AbortController()
+        abortControllerRef.current = controller
+
+        try {
+            await getInternetConnectivity(abortControllerRef.current)
+            setOnline(true)
+            if (online) {
+                onOnline()
+            }
+        } catch {
+            setOnline(false)
+        } finally {
+            timeoutRef.current = setTimeout(checkConnectivity, INTERNET_CONNECTIVITY_INTERVAL)
+        }
+    }
+    const handleOffline = () => {
+        if (timeoutRef.current) {
+            clearTimeout(timeoutRef.current)
+        }
+        abortControllerRef.current.abort()
+        setOnline(false)
+    }
+
+    const handleOnline = async () => {
+        // Verify connectivity when browser reports online
+        await checkConnectivity()
+    }
+
+    useEffect(() => {
+        if (isAirgapped) return null
+        window.addEventListener('online', handleOnline)
+        window.addEventListener('offline', handleOffline)
+
+        return () => {
+            if (timeoutRef.current) {
+                clearTimeout(timeoutRef.current)
+            }
+            window.removeEventListener('offline', handleOffline)
+            window.removeEventListener('online', handleOnline)
+            abortControllerRef.current.abort()
+        }
+    }, [isAirgapped, onOnline])
+
+    return online
+}
diff --git a/src/components/common/hooks/useVersionUpdate/index.ts b/src/components/common/hooks/useVersionUpdate/index.ts
new file mode 100644
index 0000000000..1856c1f614
--- /dev/null
+++ b/src/components/common/hooks/useVersionUpdate/index.ts
@@ -0,0 +1 @@
+export { useVersionUpdateReload } from './useVersionUpdateReload'
diff --git a/src/components/common/hooks/useVersionUpdate/types.ts b/src/components/common/hooks/useVersionUpdate/types.ts
new file mode 100644
index 0000000000..da89520f77
--- /dev/null
+++ b/src/components/common/hooks/useVersionUpdate/types.ts
@@ -0,0 +1,3 @@
+export interface VersionUpdateProps {
+    showVersionUpdateToast: boolean
+}
diff --git a/src/components/common/Banner/useVersionUpdateReload.tsx b/src/components/common/hooks/useVersionUpdate/useVersionUpdateReload.tsx
similarity index 66%
rename from src/components/common/Banner/useVersionUpdateReload.tsx
rename to src/components/common/hooks/useVersionUpdate/useVersionUpdateReload.tsx
index c72b5b5c47..42119276d7 100644
--- a/src/components/common/Banner/useVersionUpdateReload.tsx
+++ b/src/components/common/hooks/useVersionUpdate/useVersionUpdateReload.tsx
@@ -2,14 +2,28 @@ import { useEffect, useRef, useState } from 'react'
 import { useLocation } from 'react-router-dom'
 import { useRegisterSW } from 'virtual:pwa-register/react'
 
-import { API_STATUS_CODES, logExceptionToSentry, noop, refresh } from '@devtron-labs/devtron-fe-common-lib'
+import {
+    API_STATUS_CODES,
+    Icon,
+    logExceptionToSentry,
+    noop,
+    refresh,
+    ToastManager,
+    ToastVariantType,
+} from '@devtron-labs/devtron-fe-common-lib'
 
-export const useVersionUpdateReload = () => {
+import { UPDATE_AVAILABLE_TOAST_PROGRESS_BG } from '@Config/constants'
+
+import { VersionUpdateProps } from './types'
+
+export const useVersionUpdateReload = ({ showVersionUpdateToast }: VersionUpdateProps) => {
     const refreshing = useRef(false)
     const [bgUpdated, setBGUpdated] = useState(false)
     const [doesNeedRefresh, setDoesNeedRefresh] = useState(false)
     const location = useLocation()
 
+    const updateToastRef = useRef(null)
+
     const handleControllerChange = () => {
         if (refreshing.current) {
             return
@@ -18,10 +32,39 @@ export const useVersionUpdateReload = () => {
             refresh()
             refreshing.current = true
         } else {
+            if (typeof setBGUpdated !== 'function') return
             setBGUpdated(true)
         }
     }
 
+    useEffect(() => {
+        if (!bgUpdated) {
+            return
+        }
+        if (ToastManager.isToastActive(updateToastRef.current)) {
+            ToastManager.dismissToast(updateToastRef.current)
+        }
+
+        updateToastRef.current = ToastManager.showToast(
+            {
+                variant: ToastVariantType.info,
+                title: 'Update available',
+                description: 'This page has been updated. Please save any unsaved changes and refresh.',
+                buttonProps: {
+                    text: 'Reload',
+                    dataTestId: 'reload-btn',
+                    onClick: refresh,
+                    startIcon: <Icon name="ic-arrow-clockwise" color={null} />,
+                },
+                icon: <Icon name="ic-sparkle-color" color={null} />,
+                progressBarBg: UPDATE_AVAILABLE_TOAST_PROGRESS_BG,
+            },
+            {
+                autoClose: false,
+            },
+        )
+    }, [bgUpdated])
+
     useEffect(() => {
         if (navigator.serviceWorker) {
             navigator.serviceWorker.addEventListener('controllerchange', handleControllerChange)
@@ -41,7 +84,10 @@ export const useVersionUpdateReload = () => {
         return 3
     })()
 
-    function handleNeedRefresh() {
+    const handleNeedRefresh = () => {
+        if (ToastManager.isToastActive(updateToastRef.current)) {
+            ToastManager.dismissToast(updateToastRef.current)
+        }
         setDoesNeedRefresh(true)
         if (typeof Storage !== 'undefined') {
             localStorage.removeItem('serverInfo')
@@ -103,6 +149,9 @@ export const useVersionUpdateReload = () => {
     }, [needRefresh])
 
     const handleAppUpdate = () => {
+        if (ToastManager.isToastActive(updateToastRef.current) && showVersionUpdateToast) {
+            ToastManager.dismissToast(updateToastRef.current)
+        }
         // eslint-disable-next-line @typescript-eslint/no-floating-promises
         updateServiceWorker(true)
     }
@@ -116,6 +165,8 @@ export const useVersionUpdateReload = () => {
                 .then((registrations) => registrations.forEach((reg) => reg.update()))
             if (doesNeedRefresh) {
                 handleNeedRefresh()
+            } else if (ToastManager.isToastActive(updateToastRef.current) && showVersionUpdateToast) {
+                ToastManager.dismissToast(updateToastRef.current)
             }
         }
     }, [location])
@@ -126,5 +177,6 @@ export const useVersionUpdateReload = () => {
         doesNeedRefresh,
         updateServiceWorker,
         handleControllerChange,
+        updateToastRef,
     }
 }
diff --git a/src/components/common/navigation/NavigationRoutes.tsx b/src/components/common/navigation/NavigationRoutes.tsx
index 69c659888e..c2fba421b4 100644
--- a/src/components/common/navigation/NavigationRoutes.tsx
+++ b/src/components/common/navigation/NavigationRoutes.tsx
@@ -81,7 +81,7 @@ import { ENVIRONMENT_DATA_FALLBACK, INITIAL_ENV_DATA_STATE } from './constants'
 import { ParsedTabsData } from '../DynamicTabs/types'
 import { SwitchThemeDialog } from '@Pages/Shared'
 import { SwitchThemeDialogProps } from '@Pages/Shared/SwitchThemeDialog/types'
-import { EnvironmentDataStateType } from './types'
+import { EnvironmentDataStateType, NavigationRoutesTypes } from './types'
 import { Banner } from '../Banner/Banner'
 
 // Monaco Editor worker initialization
@@ -125,9 +125,8 @@ const ViewIsPipelineRBACConfigured: FunctionComponent<{
     handleUpdatePipelineRBACViewSelectedTab: (selectedTab: ViewIsPipelineRBACConfiguredRadioTabs) => void
 }> = importComponentFromFELibrary('ViewIsPipelineRBACConfigured', null, 'function')
 const LicenseInfoDialog = importComponentFromFELibrary('LicenseInfoDialog', null, 'function')
-const EnterpriseLicenseBar = importComponentFromFELibrary('EnterpriseLicenseBar', null, 'function')
 
-export default function NavigationRoutes() {
+export default function NavigationRoutes({ reloadVersionConfig, hideVersionUpdateToast }: NavigationRoutesTypes ) {
     const history = useHistory()
     const location = useLocation()
     const match = useRouteMatch()
@@ -173,6 +172,7 @@ export default function NavigationRoutes() {
         setLicenseInfoDialogType(null)
     }
 
+
     const getInit = async (_serverMode: string) => {
         const [userRole, appList, loginData] = await Promise.all([
             getUserRole(),
@@ -476,6 +476,7 @@ export default function NavigationRoutes() {
                 licenseData,
                 setLicenseData,
                 canFetchHelmAppStatus: environmentDataState.canFetchHelmAppStatus,
+                reloadVersionConfig,
             }}
         >
             <main className={_isOnboardingPage ? 'no-nav' : ''} id={DEVTRON_BASE_MAIN_ID}>
@@ -511,7 +512,7 @@ export default function NavigationRoutes() {
                     <div
                         className={`main flexbox-col bg__primary ${appTheme === AppThemeType.light ? 'dc__no-border' : 'border__primary-translucent'} m-8 br-6 dc__overflow-hidden`}
                     >
-                        <Banner />
+                        <Banner hideVersionUpdateToast={hideVersionUpdateToast} />
                         <div className="flexbox-col flex-grow-1 dc__overflow-auto">
                             <Suspense
                                 fallback={
diff --git a/src/components/common/navigation/types.ts b/src/components/common/navigation/types.ts
index 43b7643bdb..772d31d1a0 100644
--- a/src/components/common/navigation/types.ts
+++ b/src/components/common/navigation/types.ts
@@ -18,6 +18,7 @@ import {
     customEnv,
     EnvironmentDataValuesDTO,
     IconsProps,
+    ReloadVersionConfigTypes,
     URLS as CommonURLS,
 } from '@devtron-labs/devtron-fe-common-lib'
 
@@ -42,3 +43,8 @@ export interface NavigationListItemType {
 export interface EnvironmentDataStateType extends Omit<EnvironmentDataValuesDTO, 'isAirGapEnvironment'> {
     isAirgapped: boolean
 }
+
+export interface NavigationRoutesTypes {
+    reloadVersionConfig: ReloadVersionConfigTypes
+    hideVersionUpdateToast: () => void
+}
diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/terminal/TerminalWrapper.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/terminal/TerminalWrapper.component.tsx
index bb999c43d2..86f3b15aa1 100644
--- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/terminal/TerminalWrapper.component.tsx
+++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/terminal/TerminalWrapper.component.tsx
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
+import { useOnline } from '@Components/common/hooks'
 import { SocketConnectionType } from '../../../../../../ClusterNodes/constants'
-import { useOnline } from '../../../../../../common'
 import { TERMINAL_TEXT } from './constants'
 import TerminalView from './Terminal'
 import { ConnectionStripMessageType, TerminalWrapperProps } from './terminal.type'
@@ -94,7 +94,7 @@ export const RenderConnectionStrip = ({
     socketConnection,
     setSocketConnection,
 }: ConnectionStripMessageType) => {
-    const isOnline = useOnline()
+    const isOnline = useOnline({})
 
     const reconnect = () => {
         setSocketConnection(SocketConnectionType.CONNECTING)
diff --git a/src/components/v2/devtronStackManager/DevtronStackManager.service.tsx b/src/components/v2/devtronStackManager/DevtronStackManager.service.tsx
index 551c59edc4..9aad8bd21b 100644
--- a/src/components/v2/devtronStackManager/DevtronStackManager.service.tsx
+++ b/src/components/v2/devtronStackManager/DevtronStackManager.service.tsx
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import { get, getUrlWithSearchParams, post, ToastManager, ToastVariantType } from '@devtron-labs/devtron-fe-common-lib'
+import { get, getUrlWithSearchParams, post, refresh, ToastManager, ToastVariantType } from '@devtron-labs/devtron-fe-common-lib'
 import { ReactComponent as ICSparkles } from '@Icons/ic-sparkles.svg'
 import { ReactComponent as ICArrowClockwise } from '@Icons/ic-arrow-clockwise.svg'
 import { ModuleNameMap, Routes, UPDATE_AVAILABLE_TOAST_PROGRESS_BG } from '../../../config'
@@ -30,7 +30,6 @@ import {
     ReleaseNotesResponse,
     ServerInfoResponse,
 } from './DevtronStackManager.type'
-import { reloadLocation } from '../../common'
 import { INSTALLATION_TYPE_TO_REPO_MAP } from './DevtronStackManager.utils'
 
 let moduleStatusMap: Record<string, ModuleInfo> = {}
@@ -112,7 +111,7 @@ export const getModuleInfo = async (moduleName: string, forceReload?: boolean):
                 buttonProps: {
                     text: 'Reload',
                     dataTestId: 'reload-button',
-                    onClick: reloadLocation,
+                    onClick: refresh,
                     startIcon: <ICArrowClockwise />,
                 },
                 icon: <ICSparkles />,
diff --git a/src/css/base.scss b/src/css/base.scss
index 5e58f7814c..4d3e5b8dab 100644
--- a/src/css/base.scss
+++ b/src/css/base.scss
@@ -4577,21 +4577,6 @@ textarea::placeholder {
     font-size: 13px !important;
 }
 
-// Start: accouncement banner
-.announcement-banner-container {
-    &.page-header-banner {
-        width: fit-content;
-        position: absolute;
-        margin: auto;
-        left: 0;
-        right: 0;
-        top: 0;
-        text-align: center;
-    }
-}
-
-// End: accouncement banner
-
 .bulk-selection-widget {
     border: 1px solid var(--B500);
     background: var(--bg-bulk-selection-widget);
diff --git a/src/services/service.ts b/src/services/service.ts
index 6d00af6474..9f774fbdfb 100644
--- a/src/services/service.ts
+++ b/src/services/service.ts
@@ -54,7 +54,11 @@ import { LOGIN_COUNT } from '../components/onboardingGuide/onboarding.utils'
 import { getProjectList } from '@Components/project/service'
 import { OffendingWorkflowQueryParamType } from '@Components/app/details/triggerView/types'
 
-export function getAppConfigStatus(appId: number, isJobView: boolean, isTemplateView: AppConfigProps['isTemplateView']): Promise<any> {
+export function getAppConfigStatus(
+    appId: number,
+    isJobView: boolean,
+    isTemplateView: AppConfigProps['isTemplateView'],
+): Promise<any> {
     const queryParams = {
         'app-id': appId,
         appType: isJobView ? '2' : undefined,
@@ -93,13 +97,16 @@ export function getConfigOverrideWorkflowDetails(appId: string): Promise<ConfigO
     return get(`${Routes.CI_CONFIG_OVERRIDE_GET}/${appId}`)
 }
 
-export function getCDConfig(appId: number | string, isTemplateView: AppConfigProps['isTemplateView']): Promise<CDPipelines> {
+export function getCDConfig(
+    appId: number | string,
+    isTemplateView: AppConfigProps['isTemplateView'],
+): Promise<CDPipelines> {
     const URL = isTemplateView
         ? getTemplateAPIRoute({
-            type: GetTemplateAPIRouteType.CD_PIPELINE_LIST,
-            queryParams: {
-                id: appId,
-            }
+              type: GetTemplateAPIRouteType.CD_PIPELINE_LIST,
+              queryParams: {
+                  id: appId,
+              },
           })
         : `${Routes.CD_CONFIG}/${appId}`
     return get(URL).then((response) => response.result)
@@ -304,13 +311,15 @@ export function getJobCIPipeline(jobId) {
 }
 
 export function getEnvironmentConfigs(appId, envId, isTemplateView: AppConfigProps['isTemplateView']) {
-    const url = isTemplateView ? getTemplateAPIRoute({
-        type: GetTemplateAPIRouteType.CONFIG_CM,
-        queryParams: {
-            id: appId,
-            envId,
-        }
-    }) : `${Routes.APP_CREATE_ENV_CONFIG_MAP}/${appId}/${envId}`
+    const url = isTemplateView
+        ? getTemplateAPIRoute({
+              type: GetTemplateAPIRouteType.CONFIG_CM,
+              queryParams: {
+                  id: appId,
+                  envId,
+              },
+          })
+        : `${Routes.APP_CREATE_ENV_CONFIG_MAP}/${appId}/${envId}`
 
     return get(url)
 }
@@ -410,7 +419,11 @@ export function isGitOpsModuleInstalledAndConfigured(): Promise<ResponseType> {
         })
 }
 
-export function getChartReferencesForAppAndEnv(appId: number, envId: number | null, isTemplateView: AppConfigProps['isTemplateView']): Promise<ResponseType<MinChartRefDTO>> {
+export function getChartReferencesForAppAndEnv(
+    appId: number,
+    envId: number | null,
+    isTemplateView: AppConfigProps['isTemplateView'],
+): Promise<ResponseType<MinChartRefDTO>> {
     const url = isTemplateView
         ? getTemplateAPIRoute({
               type: GetTemplateAPIRouteType.CHART_REF,
@@ -423,7 +436,11 @@ export function getChartReferencesForAppAndEnv(appId: number, envId: number | nu
     return get(url)
 }
 
-export function getAppChartRefForAppAndEnv(appId: number, envId: number  | null, isTemplateView: AppConfigProps['isTemplateView']): Promise<ResponseType> {
+export function getAppChartRefForAppAndEnv(
+    appId: number,
+    envId: number | null,
+    isTemplateView: AppConfigProps['isTemplateView'],
+): Promise<ResponseType> {
     return getChartReferencesForAppAndEnv(appId, envId, isTemplateView).then((response) => {
         const {
             result: { chartRefs, latestEnvChartRef, latestAppChartRef },
@@ -520,6 +537,11 @@ export const validateContainerConfiguration = (request: any): Promise<any> => {
     return post(URL, request)
 }
 
-export const getTemplateOptions = (appId: number, envId: number): Promise<ResponseType<TemplateListDTO[]>> => (
+export const getTemplateOptions = (appId: number, envId: number): Promise<ResponseType<TemplateListDTO[]>> =>
     get(getUrlWithSearchParams(Routes.DEPLOYMENT_OPTIONS, { appId, envId }))
-)
+
+export const getInternetConnectivity = (controller: AbortController): Promise<any> => {
+    return fetch(`${window._env_?.CENTRAL_API_ENDPOINT ?? 'https://api.devtron.ai'}/${Routes.HEALTH}`, {
+        signal: controller.signal,
+    }).then((res) => res.json())
+}


Reviews:
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T06:20:11Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T06:24:54Z
user: login: AbhishekA1509
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-01T13:03:01Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:04:09Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:08:23Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:09:29Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:23:11Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:23:59Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:28:15Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T13:35:05Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:00:42Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:03:32Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:06:10Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:12:40Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:13:24Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:18:26Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:22:18Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T10:25:01Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:20:07Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:24:05Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:25:03Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:26:15Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:28:25Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T12:29:32Z
user: login: AbhishekA1509
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T13:41:31Z

Review comments:
diff_hunk: @@ -91,6 +101,7 @@ export const Banner = () => {
         if (!isOnline) return BannerVariant.INTERNET_CONNECTIVITY
         if (showOnlineBanner) return BannerVariant.INTERNET_CONNECTIVITY
         if (doesNeedRefresh || bgUpdated) return BannerVariant.VERSION_UPDATE
+        if (getIncompatibleMicroserviceName()?.length) return BannerVariant.INCOMPATIBLE_MICROSERVICES
path: src/components/common/Banner/Banner.tsx
user: login: AbhishekA1509
body: Do we need to check length here
created_at: 2025-04-30T06:20:09Z
author_association: MEMBER
diff_hunk: @@ -61,6 +66,7 @@ export const getBannerConfig = ({
     licenseType,
     enterpriseLicenseBarMessage = '',
path: src/components/common/Banner/utils.tsx
user: login: AbhishekA1509
body: Suggestion: Typing can be more strict
created_at: 2025-04-30T06:24:52Z
author_association: MEMBER
diff_hunk: @@ -520,6 +537,11 @@ export const validateContainerConfiguration = (request: any): Promise<any> => {
     return post(URL, request)
 }
 
-export const getTemplateOptions = (appId: number, envId: number): Promise<ResponseType<TemplateListDTO[]>> => (
+export const getTemplateOptions = (appId: number, envId: number): Promise<ResponseType<TemplateListDTO[]>> =>
     get(getUrlWithSearchParams(Routes.DEPLOYMENT_OPTIONS, { appId, envId }))
-)
+
+export const getInternetConnectivity = (controller: AbortController): Promise<any> => {
+    return fetch(`${window?._env_?.CENTRAL_API_ENDPOINT ?? 'https://api.devtron.ai'}/health`, {
path: src/services/service.ts
user: login: AbhishekA1509
body: Can window and env be null?
created_at: 2025-05-01T12:38:48Z
author_association: MEMBER
diff_hunk: @@ -14,10 +14,18 @@
  * limitations under the License.
  */
 
- .banner-row {
+.banner-row {
     grid-template-columns: 28px 1fr 28px;
+    grid-gap: 16px
 }
 
 .banner__version-update {
     background: linear-gradient(92deg, #4C40BF 0%, #4084BF 72.45%, #8529A3 96.63%);
-}
\ No newline at end of file
+}
+
+.banner-container {
+    position: relative;
+    min-height: 28px;
+    height: 28px;
+    overflow: hidden;
+  }
path: src/components/common/Banner/banner.scss
user: login: AbhishekA1509
body: Pls format
created_at: 2025-05-01T12:41:00Z
author_association: MEMBER
diff_hunk: @@ -14,10 +14,18 @@
  * limitations under the License.
  */
 
- .banner-row {
+.banner-row {
     grid-template-columns: 28px 1fr 28px;
+    grid-gap: 16px
 }
 
 .banner__version-update {
     background: linear-gradient(92deg, #4C40BF 0%, #4084BF 72.45%, #8529A3 96.63%);
-}
\ No newline at end of file
+}
+
+.banner-container {
+    position: relative;
+    min-height: 28px;
+    height: 28px;
path: src/components/common/Banner/banner.scss
user: login: AbhishekA1509
body: Do we need to give both min-height and height?
created_at: 2025-05-01T12:41:27Z
author_association: MEMBER
diff_hunk: @@ -394,21 +395,51 @@ export function deepEqual(configA: any, configB: any): boolean {
 
 export function useOnline() {
     const [online, setOnline] = useState(navigator.onLine)
+    const realTimeConnectivityAbortRef = useRef<AbortController | null>(null)
+    const { isAirgapped } = useMainContext()
+
     useEffect(() => {
-        function handleOnline(e) {
-            setOnline(true)
+        const handleOnline = () => setOnline(true)
+        const handleOffline = () => setOnline(false)
+
+        const checkRealConnectivity = async () => {
+            // Create a new AbortController for each check
+            const controller = new AbortController()
+            realTimeConnectivityAbortRef.current = controller
+
+            const timeoutId = setTimeout(() => controller.abort(), 5000)
+
+            try {
+                await getInternetConnectivity(controller)
+                setOnline(true)
+            } catch (error) {
+                   showError(error)
+                setOnline(false);
path: src/components/common/helpers/Helpers.tsx
user: login: AbhishekA1509
body: Pls format
created_at: 2025-05-01T12:42:20Z
author_association: MEMBER
diff_hunk: @@ -394,21 +395,51 @@ export function deepEqual(configA: any, configB: any): boolean {
 
 export function useOnline() {
     const [online, setOnline] = useState(navigator.onLine)
+    const realTimeConnectivityAbortRef = useRef<AbortController | null>(null)
+    const { isAirgapped } = useMainContext()
+
     useEffect(() => {
-        function handleOnline(e) {
-            setOnline(true)
+        const handleOnline = () => setOnline(true)
path: src/components/common/helpers/Helpers.tsx
user: login: AbhishekA1509
body: If not airgapped should early return from this method
created_at: 2025-05-01T12:48:14Z
author_association: MEMBER
diff_hunk: @@ -41,117 +44,171 @@ import {
 
 import './banner.scss'
 
-const useEnterpriseLicenseConfig = importComponentFromFELibrary('useEnterpriseLicenseConfig', null, 'function')
+const isFELibAvailable = importComponentFromFELibrary('isFELibAvailable', null, 'function')
+const useEnterpriseLicenseConfig = importComponentFromFELibrary('useEnterpriseLicenseConfig', () => null, 'function')
 
 export const Banner = () => {
     const didMountRef = useRef(false)
     const isOnline = useOnline()
-    const { isAirgapped } = useMainContext()
+    const { isAirgapped, currentServerInfo } = useMainContext()
+    const { bgUpdated, doesNeedRefresh, handleAppUpdate } = useVersionUpdateReload()
+    const licenseConfig = useEnterpriseLicenseConfig()
 
     const [showOnlineBanner, setShowOnlineBanner] = useState(false)
     const [showAnnouncementBanner, setShowAnnouncementBanner] = useState(
         ANNOUNCEMENT_CONFIG.message ? shouldShowAnnouncementBanner() : false,
     )
-    const { bgUpdated, doesNeedRefresh, handleAppUpdate } = useVersionUpdateReload()
-
-    const enterpriseLicenseConfig = useEnterpriseLicenseConfig
-
-    const licenseConfig = enterpriseLicenseConfig ? enterpriseLicenseConfig() : null
+    const [activeBannerVariant, setActiveBannerVariant] = useState<BannerVariant | null>(null)
+    const [isVisible, setIsVisible] = useState(true)
+    const prevBannerRef = useRef<BannerVariant | null>(null)
 
     useEffect(() => {
         let timer: NodeJS.Timeout
         if (didMountRef.current) {
             if (isOnline) {
                 setShowOnlineBanner(true)
-                // Auto-hide online banner after timeout
                 timer = setTimeout(() => setShowOnlineBanner(false), ONLINE_BANNER_TIMEOUT)
+                setIsVisible(false)
             }
         } else {
             didMountRef.current = true
-            setShowOnlineBanner(false)
         }
-        return () => clearTimeout(timer)
+        return () => {
+            if (timer) clearTimeout(timer)
+        }
     }, [isOnline])
 
-    const onClickCloseAnnouncementBanner = () => {
-        setShowAnnouncementBanner(false)
-        if (typeof Storage !== 'undefined') {
-            setActionWithExpiry('expiryDateOfHidingAnnouncementBanner', 1)
+    const getIncompatibleMicroserviceName = (): 'frontend' | 'backend' | null => {
+        const { serverInfo } = currentServerInfo
+        if (serverInfo) {
+            if (serverInfo.installationType !== InstallationType.ENTERPRISE && isFELibAvailable) return 'backend'
+            if (serverInfo.installationType === InstallationType.ENTERPRISE && !isFELibAvailable) return 'frontend'
         }
+        return null
     }
 
+    const incompatibleService = useMemo(() => getIncompatibleMicroserviceName(), [currentServerInfo])
path: src/components/common/Banner/Banner.tsx
user: login: AbhishekA1509
body: Can we remove this useMemo, since not a performance intensive o/p
created_at: 2025-05-01T13:04:08Z
author_association: MEMBER
diff_hunk: @@ -14,10 +14,18 @@
  * limitations under the License.
  */
 
- .banner-row {
+.banner-row {
     grid-template-columns: 28px 1fr 28px;
+    grid-gap: 16px
 }
 
 .banner__version-update {
     background: linear-gradient(92deg, #4C40BF 0%, #4084BF 72.45%, #8529A3 96.63%);
-}
\ No newline at end of file
+}
+
+.banner-container {
path: src/components/common/Banner/banner.scss
user: login: AbhishekA1509
body: Can we use base.scss
created_at: 2025-05-01T13:08:22Z
author_association: MEMBER
diff_hunk: @@ -14,10 +14,18 @@
  * limitations under the License.
  */
 
- .banner-row {
+.banner-row {
     grid-template-columns: 28px 1fr 28px;
+    grid-gap: 16px
path: src/components/common/Banner/banner.scss
user: login: AbhishekA1509
body: We should use column-gap: 16px from base
created_at: 2025-05-01T13:09:28Z
author_association: MEMBER
diff_hunk: @@ -394,21 +395,51 @@ export function deepEqual(configA: any, configB: any): boolean {
 
 export function useOnline() {
     const [online, setOnline] = useState(navigator.onLine)
+    const realTimeConnectivityAbortRef = useRef<AbortController | null>(null)
+    const { isAirgapped } = useMainContext()
+
     useEffect(() => {
-        function handleOnline(e) {
-            setOnline(true)
+        const handleOnline = () => setOnline(true)
+        const handleOffline = () => setOnline(false)
+
+        const checkRealConnectivity = async () => {
+            // Create a new AbortController for each check
+            const controller = new AbortController()
+            realTimeConnectivityAbortRef.current = controller
+
+            const timeoutId = setTimeout(() => controller.abort(), 5000)
+
+            try {
+                await getInternetConnectivity(controller)
+                setOnline(true)
+            } catch (error) {
+                   showError(error)
path: src/components/common/helpers/Helpers.tsx
user: login: AbhishekA1509
body: If request is aborted we should not show this error
created_at: 2025-05-01T13:23:10Z
author_association: MEMBER
diff_hunk: @@ -394,21 +395,51 @@ export function deepEqual(configA: any, configB: any): boolean {
 
 export function useOnline() {
     const [online, setOnline] = useState(navigator.onLine)
+    const realTimeConnectivityAbortRef = useRef<AbortController | null>(null)
path: src/components/common/helpers/Helpers.tsx
user: login: AbhishekA1509
body: 
```suggestion
    const realTimeConnectivityAbortRef = useRef<AbortController | null>(new AbortController())
```
And we can remove null check for current
created_at: 2025-05-01T13:23:58Z
author_association: MEMBER
diff_hunk: @@ -394,21 +395,51 @@ export function deepEqual(configA: any, configB: any): boolean {
 
 export function useOnline() {
     const [online, setOnline] = useState(navigator.onLine)
+    const realTimeConnectivityAbortRef = useRef<AbortController | null>(null)
+    const { isAirgapped } = useMainContext()
+
     useEffect(() => {
-        function handleOnline(e) {
-            setOnline(true)
+        const handleOnline = () => setOnline(true)
+        const handleOffline = () => setOnline(false)
+
+        const checkRealConnectivity = async () => {
+            // Create a new AbortController for each check
+            const controller = new AbortController()
+            realTimeConnectivityAbortRef.current = controller
+
+            const timeoutId = setTimeout(() => controller.abort(), 5000)
+
+            try {
+                await getInternetConnectivity(controller)
+                setOnline(true)
+            } catch (error) {
+                   showError(error)
+                setOnline(false);
+            } finally {
+                clearTimeout(timeoutId)
+            }
         }
 
-        function handleOffline(e) {
-            setOnline(false)
+        if (!isAirgapped) {
+            checkRealConnectivity()
         }
+
+        const intervalId = setInterval(() => {
+            if (!isAirgapped) {
+                checkRealConnectivity()
+            }
+        }, 30000)
+
         window.addEventListener('online', handleOnline)
path: src/components/common/helpers/Helpers.tsx
user: login: AbhishekA1509
body: I believe we can remove online event handler since computing from API
created_at: 2025-05-01T13:28:14Z
author_association: MEMBER
diff_hunk: @@ -41,117 +45,171 @@ import {
 
 import './banner.scss'
 
-const useEnterpriseLicenseConfig = importComponentFromFELibrary('useEnterpriseLicenseConfig', null, 'function')
+const isFELibAvailable = importComponentFromFELibrary('isFELibAvailable', null, 'function')
+const useEnterpriseLicenseConfig = importComponentFromFELibrary('useEnterpriseLicenseConfig', () => null, 'function')
 
 export const Banner = () => {
     const didMountRef = useRef(false)
     const isOnline = useOnline()
-    const { isAirgapped } = useMainContext()
+    const { isAirgapped, currentServerInfo } = useMainContext()
+    const { bgUpdated, doesNeedRefresh, handleAppUpdate } = useVersionUpdateReload()
+    const licenseConfig = useEnterpriseLicenseConfig()
 
     const [showOnlineBanner, setShowOnlineBanner] = useState(false)
     const [showAnnouncementBanner, setShowAnnouncementBanner] = useState(
         ANNOUNCEMENT_CONFIG.message ? shouldShowAnnouncementBanner() : false,
     )
-    const { bgUpdated, doesNeedRefresh, handleAppUpdate } = useVersionUpdateReload()
-
-    const enterpriseLicenseConfig = useEnterpriseLicenseConfig
-
-    const licenseConfig = enterpriseLicenseConfig ? enterpriseLicenseConfig() : null
+    const [activeBannerVariant, setActiveBannerVariant] = useState<BannerVariant | null>(null)
+    const [isVisible, setIsVisible] = useState(true)
+    const prevBannerRef = useRef<BannerVariant | null>(null)
 
     useEffect(() => {
         let timer: NodeJS.Timeout
         if (didMountRef.current) {
             if (isOnline) {
                 setShowOnlineBanner(true)
-                // Auto-hide online banner after timeout
                 timer = setTimeout(() => setShowOnlineBanner(false), ONLINE_BANNER_TIMEOUT)
+                setIsVisible(false)
             }
         } else {
             didMountRef.current = true
-            setShowOnlineBanner(false)
         }
-        return () => clearTimeout(timer)
+        return () => {
+            if (timer) clearTimeout(timer)
+        }
     }, [isOnline])
 
-    const onClickCloseAnnouncementBanner = () => {
-        setShowAnnouncementBanner(false)
-        if (typeof Storage !== 'undefined') {
-            setActionWithExpiry('expiryDateOfHidingAnnouncementBanner', 1)
+    const getIncompatibleMicroserviceName = (): 'frontend' | 'backend' | null => {
+        const { serverInfo } = currentServerInfo
+        if (serverInfo) {
+            if (serverInfo.installationType !== InstallationType.ENTERPRISE && isFELibAvailable) return 'backend'
+            if (serverInfo.installationType === InstallationType.ENTERPRISE && !isFELibAvailable) return 'frontend'
         }
+        return null
     }
 
+    const incompatibleService = useMemo(() => getIncompatibleMicroserviceName(), [currentServerInfo])
     const {
         message: enterpriseLicenseBarMessage = '',
         type: licenseType = InfoBlockVariant.HELP,
         iconName = '',
         handleOpenLicenseDialog,
     } = licenseConfig ?? {}
 
-    const getCurrentBanner = (): BannerVariant => {
-        if (!isOnline) return BannerVariant.INTERNET_CONNECTIVITY
-        if (showOnlineBanner) return BannerVariant.INTERNET_CONNECTIVITY
+    const getCurrentBanner = (): BannerVariant | null => {
+        if ((!isOnline || showOnlineBanner) && !isAirgapped) return BannerVariant.INTERNET_CONNECTIVITY
         if (doesNeedRefresh || bgUpdated) return BannerVariant.VERSION_UPDATE
+        if (incompatibleService) return BannerVariant.INCOMPATIBLE_MICROSERVICES
         if (showAnnouncementBanner) return BannerVariant.ANNOUNCEMENT
         if (licenseConfig?.message) return BannerVariant.LICENSE
         return null
     }
 
-    const bannerVariant: BannerVariant = getCurrentBanner()
+    const bannerVariant = getCurrentBanner()
+
+    useEffect(() => {
+        if (bannerVariant !== prevBannerRef.current) {
path: src/components/common/Banner/Banner.tsx
user: login: AbhishekA1509
body: We should avoid this useEffect and these three states can discuss on this
created_at: 2025-05-01T13:35:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>()
path: src/components/common/hooks/useOnline/useOnline.ts
user: login: AbhishekA1509
body: Pls avoid undefined across
```suggestion
    const abortControllerRef = useRef<AbortController>(new AbortController())
```
created_at: 2025-05-05T10:00:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>()
+    const timeoutRef = useRef<NodeJS.Timeout>()
+    const { isAirgapped } = useMainContext()
+
+    const checkConnectivity = async () => {
+        // Cancel any pending request
+        if (abortControllerRef.current) {
+            abortControllerRef.current.abort()
+        }
+
+        const controller = new AbortController()
+        abortControllerRef.current = controller
+
+        try {
+            await getInternetConnectivity(abortControllerRef.current)
+            setOnline(true)
+            if (!online) {
+                onOnline()
+            }
+        } catch {
+            setOnline(false)
+        } finally {
+            if (!isAirgapped && !online) {
+                timeoutRef.current = setTimeout(checkConnectivity, INTERNET_CONNECTIVITY_INTERVAL)
+            }
+        }
+    }
+    const handleOffline = () => setOnline(false)
+    const handleOnline = async () => {
+        // Verify connectivity when browser reports online
+        await checkConnectivity()
+    }
+
+    useEffect(() => {
+        if (isAirgapped) return null
+        window.addEventListener('online', handleOnline)
+        window.addEventListener('offline', handleOffline)
+
+        return () => {
+            if (timeoutRef.current) {
+                clearTimeout(timeoutRef.current)
+            }
+            window.removeEventListener('offline', handleOffline)
+            window.removeEventListener('online', handleOnline)
+            abortControllerRef.current?.abort()
+        }
+    }, [isAirgapped, handleOnline, handleOffline, checkConnectivity])
path: src/components/common/hooks/useOnline/useOnline.ts
user: login: AbhishekA1509
body: Why have we added  handleOnline, handleOffline, checkConnectivity
created_at: 2025-05-05T10:03:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>()
+    const timeoutRef = useRef<NodeJS.Timeout>()
+    const { isAirgapped } = useMainContext()
+
+    const checkConnectivity = async () => {
+        // Cancel any pending request
+        if (abortControllerRef.current) {
+            abortControllerRef.current.abort()
+        }
+
+        const controller = new AbortController()
+        abortControllerRef.current = controller
+
+        try {
+            await getInternetConnectivity(abortControllerRef.current)
+            setOnline(true)
+            if (!online) {
+                onOnline()
+            }
+        } catch {
+            setOnline(false)
+        } finally {
+            if (!isAirgapped && !online) {
+                timeoutRef.current = setTimeout(checkConnectivity, INTERNET_CONNECTIVITY_INTERVAL)
+            }
+        }
+    }
+    const handleOffline = () => setOnline(false)
path: src/components/common/hooks/useOnline/useOnline.ts
user: login: AbhishekA1509
body: We should abort timeout and signal
created_at: 2025-05-05T10:06:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>()
+    const timeoutRef = useRef<NodeJS.Timeout>()
+    const { isAirgapped } = useMainContext()
+
+    const checkConnectivity = async () => {
+        // Cancel any pending request
+        if (abortControllerRef.current) {
+            abortControllerRef.current.abort()
+        }
+
+        const controller = new AbortController()
+        abortControllerRef.current = controller
+
+        try {
+            await getInternetConnectivity(abortControllerRef.current)
+            setOnline(true)
+            if (!online) {
+                onOnline()
+            }
+        } catch {
+            setOnline(false)
+        } finally {
+            if (!isAirgapped && !online) {
path: src/components/common/hooks/useOnline/useOnline.ts
user: login: AbhishekA1509
body: Will we even call method if isAirgapped?
created_at: 2025-05-05T10:12:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>()
+    const timeoutRef = useRef<NodeJS.Timeout>()
+    const { isAirgapped } = useMainContext()
+
+    const checkConnectivity = async () => {
+        // Cancel any pending request
+        if (abortControllerRef.current) {
+            abortControllerRef.current.abort()
+        }
+
+        const controller = new AbortController()
+        abortControllerRef.current = controller
+
+        try {
+            await getInternetConnectivity(abortControllerRef.current)
+            setOnline(true)
+            if (!online) {
+                onOnline()
+            }
+        } catch {
+            setOnline(false)
+        } finally {
+            if (!isAirgapped && !online) {
+                timeoutRef.current = setTimeout(checkConnectivity, INTERNET_CONNECTIVITY_INTERVAL)
path: src/components/common/hooks/useOnline/useOnline.ts
user: login: AbhishekA1509
body: Why is there !online check?
created_at: 2025-05-05T10:13:22Z
author_association: MEMBER
diff_hunk: @@ -2,14 +2,24 @@ import { useEffect, useRef, useState } from 'react'
 import { useLocation } from 'react-router-dom'
 import { useRegisterSW } from 'virtual:pwa-register/react'
 
-import { API_STATUS_CODES, logExceptionToSentry, noop, refresh } from '@devtron-labs/devtron-fe-common-lib'
+import {
+    API_STATUS_CODES,
+    logExceptionToSentry,
+    noop,
+    refresh,
+    ToastManager,
+} from '@devtron-labs/devtron-fe-common-lib'
 
-export const useVersionUpdateReload = () => {
+import { VersionUpdateProps } from './types'
+
+export const useVersionUpdateReload = ({ showToast = true }: VersionUpdateProps) => {
path: src/components/common/hooks/useVersionUpdate/useVersionUpdateReload.tsx
user: login: AbhishekA1509
body: The useEffects in this hook will be registered twice now, we should fix this by moving this to MainContext
created_at: 2025-05-05T10:18:25Z
author_association: MEMBER
diff_hunk: @@ -34,71 +36,101 @@ import {
 import { ANNOUNCEMENT_CONFIG, BannerVariant } from './constants'
 import { BannerConfigProps, BannerConfigType } from './types'
 
+const getVariantWithIconMap = (iconName: IconsProps['name']): Partial<Record<BannerVariant, IconsProps['name']>> => ({
+    [BannerVariant.OFFLINE]: 'ic-disconnect',
+    [BannerVariant.VERSION_UPDATE]: 'ic-sparkle-color',
+    [BannerVariant.INCOMPATIBLE_MICROSERVICES]: 'ic-info-outline',
+    [BannerVariant.LICENSE]: iconName,
+    [BannerVariant.ANNOUNCEMENT]: 'ic-megaphone-left',
+})
+
+const getVariantWithIconColorMap = (
+    type: InfoBlockProps['variant'],
path: src/components/common/Banner/utils.tsx
user: login: AbhishekA1509
body: This type is not required since a const in both getVariantWithIconColorMap, getBannerIcon
created_at: 2025-05-05T10:22:17Z
author_association: MEMBER
diff_hunk: @@ -34,71 +36,101 @@ import {
 import { ANNOUNCEMENT_CONFIG, BannerVariant } from './constants'
 import { BannerConfigProps, BannerConfigType } from './types'
 
+const getVariantWithIconMap = (iconName: IconsProps['name']): Partial<Record<BannerVariant, IconsProps['name']>> => ({
+    [BannerVariant.OFFLINE]: 'ic-disconnect',
+    [BannerVariant.VERSION_UPDATE]: 'ic-sparkle-color',
+    [BannerVariant.INCOMPATIBLE_MICROSERVICES]: 'ic-info-outline',
+    [BannerVariant.LICENSE]: iconName,
+    [BannerVariant.ANNOUNCEMENT]: 'ic-megaphone-left',
+})
+
+const getVariantWithIconColorMap = (
+    type: InfoBlockProps['variant'],
+): Record<BannerVariant, IconBaseColorType | null> => ({
+    [BannerVariant.OFFLINE]: null,
+    [BannerVariant.ONLINE]: null,
+    [BannerVariant.VERSION_UPDATE]: null,
+    [BannerVariant.INCOMPATIBLE_MICROSERVICES]: 'N0',
+    [BannerVariant.LICENSE]: null,
+    [BannerVariant.ANNOUNCEMENT]: VARIANT_TO_ICON_COLOR_MAP[type],
+})
 export const getBannerIcon = (
     bannerVariant: BannerVariant,
     type: InfoBlockProps['variant'],
     iconName: IconsProps['name'],
-) => {
-    const variantWithIconMap: Record<BannerVariant, IconsProps['name'] | null> = {
-        [BannerVariant.INTERNET_CONNECTIVITY]: 'ic-disconnect',
-        [BannerVariant.VERSION_UPDATE]: 'ic-sparkle-color',
-        [BannerVariant.LICENSE]: iconName,
-        [BannerVariant.ANNOUNCEMENT]: 'ic-megaphone-left',
-    }
-
-    return (
-        <Icon
-            name={variantWithIconMap[bannerVariant]}
-            color={bannerVariant === BannerVariant.ANNOUNCEMENT ? VARIANT_TO_ICON_COLOR_MAP[type] : null}
-            size={16}
-        />
-    )
-}
+) => (
+    <Icon
+        name={getVariantWithIconMap(iconName)[bannerVariant]}
+        color={getVariantWithIconColorMap(type)[bannerVariant]}
+        size={16}
+    />
+)
 
 export const getBannerConfig = ({
     bannerVariant,
-    isOnline,
     licenseType,
     enterpriseLicenseBarMessage = '',
     hideInternetConnectivityBar = false,
-}: BannerConfigProps): BannerConfigType => {
-    const bannerConfigMap: Partial<Record<BannerVariant, BannerConfigType>> = {
-        ...(!hideInternetConnectivityBar
-            ? {
-                  [BannerVariant.INTERNET_CONNECTIVITY]: isOnline
-                      ? {
-                            text: 'You’re back online!',
-                            rootClassName: 'bcg-5',
-                        }
-                      : {
-                            text: 'You’re offline! Please check your internet connection.',
-                            icon: 'ic-disconnected',
-                            rootClassName: 'bcr-5',
-                        },
-              }
-            : {}),
+    microservice,
+}: BannerConfigProps): BannerConfigType | null => {
+    switch (bannerVariant) {
+        case BannerVariant.ONLINE:
+            if (!hideInternetConnectivityBar) {
path: src/components/common/Banner/utils.tsx
user: login: AbhishekA1509
body: I believe hideInternetConnectivityBar prop is not required in this method
created_at: 2025-05-05T10:25:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { noop, useMainContext } from '@devtron-labs/devtron-fe-common-lib'
+
+import { getInternetConnectivity } from '@Services/service'
+
+import { INTERNET_CONNECTIVITY_INTERVAL } from '../constants'
+
+export const useOnline = ({ onOnline = noop }: { onOnline?: () => void }) => {
+    const [online, setOnline] = useState(structuredClone(navigator.onLine))
+    const abortControllerRef = useRef<AbortController>(new AbortController())
+    const timeoutRef = useRef<NodeJS.Timeout>()
+    const { isAirgapped } = useMainContext()
+
+    const checkConnectivity = async () => {
+        if (isAirgapped) return
+        // Cancel any pending request
+        if (abortControllerRef.current) {
+            abortControllerRef.current.abort()
+        }
+
+        const controller = new AbortController()
+        abortControllerRef.current = controller
+
+        try {
+            await getInternetConnectivity(abortControllerRef.current)
+            setOnline(true)
+            if (online) {
+                onOnline()
+            }
+        } catch {
+            setOnline(false)
+        } finally {
+            timeoutRef.current = setTimeout(checkConnectivity, INTERNET_CONNECTIVITY_INTERVAL)
+        }
+    }
+    const handleOffline = () => {
+        if (timeoutRef.current) {
+            clearTimeout(timeoutRef.current)
+        }
+        abortControllerRef.current?.abort()
+        setOnline(false)
+    }
+
+    const handleOnline = async () => {
+        // Verify connectivity when browser reports online
+        await checkConnectivity()
+    }
+
+    useEffect(() => {
+        if (isAirgapped) return null
+        window.addEventListener('online', handleOnline)
+        window.addEventListener('offline', handleOffline)
+
+        return () => {
+            if (timeoutRef.current) {
+                clearTimeout(timeoutRef.current)
+            }
+            window.removeEventListener('offline', handleOffline)
+            window.removeEventListener('online', handleOnline)
+            abortControllerRef.current?.abort()
path: src/components/common/hooks/useOnline/useOnline.ts
user: login: AbhishekA1509
body: Will this be null?
created_at: 2025-05-05T12:20:06Z
author_association: MEMBER
diff_hunk: @@ -42,3 +45,16 @@ export interface NavigationListItemType {
 export interface EnvironmentDataStateType extends Omit<EnvironmentDataValuesDTO, 'isAirGapEnvironment'> {
     isAirgapped: boolean
 }
+
+export interface ReloadConfigTypes {
+    bgUpdated: boolean
+    handleAppUpdate: () => void
+    doesNeedRefresh: boolean
+    updateServiceWorker: () => Promise<void>
+    handleControllerChange: () => void
+    updateToastRef: MutableRefObject<ReturnType<typeof ToastManager.showToast>> | null
+}
+
+export interface NavigationRoutesTypes {
+    reloadConfig: ReloadConfigTypes
path: src/components/common/navigation/types.ts
user: login: AbhishekA1509
body: 
```suggestion
    reloadVersionConfig: ReloadConfigTypes
```
created_at: 2025-05-05T12:24:04Z
author_association: MEMBER
diff_hunk: @@ -51,6 +56,37 @@ const App = () => {
     const location = useLocation()
     const { push } = useHistory()
 
+    const { bgUpdated, handleAppUpdate, doesNeedRefresh, updateServiceWorker, handleControllerChange, updateToastRef } =
+        useVersionUpdateReload({ showToast: true })
+
+    useEffect(() => {
path: src/App.tsx
user: login: AbhishekA1509
body: This should be in useVersionUpdateReload
created_at: 2025-05-05T12:25:01Z
author_association: MEMBER
diff_hunk: @@ -51,6 +56,37 @@ const App = () => {
     const location = useLocation()
     const { push } = useHistory()
 
+    const { bgUpdated, handleAppUpdate, doesNeedRefresh, updateServiceWorker, handleControllerChange, updateToastRef } =
+        useVersionUpdateReload({ showToast: true })
+
+    useEffect(() => {
+        if (!bgUpdated) {
+            return
+        }
+        if (ToastManager.isToastActive(updateToastRef.current)) {
+            ToastManager.dismissToast(updateToastRef.current)
path: src/App.tsx
user: login: AbhishekA1509
body: Pls refer to existing App.tsx we were dismissing toast in various places pls verify
created_at: 2025-05-05T12:26:14Z
author_association: MEMBER
diff_hunk: @@ -476,6 +477,12 @@ export default function NavigationRoutes() {
                 licenseData,
                 setLicenseData,
                 canFetchHelmAppStatus: environmentDataState.canFetchHelmAppStatus,
+                bgUpdated,
path: src/components/common/navigation/NavigationRoutes.tsx
user: login: AbhishekA1509
body: This should be in single object
created_at: 2025-05-05T12:28:24Z
author_association: MEMBER
diff_hunk: @@ -34,71 +35,96 @@ import {
 import { ANNOUNCEMENT_CONFIG, BannerVariant } from './constants'
 import { BannerConfigProps, BannerConfigType } from './types'
 
-export const getBannerIcon = (
-    bannerVariant: BannerVariant,
-    type: InfoBlockProps['variant'],
-    iconName: IconsProps['name'],
-) => {
-    const variantWithIconMap: Record<BannerVariant, IconsProps['name'] | null> = {
-        [BannerVariant.INTERNET_CONNECTIVITY]: 'ic-disconnect',
-        [BannerVariant.VERSION_UPDATE]: 'ic-sparkle-color',
-        [BannerVariant.LICENSE]: iconName,
-        [BannerVariant.ANNOUNCEMENT]: 'ic-megaphone-left',
-    }
+export const getValidAnnouncementType = (type): type is InfoBlockVariantType =>
+    [
path: src/components/common/Banner/utils.tsx
user: login: AbhishekA1509
body: Should we use Object.values?
created_at: 2025-05-05T12:29:31Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-05T13:41:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=devtron-labs_dashboard&pullRequest=2680) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=devtron-labs_dashboard&pullRequest=2680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=devtron-labs_dashboard&pullRequest=2680&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=devtron-labs_dashboard&pullRequest=2680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=devtron-labs_dashboard&pullRequest=2680&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=devtron-labs_dashboard&pullRequest=2680&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=devtron-labs_dashboard&pullRequest=2680)


author_association: NONE