Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReactNode } from 'react'

import { ButtonVariantType, ComponentSizeType, Icon, InfoBlock } from '@devtron-labs/devtron-fe-common-lib'

export const ClusterFormNavButton = ({
Expand All @@ -8,7 +10,7 @@ export const ClusterFormNavButton = ({
}: {
isActive: boolean
title: string
subtitle?: string
subtitle?: ReactNode
onClick: () => void
}) => (
<button
Expand All @@ -17,7 +19,7 @@ export const ClusterFormNavButton = ({
onClick={onClick}
>
<span className={`fs-13 ${isActive ? 'cb-5 fw-6' : 'cn-9'}`}>{title}</span>
{subtitle && <span className="fs-12 cn-7">{subtitle}</span>}
{typeof subtitle === 'string' ? <span className="fs-12 cn-7">{subtitle}</span> : subtitle}
</button>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
*/

import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'

import {
AuthenticationType,
Button,
ButtonStyleType,
ButtonVariantType,
ClusterCostModuleConfigPayload,

Check failure on line 25 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Module '"@devtron-labs/devtron-fe-common-lib"' has no exported member 'ClusterCostModuleConfigPayload'.
ClusterDetailListType,
DEFAULT_SECRET_PLACEHOLDER,
Icon,
ModalSidebarPanel,
Expand All @@ -32,6 +35,8 @@
showError,
ToastManager,
ToastVariantType,
Tooltip,
URLS,
useAsync,
} from '@devtron-labs/devtron-fe-common-lib'

Expand Down Expand Up @@ -76,10 +81,25 @@
isTlsConnection: initialIsTlsConnection = false,
installationId,
category,
clusterProvider,
costModuleConfig,
}: ClusterFormProps) => {
const [clusterConfigTab, setClusterConfigTab] = useState<ClusterConfigTabEnum>(ClusterConfigTabEnum.CLUSTER_CONFIG)
const location = useLocation()

const [clusterConfigTab, setClusterConfigTab] = useState<ClusterConfigTabEnum>(
id && location.pathname.includes(URLS.COST_VISIBILITY)
? ClusterConfigTabEnum.COST_VISIBILITY
: ClusterConfigTabEnum.CLUSTER_CONFIG,
)

const [costModuleState, setCostModuleState] = useState<
Pick<ClusterDetailListType['costModuleConfig'], 'enabled'> & { config: string }

Check failure on line 96 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'costModuleConfig' does not exist on type 'ClusterDetailListType'.
>({
enabled: costModuleConfig?.enabled || false,

Check failure on line 98 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'enabled' does not exist on type 'unknown'.
config: costModuleConfig?.config ? JSON.stringify(costModuleConfig.config) : '',

Check failure on line 99 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'config' does not exist on type 'unknown'.

Check failure on line 99 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'config' does not exist on type 'unknown'.
})
const [costModuleConfigErrorState, setCostModuleErrorState] = useState<string>('')

const [costModuleEnabled, setCostModuleEnabled] = useState(false)
const [prometheusToggleEnabled, setPrometheusToggleEnabled] = useState(!!prometheusUrl)
const [prometheusAuthenticationType, setPrometheusAuthenticationType] = useState({
type: prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS,
Expand Down Expand Up @@ -159,6 +179,49 @@
setIsConnectedViaSSHTunnelTemp(false)
}

const validateCostModuleConfig = (requiredConfig: string = costModuleState.config): string => {
try {
if (requiredConfig) {
JSON.parse(requiredConfig)
}

return ''
} catch (e) {
return e.message || 'Invalid JSON'
}
}

const getParsedConfigValue = (): ClusterCostModuleConfigPayload['config'] => {
if (costModuleState.config) {
try {
const parsedConfig = JSON.parse(costModuleState.config)
return parsedConfig
} catch {
return {}
}
}
return null
}

const getCostModulePayload = (): ClusterCostModuleConfigPayload | null => {
if (!costModuleState.enabled) {
return {
enabled: false,
}
}

if (costModuleState.config) {
return {
enabled: true,
config: getParsedConfigValue(),
}
}

return {
enabled: true,
}
}

const getClusterPayload = (state) => ({
id,
insecureSkipTlsVerify: !isTlsConnection,
Expand Down Expand Up @@ -189,6 +252,7 @@
},
server_url: '',
...(getCategoryPayload ? getCategoryPayload(selectedCategory) : null),
...(clusterProvider ? { costModuleConfig: getCostModulePayload() } : null),
})

const onValidation = async (state) => {
Expand All @@ -199,6 +263,19 @@
} else {
payload.server_url = urlValue
}

if (costModuleState.enabled) {
const costConfigError = validateCostModuleConfig()
if (costConfigError) {
setCostModuleErrorState(costConfigError)
ToastManager.showToast({
variant: ToastVariantType.error,
description: 'Invalid cost visibility configuration',
})
return
}
}

if (remoteConnectionMethod === RemoteConnectionType.Proxy) {
let proxyUrlValue = state.proxyUrl?.value?.trim() ?? ''
if (proxyUrlValue.endsWith('/')) {
Expand Down Expand Up @@ -404,6 +481,23 @@
setClusterConfigTab(tab)
}

const toggleCostModule = () => {
setCostModuleState((prev) => ({
...prev,
enabled: !prev.enabled,
}))
}

const handleCostConfigChange = (newConfig: string) => {
setCostModuleState((prev) => ({
...prev,
config: newConfig,
}))

const error = validateCostModuleConfig(newConfig)
setCostModuleErrorState(error)
}

const renderFooter = () => (
<div className={`border__primary--top flexbox py-12 px-20 ${id ? 'dc__content-space' : 'dc__content-end'}`}>
{id && (
Expand Down Expand Up @@ -490,8 +584,13 @@
handleOnChange={handleOnChange}
onPrometheusAuthTypeChange={onPrometheusAuthTypeChange}
isGrafanaModuleInstalled={isGrafanaModuleInstalled}
costModuleEnabled={costModuleEnabled}
setCostModuleEnabled={setCostModuleEnabled}
costModuleEnabled={costModuleState.enabled}
toggleCostModule={toggleCostModule}
installationStatus={costModuleConfig.installationStatus}

Check failure on line 589 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'installationStatus' does not exist on type 'unknown'.
clusterProvider={clusterProvider}
handleCostConfigChange={handleCostConfigChange}
config={costModuleState.config || ''}
configError={costModuleConfigErrorState}
/>
</div>
) : null
Expand All @@ -500,6 +599,35 @@
}
}

const getCostNavSubtitle = () => {
if (!costModuleState.enabled) {
return 'Off'
}

if (costModuleConfig.installationStatus === 'Installing') {

Check failure on line 607 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'installationStatus' does not exist on type 'unknown'.
return <span className="cy-7 fs-12">Installing...</span>
}

if (costModuleConfig.installationStatus === 'Upgrading') {

Check failure on line 611 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'installationStatus' does not exist on type 'unknown'.
return <span className="cy-7 fs-12">Upgrading...</span>
}

if (costModuleConfig.installationStatus === 'Failed') {

Check failure on line 615 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'installationStatus' does not exist on type 'unknown'.
return (
<div className="flexbox dc__gap-4 dc__align-items-center">
<Icon name="ic-error" size={14} color="R500" />
<Tooltip content={costModuleConfig.installationError}>

Check failure on line 619 in src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm/ClusterForm.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'installationError' does not exist on type 'unknown'.
<span className="dc__truncate cr-5 fs-12">
Installation Error: {costModuleConfig.installationError}
</span>
</Tooltip>
</div>
)
}

return 'Enabled'
}

return (
<>
<div className="flexbox mh-0 flex-grow-1 dc__overflow-hidden">
Expand All @@ -517,7 +645,7 @@
title="Cluster Configurations"
onClick={getTabSwitchHandler(ClusterConfigTabEnum.CLUSTER_CONFIG)}
/>
<div className="divder__secondary--horizontal" />
<div className="divider__secondary--horizontal" />
<div className="flexbox-col">
<div className="px-8 py-4 fs-12 fw-6 lh-20 cn-7">INTEGRATIONS</div>
<ClusterFormNavButton
Expand All @@ -526,11 +654,11 @@
subtitle={prometheusToggleEnabled ? 'Enabled' : 'Off'}
onClick={getTabSwitchHandler(ClusterConfigTabEnum.APPLICATION_MONITORING)}
/>
{ClusterCostConfig && (
{ClusterCostConfig && id && (
<ClusterFormNavButton
isActive={clusterConfigTab === ClusterConfigTabEnum.COST_VISIBILITY}
title="Cost Visibility"
subtitle={costModuleEnabled ? 'Enabled' : 'Off'}
subtitle={getCostNavSubtitle()}
onClick={getTabSwitchHandler(ClusterConfigTabEnum.COST_VISIBILITY)}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export const EditCluster = ({ clusterList, reloadClusterList, handleClose }: Edi
installationId={cluster.installationId}
category={cluster.category}
insecureSkipTlsVerify={cluster.insecureSkipTlsVerify}
costModuleConfig={cluster.costModuleConfig}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
* limitations under the License.
*/

import { APIResponseHandler, noop, useAsync } from '@devtron-labs/devtron-fe-common-lib'
import { APIResponseHandler, noop, useQuery } from '@devtron-labs/devtron-fe-common-lib'

import { importComponentFromFELibrary } from '@Components/common'
import { URLS } from '@Config/routes'

import ClusterForm from './ClusterForm/ClusterForm'
import { getCluster } from './cluster.service'
import { EditClusterDrawerContentProps, EditClusterFormProps } from './cluster.type'
import { getEditClusterDrawerMetadata } from './cluster.service'
import { EditClusterDrawerContentProps, EditClusterDrawerMetadataType, EditClusterFormProps } from './cluster.type'

const getSSHConfig: (
...props
Expand All @@ -42,23 +42,35 @@ const EditClusterDrawerContent = ({
installationId,
category,
insecureSkipTlsVerify,
costModuleConfig,
}: EditClusterDrawerContentProps) => {
const [isPrometheusAuthLoading, prometheusAuthResult, prometheusAuthError, reloadPrometheusAuth] = useAsync(
() => getCluster(+clusterId),
[clusterId],
!!clusterId,
)
const {
isFetching: isMetadataLoading,
data: metadata,
error: metadataError,
refetch: reloadMetadata,
} = useQuery<EditClusterDrawerMetadataType, EditClusterDrawerMetadataType, [string, number], false>({
queryKey: ['get-cluster-metadata', clusterId],
queryFn: () => getEditClusterDrawerMetadata(clusterId),
enabled: !!clusterId,
})

const { prometheusAuthResult, clusterProvider } = metadata || {
prometheusAuthResult: null,
cloudProvider: null,
}

return (
<APIResponseHandler
isLoading={isPrometheusAuthLoading}
isLoading={isMetadataLoading}
progressingProps={{
pageLoader: true,
}}
error={prometheusAuthError?.code}
error={metadataError}
errorScreenManagerProps={{
code: metadataError?.code,
redirectURL: URLS.GLOBAL_CONFIG_CLUSTER,
reload: reloadPrometheusAuth,
reload: reloadMetadata,
}}
>
<ClusterForm
Expand All @@ -76,6 +88,8 @@ const EditClusterDrawerContent = ({
isTlsConnection={!insecureSkipTlsVerify}
installationId={installationId}
category={category}
clusterProvider={clusterProvider}
costModuleConfig={costModuleConfig}
/>
</APIResponseHandler>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@
* limitations under the License.
*/

import { get, post, put, trash } from '@devtron-labs/devtron-fe-common-lib'
import { ClusterProviderType, get, post, put, trash } from '@devtron-labs/devtron-fe-common-lib'

import { importComponentFromFELibrary } from '@Components/common'
import { Routes } from '@Config/constants'

import { DeleteClusterPayload, Environment, EnvironmentDTO } from './cluster.type'
import { DeleteClusterPayload, EditClusterDrawerMetadataType, Environment, EnvironmentDTO } from './cluster.type'

const getCloudProviderForCluster: (clusterId: number) => Promise<ClusterProviderType> = importComponentFromFELibrary(
'getCloudProviderForCluster',
null,
'function',
)

export const getEnvironmentList = async (): Promise<Environment[]> => {
const { result } = await get<EnvironmentDTO[]>(Routes.ENVIRONMENT)
Expand Down Expand Up @@ -86,3 +93,15 @@ export function deleteCluster(payload: DeleteClusterPayload): Promise<any> {
export function deleteEnvironment(request): Promise<any> {
return trash(Routes.ENVIRONMENT, request)
}

export const getEditClusterDrawerMetadata = async (clusterId: number): Promise<EditClusterDrawerMetadataType> => {
if (!clusterId) {
return { prometheusAuthResult: null, clusterProvider: null }
}

const [prometheusAuthResult, clusterProvider] = await Promise.all([
getCluster(+clusterId),
getCloudProviderForCluster ? getCloudProviderForCluster(+clusterId) : null,
])
return { prometheusAuthResult, clusterProvider }
}
Loading
Loading