Skip to content

Commit d18a61a

Browse files
committed
feat: add cost module configuration to ClusterForm, EditClusterDrawerContent, and NavigationRoutes
1 parent 1c718c7 commit d18a61a

File tree

5 files changed

+158
-33
lines changed

5 files changed

+158
-33
lines changed

src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
*/
1616

1717
import { useEffect, useState } from 'react'
18+
import { useLocation } from 'react-router-dom'
1819

1920
import {
2021
AuthenticationType,
2122
Button,
2223
ButtonStyleType,
2324
ButtonVariantType,
25+
ClusterCostModuleConfigDTO,
26+
ClusterDetailListType,
2427
DEFAULT_SECRET_PLACEHOLDER,
2528
Icon,
2629
ModalSidebarPanel,
@@ -32,6 +35,7 @@ import {
3235
showError,
3336
ToastManager,
3437
ToastVariantType,
38+
URLS,
3539
useAsync,
3640
} from '@devtron-labs/devtron-fe-common-lib'
3741

@@ -76,10 +80,29 @@ const ClusterForm = ({
7680
isTlsConnection: initialIsTlsConnection = false,
7781
installationId,
7882
category,
83+
clusterProvider,
84+
costModuleConfig,
7985
}: ClusterFormProps) => {
80-
const [clusterConfigTab, setClusterConfigTab] = useState<ClusterConfigTabEnum>(ClusterConfigTabEnum.CLUSTER_CONFIG)
86+
const location = useLocation()
8187

82-
const [costModuleEnabled, setCostModuleEnabled] = useState(false)
88+
const [clusterConfigTab, setClusterConfigTab] = useState<ClusterConfigTabEnum>(
89+
id && location.pathname.includes(URLS.COST_VISIBILITY)
90+
? ClusterConfigTabEnum.COST_VISIBILITY
91+
: ClusterConfigTabEnum.CLUSTER_CONFIG,
92+
)
93+
94+
const [costModuleState, setCostModuleState] = useState<
95+
Pick<ClusterDetailListType['costModuleConfig'], 'config' | 'enabled'>
96+
>({
97+
enabled: costModuleConfig?.enabled || false,
98+
config: {
99+
cloudProviderApiKey: costModuleConfig?.config?.cloudProviderApiKey || '',
100+
},
101+
})
102+
103+
const [costModuleErrorState, setCostModuleErrorState] = useState<{ cloudProviderApiKey: string }>({
104+
cloudProviderApiKey: '',
105+
})
83106
const [prometheusToggleEnabled, setPrometheusToggleEnabled] = useState(!!prometheusUrl)
84107
const [prometheusAuthenticationType, setPrometheusAuthenticationType] = useState({
85108
type: prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS,
@@ -159,6 +182,27 @@ const ClusterForm = ({
159182
setIsConnectedViaSSHTunnelTemp(false)
160183
}
161184

185+
const getCostModulePayload = (): ClusterCostModuleConfigDTO | null => {
186+
if (!costModuleState.enabled) {
187+
return {
188+
enabled: false,
189+
}
190+
}
191+
192+
if (clusterProvider === 'GCP' && costModuleState.config.cloudProviderApiKey) {
193+
return {
194+
enabled: true,
195+
config: {
196+
cloudProviderApiKey: costModuleState.config.cloudProviderApiKey,
197+
},
198+
}
199+
}
200+
201+
return {
202+
enabled: true,
203+
}
204+
}
205+
162206
const getClusterPayload = (state) => ({
163207
id,
164208
insecureSkipTlsVerify: !isTlsConnection,
@@ -189,6 +233,7 @@ const ClusterForm = ({
189233
},
190234
server_url: '',
191235
...(getCategoryPayload ? getCategoryPayload(selectedCategory) : null),
236+
...(clusterProvider ? { costModuleConfig: getCostModulePayload() } : null),
192237
})
193238

194239
const onValidation = async (state) => {
@@ -199,6 +244,19 @@ const ClusterForm = ({
199244
} else {
200245
payload.server_url = urlValue
201246
}
247+
248+
if (clusterProvider === 'GCP' && costModuleState.enabled && !costModuleState.config.cloudProviderApiKey) {
249+
setCostModuleErrorState((prev) => ({
250+
...prev,
251+
cloudProviderApiKey: 'Cloud Provider API Key is required',
252+
}))
253+
ToastManager.showToast({
254+
variant: ToastVariantType.error,
255+
description: 'Please provide Cloud Provider API Key to enable cost tracking',
256+
})
257+
return
258+
}
259+
202260
if (remoteConnectionMethod === RemoteConnectionType.Proxy) {
203261
let proxyUrlValue = state.proxyUrl?.value?.trim() ?? ''
204262
if (proxyUrlValue.endsWith('/')) {
@@ -404,6 +462,27 @@ const ClusterForm = ({
404462
setClusterConfigTab(tab)
405463
}
406464

465+
const toggleCostModule = () => {
466+
setCostModuleState((prev) => ({
467+
...prev,
468+
enabled: !prev.enabled,
469+
}))
470+
}
471+
472+
const handleProviderAPIKeyChange = (apiKey: string) => {
473+
setCostModuleState((prev) => ({
474+
...prev,
475+
config: {
476+
cloudProviderApiKey: apiKey,
477+
},
478+
}))
479+
480+
setCostModuleErrorState((prev) => ({
481+
...prev,
482+
cloudProviderApiKey: apiKey ? '' : 'Cloud Provider API Key is required',
483+
}))
484+
}
485+
407486
const renderFooter = () => (
408487
<div className={`border__primary--top flexbox py-12 px-20 ${id ? 'dc__content-space' : 'dc__content-end'}`}>
409488
{id && (
@@ -490,8 +569,14 @@ const ClusterForm = ({
490569
handleOnChange={handleOnChange}
491570
onPrometheusAuthTypeChange={onPrometheusAuthTypeChange}
492571
isGrafanaModuleInstalled={isGrafanaModuleInstalled}
493-
costModuleEnabled={costModuleEnabled}
494-
setCostModuleEnabled={setCostModuleEnabled}
572+
costModuleEnabled={costModuleState.enabled}
573+
toggleCostModule={toggleCostModule}
574+
installationStatus={costModuleConfig.installationStatus}
575+
installationError={costModuleConfig.installationError}
576+
clusterProvider={clusterProvider}
577+
handleProviderAPIKeyChange={handleProviderAPIKeyChange}
578+
providerAPIKey={costModuleState.config.cloudProviderApiKey || ''}
579+
providerAPIKeyError={costModuleErrorState.cloudProviderApiKey}
495580
/>
496581
</div>
497582
) : null
@@ -517,7 +602,7 @@ const ClusterForm = ({
517602
title="Cluster Configurations"
518603
onClick={getTabSwitchHandler(ClusterConfigTabEnum.CLUSTER_CONFIG)}
519604
/>
520-
<div className="divder__secondary--horizontal" />
605+
<div className="divider__secondary--horizontal" />
521606
<div className="flexbox-col">
522607
<div className="px-8 py-4 fs-12 fw-6 lh-20 cn-7">INTEGRATIONS</div>
523608
<ClusterFormNavButton
@@ -526,11 +611,11 @@ const ClusterForm = ({
526611
subtitle={prometheusToggleEnabled ? 'Enabled' : 'Off'}
527612
onClick={getTabSwitchHandler(ClusterConfigTabEnum.APPLICATION_MONITORING)}
528613
/>
529-
{ClusterCostConfig && (
614+
{ClusterCostConfig && id && (
530615
<ClusterFormNavButton
531616
isActive={clusterConfigTab === ClusterConfigTabEnum.COST_VISIBILITY}
532617
title="Cost Visibility"
533-
subtitle={costModuleEnabled ? 'Enabled' : 'Off'}
618+
subtitle={costModuleState.enabled ? 'Enabled' : 'Off'}
534619
onClick={getTabSwitchHandler(ClusterConfigTabEnum.COST_VISIBILITY)}
535620
/>
536621
)}

src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterList.components.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export const EditCluster = ({ clusterList, reloadClusterList, handleClose }: Edi
348348
installationId={cluster.installationId}
349349
category={cluster.category}
350350
insecureSkipTlsVerify={cluster.insecureSkipTlsVerify}
351+
costModuleConfig={cluster.costModuleConfig}
351352
/>
352353
)
353354
}

src/Pages/GlobalConfigurations/ClustersAndEnvironments/EditClusterDrawerContent.tsx

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { APIResponseHandler, noop, useAsync } from '@devtron-labs/devtron-fe-common-lib'
17+
import {
18+
APIResponseHandler,
19+
ClusterProviderType,
20+
noop,
21+
ResponseType,
22+
useAsync,
23+
} from '@devtron-labs/devtron-fe-common-lib'
1824

1925
import { importComponentFromFELibrary } from '@Components/common'
2026
import { URLS } from '@Config/routes'
@@ -28,6 +34,12 @@ const getSSHConfig: (
2834
) => Pick<EditClusterFormProps, 'sshUsername' | 'sshPassword' | 'sshAuthKey' | 'sshServerAddress'> =
2935
importComponentFromFELibrary('getSSHConfig', noop, 'function')
3036

37+
const getCloudProviderForCluster: (clusterId: number) => Promise<ClusterProviderType> = importComponentFromFELibrary(
38+
'getCloudProviderForCluster',
39+
null,
40+
'function',
41+
)
42+
3143
const EditClusterDrawerContent = ({
3244
handleModalClose,
3345
sshTunnelConfig,
@@ -42,23 +54,44 @@ const EditClusterDrawerContent = ({
4254
installationId,
4355
category,
4456
insecureSkipTlsVerify,
57+
costModuleConfig,
4558
}: EditClusterDrawerContentProps) => {
46-
const [isPrometheusAuthLoading, prometheusAuthResult, prometheusAuthError, reloadPrometheusAuth] = useAsync(
47-
() => getCluster(+clusterId),
59+
const getClusterMetadata = async (): Promise<{
60+
prometheusAuthResult: ResponseType
61+
clusterProvider: ClusterProviderType
62+
}> => {
63+
if (!clusterId) {
64+
return { prometheusAuthResult: null, clusterProvider: null }
65+
}
66+
67+
const [prometheusAuthResult, clusterProvider] = await Promise.all([
68+
getCluster(+clusterId),
69+
getCloudProviderForCluster ? getCloudProviderForCluster(+clusterId) : null,
70+
])
71+
return { prometheusAuthResult, clusterProvider }
72+
}
73+
74+
const [isMetadataLoading, metadata, metadataError, reloadMetadata] = useAsync(
75+
() => getClusterMetadata(),
4876
[clusterId],
4977
!!clusterId,
5078
)
5179

80+
const { prometheusAuthResult, clusterProvider } = metadata || {
81+
prometheusAuthResult: null,
82+
cloudProvider: null,
83+
}
84+
5285
return (
5386
<APIResponseHandler
54-
isLoading={isPrometheusAuthLoading}
87+
isLoading={isMetadataLoading}
5588
progressingProps={{
5689
pageLoader: true,
5790
}}
58-
error={prometheusAuthError?.code}
91+
error={metadataError?.code}
5992
errorScreenManagerProps={{
6093
redirectURL: URLS.GLOBAL_CONFIG_CLUSTER,
61-
reload: reloadPrometheusAuth,
94+
reload: reloadMetadata,
6295
}}
6396
>
6497
<ClusterForm
@@ -76,6 +109,8 @@ const EditClusterDrawerContent = ({
76109
isTlsConnection={!insecureSkipTlsVerify}
77110
installationId={installationId}
78111
category={category}
112+
clusterProvider={clusterProvider}
113+
costModuleConfig={costModuleConfig}
79114
/>
80115
</APIResponseHandler>
81116
)

src/Pages/GlobalConfigurations/ClustersAndEnvironments/cluster.type.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { RouteComponentProps } from 'react-router-dom'
2020
import {
2121
ClusterDetailListType,
2222
ClusterEnvironmentCategoryType,
23+
ClusterProviderType,
2324
EnvListMinDTO,
2425
FiltersTypeEnum,
2526
OptionType,
@@ -176,6 +177,26 @@ export interface ClusterTerminalParamsType {
176177

177178
export const RemoteConnectionTypeCluster = 'cluster'
178179

180+
export interface EditClusterDrawerContentProps
181+
extends Pick<
182+
ClusterDetailListType,
183+
| 'sshTunnelConfig'
184+
| 'insecureSkipTlsVerify'
185+
| 'category'
186+
| 'isProd'
187+
| 'installationId'
188+
| 'toConnectWithSSHTunnel'
189+
| 'proxyUrl'
190+
| 'prometheusUrl'
191+
| 'serverUrl'
192+
| 'clusterName'
193+
| 'clusterId'
194+
| 'costModuleConfig'
195+
> {
196+
reload: () => void
197+
handleModalClose: () => void
198+
}
199+
179200
export type EditClusterFormProps = {
180201
id: number
181202
isProd?: boolean
@@ -190,7 +211,8 @@ export type EditClusterFormProps = {
190211
sshServerAddress: string
191212
isConnectedViaSSHTunnel: boolean
192213
isTlsConnection: boolean
193-
}
214+
clusterProvider: ClusterProviderType
215+
} & Pick<EditClusterDrawerContentProps, 'costModuleConfig'>
194216

195217
export type ClusterFormProps = { reload: () => void; handleModalClose: () => void } & Pick<
196218
ClusterMetadataTypes,
@@ -234,25 +256,6 @@ export interface UserNameDropDownListProps {
234256
onChangeUserName: (selectedOption: any, clusterDetail: DataListType) => void
235257
}
236258

237-
export interface EditClusterDrawerContentProps
238-
extends Pick<
239-
ClusterDetailListType,
240-
| 'sshTunnelConfig'
241-
| 'insecureSkipTlsVerify'
242-
| 'category'
243-
| 'isProd'
244-
| 'installationId'
245-
| 'toConnectWithSSHTunnel'
246-
| 'proxyUrl'
247-
| 'prometheusUrl'
248-
| 'serverUrl'
249-
| 'clusterName'
250-
| 'clusterId'
251-
> {
252-
reload: () => void
253-
handleModalClose: () => void
254-
}
255-
256259
export interface EnvironmentDTO {
257260
id: number
258261
environment_name: string

src/components/common/navigation/NavigationRoutes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ const NavigationRoutes = ({ reloadVersionConfig }: Readonly<NavigationRoutesType
526526
installationId={clusterDetails.installationId}
527527
category={clusterDetails.category}
528528
insecureSkipTlsVerify={clusterDetails.insecureSkipTlsVerify}
529+
costModuleConfig={clusterDetails.costModuleConfig}
529530
/>
530531
)
531532

0 commit comments

Comments
 (0)