Repository: devtron-labs/dashboard
Pull request #2649: feat: Navigate button from app details to env override
Author: login: shivani170
Labels: name: PR:Ready-to-Review
description: PR:Ready-to-Review
Created: 2025-04-14T13:36:01Z | Merged: 2025-04-15T13:02:19Z

# Description

To add  navigate button from app details to env override
<img width="1361" alt="Screenshot 2025-04-14 at 7 05 44 PM" src="https://github.com/user-attachments/assets/6273c23c-d4af-475d-bccd-8b4fd24a426c" />


Fixes https://github.com/devtron-labs/sprint-tasks/issues/2102

## 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/package.json b/package.json
index 5321bfc2e6..4b508ce3ca 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
     "private": true,
     "homepage": "/dashboard",
     "dependencies": {
-        "@devtron-labs/devtron-fe-common-lib": "1.11.0-pre-3",
+        "@devtron-labs/devtron-fe-common-lib": "1.11.0-pre-4",
         "@esbuild-plugins/node-globals-polyfill": "0.2.3",
         "@rjsf/core": "^5.13.3",
         "@rjsf/utils": "^5.13.3",
diff --git a/src/components/ClusterNodes/ClusterList/ClusterList.tsx b/src/components/ClusterNodes/ClusterList/ClusterList.tsx
index 9825754454..31b02a1a58 100644
--- a/src/components/ClusterNodes/ClusterList/ClusterList.tsx
+++ b/src/components/ClusterNodes/ClusterList/ClusterList.tsx
@@ -38,11 +38,9 @@ const ClusterList = ({
     return (
         <div data-testid="cluster-list-container" className="flexbox-col flex-grow-1">
             <div className="cluster-list-row fw-6 cn-7 fs-12 dc__border-bottom pt-8 pb-8 pr-20 pl-20 dc__uppercase bg__primary dc__position-sticky dc__top-0 dc__zi-3">
+                {KubeConfigRowCheckbox ? <BulkSelection showPagination={false} /> : <div />}
                 {Object.entries(ClusterMapListSortableKeys).map(([cellName, cellKey]) => (
                     <React.Fragment key={cellName}>
-                        {KubeConfigRowCheckbox && cellKey === ClusterMapListSortableKeys.CLUSTER_NAME && (
-                            <BulkSelection showPagination={false} />
-                        )}
                         <SortableTableHeaderCell
                             key={cellName}
                             title={ClusterMapListSortableTitle[cellName]}
diff --git a/src/components/ClusterNodes/ClusterList/ClusterListRow.tsx b/src/components/ClusterNodes/ClusterList/ClusterListRow.tsx
index 20c7c67ff8..24c35a098c 100644
--- a/src/components/ClusterNodes/ClusterList/ClusterListRow.tsx
+++ b/src/components/ClusterNodes/ClusterList/ClusterListRow.tsx
@@ -67,7 +67,7 @@ const ClusterListRow = ({
         >
             {KubeConfigRowCheckbox && <KubeConfigRowCheckbox clusterData={clusterData} />}
             {!isIdentifierSelected && identifierCount === 0 && (
-                <div className="dc__visible-hover--hide-child flex left">
+                <div className={`${KubeConfigRowCheckbox ? 'dc__visible-hover--hide-child' : ''} flex left`}>
                     <Icon name="ic-bg-cluster" color={null} size={24} />
                 </div>
             )}
diff --git a/src/components/app/details/AppHeader.tsx b/src/components/app/details/AppHeader.tsx
index 3fc3d4f553..d8bab2c120 100644
--- a/src/components/app/details/AppHeader.tsx
+++ b/src/components/app/details/AppHeader.tsx
@@ -28,7 +28,6 @@ import {
     useBreadcrumb,
 } from '@devtron-labs/devtron-fe-common-lib'
 
-import { ReactComponent as Settings } from '../../../assets/icons/ic-settings.svg'
 import { URLS } from '../../../config'
 import { FilterParentType } from '../../ApplicationGroup/AppGroup.types'
 import AppGroupAppFilter from '../../ApplicationGroup/AppGroupAppFilter'
@@ -231,7 +230,7 @@ export const AppHeader = ({
                 id: 'app-configuration-tab',
                 label: 'Configurations',
                 tabType: 'navLink',
-                icon: Settings,
+                icon: 'ic-sliders-vertical',
                 props: {
                     to: `${match.url}/${CommonURLS.APP_CONFIG}`,
                     'data-action': 'App Configuration Clicked',
diff --git a/src/components/app/details/appDetails/AppDetailsCDButton.tsx b/src/components/app/details/appDetails/AppDetailsCDButton.tsx
index 44f8e8d9de..a5b20e7f31 100644
--- a/src/components/app/details/appDetails/AppDetailsCDButton.tsx
+++ b/src/components/app/details/appDetails/AppDetailsCDButton.tsx
@@ -15,6 +15,7 @@
  */
 
 import React from 'react'
+import ReactGA from 'react-ga4'
 import { useHistory, useLocation } from 'react-router-dom'
 
 import {
@@ -39,6 +40,7 @@ import { AppDetailsCDButtonType } from '../../types'
 import CDMaterial from '../triggerView/cdMaterial'
 import { TRIGGER_VIEW_PARAMS } from '../triggerView/Constants'
 import { MATERIAL_TYPE } from '../triggerView/types'
+import { DA_APP_DETAILS_GA_EVENTS } from './constants'
 import { getDeployButtonStyle } from './utils'
 
 const ApprovalMaterialModal = importComponentFromFELibrary('ApprovalMaterialModal')
@@ -69,6 +71,8 @@ const AppDetailsCDButton = ({
         history.push({
             search: new URLSearchParams(newParams).toString(),
         })
+
+        ReactGA.event(DA_APP_DETAILS_GA_EVENTS.DeployButtonClicked)
     }
 
     const closeCDModal = (e: React.MouseEvent): void => {
diff --git a/src/components/app/details/appDetails/SourceInfo.tsx b/src/components/app/details/appDetails/SourceInfo.tsx
index 118a830bad..11031da1d1 100644
--- a/src/components/app/details/appDetails/SourceInfo.tsx
+++ b/src/components/app/details/appDetails/SourceInfo.tsx
@@ -15,7 +15,8 @@
  */
 
 import { useMemo, useState } from 'react'
-import { Link, useParams } from 'react-router-dom'
+import { Link, useHistory, useParams } from 'react-router-dom'
+import ReactGA from 'react-ga4'
 import moment from 'moment'
 import {
     Button,
@@ -26,13 +27,14 @@ import {
     DATE_TIME_FORMATS,
     DeploymentAppTypes,
     handleUTCTime,
+    Icon,
     Progressing,
     ReleaseMode,
     showError,
     Tooltip,
 } from '@devtron-labs/devtron-fe-common-lib'
 import { ReactComponent as ICCamera } from '@Icons/ic-camera.svg'
-import { URLS } from '../../../../config'
+import { APP_COMPOSE_STAGE, getAppComposeURL, URLS } from '../../../../config'
 import { EnvSelector } from './AppDetails'
 import { DeploymentAppTypeNameMapping } from '../../../../config/constantMessaging'
 import { Nodes, SourceInfoType } from '../../types'
@@ -51,6 +53,7 @@ import { ReactComponent as Trash } from '../../../../assets/icons/ic-delete-dots
 import { ReactComponent as ScaleDown } from '../../../../assets/icons/ic-scale-down.svg'
 import HelmAppConfigApplyStatusCard from '@Components/v2/appDetails/sourceInfo/environmentStatus/HelmAppConfigApplyStatusCard'
 import { HibernationModalTypes } from './appDetails.type'
+import { DA_APP_DETAILS_GA_EVENTS } from './constants'
 
 const AppDetailsDownloadCard = importComponentFromFELibrary('AppDetailsDownloadCard')
 const DeploymentWindowStatusCard = importComponentFromFELibrary('DeploymentWindowStatusCard')
@@ -93,6 +96,8 @@ export const SourceInfo = ({
         appDetails?.releaseMode === ReleaseMode.MIGRATE_EXTERNAL_APPS && !appDetails?.isPipelineTriggered
     const isIsolatedEnv = isVirtualEnvironment && !!appDetails?.resourceTree
 
+    const history = useHistory()
+
     if (
         ['progressing', 'degraded'].includes(status?.toLowerCase()) &&
         Array.isArray(conditions) &&
@@ -151,6 +156,11 @@ export const SourceInfo = ({
         return <div className="flex left mb-16">{loadingCards}</div>
     }
 
+    const onClickSliderVerticalButton = () => {
+        history.push(`${getAppComposeURL(params.appId, APP_COMPOSE_STAGE.ENV_OVERRIDE, false, false)}/${params.envId}`)
+        ReactGA.event(DA_APP_DETAILS_GA_EVENTS.GoToEnvironmentConfiguration)
+    }
+
     const renderDevtronAppsEnvironmentSelector = (environment) => {
         // If moving to a component then move getIsApprovalConfigured with it as well with memoization.
         const isApprovalConfigured = appDetails?.isApprovalPolicyApplicable ?? false
@@ -256,7 +266,7 @@ export const SourceInfo = ({
                                         showTooltip={isApprovalConfigured}
                                         tooltipProps={{
                                             content: 'Application deployment requiring approval cannot be hibernated.',
-                                            placement: 'bottom-end',
+                                            placement: 'bottom',
                                         }}
                                     />
                                 )}
@@ -274,10 +284,25 @@ export const SourceInfo = ({
                                         showTooltip={isApprovalConfigured}
                                         tooltipProps={{
                                             content: 'Application deployment requiring approval cannot be hibernated.',
-                                            placement: 'bottom-end',
+                                            placement: 'bottom',
                                         }}
                                     />
                                 )}
+                                <Button
+                                    dataTestId="app-details-env-config-button"
+                                    size={ComponentSizeType.small}
+                                    icon={<Icon name="ic-sliders-vertical" color={null} />}
+                                    variant={ButtonVariantType.secondary}
+                                    onClick={onClickSliderVerticalButton}
+                                    component={ButtonComponentType.button}
+                                    style={ButtonStyleType.neutral}
+                                    ariaLabel="Go to Environment Configuration"
+                                    showTooltip
+                                    tooltipProps={{
+                                        content: 'Go to Environment Config',
+                                        placement: 'bottom',
+                                    }}
+                                />
                                 {window._env_.FEATURE_SWAP_TRAFFIC_ENABLE &&
                                     SwapTraffic &&
                                     !!appDetails.pcoId &&
@@ -395,13 +420,14 @@ export const SourceInfo = ({
                                   filteredEnvIds={filteredEnvIds}
                               />
                           )}
-                          {!appDetails?.deploymentAppDeleteRequest && !appMigratedFromExternalSourceAndIsNotTriggered && (
-                              <SecurityVulnerabilityCard
-                                  cardLoading={cardLoading}
-                                  appId={params.appId}
-                                  envId={params.envId}
-                              />
-                          )}
+                          {!appDetails?.deploymentAppDeleteRequest &&
+                              !appMigratedFromExternalSourceAndIsNotTriggered && (
+                                  <SecurityVulnerabilityCard
+                                      cardLoading={cardLoading}
+                                      appId={params.appId}
+                                      envId={params.envId}
+                                  />
+                              )}
                           <div className="flex right ml-auto">
                               {appDetails?.appStoreChartId && (
                                   <>
diff --git a/src/components/app/details/appDetails/constants.ts b/src/components/app/details/appDetails/constants.ts
index 31312bd408..571a274ef3 100644
--- a/src/components/app/details/appDetails/constants.ts
+++ b/src/components/app/details/appDetails/constants.ts
@@ -14,4 +14,25 @@
  * limitations under the License.
  */
 
+import ReactGA from 'react-ga4'
+
 export const APP_METRICS_CALENDAR_INPUT_DATE_FORMAT = 'DD-MM-YYYY hh:mm:ss'
+
+export const DA_APP_DETAILS_GA_EVENTS: Record<string, Parameters<typeof ReactGA.event>[0]> = {
+    DeployButtonClicked: {
+        category: 'App Details',
+        action: 'DA_APP_DETAIL_DEPLOY',
+    },
+    GoToEnvironmentConfiguration: {
+        category: 'App Details',
+        action: 'DA_APP_DETAIL_GO_TO_ENV_CONFIG',
+    },
+    MetricsApplyTimeChange: {
+        category: 'App Metrics',
+        action: 'DA_APP_DETAIL_METRICS_APPLY_TIME_RANGE',
+    },
+    MetricsPresetTimeRange: {
+        category: 'App Metrics',
+        action: 'DA_APP_DETAIL_METRICS_PRESET_TIME_RANGE',
+    },
+}
diff --git a/src/components/common/DatePickers/DayPickerRangeController.tsx b/src/components/common/DatePickers/DayPickerRangeController.tsx
index 7ec1314c30..81204d69a0 100644
--- a/src/components/common/DatePickers/DayPickerRangeController.tsx
+++ b/src/components/common/DatePickers/DayPickerRangeController.tsx
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import React, { Component } from 'react'
+import { Component } from 'react'
 import 'react-dates/initialize'
 import 'react-dates/lib/css/_datepicker.css'
 import CustomizableCalendarDay from 'react-dates/esm/components/CustomizableCalendarDay.js'
@@ -23,6 +23,7 @@ import moment, { Moment } from 'moment'
 import { isInclusivelyBeforeDay, DayPickerRangeController } from 'react-dates'
 import './calendar.css'
 import { ReactComponent as ArrowDown } from '../../../assets/icons/ic-chevron-down.svg'
+import { DA_APP_DETAILS_GA_EVENTS } from '@Components/app/details/appDetails/constants'
 
 interface DatePickerType2Props {
     calendar
@@ -126,12 +127,16 @@ export class DatePickerType2 extends Component<DatePickerType2Props, any> {
         this.renderDatePresets = this.renderDatePresets.bind(this)
     }
 
-    getInitialVisibleMonth = () => {
-        return this.props.calendar.endDate
+    onClickApplyTimeChange = () => {
+        this.setState({ showCalendar: false })
+        this.props.handleApply()
+        ReactGA.event(DA_APP_DETAILS_GA_EVENTS.MetricsApplyTimeChange)
     }
 
-    handleIsDayBlocked(day) {
-        return false
+    onClickPredefinedTimeRange = (startDate: Moment, endDate: Moment, endStr: string) => () => {
+        ReactGA.event(DA_APP_DETAILS_GA_EVENTS.MetricsPresetTimeRange)
+        this.props.handlePredefinedRange(startDate, endDate, endStr)
+        this.setState({ showCalendar: false })
     }
 
     renderDatePresets() {
@@ -176,14 +181,7 @@ export class DatePickerType2 extends Component<DatePickerType2Props, any> {
                                 }}
                             />
                         </label>
-                        <button
-                            type="button"
-                            className="cta small"
-                            onClick={() => {
-                                this.setState({ showCalendar: false })
-                                this.props.handleApply()
-                            }}
-                        >
+                        <button type="button" className="cta small" onClick={this.onClickApplyTimeChange}>
                             Apply Time Range
                         </button>
                     </div>
@@ -209,15 +207,7 @@ export class DatePickerType2 extends Component<DatePickerType2Props, any> {
                                 type="button"
                                 key={text}
                                 style={{ ...buttonStyles, textAlign: 'left' }}
-                                onClick={() => {
-                                    ReactGA.event({
-                                        category: 'Deployment Metrics',
-                                        action: 'Date Range Changed',
-                                        label: 'Predefined',
-                                    })
-                                    this.props.handlePredefinedRange(startDate, endDate, endStr)
-                                    this.setState({ showCalendar: false })
-                                }}
+                                onClick={this.onClickPredefinedTimeRange(startDate, endDate, endStr)}
                             >
                                 {text}
                             </button>
diff --git a/yarn.lock b/yarn.lock
index 4fb89f1afd..f276934642 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1126,10 +1126,10 @@
   dependencies:
     "@jridgewell/trace-mapping" "0.3.9"
 
-"@devtron-labs/devtron-fe-common-lib@1.11.0-pre-3":
-  version "1.11.0-pre-3"
-  resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.11.0-pre-3.tgz#cbad0dd604840f4c4101ea3ee217cf7d67b3bd17"
-  integrity sha512-L7i1VqQFXgrwUrm5KLZ9jGqtVgqJSps/LjbycprN8fiuNkDaPZCb3uevcP8iTmVoxyr+wo83zoS0U+sNcdP8fg==
+"@devtron-labs/devtron-fe-common-lib@1.11.0-pre-4":
+  version "1.11.0-pre-4"
+  resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.11.0-pre-4.tgz#35a29201d47748fde6fee40d443e54d90eb4d63c"
+  integrity sha512-I6u/ZXCKD+Vg0UEbFC+Pwt6ZGUa7nRBtG4ZWgNrAxNGFqnmbTl3odwfQCvVXkHe9FisQIcwRkH1E/Yl+JjeJuQ==
   dependencies:
     "@codemirror/lang-json" "6.0.1"
     "@codemirror/lang-yaml" "6.1.2"


Reviews:
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:03:09Z
user: login: AbhishekA1509
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:03:49Z
user: login: vivek-devtron
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:45:32Z
user: login: AbhishekA1509
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-15T11:09:18Z
user: login: AbhishekA1509
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T12:53:51Z
user: login: vivek-devtron
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:01:15Z

Review comments:
diff_hunk: @@ -151,6 +154,10 @@ export const SourceInfo = ({
         return <div className="flex left mb-16">{loadingCards}</div>
     }
 
+    const onClickSliderVerticalButton = () => {
+        history.push(`${URLS.APP}/${params.appId}/edit/${URLS.APP_ENV_OVERRIDE_CONFIG}/${params.envId}`)
path: src/components/app/details/appDetails/SourceInfo.tsx
user: login: AbhishekA1509
body: We can use getAppComposeURL to get path till APP_ENV_OVERRIDE_CONFIG
created_at: 2025-04-14T15:03:08Z
author_association: MEMBER
diff_hunk: @@ -278,6 +285,16 @@ export const SourceInfo = ({
                                         }}
                                     />
                                 )}
+                                <Button
path: src/components/app/details/appDetails/SourceInfo.tsx
user: login: AbhishekA1509
body: Pls add suitable test id
created_at: 2025-04-14T15:03:48Z
author_association: MEMBER
diff_hunk: @@ -38,6 +38,7 @@ const ClusterList = ({
     return (
         <div data-testid="cluster-list-container" className="flexbox-col flex-grow-1">
             <div className="cluster-list-row fw-6 cn-7 fs-12 dc__border-bottom pt-8 pb-8 pr-20 pl-20 dc__uppercase bg__primary dc__position-sticky dc__top-0 dc__zi-3">
+                {KubeConfigRowCheckbox === null && <div />}
path: src/components/ClusterNodes/ClusterList/ClusterList.tsx
user: login: AbhishekA1509
body: Ideally ternary check should be here

created_at: 2025-04-15T11:08:56Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-15T12:53:58Z
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=2649) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=devtron-labs_dashboard&pullRequest=2649&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=2649&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=2649&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=2649&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=2649&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=devtron-labs_dashboard&pullRequest=2649)


author_association: NONE