{baseTemplateConfiguration?.codeEditorValue?.displayName}
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/helpers.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/helpers.tsx
new file mode 100644
index 000000000..ca5b08981
--- /dev/null
+++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/helpers.tsx
@@ -0,0 +1,96 @@
+import { NavLink } from 'react-router-dom'
+
+import { ReactComponent as ICFileCode } from '@Icons/ic-file-code.svg'
+import { ReactComponent as ICDocker } from '@Icons/ic-docker.svg'
+import {
+ DeploymentConfigDiffProps,
+ DeploymentConfigDiffState,
+ diffStateIconMap,
+ diffStateTextColorMap,
+ diffStateTextMap,
+} from '@Shared/Components/DeploymentConfigDiff'
+
+import { History } from '../types'
+import { DeploymentHistoryConfigDiffProps } from './types'
+
+const renderState = (diffState: DeploymentConfigDiffState) => {
+ const Icon = diffStateIconMap[diffState]
+
+ return (
+
+ {Icon && }
+ {diffStateTextMap[diffState]}
+
+ )
+}
+
+export const renderDeploymentHistoryConfig = (
+ config: DeploymentConfigDiffProps['configList'],
+ heading: string,
+ pathname: string,
+ hideDiffState: boolean,
+) => (
+
+ {heading && (
+
+
{heading}
+
+ )}
+ {config.map(({ id, title, name, diffState, pathType }, index) => {
+ const href = `${pathname}/${name ? `${pathType}/${name}` : pathType}`
+
+ return (
+
+
+
+
+ {name || title}
+
+
+ {!hideDiffState && renderState(diffState)}
+
+ )
+ })}
+
+)
+
+export const renderPipelineDeploymentStatusIcon = (status: string) => (
+
+)
+
+export const renderPipelineDeploymentOptionDescription = ({
+ stage,
+ triggeredBy,
+ triggeredByEmail,
+ artifact,
+ renderRunSource,
+ resourceId,
+ runSource,
+}: Pick
&
+ Pick) => (
+
+
+ {stage}
+
+ {artifact && (
+
+
+ {artifact.split(':')[1].slice(-12)}
+
+ )}
+
+ {triggeredBy === 1 ? 'auto trigger' : triggeredByEmail}
+
+ {runSource && renderRunSource && renderRunSource(runSource, resourceId === runSource?.id)}
+
+)
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/index.ts b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/index.ts
new file mode 100644
index 000000000..ef8a5ead2
--- /dev/null
+++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/index.ts
@@ -0,0 +1,2 @@
+export * from './DeploymentHistoryConfigDiff'
+export { default as DeploymentHistoryDiffView } from './DeploymentHistoryDiffView'
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/styles.scss b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/styles.scss
similarity index 100%
rename from src/Shared/Components/CICDHistory/DeploymentHistoryDiff/styles.scss
rename to src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/styles.scss
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/types.ts b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/types.ts
new file mode 100644
index 000000000..55f4bcbed
--- /dev/null
+++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/types.ts
@@ -0,0 +1,52 @@
+import { Dispatch, SetStateAction } from 'react'
+
+import { DeploymentConfigDiffProps } from '@Shared/Components/DeploymentConfigDiff'
+import { EnvResourceType } from '@Shared/Services'
+
+import { History, HistoryLogsProps } from '../types'
+
+export interface DeploymentHistoryConfigDiffProps
+ extends Required> {
+ appName: string
+ envName: string
+ pipelineId: number
+ wfrId: number
+ triggerHistory: Map
+ setFullScreenView: (fullscreen: boolean) => void
+}
+
+export type DeploymentHistoryDiffDetailedProps = Pick<
+ DeploymentConfigDiffProps,
+ 'collapsibleNavList' | 'configList' | 'errorConfig' | 'isLoading' | 'navList' | 'hideDiffState'
+> &
+ Required<
+ Pick<
+ DeploymentHistoryConfigDiffProps,
+ 'setFullScreenView' | 'wfrId' | 'envName' | 'renderRunSource' | 'resourceId' | 'triggerHistory'
+ >
+ > & {
+ pipelineDeployments: History[]
+ previousWfrId: number
+ convertVariables: boolean
+ setConvertVariables: Dispatch>
+ isCompareDeploymentConfigNotAvailable?: boolean
+ }
+
+export interface DeploymentHistoryConfigDiffQueryParams {
+ compareWfrId: number
+}
+
+export interface DeploymentHistoryConfigDiffRouteParams {
+ resourceType: EnvResourceType
+ resourceName: string
+}
+
+export interface DeploymentHistoryParamsType {
+ appId: string
+ pipelineId?: string
+ historyComponent?: string
+ baseConfigurationId?: string
+ historyComponentName?: string
+ envId?: string
+ triggerId?: string
+}
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/utils.ts b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/utils.ts
new file mode 100644
index 000000000..f3cc47d20
--- /dev/null
+++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/utils.ts
@@ -0,0 +1,73 @@
+import moment from 'moment'
+
+import { DATE_TIME_FORMATS, ERROR_STATUS_CODE } from '@Common/Constants'
+import { DeploymentStageType } from '@Shared/constants'
+import { SelectPickerOptionType } from '@Shared/Components/SelectPicker'
+
+import { History } from '../types'
+import { renderPipelineDeploymentOptionDescription, renderPipelineDeploymentStatusIcon } from './helpers'
+import { DeploymentHistoryConfigDiffProps } from './types'
+
+export const getPipelineDeployments = (triggerHistory: DeploymentHistoryConfigDiffProps['triggerHistory']) =>
+ Array.from(triggerHistory)
+ .filter(([, value]) => value.stage === DeploymentStageType.DEPLOY)
+ .map(([, value]) => value)
+
+export const getPipelineDeploymentsWfrIds = ({
+ pipelineDeployments,
+ wfrId,
+}: { pipelineDeployments: History[] } & Pick) => {
+ const currentDeploymentIndex = pipelineDeployments.findIndex(({ id }) => id === wfrId)
+ const previousWfrId = currentDeploymentIndex > -1 ? pipelineDeployments[currentDeploymentIndex + 1]?.id : null
+
+ return {
+ currentWfrId: wfrId,
+ previousWfrId,
+ }
+}
+
+export const getPipelineDeploymentsOptions = ({
+ pipelineDeployments,
+ wfrId,
+ renderRunSource,
+ resourceId,
+ triggerHistory,
+}: Required> & {
+ pipelineDeployments: History[]
+ wfrId: number
+}) => {
+ const currentDeploymentIndex = pipelineDeployments.findIndex(({ id }) => id === wfrId)
+ const previousDeployments = pipelineDeployments.slice(currentDeploymentIndex + 1)
+
+ const pipelineDeploymentsOptions: SelectPickerOptionType[] = previousDeployments.map(
+ ({ id, startedOn, stage, triggeredBy, triggeredByEmail, status, artifact }) => ({
+ value: id,
+ label: moment(startedOn).format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT),
+ description: renderPipelineDeploymentOptionDescription({
+ stage,
+ triggeredByEmail,
+ triggeredBy,
+ artifact,
+ renderRunSource,
+ resourceId,
+ runSource: triggerHistory.get(id).runSource,
+ }),
+ startIcon: renderPipelineDeploymentStatusIcon(status),
+ }),
+ )
+ const currentDeployment = moment(pipelineDeployments[currentDeploymentIndex].startedOn).format(
+ DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT,
+ )
+
+ return { currentDeployment, pipelineDeploymentsOptions }
+}
+
+export const parseDeploymentHistoryDiffSearchParams = (compareWfrId: number) => (searchParams: URLSearchParams) => ({
+ compareWfrId: +(searchParams.get('compareWfrId') || compareWfrId),
+})
+
+export const isDeploymentHistoryConfigDiffNotFoundError = (res: PromiseSettledResult) =>
+ res.status === 'rejected' && res.reason?.code === ERROR_STATUS_CODE.NOT_FOUND
+
+export const getDeploymentHistoryConfigDiffError = (res: PromiseSettledResult) =>
+ res.status === 'rejected' && res.reason?.code !== ERROR_STATUS_CODE.NOT_FOUND ? res.reason : null
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryConfigList.component.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryConfigList.component.tsx
deleted file mode 100644
index 58875ffd3..000000000
--- a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryConfigList.component.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/* eslint-disable no-nested-ternary */
-import { useEffect, useState } from 'react'
-import { NavLink, useRouteMatch, useParams } from 'react-router-dom'
-import { GenericEmptyState, Progressing } from '../../../../Common'
-import { ReactComponent as ICChevron } from '../../../../Assets/Icon/ic-chevron-down.svg'
-import { DeploymentHistoryParamsType, TemplateConfiguration } from './types'
-import { getDeploymentHistoryList } from '../service'
-import { EMPTY_STATE_STATUS, DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP } from '../../../constants'
-
-const DeploymentHistoryConfigList = ({
- setFullScreenView,
- deploymentHistoryList,
- setDeploymentHistoryList,
-}: TemplateConfiguration) => {
- const match = useRouteMatch()
- const { appId, pipelineId, triggerId } = useParams()
- const [deploymentListLoader, setDeploymentListLoader] = useState(false)
-
- useEffect(() => {
- setDeploymentListLoader(true)
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- getDeploymentHistoryList(appId, pipelineId, triggerId).then((response) => {
- setDeploymentHistoryList(response.result)
- setDeploymentListLoader(false)
- })
- }, [triggerId])
-
- const getNavLink = (
- index: number,
- componentId: number,
- componentName: string,
- key: string,
- childComponentName?: string,
- ) => {
- const currentComponent = DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP[componentName]
- const configURL = `${match.url}/${currentComponent.VALUE}/${componentId}${
- childComponentName ? `/${childComponentName}` : ''
- }`
- return (
- {
- setFullScreenView(false)
- }}
- data-testid={`configuration-link-option-${index}`}
- className="bcb-1 dc__no-decor bcn-0 cn-9 pl-16 pr-16 pt-12 pb-12 br-4 en-2 bw-1 flex dc__content-space cursor lh-20"
- >
- {childComponentName || currentComponent.DISPLAY_NAME}
-
-
- )
- }
-
- return (
- // eslint-disable-next-line react/jsx-no-useless-fragment
- <>
- {!deploymentHistoryList && !deploymentListLoader ? (
-
- ) : deploymentListLoader ? (
-
- ) : (
- deploymentHistoryList &&
- deploymentHistoryList.map((historicalComponent, index) => (
- // eslint-disable-next-line react/no-array-index-key
-
- {historicalComponent.childList?.length > 0 ? (
- <>
-
- {DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP[historicalComponent.name].DISPLAY_NAME}
-
- {historicalComponent.childList.map((historicalComponentName, childIndex) =>
- getNavLink(
- index,
- historicalComponent.id,
- historicalComponent.name,
- `config-${index}-${childIndex}`,
- historicalComponentName,
- ),
- )}
- >
- ) : (
- getNavLink(index, historicalComponent.id, historicalComponent.name, `config-${index}`)
- )}
-
- ))
- )}
- >
- )
-}
-
-export default DeploymentHistoryConfigList
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryDetailedView.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryDetailedView.tsx
deleted file mode 100644
index 296aa3549..000000000
--- a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryDetailedView.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { useEffect, useState } from 'react'
-import { useParams } from 'react-router-dom'
-import { showError, Progressing } from '../../../../Common'
-import DeploymentHistoryHeader from './DeploymentHistoryHeader'
-import DeploymentHistoryDiffView from './DeploymentHistoryDiffView'
-import DeploymentHistorySidebar from './DeploymentHistorySidebar'
-import { CompareViewDeploymentType, DeploymentHistoryParamsType, DeploymentTemplateOptions } from './types'
-import { DeploymentHistoryDetail } from '../types'
-import { getDeploymentHistoryDetail, prepareHistoryData } from '../service'
-
-const DeploymentHistoryDetailedView = ({
- setFullScreenView,
- deploymentHistoryList,
- setDeploymentHistoryList,
- renderRunSource,
- resourceId,
-}: CompareViewDeploymentType) => {
- const { appId, pipelineId, historyComponent, baseConfigurationId, historyComponentName } =
- useParams()
- const [selectedDeploymentTemplate, setSelectedDeploymentTemplate] = useState()
- const [currentConfiguration, setCurrentConfiguration] = useState()
- const [baseTemplateConfiguration, setBaseTemplateConfiguration] = useState()
- const [previousConfigAvailable, setPreviousConfigAvailable] = useState(true)
- const [loader, setLoader] = useState(true)
-
- useEffect(() => {
- if (selectedDeploymentTemplate) {
- setLoader(true)
- if (selectedDeploymentTemplate.value === 'NA') {
- setLoader(false)
- } else {
- try {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- getDeploymentHistoryDetail(
- appId,
- pipelineId,
- selectedDeploymentTemplate.value,
- historyComponent,
- historyComponentName,
- ).then((response) => {
- setCurrentConfiguration(prepareHistoryData(response.result, historyComponent))
- setLoader(false)
- })
- } catch (err) {
- showError(err)
- setLoader(false)
- }
- }
- }
- }, [selectedDeploymentTemplate])
-
- useEffect(() => {
- try {
- setLoader(true)
- setSelectedDeploymentTemplate(null)
- setCurrentConfiguration(null)
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- getDeploymentHistoryDetail(
- appId,
- pipelineId,
- baseConfigurationId,
- historyComponent,
- historyComponentName,
- ).then((response) => {
- setBaseTemplateConfiguration(prepareHistoryData(response.result, historyComponent))
- })
- } catch (err) {
- showError(err)
- setLoader(false)
- }
- }, [baseConfigurationId, historyComponent, historyComponentName])
-
- useEffect(() => {
- // show template showing historical diff detailed view
- // in case if !showTemplate CD detail component being rendered
- setFullScreenView(true)
-
- return (): void => {
- setFullScreenView(false)
- }
- }, [])
-
- return (
- <>
-
-
-
-
- {loader ? (
-
- ) : (
-
-
-
- )}
-
- >
- )
-}
-
-export default DeploymentHistoryDetailedView
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryHeader.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryHeader.tsx
deleted file mode 100644
index 7e750a6d8..000000000
--- a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistoryHeader.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { useEffect, useState } from 'react'
-import { useHistory, useRouteMatch, useParams, NavLink } from 'react-router-dom'
-import moment from 'moment'
-import Tippy from '@tippyjs/react'
-import { SelectPicker } from '@Shared/Components/SelectPicker'
-import { DATE_TIME_FORMATS, URLS, showError } from '../../../../Common'
-import { DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP } from '../../../constants'
-import { ReactComponent as LeftIcon } from '../../../../Assets/Icon/ic-arrow-backward.svg'
-import { DeploymentTemplateOptions, DeploymentHistoryParamsType, CompareWithBaseConfiguration } from './types'
-import { getDeploymentDiffSelector } from '../service'
-
-const DeploymentHistoryHeader = ({
- selectedDeploymentTemplate,
- setSelectedDeploymentTemplate,
- setFullScreenView,
- setLoader,
- setPreviousConfigAvailable,
- renderRunSource,
- resourceId,
-}: CompareWithBaseConfiguration) => {
- const { url } = useRouteMatch()
- const history = useHistory()
- const { pipelineId, historyComponent, baseConfigurationId, historyComponentName } =
- useParams()
- const [baseTemplateTimeStamp, setBaseTemplateTimeStamp] = useState('')
- const [deploymentTemplateOption, setDeploymentTemplateOption] = useState([])
-
- const onClickTimeStampSelector = (selected: { label: string; value: string }) => {
- setSelectedDeploymentTemplate(selected)
- }
-
- useEffect(() => {
- if (pipelineId && historyComponent && baseConfigurationId) {
- try {
- setLoader(true)
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- getDeploymentDiffSelector(pipelineId, historyComponent, baseConfigurationId, historyComponentName).then(
- (response) => {
- const deploymentTemplateOpt = []
- if (response.result) {
- const resultLen = response.result.length
- for (let i = 0; i < resultLen; i++) {
- if (response.result[i].id.toString() === baseConfigurationId) {
- setBaseTemplateTimeStamp(response.result[i].deployedOn)
- } else {
- deploymentTemplateOpt.push({
- value: String(response.result[i].id),
- label: moment(response.result[i].deployedOn).format(
- DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT,
- ),
- author: response.result[i].deployedBy,
- status: response.result[i].deploymentStatus,
- runSource: response.result[i].runSource,
- renderRunSource,
- resourceId,
- })
- }
- }
- }
- setPreviousConfigAvailable(deploymentTemplateOpt.length > 0)
- setDeploymentTemplateOption(deploymentTemplateOpt)
- setSelectedDeploymentTemplate(
- deploymentTemplateOpt[0] || {
- label: 'NA',
- value: 'NA',
- author: 'NA',
- status: 'NA',
- runSource: null,
- },
- )
- },
- )
- } catch (err) {
- showError(err)
- setLoader(false)
- }
- }
- }, [historyComponent, baseConfigurationId, historyComponentName])
-
- const renderGoBackToConfiguration = () => (
- {
- e.preventDefault()
- setFullScreenView(false)
- history.push(
- `${url.split(URLS.DEPLOYMENT_HISTORY_CONFIGURATIONS)[0]}${URLS.DEPLOYMENT_HISTORY_CONFIGURATIONS}`,
- )
- }}
- >
-
-
- )
-
- const renderCompareDeploymentConfig = () => (
-
-
- Compare with
-
-
- {deploymentTemplateOption.length > 0 ? (
-
- ) : (
-
-
- {
- DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP[
- historyComponent.replace('-', '_').toUpperCase()
- ]?.DISPLAY_NAME
- }
- {historyComponentName ? ` “${historyComponentName}”` : ''} was added in this
- deployment. There is no previous instance to compare with.
-
- }
- >
- No options
-
-
- )}
-
-
- )
-
- const renderBaseDeploymentConfig = () => (
-
-
- Base configuration
-
-
- {baseTemplateTimeStamp && moment(baseTemplateTimeStamp).format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT)}
-
-
- )
- return (
-
- {renderGoBackToConfiguration()}
- {renderCompareDeploymentConfig()}
- {renderBaseDeploymentConfig()}
-
- )
-}
-
-export default DeploymentHistoryHeader
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistorySidebar.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistorySidebar.tsx
deleted file mode 100644
index 3763fd8e1..000000000
--- a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/DeploymentHistorySidebar.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { useEffect } from 'react'
-import { NavLink, useRouteMatch, useParams } from 'react-router-dom'
-import { DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP } from '../../../constants'
-import { DeploymentHistoryParamsType } from './types'
-import { getDeploymentHistoryList } from '../service'
-import { DeploymentHistorySidebarType } from '../types'
-import { URLS } from '../../../../Common'
-
-const DeploymentHistorySidebar = ({
- deploymentHistoryList,
- setDeploymentHistoryList,
-}: DeploymentHistorySidebarType) => {
- const match = useRouteMatch()
- const { appId, pipelineId, triggerId } = useParams()
- useEffect(() => {
- if (!deploymentHistoryList) {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- getDeploymentHistoryList(appId, pipelineId, triggerId).then((response) => {
- setDeploymentHistoryList(response.result)
- })
- }
- }, [deploymentHistoryList])
-
- const getNavLink = (componentId: number, componentName: string, key: string, childComponentName?: string) => {
- const currentComponent = DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP[componentName]
- const childComponentDetail = childComponentName ? `/${childComponentName}` : ''
- const configURL = `${match.url.split(URLS.DEPLOYMENT_HISTORY_CONFIGURATIONS)[0]}${
- URLS.DEPLOYMENT_HISTORY_CONFIGURATIONS
- }/${currentComponent.VALUE}/${componentId}${childComponentDetail}`
- return (
-
-
- {currentComponent.DISPLAY_NAME + childComponentDetail}
-
-
- )
- }
-
- return (
-
- {deploymentHistoryList?.map((historicalComponent, index) =>
- historicalComponent.childList?.length > 0
- ? historicalComponent.childList.map((historicalComponentName, childIndex) =>
- getNavLink(
- historicalComponent.id,
- historicalComponent.name,
- `config-${index}-${childIndex}`,
- historicalComponentName,
- ),
- )
- : getNavLink(historicalComponent.id, historicalComponent.name, `config-${index}`),
- )}
-
- )
-}
-
-export default DeploymentHistorySidebar
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/index.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/index.tsx
deleted file mode 100644
index 94dcac337..000000000
--- a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/index.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export { default as DeploymentHistoryDetailedView } from './DeploymentHistoryDetailedView'
-export { default as DeploymentHistoryConfigList } from './DeploymentHistoryConfigList.component'
-export { default as DeploymentHistoryDiffView } from './DeploymentHistoryDiffView'
diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/types.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/types.tsx
deleted file mode 100644
index 74879b908..000000000
--- a/src/Shared/Components/CICDHistory/DeploymentHistoryDiff/types.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { OptionType } from '../../../../Common'
-import { DeploymentTemplateList, RunSourceType, RenderRunSourceType } from '../types'
-
-export interface DeploymentHistoryParamsType {
- appId: string
- pipelineId?: string
- historyComponent?: string
- baseConfigurationId?: string
- historyComponentName?: string
- envId?: string
- triggerId?: string
-}
-
-export interface CompareViewDeploymentType extends RenderRunSourceType {
- setFullScreenView: React.Dispatch>
- deploymentHistoryList: DeploymentTemplateList[]
- setDeploymentHistoryList: React.Dispatch>
- resourceId?: number
-}
-
-export interface DeploymentTemplateOptions extends OptionType {
- author: string
- status: string
- runSource?: RunSourceType
-}
-
-export interface CompareWithBaseConfiguration extends RenderRunSourceType {
- selectedDeploymentTemplate: DeploymentTemplateOptions
- setSelectedDeploymentTemplate: (selected) => void
- setFullScreenView: React.Dispatch>
- setLoader: React.Dispatch>
- setPreviousConfigAvailable: React.Dispatch>
- resourceId?: number
-}
-
-export interface TemplateConfiguration {
- setFullScreenView: React.Dispatch>
- deploymentHistoryList: DeploymentTemplateList[]
- setDeploymentHistoryList: React.Dispatch>
-}
diff --git a/src/Shared/Components/CICDHistory/History.components.tsx b/src/Shared/Components/CICDHistory/History.components.tsx
index f70cee230..5fa8b4e81 100644
--- a/src/Shared/Components/CICDHistory/History.components.tsx
+++ b/src/Shared/Components/CICDHistory/History.components.tsx
@@ -16,14 +16,13 @@
import { useCallback, useEffect } from 'react'
import { useLocation } from 'react-router-dom'
-import { withShortcut, IWithShortcut } from 'react-keybind'
import {
ClipboardButton,
GenericEmptyState,
Tooltip,
extractImage,
- IS_PLATFORM_MAC_OS,
useSuperAdmin,
+ useRegisterShortcut,
} from '../../../Common'
import { EMPTY_STATE_STATUS } from '../../constants'
import { ReactComponent as DropDownIcon } from '../../../Assets/Icon/ic-chevron-down.svg'
@@ -34,69 +33,56 @@ import { ReactComponent as ZoomIn } from '../../../Assets/Icon/ic-fullscreen.svg
import { ReactComponent as ZoomOut } from '../../../Assets/Icon/ic-exit-fullscreen.svg'
import './cicdHistory.scss'
-export const LogResizeButton = withShortcut(
- ({
- shortcutCombo = ['F'],
- showOnlyWhenPathIncludesLogs = true,
- fullScreenView,
- setFullScreenView,
- shortcut,
- }: LogResizeButtonType & IWithShortcut): JSX.Element => {
- const { pathname } = useLocation()
+export const LogResizeButton = ({
+ shortcutCombo = ['F'],
+ showOnlyWhenPathIncludesLogs = true,
+ fullScreenView,
+ setFullScreenView,
+}: LogResizeButtonType): JSX.Element => {
+ const { pathname } = useLocation()
+ const { registerShortcut, unregisterShortcut } = useRegisterShortcut()
- const toggleFullScreen = useCallback((): void => {
- setFullScreenView(!fullScreenView)
- }, [fullScreenView])
+ const toggleFullScreen = useCallback((): void => {
+ setFullScreenView(!fullScreenView)
+ }, [fullScreenView])
- const showButton = !showOnlyWhenPathIncludesLogs || pathname.includes('/logs')
- const doesShortcutContainCmdKey = shortcutCombo.some((key) => key === 'Control') && IS_PLATFORM_MAC_OS
+ const showButton = !showOnlyWhenPathIncludesLogs || pathname.includes('/logs')
- useEffect(() => {
- const combo = shortcutCombo
- .map((key) => {
- if (key === 'Control') {
- return IS_PLATFORM_MAC_OS ? 'cmd' : 'ctrl'
- }
- return key.toLowerCase()
- })
- .join('+')
+ useEffect(() => {
+ if (showButton && shortcutCombo.length) {
+ registerShortcut({ callback: toggleFullScreen, keys: shortcutCombo })
+ }
- // FIXME: disabling shortcut for macos since pressing cmd breaks shortcuts through react-keybind
- if (showButton && shortcutCombo.length && !doesShortcutContainCmdKey) {
- shortcut.registerShortcut(toggleFullScreen, [combo], 'ToggleFullscreen', 'Enter/Exit fullscreen')
- }
+ return () => {
+ unregisterShortcut(shortcutCombo)
+ }
+ }, [showButton, toggleFullScreen])
- return () => {
- shortcut.unregisterShortcut([combo])
- }
- }, [showButton, toggleFullScreen])
-
- return (
- showButton && (
-
+
- )
+ {fullScreenView ? (
+
+ ) : (
+
+ )}
+
+
)
- },
-)
+ )
+}
export const Scroller = ({ scrollToTop, scrollToBottom, style }: ScrollerType): JSX.Element => (
diff --git a/src/Shared/Components/CICDHistory/LogsRenderer.tsx b/src/Shared/Components/CICDHistory/LogsRenderer.tsx
index 480f34edf..5cf8dcfa8 100644
--- a/src/Shared/Components/CICDHistory/LogsRenderer.tsx
+++ b/src/Shared/Components/CICDHistory/LogsRenderer.tsx
@@ -22,7 +22,6 @@ import { ANSI_UP_REGEX, ComponentSizeType } from '@Shared/constants'
import { escapeRegExp } from '@Shared/Helpers'
import { ReactComponent as ICExpandAll } from '@Icons/ic-expand-all.svg'
import { ReactComponent as ICCollapseAll } from '@Icons/ic-collapse-all.svg'
-import { withShortcut, IWithShortcut } from 'react-keybind'
import { ReactComponent as ICArrow } from '@Icons/ic-caret-down.svg'
import {
Progressing,
@@ -33,6 +32,7 @@ import {
SearchBar,
useUrlFilters,
Tooltip,
+ useRegisterShortcut,
} from '../../../Common'
import LogStageAccordion from './LogStageAccordion'
import {
@@ -173,13 +173,7 @@ const useCIEventSource = (url: string, maxLength?: number): [string[], EventSour
return [dataVal, eventSourceRef.current, logsNotAvailableError]
}
-const LogsRenderer = ({
- triggerDetails,
- isBlobStorageConfigured,
- parentType,
- fullScreenView,
- shortcut,
-}: LogsRendererType & IWithShortcut) => {
+const LogsRenderer = ({ triggerDetails, isBlobStorageConfigured, parentType, fullScreenView }: LogsRendererType) => {
const { pipelineId, envId, appId } = useParams
()
const logsURL =
parentType === HistoryComponentType.CI
@@ -200,6 +194,8 @@ const LogsRenderer = ({
const areAllStagesExpanded = useMemo(() => stageList.every((item) => item.isOpen), [stageList])
const shortcutTippyText = areAllStagesExpanded ? 'Collapse all stages' : 'Expand all stages'
+ const { registerShortcut, unregisterShortcut } = useRegisterShortcut()
+
const areStagesAvailable =
(window._env_.FEATURE_STEP_WISE_LOGS_ENABLE && streamDataList[0]?.startsWith(LOGS_STAGE_IDENTIFIER)) || false
@@ -431,15 +427,10 @@ const LogsRenderer = ({
}, [areAllStagesExpanded])
useEffect(() => {
- shortcut.registerShortcut(
- handleToggleOpenAllStages,
- ['e'],
- 'ExpandCollapseLogStages',
- 'Expand/Collapse all log stages',
- )
+ registerShortcut({ callback: handleToggleOpenAllStages, keys: ['E'] })
return () => {
- shortcut.unregisterShortcut(['e'])
+ unregisterShortcut(['E'])
}
}, [handleToggleOpenAllStages])
@@ -623,4 +614,4 @@ const LogsRenderer = ({
: renderLogs()
}
-export default withShortcut(LogsRenderer)
+export default LogsRenderer
diff --git a/src/Shared/Components/CICDHistory/TriggerOutput.tsx b/src/Shared/Components/CICDHistory/TriggerOutput.tsx
index 87fc4953b..1113074dc 100644
--- a/src/Shared/Components/CICDHistory/TriggerOutput.tsx
+++ b/src/Shared/Components/CICDHistory/TriggerOutput.tsx
@@ -24,8 +24,8 @@ import { CommitChipCell } from '@Shared/Components/CommitChipCell'
import { ReactComponent as ICLines } from '@Icons/ic-lines.svg'
import { ReactComponent as ICPulsateStatus } from '@Icons/ic-pulsate-status.svg'
import { ReactComponent as ICArrowRight } from '@Icons/ic-arrow-right.svg'
-import { getDeploymentStageTitle } from '@Pages/App'
import { ToastManager, ToastVariantType } from '@Shared/Services'
+import { getDeploymentStageTitle } from '@Pages/Applications'
import {
ConfirmationDialog,
DATE_TIME_FORMATS,
@@ -66,7 +66,7 @@ import { GitTriggers } from '../../types'
import warn from '../../../Assets/Icon/ic-warning.svg'
import LogsRenderer from './LogsRenderer'
import DeploymentDetailSteps from './DeploymentDetailSteps'
-import { DeploymentHistoryDetailedView, DeploymentHistoryConfigList } from './DeploymentHistoryDiff'
+import { DeploymentHistoryConfigDiff } from './DeploymentHistoryConfigDiff'
import { GitChanges, Scroller } from './History.components'
import Artifacts from './Artifacts'
import { statusColor as colorMap, EMPTY_STATE_STATUS, PULSATING_STATUS_MAP } from '../../constants'
@@ -297,7 +297,7 @@ const StartDetails = ({
return (
-
+
Start
@@ -488,8 +488,6 @@ const HistoryLogs: React.FC
= ({
triggerDetails,
loading,
setFullScreenView,
- deploymentHistoryList,
- setDeploymentHistoryList,
deploymentAppType,
isBlobStorageConfigured,
userApprovalMetadata,
@@ -509,6 +507,8 @@ const HistoryLogs: React.FC = ({
scrollToTop,
scrollToBottom,
fullScreenView,
+ appName,
+ triggerHistory,
}) => {
const { path } = useRouteMatch()
const { appId, pipelineId, triggerId, envId } = useParams<{
@@ -528,7 +528,7 @@ const HistoryLogs: React.FC = ({
const CDBuildReportUrl = `app/cd-pipeline/workflow/download/${appId}/${envId}/${pipelineId}/${triggerId}`
return (
-
+
{loading ? (
) : (
@@ -591,24 +591,16 @@ const HistoryLogs: React.FC
= ({
/>
{triggerDetails.stage === 'DEPLOY' && (
-
-
+
-
- )}
- {triggerDetails.stage === 'DEPLOY' && (
-
-
)}
@@ -662,8 +654,6 @@ const TriggerOutput = ({
triggerHistory,
setTriggerHistory,
setFullScreenView,
- setDeploymentHistoryList,
- deploymentHistoryList,
deploymentAppType,
isBlobStorageConfigured,
appReleaseTags,
@@ -683,6 +673,7 @@ const TriggerOutput = ({
scrollToTop,
scrollToBottom,
renderTargetConfigInfo,
+ appName,
}: TriggerOutputProps) => {
const { appId, triggerId, envId, pipelineId } = useParams<{
appId: string
@@ -913,8 +904,6 @@ const TriggerOutput = ({
userApprovalMetadata={triggerDetailsResult?.result?.userApprovalMetadata}
triggeredByEmail={triggerDetailsResult?.result?.triggeredByEmail}
setFullScreenView={setFullScreenView}
- setDeploymentHistoryList={setDeploymentHistoryList}
- deploymentHistoryList={deploymentHistoryList}
deploymentAppType={deploymentAppType}
isBlobStorageConfigured={isBlobStorageConfigured}
artifactId={triggerDetailsResult?.result?.artifactId}
@@ -932,6 +921,8 @@ const TriggerOutput = ({
scrollToTop={scrollToTop}
scrollToBottom={scrollToBottom}
fullScreenView={fullScreenView}
+ appName={appName}
+ triggerHistory={triggerHistory}
/>
>
)
diff --git a/src/Shared/Components/CICDHistory/cicdHistory.scss b/src/Shared/Components/CICDHistory/cicdHistory.scss
index 2080de484..7df3367f4 100644
--- a/src/Shared/Components/CICDHistory/cicdHistory.scss
+++ b/src/Shared/Components/CICDHistory/cicdHistory.scss
@@ -114,8 +114,17 @@
.deployment-diff__upper {
display: grid;
- grid-template-columns: 50% 50%;
- grid-template-rows: auto;
+ grid-template-columns: repeat(2, 1fr);
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ width: 1px;
+ height: 100%;
+ left: calc(50% - 1px);
+ background: var(--N200);
+ }
}
.historical-diff__left {
diff --git a/src/Shared/Components/CICDHistory/index.tsx b/src/Shared/Components/CICDHistory/index.tsx
index 0940f1876..002bcd92f 100644
--- a/src/Shared/Components/CICDHistory/index.tsx
+++ b/src/Shared/Components/CICDHistory/index.tsx
@@ -27,6 +27,6 @@ export * from './History.components'
export * from './utils'
export * from './TriggerOutput'
export { default as LogsRenderer } from './LogsRenderer'
-export * from './DeploymentHistoryDiff'
+export * from './DeploymentHistoryConfigDiff'
export * from './CiPipelineSourceConfig'
export * from './StatusFilterButtonComponent'
diff --git a/src/Shared/Components/CICDHistory/service.tsx b/src/Shared/Components/CICDHistory/service.tsx
index 437e67ae9..2763c2eb1 100644
--- a/src/Shared/Components/CICDHistory/service.tsx
+++ b/src/Shared/Components/CICDHistory/service.tsx
@@ -197,8 +197,11 @@ export const prepareConfigMapAndSecretData = (
let typeValue = 'Environment Variable'
if (rawData.type === 'volume') {
typeValue = 'Data Volume'
- if (rawData.mountPath) {
- secretValues['mountPath'] = { displayName: 'Volume mount path', value: rawData.mountPath }
+ if (rawData.mountPath || rawData.defaultMountPath) {
+ secretValues['mountPath'] = {
+ displayName: 'Volume mount path',
+ value: rawData.mountPath || rawData.defaultMountPath,
+ }
}
if (rawData.subPath) {
secretValues['subPath'] = { displayName: 'Set SubPath', value: 'Yes' }
diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx
index 4ef8ce948..558b5806a 100644
--- a/src/Shared/Components/CICDHistory/types.tsx
+++ b/src/Shared/Components/CICDHistory/types.tsx
@@ -15,6 +15,7 @@
*/
import { CSSProperties, ReactElement } from 'react'
+import { SupportedKeyboardKeysType } from '@Common/Hooks/UseRegisterShortcut/types'
import {
OptionType,
UserApprovalMetadataType,
@@ -27,7 +28,6 @@ import {
PaginationProps,
useScrollable,
SortingOrder,
- SupportedKeyboardKeysType,
} from '../../../Common'
import { DeploymentStageType } from '../../constants'
import { AggregationKeys, GitTriggers, Node, NodeType, ResourceKindType, ResourceVersionType } from '../../types'
@@ -388,14 +388,13 @@ export interface TriggerOutputProps extends RenderRunSourceType, Pick
setFullScreenView: React.Dispatch>
- deploymentHistoryList: DeploymentTemplateList[]
- setDeploymentHistoryList: React.Dispatch>
deploymentAppType: DeploymentAppTypes
isBlobStorageConfigured: boolean
appReleaseTags: string[]
tagsEditable: boolean
hideImageTaggingHardDelete: boolean
fetchIdData: FetchIdDataStatus
+ appName: string
selectedEnvironmentName?: string
renderCIListHeader?: (renderCIListHeaderProps: RenderCIListHeaderProps) => JSX.Element
renderDeploymentApprovalInfo?: (userApprovalMetadata: UserApprovalMetadataType) => JSX.Element
@@ -418,8 +417,6 @@ export interface HistoryLogsProps
| 'scrollToTop'
| 'scrollToBottom'
| 'setFullScreenView'
- | 'deploymentHistoryList'
- | 'setDeploymentHistoryList'
| 'deploymentAppType'
| 'isBlobStorageConfigured'
| 'appReleaseTags'
@@ -431,6 +428,8 @@ export interface HistoryLogsProps
| 'renderCIListHeader'
| 'renderVirtualHistoryArtifacts'
| 'fullScreenView'
+ | 'appName'
+ | 'triggerHistory'
> {
triggerDetails: History
loading: boolean
@@ -489,8 +488,11 @@ export interface DeploymentTemplateHistoryType {
isUnpublished?: boolean
isDeleteDraft?: boolean
rootClassName?: string
- comparisonBodyClassName?: string
- sortOrder?: SortingOrder
+ codeEditorKey?: React.Key
+ sortingConfig?: {
+ sortBy: string
+ sortOrder: SortingOrder
+ }
}
export interface DeploymentHistoryDetailRes extends ResponseType {
result?: DeploymentHistoryDetail
diff --git a/src/Shared/Components/Collapse/Collapse.tsx b/src/Shared/Components/Collapse/Collapse.tsx
index 26e9b8768..273dd55a3 100644
--- a/src/Shared/Components/Collapse/Collapse.tsx
+++ b/src/Shared/Components/Collapse/Collapse.tsx
@@ -1,6 +1,62 @@
+import { useEffect, useRef, useState } from 'react'
import { CollapseProps } from './types'
-export const Collapse = ({ expand, children }: CollapseProps) => (
- // TODO: removed animation because of miscalculations (broken with auto editor height)
- {expand ? children : null}
-)
+/**
+ * Collapse component for smoothly expanding or collapsing its content.
+ * Dynamically calculates the content height and applies smooth transitions.
+ * It also supports a callback when the transition ends.
+ */
+export const Collapse = ({ expand, onTransitionEnd, children }: CollapseProps) => {
+ // Reference to the content container to calculate its height
+ const contentRef = useRef(null)
+
+ // State to store the dynamic height of the content; initially set to 0 if collapsed
+ const [contentHeight, setContentHeight] = useState(!expand ? 0 : null)
+
+ /**
+ * Effect to observe changes in the content size when expanded and recalculate the height.
+ * Uses a ResizeObserver to handle dynamic content size changes.
+ */
+ useEffect(() => {
+ if (!contentHeight || !expand || !contentRef.current) {
+ return null
+ }
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ // Update the height when content size changes
+ setContentHeight(entries[0].contentRect.height)
+ })
+
+ // Observe the content container for resizing
+ resizeObserver.observe(contentRef.current)
+
+ // Clean up the observer when the component unmounts or content changes
+ return () => {
+ resizeObserver.disconnect()
+ }
+ }, [contentHeight, expand])
+
+ /**
+ * Effect to handle the initial setting of content height during expansion or collapse.
+ * Sets height to the content's full height when expanded, or 0 when collapsed.
+ */
+ useEffect(() => {
+ if (expand) {
+ // Set the content height when expanded
+ setContentHeight(contentRef.current?.getBoundingClientRect().height)
+ } else {
+ // Collapse content by setting the height to 0
+ setContentHeight(0)
+ }
+ }, [expand])
+
+ return (
+
+ {/* The container that holds the collapsible content */}
+
{children}
+
+ )
+}
diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx
index 0460df09b..3928907f1 100644
--- a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx
+++ b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx
@@ -60,7 +60,7 @@ export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListP
) : (
- items.map(({ title, href, iconConfig, subtitle, onClick }) => (
+ items.map(({ title, strikeThrough, href, iconConfig, subtitle, onClick }) => (
-
+
{title}
{subtitle && (
@@ -88,7 +90,7 @@ export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListP
>
)}
diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts b/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts
index be4944f82..e67b599ff 100644
--- a/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts
+++ b/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts
@@ -10,6 +10,10 @@ export interface CollapsibleListItem {
* The subtitle of the list item.
*/
subtitle?: string
+ /**
+ * If true, the title will be rendered with line-through.
+ */
+ strikeThrough?: boolean
/**
* Configuration for the icon.
*/
diff --git a/src/Shared/Components/DatePicker/DateTimePicker.tsx b/src/Shared/Components/DatePicker/DateTimePicker.tsx
index 3cf86875a..515f04975 100644
--- a/src/Shared/Components/DatePicker/DateTimePicker.tsx
+++ b/src/Shared/Components/DatePicker/DateTimePicker.tsx
@@ -110,7 +110,6 @@ const DateTimePicker = ({
onChange={handleTimeChange}
data-testid={dataTestIdForTime}
menuSize={ComponentSizeType.xs}
- menuPosition="fixed"
size={ComponentSizeType.large}
shouldMenuAlignRight
/>
diff --git a/src/Shared/Components/DatePicker/TimeSelect.tsx b/src/Shared/Components/DatePicker/TimeSelect.tsx
index 3ffc30aa4..01c8ba7d0 100644
--- a/src/Shared/Components/DatePicker/TimeSelect.tsx
+++ b/src/Shared/Components/DatePicker/TimeSelect.tsx
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-import { ReactComponent as ErrorIcon } from '@Icons/ic-warning.svg'
import { ReactComponent as ClockIcon } from '@Icons/ic-clock.svg'
import { ComponentSizeType } from '@Shared/constants'
import { DEFAULT_TIME_OPTIONS } from './utils'
@@ -29,26 +28,18 @@ export const TimePickerSelect = ({
error,
selectedTimeOption,
}: TimeSelectProps) => (
- <>
- }
- onChange={onChange}
- data-testid={DATE_PICKER_IDS.TIME}
- size={ComponentSizeType.large}
- menuPosition="fixed"
- />
- {error && (
-
-
- {error}
-
- )}
- >
+ }
+ onChange={onChange}
+ data-testid={DATE_PICKER_IDS.TIME}
+ size={ComponentSizeType.large}
+ error={error}
+ />
)
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.component.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.component.tsx
index 7c48506d8..b12795bde 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.component.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.component.tsx
@@ -11,10 +11,17 @@ export const DeploymentConfigDiff = ({
goBackURL,
navHeading,
navHelpText,
+ isNavHelpTextShowingError,
tabConfig,
+ errorConfig,
+ showDetailedDiffState,
+ hideDiffState,
+ renderedInDrawer,
...resProps
}: DeploymentConfigDiffProps) => (
-
+
+
-
)
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.constants.ts b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.constants.ts
new file mode 100644
index 000000000..590cdaf86
--- /dev/null
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.constants.ts
@@ -0,0 +1,35 @@
+import { FunctionComponent, SVGProps } from 'react'
+
+import { ReactComponent as ICDiffFileUpdated } from '@Icons/ic-diff-file-updated.svg'
+import { ReactComponent as ICDiffFileAdded } from '@Icons/ic-diff-file-added.svg'
+import { ReactComponent as ICDiffFileRemoved } from '@Icons/ic-diff-file-removed.svg'
+
+import { DeploymentConfigDiffState } from './DeploymentConfigDiff.types'
+
+export const diffStateTextMap: Record
= {
+ hasDiff: 'Has difference',
+ added: 'Added',
+ deleted: 'Deleted',
+ noDiff: 'No change',
+}
+
+export const diffStateIconMap: Record>> = {
+ hasDiff: ICDiffFileUpdated,
+ added: ICDiffFileAdded,
+ deleted: ICDiffFileRemoved,
+ noDiff: null,
+}
+
+export const diffStateTooltipTextMap: Record = {
+ hasDiff: 'File has difference',
+ added: 'File has been added',
+ deleted: 'File has been deleted',
+ noDiff: null,
+}
+
+export const diffStateTextColorMap: Record = {
+ hasDiff: 'cy-7',
+ added: 'cg-5',
+ deleted: 'cr-5',
+ noDiff: 'cn-7',
+}
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.scss b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.scss
index 9e0859448..e5f8a18d5 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.scss
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.scss
@@ -3,6 +3,10 @@
grid-template-columns: 255px 1fr;
height: 100%;
+ &--drawer {
+ grid-template-columns: 220px 1fr;
+ }
+
&__accordion {
scroll-margin-top: 12px;
}
@@ -18,8 +22,7 @@
}
&__main-content {
- height: calc(100vh - 122px);
- overflow: auto;
+ flex-grow: 1;
&__heading {
display: grid;
@@ -27,10 +30,6 @@
grid-template-columns: calc(50% - 15px) calc(50% - 15px);
grid-template-rows: auto;
}
-
- &__comparison {
- margin: 16px 0 0;
- }
}
& .react-monaco-editor-container {
@@ -39,7 +38,7 @@
&__tab-list {
label {
- flex-grow: 1
+ flex-grow: 1;
}
.radio__item-label {
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts
index 9064785a4..f23f7d9c8 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts
@@ -5,14 +5,22 @@ import {
ConfigMapSecretDataConfigDatumDTO,
DeploymentTemplateDTO,
EnvResourceType,
- ManifestTemplateDTO,
+ TemplateListDTO,
} from '@Shared/Services'
+import { ManifestTemplateDTO } from '@Pages/Applications'
import { DeploymentHistoryDetail } from '../CICDHistory'
import { CollapsibleListConfig, CollapsibleListItem } from '../CollapsibleList'
import { SelectPickerProps } from '../SelectPicker'
import { CollapseProps } from '../Collapse'
+export enum DeploymentConfigDiffState {
+ NO_DIFF = 'noDiff',
+ HAS_DIFF = 'hasDiff',
+ ADDED = 'added',
+ DELETED = 'deleted',
+}
+
export interface DeploymentConfigType {
list: DeploymentHistoryDetail
heading: React.ReactNode
@@ -21,10 +29,13 @@ export interface DeploymentConfigType {
export interface DeploymentConfigListItem {
id: string
title: string
+ name?: string
+ pathType: EnvResourceType
primaryConfig: DeploymentConfigType
secondaryConfig: DeploymentConfigType
- hasDiff?: boolean
- isDeploymentTemplate?: boolean
+ diffState: DeploymentConfigDiffState
+ singleView?: boolean
+ groupHeader?: string
}
export type DeploymentConfigDiffSelectPickerProps =
@@ -42,7 +53,8 @@ export type DeploymentConfigDiffSelectPickerProps =
}
export interface DeploymentConfigDiffNavigationItem extends Pick {
- hasDiff?: boolean
+ Icon?: React.FunctionComponent>
+ diffState: DeploymentConfigListItem['diffState']
}
export interface DeploymentConfigDiffNavigationCollapsibleItem
@@ -55,14 +67,19 @@ export interface DeploymentConfigDiffProps {
errorConfig?: {
error: boolean
code: number
+ message?: string
+ redirectURL?: string
reload: () => void
}
configList: DeploymentConfigListItem[]
+ showDetailedDiffState?: boolean
+ hideDiffState?: boolean
headerText?: string
scrollIntoViewId?: string
selectorsConfig: {
primaryConfig: DeploymentConfigDiffSelectPickerProps[]
secondaryConfig: DeploymentConfigDiffSelectPickerProps[]
+ hideDivider?: boolean
}
sortingConfig?: {
sortBy: string
@@ -74,17 +91,33 @@ export interface DeploymentConfigDiffProps {
goBackURL?: string
navHeading: string
navHelpText?: string
+ isNavHelpTextShowingError?: boolean
tabConfig?: {
tabs: string[]
activeTab: string
onClick: (tab: string) => void
}
+ scopeVariablesConfig?: {
+ convertVariables: boolean
+ onConvertVariablesClick: () => void
+ }
+ renderedInDrawer?: boolean
}
export interface DeploymentConfigDiffNavigationProps
extends Pick<
DeploymentConfigDiffProps,
- 'isLoading' | 'navList' | 'collapsibleNavList' | 'goBackURL' | 'navHeading' | 'navHelpText' | 'tabConfig'
+ | 'isLoading'
+ | 'navList'
+ | 'collapsibleNavList'
+ | 'goBackURL'
+ | 'navHeading'
+ | 'navHelpText'
+ | 'tabConfig'
+ | 'errorConfig'
+ | 'isNavHelpTextShowingError'
+ | 'showDetailedDiffState'
+ | 'hideDiffState'
> {}
export interface DeploymentConfigDiffMainProps
@@ -97,16 +130,20 @@ export interface DeploymentConfigDiffMainProps
| 'scrollIntoViewId'
| 'selectorsConfig'
| 'sortingConfig'
+ | 'scopeVariablesConfig'
+ | 'showDetailedDiffState'
+ | 'hideDiffState'
> {}
-export interface DeploymentConfigDiffAccordionProps extends Pick {
- id: string
- title: string
- children: React.ReactNode
- hasDiff?: boolean
- isExpanded?: boolean
- onClick?: (e: React.MouseEvent) => void
-}
+export type DeploymentConfigDiffAccordionProps = Pick &
+ Pick & {
+ id: string
+ title: string
+ children: React.ReactNode
+ diffState: DeploymentConfigDiffState
+ isExpanded?: boolean
+ onClick?: (e: React.MouseEvent) => void
+ }
export type DiffHeadingDataType = DeploymentTemplate extends true
? DeploymentTemplateDTO
@@ -116,13 +153,16 @@ export type AppEnvDeploymentConfigListParams = (IsManifestView e
? {
currentList: ManifestTemplateDTO
compareList: ManifestTemplateDTO
- sortOrder?: never
+ compareToTemplateOptions?: never
+ compareWithTemplateOptions?: never
}
: {
currentList: AppEnvDeploymentConfigDTO
compareList: AppEnvDeploymentConfigDTO
- sortOrder?: SortingOrder
+ compareToTemplateOptions?: TemplateListDTO[]
+ compareWithTemplateOptions?: TemplateListDTO[]
}) & {
getNavItemHref: (resourceType: EnvResourceType, resourceName: string) => string
isManifestView?: IsManifestView
+ convertVariables?: boolean
}
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx
index f4845458a..9329419b3 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx
@@ -1,18 +1,21 @@
import { ReactComponent as ICCheck } from '@Icons/ic-check.svg'
import { ReactComponent as ICStamp } from '@Icons/ic-stamp.svg'
import { ReactComponent as ICEditFile } from '@Icons/ic-edit-file.svg'
-import { stringComparatorBySortOrder, yamlComparatorBySortOrder } from '@Shared/Helpers'
+import { ReactComponent as ICFileCode } from '@Icons/ic-file-code.svg'
+import { stringComparatorBySortOrder } from '@Shared/Helpers'
import { DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP } from '@Shared/constants'
-import { YAMLStringify } from '@Common/Helper'
-import { SortingOrder } from '@Common/Constants'
import {
DeploymentConfigDiffProps,
+ DeploymentConfigDiffState,
+ DeploymentHistoryDetail,
DeploymentHistorySingleValue,
AppEnvDeploymentConfigListParams,
DiffHeadingDataType,
prepareHistoryData,
} from '@Shared/Components'
+import { deepEqual } from '@Common/Helper'
+import { ManifestTemplateDTO } from '@Pages/Applications'
import {
ConfigMapSecretDataConfigDatumDTO,
ConfigMapSecretDataDTO,
@@ -20,11 +23,39 @@ import {
DeploymentTemplateDTO,
DraftState,
EnvResourceType,
- ManifestTemplateDTO,
+ PipelineConfigDataDTO,
TemplateListDTO,
TemplateListType,
} from '../../Services/app.types'
+export const getDeploymentTemplateData = (data: DeploymentTemplateDTO) => {
+ const parsedDraftData = JSON.parse(data?.deploymentDraftData?.configData[0].draftMetadata.data || null)
+
+ return (
+ parsedDraftData?.envOverrideValues ||
+ parsedDraftData?.valuesOverride ||
+ parsedDraftData?.defaultAppOverride ||
+ data?.data ||
+ null
+ )
+}
+
+const getDeploymentTemplateAppMetricsAndTemplateVersion = (
+ data: DeploymentTemplateDTO,
+ templateOptions: TemplateListDTO[],
+) => {
+ const parsedDraftData = JSON.parse(data?.deploymentDraftData?.configData[0].draftMetadata.data || null)
+ const draftTemplateVersion = templateOptions?.find(
+ ({ chartRefId }) => parsedDraftData?.chartRefId === chartRefId,
+ )?.chartVersion
+
+ return {
+ isAppMetricsEnabled:
+ parsedDraftData || data ? parsedDraftData?.isAppMetricsEnabled || data?.isAppMetricsEnabled || false : null,
+ templateVersion: draftTemplateVersion || data?.templateVersion,
+ }
+}
+
/**
* Retrieves the draft data from the given configuration data object.
*
@@ -243,6 +274,32 @@ const getCodeEditorData = (
return { compareToCodeEditorData, compareWithCodeEditorData }
}
+/**
+ * Compares two values and returns the appropriate deployment configuration difference state.
+ *
+ * @param compareToValue - The original value to compare.
+ * @param compareWithValue - The new value to compare against the original.
+ * @returns `DeploymentConfigDiffState` enum value indicating the difference between the two values
+ */
+const getDiffState = (compareToValue: DeploymentHistoryDetail, compareWithValue: DeploymentHistoryDetail) => {
+ const isCompareToPresent = !!compareToValue.codeEditorValue.value
+ const isCompareWithPresent = !!compareWithValue.codeEditorValue.value
+
+ if (!isCompareToPresent && isCompareWithPresent) {
+ return DeploymentConfigDiffState.DELETED
+ }
+
+ if (isCompareToPresent && !isCompareWithPresent) {
+ return DeploymentConfigDiffState.ADDED
+ }
+
+ if (!deepEqual(compareToValue, compareWithValue)) {
+ return DeploymentConfigDiffState.HAS_DIFF
+ }
+
+ return DeploymentConfigDiffState.NO_DIFF
+}
+
/**
* Prepares the data for displaying the diff view between two configuration items.
*
@@ -289,37 +346,26 @@ const getDiffViewData = (
type === ConfigResourceType.Secret && !compareWithIsAdmin,
)
- // Check if there is a difference between the compareTo and compareWith data
- const hasDiff = compareWithCodeEditorData.value !== compareToCodeEditorData.value
-
// Return the combined diff data
return {
compareToDiff,
compareWithDiff,
- hasDiff,
+ diffState: getDiffState(compareToDiff, compareWithDiff),
}
}
-const getDeploymentTemplateDiffViewData = (data: DeploymentTemplateDTO | null, sortOrder: SortingOrder) => {
- const parsedDraftData = JSON.parse(data?.deploymentDraftData?.configData[0].draftMetadata.data || null)
- const _data =
- parsedDraftData?.envOverrideValues ||
- parsedDraftData?.valuesOverride ||
- parsedDraftData?.defaultAppOverride ||
- data?.data ||
- null
-
+const getDeploymentTemplateDiffViewData = (data: DeploymentTemplateDTO | null, templateOptions: TemplateListDTO[]) => {
+ const _data = getDeploymentTemplateData(data)
const codeEditorValue = {
displayName: 'data',
- value: _data
- ? YAMLStringify(_data, {
- sortMapEntries: (a, b) => yamlComparatorBySortOrder(a, b, sortOrder),
- }) ?? ''
- : '',
+ value: _data ? JSON.stringify(_data) : '',
}
const diffViewData = prepareHistoryData(
- { codeEditorValue },
+ {
+ ...getDeploymentTemplateAppMetricsAndTemplateVersion(data, templateOptions),
+ codeEditorValue,
+ },
DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP.DEPLOYMENT_TEMPLATE.VALUE,
)
@@ -340,6 +386,20 @@ const getManifestDiffViewData = (data: ManifestTemplateDTO) => {
return diffViewData
}
+const getPipelineConfigDiffViewData = (data: PipelineConfigDataDTO) => {
+ const codeEditorValue = {
+ displayName: 'data',
+ value: data?.data ? JSON.stringify(data.data) : '',
+ }
+
+ const diffViewData = prepareHistoryData(
+ { ...(data || {}), strategy: data?.Strategy, codeEditorValue },
+ DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP.PIPELINE_STRATEGY.VALUE,
+ )
+
+ return diffViewData
+}
+
const getDiffHeading = (
data: DiffHeadingDataType,
deploymentTemplate?: DeploymentTemplate,
@@ -396,20 +456,50 @@ const getDiffHeading = (
)
}
+const getConfigMapSecretResolvedValues = (configMapSecretData: ConfigMapSecretDataDTO, convertVariables: boolean) => {
+ const resolvedData: ConfigMapSecretDataConfigDatumDTO[] =
+ ((convertVariables && JSON.parse(configMapSecretData?.resolvedValue || null)) || configMapSecretData?.data)
+ ?.configData || []
+
+ const data =
+ (convertVariables &&
+ resolvedData.map((item, index) => {
+ if (configMapSecretData.data.configData[index].draftMetadata) {
+ const resolvedDraftData =
+ configMapSecretData.data.configData[index].draftMetadata.draftResolvedValue ||
+ configMapSecretData.data.configData[index].draftMetadata.data
+
+ return {
+ ...configMapSecretData.data.configData[index],
+ draftMetadata: {
+ ...configMapSecretData.data.configData[index].draftMetadata,
+ data: resolvedDraftData,
+ },
+ }
+ }
+
+ return item
+ })) ||
+ resolvedData
+
+ return data
+}
+
const getConfigMapSecretData = (
compareToList: ConfigMapSecretDataDTO,
compareWithList: ConfigMapSecretDataDTO,
resourceType: ConfigResourceType,
compareToIsAdmin: boolean,
compareWithIsAdmin: boolean,
+ convertVariables: boolean,
) => {
const combinedList = mergeConfigDataArraysByName(
- compareToList?.data.configData || [],
- compareWithList?.data.configData || [],
+ getConfigMapSecretResolvedValues(compareToList, convertVariables),
+ getConfigMapSecretResolvedValues(compareWithList, convertVariables),
)
- const deploymentConfig = combinedList.map(([currentItem, compareItem]) => {
- const { compareToDiff, compareWithDiff, hasDiff } = getDiffViewData(
+ const deploymentConfig: DeploymentConfigDiffProps['configList'] = combinedList.map(([currentItem, compareItem]) => {
+ const { compareToDiff, compareWithDiff, diffState } = getDiffViewData(
currentItem,
compareItem,
resourceType,
@@ -417,8 +507,12 @@ const getConfigMapSecretData = (
compareWithIsAdmin,
)
+ const pathType =
+ resourceType === ConfigResourceType.ConfigMap ? EnvResourceType.ConfigMap : EnvResourceType.Secret
+
return {
- id: `${resourceType === ConfigResourceType.ConfigMap ? EnvResourceType.ConfigMap : EnvResourceType.Secret}-${currentItem?.name || compareItem?.name}`,
+ id: `${pathType}-${currentItem?.name || compareItem?.name}`,
+ pathType,
title: `${resourceType === ConfigResourceType.ConfigMap ? 'ConfigMap' : 'Secret'} / ${currentItem?.name || compareItem?.name}`,
name: currentItem?.name || compareItem?.name,
primaryConfig: {
@@ -429,13 +523,76 @@ const getConfigMapSecretData = (
heading: getDiffHeading(currentItem),
list: compareToDiff,
},
- hasDiff,
+ diffState,
+ groupHeader: resourceType === ConfigResourceType.ConfigMap ? 'CONFIGMAPS' : 'SECRETS',
}
})
return deploymentConfig
}
+const getDeploymentTemplateResolvedData = (deploymentTemplate: DeploymentTemplateDTO): DeploymentTemplateDTO => {
+ try {
+ if (deploymentTemplate.deploymentDraftData) {
+ const parsedDraftResolvedData = JSON.parse(
+ deploymentTemplate.deploymentDraftData.configData[0].draftMetadata.draftResolvedValue,
+ )
+
+ return {
+ ...deploymentTemplate,
+ deploymentDraftData: {
+ ...deploymentTemplate.deploymentDraftData,
+ configData: [
+ {
+ ...deploymentTemplate.deploymentDraftData.configData[0],
+ draftMetadata: {
+ ...deploymentTemplate.deploymentDraftData.configData[0].draftMetadata,
+ data: JSON.stringify({
+ ...JSON.parse(
+ deploymentTemplate.deploymentDraftData.configData[0].draftMetadata.data,
+ ),
+ envOverrideValues: parsedDraftResolvedData,
+ }),
+ },
+ },
+ ],
+ },
+ }
+ }
+
+ return {
+ ...deploymentTemplate,
+ data: deploymentTemplate.resolvedValue,
+ }
+ } catch {
+ return null
+ }
+}
+
+const getConfigDataWithResolvedDeploymentTemplate = (
+ data: AppEnvDeploymentConfigListParams['compareList'],
+ convertVariables: boolean,
+): AppEnvDeploymentConfigListParams['compareList'] => {
+ if (!data) {
+ return {
+ deploymentTemplate: null,
+ configMapData: null,
+ isAppAdmin: null,
+ secretsData: null,
+ pipelineConfigData: null,
+ }
+ }
+
+ if (!data.deploymentTemplate || !convertVariables) {
+ return data
+ }
+
+ return {
+ ...data,
+ deploymentTemplate: getDeploymentTemplateResolvedData(data.deploymentTemplate),
+ }
+}
+
/**
* Generates a list of deployment configurations for application environments and identifies changes between the current and compare lists.
*
@@ -453,70 +610,129 @@ export const getAppEnvDeploymentConfigList = ): {
configList: DeploymentConfigDiffProps['configList']
navList: DeploymentConfigDiffProps['navList']
collapsibleNavList: DeploymentConfigDiffProps['collapsibleNavList']
} => {
if (!isManifestView) {
- const _currentList = currentList as AppEnvDeploymentConfigListParams['currentList']
- const _compareList = compareList as AppEnvDeploymentConfigListParams['compareList']
- const currentDeploymentData = getDeploymentTemplateDiffViewData(_currentList.deploymentTemplate, sortOrder)
- const compareDeploymentData = getDeploymentTemplateDiffViewData(_compareList.deploymentTemplate, sortOrder)
+ const compareToObject = getConfigDataWithResolvedDeploymentTemplate(
+ currentList as AppEnvDeploymentConfigListParams['currentList'],
+ convertVariables,
+ )
+ const compareWithObject = getConfigDataWithResolvedDeploymentTemplate(
+ compareList as AppEnvDeploymentConfigListParams['compareList'],
+ convertVariables,
+ )
+ const currentDeploymentData = getDeploymentTemplateDiffViewData(
+ compareToObject.deploymentTemplate,
+ compareToTemplateOptions,
+ )
+ const compareDeploymentData = getDeploymentTemplateDiffViewData(
+ compareWithObject.deploymentTemplate,
+ compareWithTemplateOptions,
+ )
const deploymentTemplateData = {
id: EnvResourceType.DeploymentTemplate,
+ pathType: EnvResourceType.DeploymentTemplate,
title: 'Deployment Template',
primaryConfig: {
- heading: getDiffHeading(_compareList.deploymentTemplate, true),
+ heading: getDiffHeading(compareWithObject.deploymentTemplate, true),
list: compareDeploymentData,
},
secondaryConfig: {
- heading: getDiffHeading(_currentList.deploymentTemplate, true),
+ heading: getDiffHeading(compareToObject.deploymentTemplate, true),
list: currentDeploymentData,
},
- hasDiff: currentDeploymentData.codeEditorValue.value !== compareDeploymentData.codeEditorValue.value,
- isDeploymentTemplate: true,
+ diffState: getDiffState(currentDeploymentData, compareDeploymentData),
+ }
+
+ let currentPipelineConfigData: DeploymentHistoryDetail
+ let comparePipelineConfigData: DeploymentHistoryDetail
+ let pipelineConfigData: DeploymentConfigDiffProps['configList'][0]
+
+ if (compareToObject.pipelineConfigData || compareWithObject.pipelineConfigData) {
+ currentPipelineConfigData = getPipelineConfigDiffViewData(compareToObject.pipelineConfigData)
+ comparePipelineConfigData = getPipelineConfigDiffViewData(compareWithObject.pipelineConfigData)
+ pipelineConfigData = {
+ id: EnvResourceType.PipelineStrategy,
+ pathType: EnvResourceType.PipelineStrategy,
+ title: 'Pipeline Configuration',
+ primaryConfig: {
+ heading: null,
+ list: comparePipelineConfigData,
+ },
+ secondaryConfig: {
+ heading: null,
+ list: currentPipelineConfigData,
+ },
+ diffState: getDiffState(currentPipelineConfigData, comparePipelineConfigData),
+ }
}
const cmData = getConfigMapSecretData(
- _currentList.configMapData,
- _compareList.configMapData,
+ compareToObject.configMapData,
+ compareWithObject.configMapData,
ConfigResourceType.ConfigMap,
- _currentList.isAppAdmin,
- _compareList.isAppAdmin,
+ compareToObject.isAppAdmin,
+ compareWithObject.isAppAdmin,
+ convertVariables,
)
const secretData = getConfigMapSecretData(
- _currentList.secretsData,
- _compareList.secretsData,
+ compareToObject.secretsData,
+ compareWithObject.secretsData,
ConfigResourceType.Secret,
- _currentList.isAppAdmin,
- _compareList.isAppAdmin,
+ compareToObject.isAppAdmin,
+ compareWithObject.isAppAdmin,
+ convertVariables,
)
- const configList: DeploymentConfigDiffProps['configList'] = [deploymentTemplateData, ...cmData, ...secretData]
+ const configList: DeploymentConfigDiffProps['configList'] = [
+ deploymentTemplateData,
+ ...(pipelineConfigData ? [pipelineConfigData] : []),
+ ...cmData,
+ ...secretData,
+ ]
const navList: DeploymentConfigDiffProps['navList'] = [
{
title: deploymentTemplateData.title,
- hasDiff: deploymentTemplateData.hasDiff,
+ diffState: deploymentTemplateData.diffState,
href: getNavItemHref(EnvResourceType.DeploymentTemplate, null),
onClick: () => {
const element = document.querySelector(`#${deploymentTemplateData.id}`)
element?.scrollIntoView({ block: 'start' })
},
+ Icon: ICFileCode,
},
+ ...(pipelineConfigData
+ ? [
+ {
+ title: pipelineConfigData.title,
+ diffState: pipelineConfigData.diffState,
+ href: getNavItemHref(EnvResourceType.PipelineStrategy, null),
+ onClick: () => {
+ const element = document.querySelector(`#${pipelineConfigData.id}`)
+ element?.scrollIntoView({ block: 'start' })
+ },
+ Icon: ICFileCode,
+ },
+ ]
+ : []),
]
const collapsibleNavList: DeploymentConfigDiffProps['collapsibleNavList'] = [
{
header: 'ConfigMaps',
id: EnvResourceType.ConfigMap,
- items: cmData.map(({ name, hasDiff, id }) => ({
+ items: cmData.map(({ name, diffState, id }) => ({
title: name,
- hasDiff,
+ diffState,
href: getNavItemHref(EnvResourceType.ConfigMap, name),
onClick: () => {
const element = document.querySelector(`#${id}`)
@@ -528,9 +744,9 @@ export const getAppEnvDeploymentConfigList = ({
+ items: secretData.map(({ name, diffState, id }) => ({
title: name,
- hasDiff,
+ diffState,
href: getNavItemHref(EnvResourceType.Secret, name),
onClick: () => {
const element = document.querySelector(`#${id}`)
@@ -548,14 +764,15 @@ export const getAppEnvDeploymentConfigList = ['currentList']
- const _compareList = compareList as AppEnvDeploymentConfigListParams['compareList']
+ const compareToObject = currentList as AppEnvDeploymentConfigListParams['currentList']
+ const compareWithObject = compareList as AppEnvDeploymentConfigListParams['compareList']
- const currentManifestData = getManifestDiffViewData(_currentList)
- const compareManifestData = getManifestDiffViewData(_compareList)
+ const currentManifestData = getManifestDiffViewData(compareToObject)
+ const compareManifestData = getManifestDiffViewData(compareWithObject)
const manifestData = {
id: EnvResourceType.Manifest,
+ pathType: EnvResourceType.Manifest,
title: 'Manifest Output',
primaryConfig: {
heading: Generated Manifest,
@@ -565,8 +782,8 @@ export const getAppEnvDeploymentConfigList = Generated Manifest,
list: currentManifestData,
},
- hasDiff: currentManifestData.codeEditorValue.value !== compareManifestData.codeEditorValue.value,
- isDeploymentTemplate: true,
+ diffState: getDiffState(currentManifestData, compareManifestData),
+ singleView: true,
}
const configList: DeploymentConfigDiffProps['configList'] = [manifestData]
@@ -574,7 +791,7 @@ export const getAppEnvDeploymentConfigList = {
const element = document.querySelector(`#${manifestData.id}`)
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffAccordion.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffAccordion.tsx
index 64b720431..a13f67817 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffAccordion.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffAccordion.tsx
@@ -1,34 +1,44 @@
-import { forwardRef } from 'react'
-
import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg'
import { Collapse } from '../Collapse'
-import { DeploymentConfigDiffAccordionProps } from './DeploymentConfigDiff.types'
+import { DeploymentConfigDiffAccordionProps, DeploymentConfigDiffState } from './DeploymentConfigDiff.types'
+import { diffStateTextColorMap, diffStateTextMap } from './DeploymentConfigDiff.constants'
-export const DeploymentConfigDiffAccordion = forwardRef(
- (
- { hasDiff, children, title, id, isExpanded, onClick, onTransitionEnd }: DeploymentConfigDiffAccordionProps,
- ref,
- ) => (
-
-
-
- {title}
+export const DeploymentConfigDiffAccordion = ({
+ diffState,
+ showDetailedDiffState,
+ hideDiffState,
+ children,
+ title,
+ id,
+ isExpanded,
+ onClick,
+ onTransitionEnd,
+}: DeploymentConfigDiffAccordionProps) => (
+
+
+
+ {title}
+ {!hideDiffState && (
{`${hasDiff ? 'Has' : 'No'} difference`}
-
-
- {children}
-
-
- ),
+ className={`m-0 fs-13 lh-20 fw-6 ${showDetailedDiffState ? diffStateTextColorMap[diffState] : (diffState !== DeploymentConfigDiffState.NO_DIFF && 'cy-7') || 'cg-7'}`}
+ >
+ {showDetailedDiffState
+ ? diffStateTextMap[diffState]
+ : `${diffState !== DeploymentConfigDiffState.NO_DIFF ? 'Has' : 'No'} difference`}
+
+ )}
+
+
+ {children}
+
+
)
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx
index 5ef7c7081..a512705d2 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx
@@ -1,16 +1,26 @@
-import { Fragment, TransitionEvent, useEffect, useState } from 'react'
+import { Fragment, useEffect, useRef, useState } from 'react'
+import Tippy from '@tippyjs/react'
import { ReactComponent as ICSortArrowDown } from '@Icons/ic-sort-arrow-down.svg'
import { ReactComponent as ICSort } from '@Icons/ic-arrow-up-down.svg'
+import { ReactComponent as ICViewVariableToggle } from '@Icons/ic-view-variable-toggle.svg'
import { Progressing } from '@Common/Progressing'
import { CodeEditor } from '@Common/CodeEditor'
import { MODES, SortingOrder } from '@Common/Constants'
import ErrorScreenManager from '@Common/ErrorScreenManager'
+import Toggle from '@Common/Toggle/Toggle'
+import { ComponentSizeType } from '@Shared/constants'
+import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
import { SelectPicker } from '../SelectPicker'
import { DeploymentHistoryDiffView } from '../CICDHistory'
import { DeploymentConfigDiffAccordion } from './DeploymentConfigDiffAccordion'
-import { DeploymentConfigDiffMainProps, DeploymentConfigDiffSelectPickerProps } from './DeploymentConfigDiff.types'
+import {
+ DeploymentConfigDiffMainProps,
+ DeploymentConfigDiffSelectPickerProps,
+ DeploymentConfigDiffState,
+ DeploymentConfigDiffAccordionProps,
+} from './DeploymentConfigDiff.types'
export const DeploymentConfigDiffMain = ({
isLoading,
@@ -20,10 +30,17 @@ export const DeploymentConfigDiffMain = ({
selectorsConfig,
sortingConfig,
scrollIntoViewId,
+ scopeVariablesConfig,
+ showDetailedDiffState,
+ hideDiffState,
}: DeploymentConfigDiffMainProps) => {
// STATES
const [expandedView, setExpandedView] = useState>({})
+ // REFS
+ /** Ref to track if the element should scroll into view after expanding */
+ const scrollIntoViewAfterExpand = useRef(false)
+
const handleAccordionClick = (id: string) => () => {
setExpandedView({
...expandedView,
@@ -31,10 +48,12 @@ export const DeploymentConfigDiffMain = ({
})
}
- const handleTransitionEnd = (id: string) => (e: TransitionEvent) => {
- if (e.target === e.currentTarget && scrollIntoViewId === id) {
+ const onTransitionEnd: DeploymentConfigDiffAccordionProps['onTransitionEnd'] = (e) => {
+ if (scrollIntoViewAfterExpand.current && e.target === e.currentTarget) {
const element = document.querySelector(`#${scrollIntoViewId}`)
element?.scrollIntoView({ block: 'start' })
+ // Reset ref after scrolling into view
+ scrollIntoViewAfterExpand.current = false
}
}
@@ -42,7 +61,10 @@ export const DeploymentConfigDiffMain = ({
if (!isLoading) {
setExpandedView(
configList.reduce(
- (acc, curr) => ({ ...acc, [curr.id]: scrollIntoViewId === curr.id || curr.hasDiff }),
+ (acc, curr) => ({
+ ...acc,
+ [curr.id]: scrollIntoViewId === curr.id || curr.diffState !== DeploymentConfigDiffState.NO_DIFF,
+ }),
{},
),
)
@@ -51,6 +73,7 @@ export const DeploymentConfigDiffMain = ({
useEffect(() => {
if (scrollIntoViewId) {
+ scrollIntoViewAfterExpand.current = true
setExpandedView((prev) => ({ ...prev, [scrollIntoViewId]: true }))
}
}, [scrollIntoViewId])
@@ -65,7 +88,9 @@ export const DeploymentConfigDiffMain = ({
) : (
configItem.text
)}
- {index !== list.length - 1 && /}
+ {!selectorsConfig?.hideDivider && index !== list.length - 1 && (
+ /
+ )}
)
}
@@ -80,7 +105,9 @@ export const DeploymentConfigDiffMain = ({
isDisabled={isLoading || selectPickerProps?.isDisabled}
/>
- {index !== list.length - 1 && /}
+ {!selectorsConfig?.hideDivider && index !== list.length - 1 && (
+ /
+ )}
)
})
@@ -90,26 +117,55 @@ export const DeploymentConfigDiffMain = ({
const { handleSorting, sortBy, sortOrder } = sortingConfig
return (
-
-
- {sortBy ? (
+
) : (
-
- )}
- Sort keys
-
-
+
+ )
+ }
+ onClick={handleSorting}
+ disabled={isLoading}
+ />
+ )
+ }
+
+ return null
+ }
+
+ const renderScopeVariablesButton = () => {
+ if (scopeVariablesConfig) {
+ const { convertVariables } = scopeVariablesConfig
+
+ return (
+
+
+
+
+
)
}
@@ -117,7 +173,7 @@ export const DeploymentConfigDiffMain = ({
}
const renderDiffs = () =>
- configList.map(({ id, isDeploymentTemplate, primaryConfig, secondaryConfig, title, hasDiff }) => {
+ configList.map(({ id, primaryConfig, secondaryConfig, title, diffState, singleView }) => {
const { heading: primaryHeading, list: primaryList } = primaryConfig
const { heading: secondaryHeading, list: secondaryList } = secondaryConfig
@@ -127,18 +183,20 @@ export const DeploymentConfigDiffMain = ({
id={id}
title={title}
isExpanded={expandedView[id]}
- hasDiff={hasDiff}
+ diffState={diffState}
onClick={handleAccordionClick(id)}
- onTransitionEnd={handleTransitionEnd(id)}
+ onTransitionEnd={onTransitionEnd}
+ showDetailedDiffState={showDetailedDiffState}
+ hideDiffState={hideDiffState}
>
- {isDeploymentTemplate ? (
+ {singleView ? (
<>
{primaryHeading}
{secondaryHeading}
) : (
-
-
{primaryHeading}
-
{secondaryHeading}
-
+ {primaryHeading && secondaryHeading && (
+
+
{primaryHeading}
+
{secondaryHeading}
+
+ )}
)}
@@ -169,29 +229,45 @@ export const DeploymentConfigDiffMain = ({
)
})
+ const renderContent = () => {
+ if (isLoading) {
+ return
+ }
+
+ if (errorConfig?.error) {
+ return (
+
+ )
+ }
+
+ return {renderDiffs()}
+ }
+
return (
-
+
-
{headerText}
+ {!!headerText &&
{headerText}
}
{renderHeaderSelectors(selectorsConfig.primaryConfig)}
{renderHeaderSelectors(selectorsConfig.secondaryConfig)}
- {renderSortButton()}
+ {(sortingConfig || scopeVariablesConfig) && (
+
+ {renderSortButton()}
+ {renderScopeVariablesButton()}
+
+ )}
-
- {errorConfig?.error &&
}
- {!errorConfig?.error &&
- (isLoading ? (
-
- ) : (
-
{renderDiffs()}
- ))}
-
+
{renderContent()}
)
}
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx
index b8f2d8307..1211a2901 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx
@@ -4,11 +4,12 @@ import Tippy from '@tippyjs/react'
import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
import { ReactComponent as ICInfoOutlined } from '@Icons/ic-info-outlined.svg'
-import { ReactComponent as ICDiffFileUpdated } from '@Icons/ic-diff-file-updated.svg'
+import { ReactComponent as ICError } from '@Icons/ic-error.svg'
import { StyledRadioGroup } from '@Common/index'
-import { CollapsibleList } from '../CollapsibleList'
-import { DeploymentConfigDiffNavigationProps } from './DeploymentConfigDiff.types'
+import { CollapsibleList, CollapsibleListConfig } from '../CollapsibleList'
+import { DeploymentConfigDiffNavigationProps, DeploymentConfigDiffState } from './DeploymentConfigDiff.types'
+import { diffStateIconMap, diffStateTooltipTextMap } from './DeploymentConfigDiff.constants'
// LOADING SHIMMER
const ShimmerText = ({ width }: { width: string }) => (
@@ -24,7 +25,11 @@ export const DeploymentConfigDiffNavigation = ({
goBackURL,
navHeading,
navHelpText,
+ isNavHelpTextShowingError,
tabConfig,
+ errorConfig,
+ showDetailedDiffState,
+ hideDiffState,
}: DeploymentConfigDiffNavigationProps) => {
// STATES
const [expandedIds, setExpandedIds] = useState
>({})
@@ -34,17 +39,23 @@ export const DeploymentConfigDiffNavigation = ({
}, [collapsibleNavList])
/** Collapsible List Config. */
- const collapsibleListConfig = collapsibleNavList.map(({ items, ...resListItem }) => ({
+ const collapsibleListConfig = collapsibleNavList.map(({ items, ...resListItem }) => ({
...resListItem,
isExpanded: expandedIds[resListItem.id],
- items: items.map(({ hasDiff, ...resItem }) => ({
+ items: items.map(({ diffState, ...resItem }) => ({
...resItem,
- ...(hasDiff
+ strikeThrough: showDetailedDiffState && diffState === DeploymentConfigDiffState.DELETED,
+ ...(!hideDiffState && diffState !== DeploymentConfigDiffState.NO_DIFF
? {
iconConfig: {
- Icon: ICDiffFileUpdated,
- props: { className: 'icon-dim-16 dc__no-shrink' },
- tooltipProps: { content: 'File has difference', arrow: false, placement: 'right' as const },
+ Icon: showDetailedDiffState ? diffStateIconMap[diffState] : diffStateIconMap.hasDiff,
+ tooltipProps: {
+ content: showDetailedDiffState
+ ? diffStateTooltipTextMap[diffState]
+ : diffStateTooltipTextMap.hasDiff,
+ arrow: false,
+ placement: 'right' as const,
+ },
},
}
: {}),
@@ -101,45 +112,81 @@ export const DeploymentConfigDiffNavigation = ({
)
}
- const renderContent = () => (
+ const renderNavigation = () => (
<>
- {navList.map(({ title, href, onClick, hasDiff }) => (
-
- {title}
- {hasDiff && (
-
-
-
-
-
- )}
-
- ))}
+ {navList.map(({ title, href, onClick, diffState, Icon }) => {
+ const DiffIcon = showDetailedDiffState ? diffStateIconMap[diffState] : diffStateIconMap.hasDiff
+ return (
+
+ {Icon && }
+ {title}
+ {!hideDiffState && diffState !== DeploymentConfigDiffState.NO_DIFF && (
+
+
+
+
+
+ )}
+
+ )
+ })}
{navHelpText && (
-
+ {isNavHelpTextShowingError ? (
+
+ ) : (
+
+ )}
-
{navHelpText}
+
{navHelpText}
)}
>
)
- const renderLoading = () => ['90', '70', '50'].map((item) => )
+ const renderContent = () => {
+ if (isLoading) {
+ return ['90', '70', '50'].map((item) => )
+ }
+
+ if (errorConfig?.error) {
+ return (
+
+
+
+
+
+ Failed to load files. Please reload or select a different reference to compare with.
+
+
+ )
+ }
+
+ return renderNavigation()
+ }
return (
-
+
{renderTopContent()}
{!!tabConfig?.tabs.length && renderTabConfig()}
-
{isLoading ? renderLoading() : renderContent()}
+
{renderContent()}
)
}
diff --git a/src/Shared/Components/DeploymentConfigDiff/index.ts b/src/Shared/Components/DeploymentConfigDiff/index.ts
index c84340d18..0b8c7a4d5 100644
--- a/src/Shared/Components/DeploymentConfigDiff/index.ts
+++ b/src/Shared/Components/DeploymentConfigDiff/index.ts
@@ -1,3 +1,4 @@
export * from './DeploymentConfigDiff.component'
export * from './DeploymentConfigDiff.types'
export * from './DeploymentConfigDiff.utils'
+export * from './DeploymentConfigDiff.constants'
diff --git a/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx b/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx
index d9ca3b71b..e27666807 100644
--- a/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx
+++ b/src/Shared/Components/Header/HeaderWithCreateButton/HeaderWithCreateButon.tsx
@@ -16,13 +16,14 @@
import { useState } from 'react'
import { useHistory, useLocation, useParams } from 'react-router-dom'
-import { Modal, SERVER_MODE, URLS } from '../../../../Common'
+import Button from '@Shared/Components/Button/Button.component'
+import { ReactComponent as DropDown } from '@Icons/ic-caret-down-small.svg'
+import { ReactComponent as ChartIcon } from '@Icons/ic-charts.svg'
+import { ReactComponent as AddIcon } from '@Icons/ic-add.svg'
+import { ReactComponent as JobIcon } from '@Icons/ic-k8s-job.svg'
import PageHeader from '../PageHeader'
-import { ReactComponent as DropDown } from '../../../../Assets/Icon/ic-dropdown-filled.svg'
-import { ReactComponent as ChartIcon } from '../../../../Assets/Icon/ic-charts.svg'
-import { ReactComponent as AddIcon } from '../../../../Assets/Icon/ic-add.svg'
-import { ReactComponent as JobIcon } from '../../../../Assets/Icon/ic-k8s-job.svg'
-import { AppListConstants } from '../../../constants'
+import { Modal, SERVER_MODE, URLS } from '../../../../Common'
+import { AppListConstants, ComponentSizeType } from '../../../constants'
import './HeaderWithCreateButton.scss'
import { useMainContext } from '../../../Providers'
@@ -56,23 +57,28 @@ export const HeaderWithCreateButton = ({ headerName }: HeaderWithCreateButtonPro
const renderActionButtons = () =>
serverMode === SERVER_MODE.FULL ? (
-
- Create
-
-
+ dataTestId="create-app-button-on-header"
+ endIcon={
}
+ size={ComponentSizeType.small}
+ />
) : (
-
- Deploy helm charts
-
+
)
const renderCreateSelectionModal = () => (
-
+
{
+ const [showEmbeddedIframeModal, setEmbeddedIframeModal] = useState(false)
+
+ const {
+ FEATURE_PROMO_EMBEDDED_BUTTON_TEXT,
+ FEATURE_PROMO_EMBEDDED_MODAL_TITLE,
+ FEATURE_PROMO_EMBEDDED_IFRAME_URL,
+ } = window._env_
+
+ const onClickShowIframeModal = useCallback(() => setEmbeddedIframeModal(true), [])
+ const onClickCloseIframeModal = useCallback(() => setEmbeddedIframeModal(false), [])
+
+ const renderIframeDrawer = () => (
+
+
+
+
+ {FEATURE_PROMO_EMBEDDED_MODAL_TITLE || FEATURE_PROMO_EMBEDDED_BUTTON_TEXT}
+
+ }
+ showAriaLabelInTippy={false}
+ />
+
+ {FEATURE_PROMO_EMBEDDED_IFRAME_URL ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+
+ return (
+
+ {FEATURE_PROMO_EMBEDDED_BUTTON_TEXT && (
+
+ )}
+ {showEmbeddedIframeModal && renderIframeDrawer()}
+
+ )
+}
diff --git a/src/Shared/Components/Header/PageHeader.tsx b/src/Shared/Components/Header/PageHeader.tsx
index eddfc87ac..aa2bc51eb 100644
--- a/src/Shared/Components/Header/PageHeader.tsx
+++ b/src/Shared/Components/Header/PageHeader.tsx
@@ -32,6 +32,7 @@ import { ReactComponent as DropDownIcon } from '../../../Assets/Icon/ic-chevron-
import AnnouncementBanner from '../AnnouncementBanner/AnnouncementBanner'
import { useMainContext, useUserEmail } from '../../Providers'
import { InfoIconTippy } from '../InfoIconTippy'
+import { IframePromoButton } from './IframePromoButton'
const PageHeader = ({
headerName,
@@ -121,8 +122,8 @@ const PageHeader = ({
const renderLogoutHelpSection = () => (
<>
-
-
+
+
@@ -156,6 +157,8 @@ const PageHeader = ({
Beta
)
+ const renderIframeButton = () =>
+
return (
{showTabs && (
-
+
+ {renderIframeButton()}
{typeof renderActionButtons === 'function' && renderActionButtons()}
{renderLogoutHelpSection()}
@@ -258,8 +262,9 @@ const PageHeader = ({
/>
)}
{!showTabs && (
-
+
{typeof renderActionButtons === 'function' && renderActionButtons()}
+ {renderIframeButton()}
{renderLogoutHelpSection()}
)}
diff --git a/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx b/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx
index c482227f0..56d5b6025 100644
--- a/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx
+++ b/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx
@@ -15,6 +15,7 @@
*/
import Tippy from '@tippyjs/react'
+import { Tooltip } from '@Common/Tooltip'
import { DefaultUserKey } from '../../../types'
import { ImagePathTippyContentProps } from './types'
import { ArtifactInfoProps } from '../types'
@@ -57,8 +58,8 @@ const ArtifactInfo = ({
}
return (
-
-
+
+
{deployedTime}
)
@@ -88,7 +89,9 @@ const ArtifactInfo = ({
>
{deployedBy[0]}
-
{deployedBy}
+
+ {deployedBy}
+
)
}
diff --git a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx
index 729ddae89..1af043d41 100644
--- a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx
+++ b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx
@@ -26,7 +26,7 @@ const InfoIconTippy = ({
documentationLink,
documentationLinkText,
additionalContent,
- iconClassName = 'icon-dim-16',
+ iconClassName = 'icon-dim-16 dc__no-shrink',
placement = 'bottom',
dataTestid = 'info-tippy-button',
children,
@@ -49,7 +49,7 @@ const InfoIconTippy = ({
{children || (
diff --git a/src/Shared/Components/InvalidYAMLTippy/InvalidYAMLTippyWrapper.tsx b/src/Shared/Components/InvalidYAMLTippy/InvalidYAMLTippyWrapper.tsx
new file mode 100644
index 000000000..fbc5f5444
--- /dev/null
+++ b/src/Shared/Components/InvalidYAMLTippy/InvalidYAMLTippyWrapper.tsx
@@ -0,0 +1,30 @@
+import { Tooltip } from '@Common/Tooltip'
+import { InvalidYAMLTippyWrapperProps } from './types'
+import { getInvalidTippyContent } from './utils'
+
+const InvalidYAMLTippy = ({ parsingError, restoreLastSavedYAML, children }: InvalidYAMLTippyWrapperProps) => (
+
+ {children}
+
+)
+
+const InvalidYAMLTippyWrapper = ({ parsingError, restoreLastSavedYAML, children }: InvalidYAMLTippyWrapperProps) => {
+ if (parsingError) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return children
+}
+
+export default InvalidYAMLTippyWrapper
diff --git a/src/Shared/Components/InvalidYAMLTippy/constants.ts b/src/Shared/Components/InvalidYAMLTippy/constants.ts
new file mode 100644
index 000000000..a7ec74303
--- /dev/null
+++ b/src/Shared/Components/InvalidYAMLTippy/constants.ts
@@ -0,0 +1 @@
+export const DEFAULT_INVALID_YAML_ERROR = 'The provided YAML is invalid. Please provide valid YAML.'
diff --git a/src/Shared/Components/InvalidYAMLTippy/index.ts b/src/Shared/Components/InvalidYAMLTippy/index.ts
new file mode 100644
index 000000000..acc50f2d6
--- /dev/null
+++ b/src/Shared/Components/InvalidYAMLTippy/index.ts
@@ -0,0 +1,2 @@
+export { default as InvalidYAMLTippyWrapper } from './InvalidYAMLTippyWrapper'
+export { getInvalidTippyContent } from './utils'
diff --git a/src/Shared/Components/InvalidYAMLTippy/types.ts b/src/Shared/Components/InvalidYAMLTippy/types.ts
new file mode 100644
index 000000000..c83642e59
--- /dev/null
+++ b/src/Shared/Components/InvalidYAMLTippy/types.ts
@@ -0,0 +1,7 @@
+import { TooltipProps } from '@Common/Tooltip/types'
+
+export interface InvalidYAMLTippyWrapperProps {
+ parsingError: string
+ restoreLastSavedYAML?: () => void
+ children: TooltipProps['children']
+}
diff --git a/src/Shared/Components/InvalidYAMLTippy/utils.tsx b/src/Shared/Components/InvalidYAMLTippy/utils.tsx
new file mode 100644
index 000000000..8b8daf590
--- /dev/null
+++ b/src/Shared/Components/InvalidYAMLTippy/utils.tsx
@@ -0,0 +1,29 @@
+import { ReactComponent as ICArrowCounterClockwise } from '@Icons/ic-arrow-counter-clockwise.svg'
+import { DEFAULT_INVALID_YAML_ERROR } from './constants'
+import { InvalidYAMLTippyWrapperProps } from './types'
+
+export const getInvalidTippyContent = ({
+ parsingError,
+ restoreLastSavedYAML,
+}: Pick) => (
+
+
+
Invalid YAML
+
+ {parsingError || DEFAULT_INVALID_YAML_ERROR}
+
+
+
+ {restoreLastSavedYAML && (
+
+
+ Restore last saved YAML
+
+ )}
+
+)
diff --git a/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx b/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx
index 0e18762ec..a978203ef 100644
--- a/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx
+++ b/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx
@@ -308,10 +308,8 @@ export const KeyValueTable = ({
const onRowDataBlur = (row: KeyValueRow, key: K) => (e: React.FocusEvent) => {
const { value } = e.target
- if (value || row.data[key === firstHeaderKey ? secondHeaderKey : firstHeaderKey].value) {
- onChange?.(row.id, key, value)
- onError?.(!checkAllRowsAreValid(updatedRows))
- }
+ onChange?.(row.id, key, value)
+ onError?.(!checkAllRowsAreValid(updatedRows))
}
const renderFirstHeader = (key: K, label: string, className: string) => (
diff --git a/src/Shared/Components/SelectPicker/common.tsx b/src/Shared/Components/SelectPicker/common.tsx
index fbf8cf465..83771f797 100644
--- a/src/Shared/Components/SelectPicker/common.tsx
+++ b/src/Shared/Components/SelectPicker/common.tsx
@@ -36,9 +36,30 @@ import { CHECKBOX_VALUE } from '@Common/Types'
import { Checkbox } from '@Common/Checkbox'
import { ReactSelectInputAction } from '@Common/Constants'
import { isNullOrUndefined } from '@Shared/Helpers'
+import { Tooltip } from '@Common/Tooltip'
+import { TooltipProps } from '@Common/Tooltip/types'
import { SelectPickerGroupHeadingProps, SelectPickerOptionType, SelectPickerProps } from './type'
import { getGroupCheckboxValue } from './utils'
+const getTooltipProps = (tooltipProps: SelectPickerOptionType['tooltipProps'] = {}): TooltipProps => {
+ if (tooltipProps) {
+ if (Object.hasOwn(tooltipProps, 'shortcutKeyCombo') && 'shortcutKeyCombo' in tooltipProps) {
+ return tooltipProps
+ }
+
+ return {
+ // TODO: using some typing somersaults here, clean it up later
+ alwaysShowTippyOnHover: !!(tooltipProps as Required>)?.content,
+ ...(tooltipProps as Required>),
+ }
+ }
+
+ return {
+ alwaysShowTippyOnHover: false,
+ content: null,
+ }
+}
+
export const SelectPickerDropdownIndicator = (
props: DropdownIndicatorProps>,
) => {
@@ -46,7 +67,7 @@ export const SelectPickerDropdownIndicator = (
return (
-
+
)
}
@@ -55,7 +76,7 @@ export const SelectPickerClearIndicator = (
props: ClearIndicatorProps>,
) => (
-
+
)
@@ -100,6 +121,7 @@ export const SelectPickerValueContainer =
+ {/* Size will not work here need to go in details later when prioritized */}
{showSelectedOptionsCount && selectedOptionsLength > 0 && (
{selectedOptionsLength}
@@ -124,7 +146,7 @@ export const SelectPickerOption =
({
isDisabled,
isSelected,
} = props
- const { description, startIcon, endIcon } = data ?? {}
+ const { description, startIcon, endIcon, tooltipProps } = data ?? {}
const showDescription = !!description
// __isNew__ denotes the new option to be created
const isCreatableOption = '__isNew__' in data && data.__isNew__
@@ -137,39 +159,44 @@ export const SelectPickerOption = ({
return (
-
- {isMulti && !isCreatableOption && (
-
- )}
-
- {startIcon && (
-
{startIcon}
+
+
+ {isMulti && !isCreatableOption && (
+
)}
-
-
- {label}
-
- {/* Add support for custom ellipsis if required */}
- {showDescription && (
-
- {description}
-
+
+ {startIcon && (
+
{startIcon}
+ )}
+
+
+ {label}
+
+ {/* Add support for custom ellipsis if required */}
+ {showDescription &&
+ (typeof description === 'string' ? (
+
+ {description}
+
+ ) : (
+
{description}
+ ))}
+
+ {endIcon && (
+
{endIcon}
)}
- {endIcon && (
-
{endIcon}
- )}
-
+
)
}
diff --git a/src/Shared/Components/SelectPicker/constants.ts b/src/Shared/Components/SelectPicker/constants.ts
new file mode 100644
index 000000000..ef7b421e5
--- /dev/null
+++ b/src/Shared/Components/SelectPicker/constants.ts
@@ -0,0 +1,21 @@
+import { CSSProperties } from 'react'
+import { ComponentSizeType } from '@Shared/constants'
+import { SelectPickerProps } from './type'
+
+export const SELECT_PICKER_FONT_SIZE_MAP: Record
= {
+ [ComponentSizeType.small]: '12px',
+ [ComponentSizeType.medium]: '13px',
+ [ComponentSizeType.large]: '13px',
+}
+
+export const SELECT_PICKER_ICON_SIZE_MAP: Record> = {
+ [ComponentSizeType.small]: { width: '12px', height: '12px' },
+ [ComponentSizeType.medium]: { width: '16px', height: '16px' },
+ [ComponentSizeType.large]: { width: '16px', height: '16px' },
+}
+
+export const SELECT_PICKER_CONTROL_SIZE_MAP: Record = {
+ [ComponentSizeType.small]: 'auto',
+ [ComponentSizeType.medium]: 'auto',
+ [ComponentSizeType.large]: '36px',
+}
diff --git a/src/Shared/Components/SelectPicker/type.ts b/src/Shared/Components/SelectPicker/type.ts
index 580b4cc4f..38eef3603 100644
--- a/src/Shared/Components/SelectPicker/type.ts
+++ b/src/Shared/Components/SelectPicker/type.ts
@@ -22,12 +22,13 @@ import { GroupBase, GroupHeadingProps, Props as ReactSelectProps, SelectInstance
import { CreatableProps } from 'react-select/creatable'
// This import allows to extend the base interface in react-select module via module augmentation
import type {} from 'react-select/base'
+import { TooltipProps } from '@Common/Tooltip/types'
export interface SelectPickerOptionType extends OptionType {
/**
* Description to be displayed for the option
*/
- description?: string
+ description?: ReactNode
/**
* Icon at the start of the option
*/
@@ -36,6 +37,13 @@ export interface SelectPickerOptionType extends O
* Icon at the end of the option
*/
endIcon?: ReactElement
+ /**
+ * Props passed to show the tippy on option
+ */
+ tooltipProps?:
+ | Omit
+ | (Omit &
+ Required>)
}
type SelectProps = ReactSelectProps<
@@ -148,11 +156,11 @@ export type SelectPickerProps
+ size?: Extract
/**
* Content to be shown in a tippy when disabled
*/
diff --git a/src/Shared/Components/SelectPicker/utils.ts b/src/Shared/Components/SelectPicker/utils.ts
index fa765114c..4d39f3164 100644
--- a/src/Shared/Components/SelectPicker/utils.ts
+++ b/src/Shared/Components/SelectPicker/utils.ts
@@ -18,6 +18,7 @@ import { CHECKBOX_VALUE } from '@Common/Types'
import { ComponentSizeType } from '@Shared/constants'
import { GroupBase, MultiValue, OptionsOrGroups, StylesConfig } from 'react-select'
import { SelectPickerOptionType, SelectPickerProps, SelectPickerVariantType } from './type'
+import { SELECT_PICKER_CONTROL_SIZE_MAP, SELECT_PICKER_FONT_SIZE_MAP, SELECT_PICKER_ICON_SIZE_MAP } from './constants'
const getMenuWidthFromSize = (
menuSize: SelectPickerProps['menuSize'],
@@ -117,7 +118,7 @@ export const getCommonSelectStyle = ({
}),
control: (base, state) => ({
...base,
- minHeight: size === ComponentSizeType.medium ? 'auto' : '36px',
+ minHeight: SELECT_PICKER_CONTROL_SIZE_MAP[size],
minWidth: '56px',
boxShadow: 'none',
backgroundColor: 'var(--N50)',
@@ -165,6 +166,10 @@ export const getCommonSelectStyle = ({
}),
dropdownIndicator: (base, state) => ({
...base,
+ ...SELECT_PICKER_ICON_SIZE_MAP[size],
+ display: 'flex',
+ alignItems: 'center',
+ flexShrink: '0',
color: 'var(--N600)',
padding: '0',
transition: 'all .2s ease',
@@ -172,7 +177,11 @@ export const getCommonSelectStyle = ({
}),
clearIndicator: (base) => ({
...base,
+ ...SELECT_PICKER_ICON_SIZE_MAP[size],
padding: 0,
+ display: 'flex',
+ alignItems: 'center',
+ flexShrink: '0',
'&:hover': {
backgroundColor: 'transparent',
@@ -284,7 +293,7 @@ export const getCommonSelectStyle = ({
placeholder: (base) => ({
...base,
color: 'var(--N500)',
- fontSize: '13px',
+ fontSize: SELECT_PICKER_FONT_SIZE_MAP[size],
lineHeight: '20px',
fontWeight: 400,
margin: 0,
@@ -301,7 +310,7 @@ export const getCommonSelectStyle = ({
...base,
margin: 0,
color: 'var(--N900)',
- fontSize: '13px',
+ fontSize: SELECT_PICKER_FONT_SIZE_MAP[size],
fontWeight: 400,
lineHeight: '20px',
...(getVariantOverrides(variant)?.singleValue(base, state) || {}),
diff --git a/src/Shared/Components/TabGroup/TabGroup.component.tsx b/src/Shared/Components/TabGroup/TabGroup.component.tsx
index c5f65879b..26d86ca95 100644
--- a/src/Shared/Components/TabGroup/TabGroup.component.tsx
+++ b/src/Shared/Components/TabGroup/TabGroup.component.tsx
@@ -1,6 +1,7 @@
import { Link, NavLink } from 'react-router-dom'
import { ComponentSizeType } from '@Shared/constants'
+import { Tooltip } from '@Common/Tooltip'
import { TabGroupProps, TabProps } from './TabGroup.types'
import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils'
@@ -23,6 +24,8 @@ const Tab = ({
showWarning,
disabled,
description,
+ shouldWrapTooltip,
+ tooltipProps,
}: TabProps & Pick) => {
const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({
hideTopPadding,
@@ -34,7 +37,7 @@ const Tab = ({
React.MouseEvent &
React.MouseEvent,
) => {
- if (active || e.currentTarget.classList.contains('active')) {
+ if (active || e.currentTarget.classList.contains('active') || (tabType === 'navLink' && disabled)) {
e.preventDefault()
}
props?.onClick?.(e)
@@ -100,13 +103,19 @@ const Tab = ({
}
}
- return (
+ const renderTabContainer = () => (
{getTabComponent()}
)
+
+ if (shouldWrapTooltip) {
+ return {renderTabContainer()}
+ }
+
+ return renderTabContainer()
}
export const TabGroup = ({
diff --git a/src/Shared/Components/TabGroup/TabGroup.scss b/src/Shared/Components/TabGroup/TabGroup.scss
index 3f863feef..f968e8167 100644
--- a/src/Shared/Components/TabGroup/TabGroup.scss
+++ b/src/Shared/Components/TabGroup/TabGroup.scss
@@ -33,7 +33,7 @@
bottom: -1px;
}
- &:hover:not(.tab-group__tab--block) {
+ &:hover:not(.tab-group__tab--block):not(.dc__disabled) {
color: var(--B500);
@include svg-styles(var(--B500));
}
@@ -76,7 +76,7 @@
color: var(--N900);
}
- &:hover {
+ &:hover:not([aria-disabled="true"]) {
color: var(--B500);
}
}
diff --git a/src/Shared/Components/TabGroup/TabGroup.types.ts b/src/Shared/Components/TabGroup/TabGroup.types.ts
index 893125b9b..f9c60caf5 100644
--- a/src/Shared/Components/TabGroup/TabGroup.types.ts
+++ b/src/Shared/Components/TabGroup/TabGroup.types.ts
@@ -2,6 +2,7 @@ import { LinkProps, NavLinkProps } from 'react-router-dom'
import { ComponentSizeType } from '@Shared/constants'
import { DataAttributes } from '@Shared/types'
+import { TooltipProps } from '@Common/Tooltip/types'
type TabComponentProps = TabTypeProps & DataAttributes
@@ -64,6 +65,16 @@ type ConditionalTabType =
active?: never | false
}
+type TabTooltipProps =
+ | {
+ shouldWrapTooltip: boolean
+ tooltipProps: TooltipProps
+ }
+ | {
+ shouldWrapTooltip?: never
+ tooltipProps?: never
+ }
+
export type TabProps = {
/**
* Unique identifier for the tab.
@@ -105,7 +116,8 @@ export type TabProps = {
* Disables the tab, preventing interaction and indicating an inactive state.
*/
disabled?: boolean
-} & ConditionalTabType
+} & ConditionalTabType &
+ TabTooltipProps
export interface TabGroupProps {
/**
diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts
index ba901ef62..87fccf22f 100644
--- a/src/Shared/Components/index.ts
+++ b/src/Shared/Components/index.ts
@@ -56,3 +56,4 @@ export * from './EditImageFormField'
export * from './Collapse'
export * from './Security'
export * from './Button'
+export * from './InvalidYAMLTippy'
diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx
index 5dff56f68..5eaac04f8 100644
--- a/src/Shared/Helpers.tsx
+++ b/src/Shared/Helpers.tsx
@@ -821,3 +821,34 @@ export const getDefaultValueFromType = (value: unknown) => {
return null
}
}
+
+/**
+ * Groups an array of objects by a specified key.
+ *
+ * This function takes an array of objects and a key, and groups the objects in the array
+ * based on the value of the specified key. If an object does not have the specified key,
+ * it will be grouped under the `'UNGROUPED'` key.
+ *
+ * @param array - The array of objects to be grouped.
+ * @param key - The key of the object used to group the array.
+ * @returns An object where the keys are the unique values of the specified key in the array,
+ * and the values are arrays of objects that share the same key value.
+ */
+export const groupArrayByObjectKey = , K extends keyof T>(
+ array: T[],
+ key: K,
+): Record =>
+ array.reduce(
+ (result, currentValue) => {
+ const groupKey = currentValue[key] ?? 'UNGROUPED'
+
+ if (!result[groupKey]) {
+ Object.assign(result, { [groupKey]: [] })
+ }
+
+ result[groupKey].push(currentValue)
+
+ return result
+ },
+ {} as Record,
+ )
diff --git a/src/Shared/Hooks/useForm/useForm.ts b/src/Shared/Hooks/useForm/useForm.ts
index e3362ede5..f921bb545 100644
--- a/src/Shared/Hooks/useForm/useForm.ts
+++ b/src/Shared/Hooks/useForm/useForm.ts
@@ -1,4 +1,6 @@
-import { ChangeEvent, FormEvent, useState } from 'react'
+import { BaseSyntheticEvent, ChangeEvent, useState } from 'react'
+
+import { deepEqual } from '@Common/Helper'
import { checkValidation } from './useForm.utils'
import {
@@ -8,6 +10,7 @@ import {
UseFormSubmitHandler,
UseFormValidation,
UseFormValidations,
+ UseFormErrorHandler,
} from './useForm.types'
/**
@@ -26,7 +29,7 @@ export const useForm = = {}>(options?: {
* - 'onBlur': Validation occurs when the input loses focus.
* @default 'onChange'
*/
- validationMode?: 'onChange' | 'onBlur'
+ validationMode?: 'onChange' | 'onBlur' | 'onSubmit'
}) => {
const [data, setData] = useState((options?.initialValues || {}) as T)
const [dirtyFields, setDirtyFields] = useState>({})
@@ -34,31 +37,74 @@ export const useForm = = {}>(options?: {
const [errors, setErrors] = useState>({})
const [enableValidationOnChange, setEnableValidationOnChange] = useState>>({})
+ /**
+ * Retrieves the validation rules for the form fields based on the current form data.
+ *
+ * @template T - A record type representing form data.
+ * @param formData (optional) - The form data to be used for generating dynamic validations. Defaults to the current form data (`data`).
+ * @returns A partial record containing validation rules for each form field, or an empty object if no validations are provided.
+ */
+ const getValidations = (formData = data): Partial> => {
+ if (options?.validations) {
+ const validations =
+ typeof options.validations === 'function' ? options.validations(formData) : options.validations
+ return validations
+ }
+ return {}
+ }
+
/**
* Handles change events for form fields, updates the form data, and triggers validation.
*
+ * @template Value - The type of the value received from the event (used if `isCustomComponent` is true).
+ * @template SFnReturnType - The type returned by the optional `sanitizeFn` function.
+ * @template CustomComponent - A boolean indicating whether the component is custom (e.g., not a standard HTML input).
+ *
* @param key - The key of the form field to be updated.
- * @param sanitizeFn - An optional function to sanitize the input value.
- * @returns The event handler for input changes.
+ * @param sanitizeFn - An optional function to sanitize the input value. If `isCustomComponent` is `true`,
+ * the `sanitizeFn` will receive `Value` as its argument, otherwise it will receive a `string`.
+ * @param isCustomComponent - A boolean indicating whether the event is coming from a custom component (default is `false`).
+ * @returns The event handler for input changes. The event type will be `Value` if `isCustomComponent` is `true`, otherwise it will be a `ChangeEvent`.
*/
const onChange =
- (key: keyof T, sanitizeFn?: (value: V) => S) =>
- // TODO: add support for `Checkbox`, `SelectPicker` and `RadioGroup` components
- (e: ChangeEvent) => {
- const value = sanitizeFn ? sanitizeFn(e.target.value as V) : e.target.value
- setData({
- ...data,
- [key]: value,
+ <
+ Value extends unknown = unknown,
+ SFnReturnType extends unknown = unknown,
+ CustomComponent extends boolean = false,
+ >(
+ key: keyof T,
+ sanitizeFn?: (value: CustomComponent extends true ? Value : string) => SFnReturnType,
+ isCustomComponent?: CustomComponent,
+ ) =>
+ (e: CustomComponent extends true ? Value : ChangeEvent) => {
+ // Extract value based on whether it's a custom component or standard input.
+ const conditionalValue = isCustomComponent
+ ? (e as Value) // For custom component, the event itself holds the value.
+ : (e as ChangeEvent).target.value // For standard input, get the value from event's target.
+
+ // Apply the sanitization function if provided, else use the value as is.
+ const value = sanitizeFn
+ ? sanitizeFn(conditionalValue as CustomComponent extends true ? Value : string)
+ : conditionalValue
+
+ // Update the form data and trigger validation if necessary.
+ setData((prev) => {
+ const updatedData = { ...prev, [key]: value }
+ const validationMode = options?.validationMode ?? 'onChange'
+
+ // If validation should occur (based on mode or field state), check validation for the field.
+ if (validationMode === 'onChange' || enableValidationOnChange[key] || errors[key]) {
+ const validations = getValidations(updatedData)
+ const error = checkValidation(value as T[keyof T], validations[key as string])
+ setErrors({ ...errors, [key]: error })
+ }
+ return updatedData
})
- const initialValues: Partial = options?.initialValues ?? {}
- setDirtyFields({ ...dirtyFields, [key]: initialValues[key] !== value })
- const validationMode = options?.validationMode ?? 'onChange'
- if (validationMode === 'onChange' || enableValidationOnChange[key] || errors[key]) {
- const validations = options?.validations ?? {}
- const error = checkValidation(value as T[keyof T], validations[key as string])
- setErrors({ ...errors, [key]: error })
- }
+ // Check if the field is dirty (i.e., if its value has changed from the initial one).
+ const initialValues: Partial = options?.initialValues ?? {}
+ // Set dirty field state.
+ setDirtyFields((prev) => ({ ...prev, [key]: !deepEqual(initialValues[key], value) }))
}
/**
@@ -69,12 +115,12 @@ export const useForm = = {}>(options?: {
*/
const onBlur = (key: keyof T, noTrim: boolean) => () => {
if (!noTrim) {
- setData({ ...data, [key]: data[key].trim() })
+ setData({ ...data, [key]: data[key]?.trim() })
}
if (options?.validationMode === 'onBlur') {
- const validations = options?.validations ?? {}
- const error = checkValidation(data[key] as T[keyof T], validations[key as string])
+ const validations = getValidations()
+ const error = checkValidation(data[key], validations[key as string])
if (error && !enableValidationOnChange[key]) {
setEnableValidationOnChange({ ...enableValidationOnChange, [key]: true })
}
@@ -89,47 +135,56 @@ export const useForm = = {}>(options?: {
* @return The event handler for the focus event.
*/
const onFocus = (key: keyof T) => () => {
- setTouchedFields({
- ...touchedFields,
- [key]: true,
- })
+ setTouchedFields((prev) => ({ ...prev, [key]: true }))
}
/**
- * Handles form submission, validates all form fields, and calls the provided `onValid` function if valid.
+ * Handles form submission, validates all form fields, and calls the provided `onValid` function if the form data is valid.
+ * If validation errors are found, it will call the optional `onError` function.
*
- * @param onValid - A function to handle valid form data on submission.
- * @returns The event handler for form submission.
+ * @param onValid - A function to handle valid form data on submission. Called when all fields pass validation.
+ * @param onError - (Optional) A function to handle validation errors if the form submission fails validation.
+ * Receives the validation errors and the form event.
+ * @returns The event handler for form submission, which prevents the default form submission,
+ * performs validation, and triggers either `onValid` or `onError` based on the result.
*/
- const handleSubmit = (onValid: UseFormSubmitHandler) => (e: FormEvent) => {
- e.preventDefault()
+ const handleSubmit =
+ (onValid: UseFormSubmitHandler, onError?: UseFormErrorHandler) =>
+ (e?: BaseSyntheticEvent): Promise => {
+ e?.preventDefault()
- // Enables validation for all form fields if not enabled after form submission.
- if (Object.keys(enableValidationOnChange).length !== Object.keys(data).length) {
- setEnableValidationOnChange(Object.keys(data).reduce((acc, key) => ({ ...acc, [key]: true }), {}))
- }
+ // Enables validation for all form fields if not enabled yet after form submission.
+ if (Object.keys(enableValidationOnChange).length !== Object.keys(data).length) {
+ setEnableValidationOnChange(Object.keys(data).reduce((acc, key) => ({ ...acc, [key]: true }), {}))
+ }
- const validations = options?.validations
- if (validations) {
- const newErrors: UseFormErrors = {}
+ const validations = getValidations()
+ if (validations) {
+ const newErrors: UseFormErrors = {}
- Object.keys(validations).forEach((key) => {
- const validation: UseFormValidation = validations[key]
- const error = checkValidation(data[key], validation)
- if (error) {
- newErrors[key] = error
- }
- })
+ // Validates each form field based on its corresponding validation rule.
+ Object.keys(validations).forEach((key) => {
+ const validation: UseFormValidation = validations[key]
+ const error = checkValidation(data[key], validation)
+ if (error) {
+ newErrors[key] = error
+ }
+ })
- if (Object.keys(newErrors).length) {
- setErrors(newErrors)
- return
+ // If validation errors exist, set the error state and call the `onError` function if provided.
+ if (Object.keys(newErrors).length) {
+ setErrors(newErrors)
+ onError?.(newErrors, e)
+ // Stops execution if there are errors.
+ return
+ }
}
- }
- setErrors({})
- onValid(data, e)
- }
+ // Clears any previous errors if no validation errors were found.
+ setErrors({})
+ // Calls the valid handler with the current form data and event.
+ onValid(data, e)
+ }
/**
* Manually triggers validation for specific form fields.
@@ -138,7 +193,7 @@ export const useForm = = {}>(options?: {
* @returns The validation error(s), if any.
*/
const trigger = (name: keyof T | (keyof T)[]): (string | string[]) | (string | string[])[] => {
- const validations = options?.validations
+ const validations = getValidations()
if (Array.isArray(name)) {
const newErrors: UseFormErrors = {}
@@ -177,16 +232,85 @@ export const useForm = = {}>(options?: {
}
/**
- * Registers form input fields with onChange, onBlur and onFocus handlers.
+ * Sets the value of a specified form field and updates the dirty and touched state based on options.
+ *
+ * @param name - The key of the form field to be updated.
+ * @param value - The new value to set for the specified form field.
+ * @param valueOptions - Optional parameters to control the dirty and touched state.
+ */
+ const setValue = (
+ name: keyof T,
+ value: T[keyof T],
+ valueOptions?: {
+ /** A boolean indicating whether to mark the field as dirty after setting the value. */
+ shouldDirty?: boolean
+ /** A boolean indicating whether to mark the field as touched after setting the value. */
+ shouldTouch?: boolean
+ },
+ ) => {
+ // Update the form data with the new value.
+ setData((prev) => ({ ...prev, [name]: value }))
+ if (valueOptions?.shouldDirty) {
+ const initialValues: Partial = options?.initialValues ?? {}
+ // Mark the field as dirty if the new value differs from the initial value.
+ setDirtyFields((prev) => ({ ...prev, [name]: !deepEqual(initialValues[name], value) }))
+ }
+ if (valueOptions?.shouldTouch) {
+ // Mark the field as touched.
+ setTouchedFields((prev) => ({ ...prev, [name]: true }))
+ }
+ }
+
+ /**
+ * Resets the form state to the specified data, with options to keep certain states intact.
+ *
+ * @param formData - The data to reset the form to.
+ * @param resetOptions - Optional parameters to control which states to keep on reset.
+ */
+ const reset = (
+ formData: T,
+ resetOptions?: {
+ /** A boolean indicating whether to retain the current dirty state of the form fields. */
+ keepDirty?: boolean
+ /** A boolean indicating whether to retain the current touched state of the form fields. */
+ keepTouched?: boolean
+ /** A boolean indicating whether to retain the current error state of the form fields. */
+ keepErrors?: boolean
+ },
+ ) => {
+ const { keepDirty = false, keepTouched = false, keepErrors = false } = resetOptions ?? {} // Destructure reset options with defaults.
+ setData(formData)
+ if (!keepErrors) {
+ setErrors({})
+ }
+ if (!keepDirty) {
+ setDirtyFields({})
+ }
+ if (!keepTouched) {
+ setTouchedFields({})
+ }
+ }
+
+ /**
+ * Registers form input fields with onChange, onBlur, and onFocus handlers.
*
* @param name - The key of the form field to register.
- * @param sanitizeFn - An optional function to sanitize the input value.
- * @returns An object containing form field `name`, `onChange`, `onBlur` and `onFocus` event handlers.
+ * @param registerOptions - Optional parameters to customize the registration of the field.
+ * @returns An object containing the following:
+ * - `onChange`: A handler function that updates the form data when the input value changes.
+ * - `onBlur`: A handler function that triggers validation when the input loses focus.
+ * - `onFocus`: A handler function that can be used to manage focus state.
+ * - `name`: The key of the form field being registered.
*/
- const register = (
+ const register = (
name: keyof T,
- sanitizeFn?: (value: V) => S,
registerOptions?: {
+ /**
+ * A function to sanitize the input value.
+ * @param value The input value.
+ * @returns The sanitized value.
+ */
+ sanitizeFn?: (value: CustomComponent extends true ? Value : string) => SFnReturnType // Function to sanitize the input value.
/**
* Prevents the input value from being trimmed.
*
@@ -195,10 +319,12 @@ export const useForm = = {}>(options?: {
*
* @default false - By default, the input will be trimmed.
*/
- noTrim?: boolean
+ noTrim?: boolean // Prevents the input value from being trimmed.
+ /** A boolean flag indicating if the input is a custom component. */
+ isCustomComponent?: CustomComponent
},
) => ({
- onChange: onChange(name, sanitizeFn),
+ onChange: onChange(name, registerOptions?.sanitizeFn, registerOptions?.isCustomComponent),
onBlur: onBlur(name, registerOptions?.noTrim),
onFocus: onFocus(name),
name,
@@ -209,28 +335,11 @@ export const useForm = = {}>(options?: {
data,
/** An object containing validation errors for each form field. */
errors,
- /**
- * Registers form input fields with onChange, onBlur and onFocus handlers.
- *
- * @param name - The key of the form field to register.
- * @param sanitizeFn - An optional function to sanitize the input value.
- * @returns An object containing form field `name`, `onChange`, `onBlur` and `onFocus` event handlers.
- */
register,
- /**
- * Handles form submission, validates all form fields, and calls the provided `onValid` function if valid.
- *
- * @param onValid - A function to handle valid form data on submission.
- * @returns The event handler for form submission.
- */
handleSubmit,
- /**
- * Manually triggers validation for specific form fields.
- *
- * @param name - The key(s) of the form field(s) to validate.
- * @returns The validation error(s), if any.
- */
trigger,
+ setValue,
+ reset,
/** An object representing additional form state. */
formState: {
/** An object indicating which fields have been touched (interacted with). */
diff --git a/src/Shared/Hooks/useForm/useForm.types.ts b/src/Shared/Hooks/useForm/useForm.types.ts
index c15f3097c..3e0d27b74 100644
--- a/src/Shared/Hooks/useForm/useForm.types.ts
+++ b/src/Shared/Hooks/useForm/useForm.types.ts
@@ -1,4 +1,4 @@
-import { FormEvent } from 'react'
+import { BaseSyntheticEvent } from 'react'
/**
* Describes the "required" validation rule.
@@ -32,11 +32,11 @@ type ValidationPattern =
*/
type ValidationCustom =
| {
- isValid: (value: string) => boolean
+ isValid: (value: any) => boolean
message: string
}
| {
- isValid: (value: string) => boolean
+ isValid: (value: any) => boolean
message: string
}[]
@@ -52,9 +52,9 @@ export interface UseFormValidation {
/**
* Represents the structure for form validation errors.
- * Maps each field to an error message or an array of error messages.
+ * Maps each field to an array of error messages.
*/
-export type UseFormErrors = Partial>
+export type UseFormErrors = Partial>
/**
* Represents the fields that have been modified ("dirty") in the form.
@@ -72,12 +72,22 @@ export type TouchedFields = Partial>
* Defines the structure for form validations.
* Maps each form field to its corresponding validation rules.
*/
-export type UseFormValidations = Partial>
+export type UseFormValidations =
+ | ((formData: T) => Partial>)
+ | Partial>
/**
* Describes the function signature for handling form submission.
*
* @param data - The form data collected during submission.
- * @param e - The form event, optionally passed when the form is submitted.
+ * @param e - The event, optionally passed when `handleSubmit` is called.
*/
-export type UseFormSubmitHandler = (data: T, e?: FormEvent) => void
+export type UseFormSubmitHandler = (data: T, e?: BaseSyntheticEvent) => void
+
+/**
+ * A type defining the function signature for handling form validation errors.
+ *
+ * @param errors - An object containing the validation errors for form fields.
+ * @param e - The event, optionally passed when `handleSubmit` is called.
+ */
+export type UseFormErrorHandler = (errors: UseFormErrors, e?: BaseSyntheticEvent) => void
diff --git a/src/Shared/Hooks/useForm/useForm.utils.ts b/src/Shared/Hooks/useForm/useForm.utils.ts
index 4c9ee0acf..9c44922c0 100644
--- a/src/Shared/Hooks/useForm/useForm.utils.ts
+++ b/src/Shared/Hooks/useForm/useForm.utils.ts
@@ -11,16 +11,16 @@ import { UseFormValidation } from './useForm.types'
export const checkValidation = = {}>(
value: T[keyof T],
validation: UseFormValidation,
-): string | string[] | null => {
+): string[] | null => {
if (
- (typeof validation?.required === 'object' ? validation.required.value : validation.required) &&
+ validation?.required &&
+ (typeof validation.required === 'object' ? validation.required.value : validation.required) &&
(value === null || value === undefined || value === '')
) {
- return typeof validation?.required === 'object' ? validation.required.message : 'This is a required field'
+ return [typeof validation.required === 'object' ? validation.required.message : 'This is a required field']
}
const errors = []
-
const pattern = validation?.pattern
if (Array.isArray(pattern)) {
const error = pattern.reduce((acc, p) => {
diff --git a/src/Shared/Services/app.service.ts b/src/Shared/Services/app.service.ts
index 2f1684359..a244b84f6 100644
--- a/src/Shared/Services/app.service.ts
+++ b/src/Shared/Services/app.service.ts
@@ -55,8 +55,11 @@ export const getArtifactInfo = async (
}
}
-export const getAppEnvDeploymentConfig = (
- params: AppEnvDeploymentConfigPayloadType,
- signal?: AbortSignal,
-): Promise> =>
+export const getAppEnvDeploymentConfig = ({
+ params,
+ signal,
+}: {
+ params: AppEnvDeploymentConfigPayloadType
+ signal?: AbortSignal
+}): Promise> =>
get(getUrlWithSearchParams(ROUTES.CONFIG_DATA, params), { signal })
diff --git a/src/Shared/Services/app.types.ts b/src/Shared/Services/app.types.ts
index 88c4bd402..a580762a2 100644
--- a/src/Shared/Services/app.types.ts
+++ b/src/Shared/Services/app.types.ts
@@ -148,19 +148,33 @@ export enum AppEnvDeploymentConfigType {
DEFAULT_VERSION = 'DefaultVersion',
}
+export enum DraftState {
+ Init = 1,
+ Discarded = 2,
+ Published = 3,
+ AwaitApproval = 4,
+}
+
+export enum DraftAction {
+ Add = 1,
+ Update = 2,
+ Delete = 3,
+}
+
export interface DraftMetadataDTO {
appId: number
envId: number
resource: number
resourceName: string
- action: number
+ action: DraftAction
data: string
userComment: string
changeProposed: boolean
protectNotificationConfig: { [key: string]: null }
draftId: number
draftVersionId: number
- draftState: number
+ draftState: DraftState
+ draftResolvedValue: string
approvers: string[]
canApprove: boolean
commentsCount: number
@@ -168,24 +182,38 @@ export interface DraftMetadataDTO {
isAppAdmin: boolean
}
+export enum CMSecretExternalType {
+ Internal = '',
+ KubernetesConfigMap = 'KubernetesConfigMap',
+ KubernetesSecret = 'KubernetesSecret',
+ AWSSecretsManager = 'AWSSecretsManager',
+ AWSSystemManager = 'AWSSystemManager',
+ HashiCorpVault = 'HashiCorpVault',
+ ESO_GoogleSecretsManager = 'ESO_GoogleSecretsManager',
+ ESO_AWSSecretsManager = 'ESO_AWSSecretsManager',
+ ESO_AzureSecretsManager = 'ESO_AzureSecretsManager',
+ ESO_HashiCorpVault = 'ESO_HashiCorpVault',
+}
+
export interface ConfigDatum {
name: string
type: string
external: boolean
- data: Record
- defaultData: Record
+ data: Record
+ defaultData: Record
global: boolean
- externalType: string
- esoSecretData: {}
- defaultESOSecretData: {}
- secretData: Record
- defaultSecretData: Record
+ externalType: CMSecretExternalType
+ esoSecretData: Record
+ defaultESOSecretData: Record
+ secretData: Record[]
+ defaultSecretData: Record[]
roleARN: string
subPath: boolean
filePermission: string
overridden: boolean
- mountPath?: string
- defaultMountPath?: string
+ mountPath: string
+ defaultMountPath: string
+ esoSubPath: string[]
}
export interface ConfigMapSecretDataConfigDatumDTO extends ConfigDatum {
@@ -202,36 +230,62 @@ export enum ConfigResourceType {
ConfigMap = 'ConfigMap',
Secret = 'Secret',
DeploymentTemplate = 'Deployment Template',
+ PipelineStrategy = 'Pipeline Strategy',
}
export interface DeploymentTemplateDTO {
resourceType: ConfigResourceType.DeploymentTemplate
- data: { [key: string]: any }
+ data: Record
deploymentDraftData: ConfigMapSecretDataType | null
+ variableSnapshot: {
+ 'Deployment Template': Record
+ }
+ templateVersion: string
+ isAppMetricsEnabled?: true
+ resolvedValue: Record
}
export interface ConfigMapSecretDataDTO {
resourceType: Extract
data: ConfigMapSecretDataType
+ variableSnapshot: Record>
+ resolvedValue: string
+}
+
+export interface PipelineConfigDataDTO {
+ resourceType: ConfigResourceType.PipelineStrategy
+ data: Record
+ pipelineTriggerType: string
+ Strategy: string
}
export interface AppEnvDeploymentConfigDTO {
deploymentTemplate: DeploymentTemplateDTO | null
configMapData: ConfigMapSecretDataDTO | null
secretsData: ConfigMapSecretDataDTO | null
+ pipelineConfigData?: PipelineConfigDataDTO
isAppAdmin: boolean
}
-export interface AppEnvDeploymentConfigPayloadType {
- appName: string
- envName: string
- configType: AppEnvDeploymentConfigType
- identifierId?: number
- pipelineId?: number
- resourceType?: ConfigResourceType
- resourceId?: number
- resourceName?: string
-}
+export type AppEnvDeploymentConfigPayloadType =
+ | {
+ appName: string
+ envName: string
+ configType: AppEnvDeploymentConfigType
+ identifierId?: number
+ pipelineId?: number
+ resourceType?: ConfigResourceType
+ resourceId?: number
+ resourceName?: string
+ configArea?: 'AppConfiguration'
+ }
+ | {
+ appName: string
+ envName: string
+ pipelineId: number
+ configArea: 'CdRollback' | 'DeploymentHistory'
+ wfrId: number
+ }
export enum TemplateListType {
DefaultVersions = 1,
@@ -251,19 +305,7 @@ export interface TemplateListDTO {
finishedOn?: string
status?: string
pipelineId?: number
-}
-
-export interface ManifestTemplateDTO {
- data: string
- resolvedData: string
- variableSnapshot: null
-}
-
-export enum DraftState {
- Init = 1,
- Discarded = 2,
- Published = 3,
- AwaitApproval = 4,
+ wfrId?: number
}
export enum EnvResourceType {
@@ -271,4 +313,5 @@ export enum EnvResourceType {
Secret = 'secrets',
DeploymentTemplate = 'deployment-template',
Manifest = 'manifest',
+ PipelineStrategy = 'pipeline-strategy',
}
diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx
index 04b606701..0b4f098e9 100644
--- a/src/Shared/constants.tsx
+++ b/src/Shared/constants.tsx
@@ -15,7 +15,7 @@
*/
import { OptionType } from '@Common/Types'
-import { CDMaterialSidebarType } from './types'
+import { CDMaterialSidebarType, ConfigKeysWithLockType, ConfigurationType } from './types'
export const ARTIFACT_STATUS = {
PROGRESSING: 'Progressing',
@@ -490,3 +490,10 @@ export const CD_MATERIAL_SIDEBAR_TABS: OptionType = {
+ config: [],
+ allowed: false,
+}
diff --git a/src/Shared/types.ts b/src/Shared/types.ts
index 513ec54da..7ec58c71f 100644
--- a/src/Shared/types.ts
+++ b/src/Shared/types.ts
@@ -672,6 +672,21 @@ export type BaseFilterQueryParams = {
showAll?: boolean
} & SortingParams
+export enum ConfigurationType {
+ GUI = 'GUI',
+ YAML = 'YAML',
+}
+
+export interface BaseURLParams {
+ appId: string
+ envId: string
+}
+
+export interface ConfigKeysWithLockType {
+ config: string[]
+ allowed: boolean
+}
+
export type DataAttributes = Record<`data-${string}`, unknown>
export interface RuntimeParamsListItemType extends KeyValueListType {
@@ -682,3 +697,44 @@ export enum RuntimeParamsHeadingType {
KEY = 'key',
VALUE = 'value',
}
+
+export enum ACCESS_TYPE_MAP {
+ DEVTRON_APPS = 'devtron-app', // devtron app work flow
+ HELM_APPS = 'helm-app', // helm app work flow
+ JOBS = '', // Empty string is intentional since there is no bifurcation in jobs as of now
+}
+
+export enum EntityTypes {
+ CHART_GROUP = 'chart-group',
+ DIRECT = 'apps',
+ JOB = 'jobs',
+ DOCKER = 'docker',
+ GIT = 'git',
+ CLUSTER = 'cluster',
+ NOTIFICATION = 'notification',
+}
+
+export interface CustomRoles {
+ id: number
+ roleName: string
+ roleDisplayName: string
+ roleDescription: string
+ entity: EntityTypes
+ accessType: ACCESS_TYPE_MAP.DEVTRON_APPS | ACCESS_TYPE_MAP.HELM_APPS
+}
+
+export type MetaPossibleRoles = Record<
+ CustomRoles['roleName'],
+ {
+ value: CustomRoles['roleDisplayName']
+ description: CustomRoles['roleDescription']
+ }
+>
+
+export interface CustomRoleAndMeta {
+ customRoles: CustomRoles[]
+ possibleRolesMeta: MetaPossibleRoles
+ possibleRolesMetaForHelm: MetaPossibleRoles
+ possibleRolesMetaForCluster: MetaPossibleRoles
+ possibleRolesMetaForJob: MetaPossibleRoles
+}
diff --git a/src/index.ts b/src/index.ts
index 57d0183d5..78644c17e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -76,6 +76,9 @@ export interface customEnv {
SYSTEM_CONTROLLER_LISTING_TIMEOUT?: number
FEATURE_STEP_WISE_LOGS_ENABLE?: boolean
FEATURE_IMAGE_PROMOTION_ENABLE?: boolean
+ FEATURE_PROMO_EMBEDDED_BUTTON_TEXT?: string
+ FEATURE_PROMO_EMBEDDED_MODAL_TITLE?: string
+ FEATURE_PROMO_EMBEDDED_IFRAME_URL?: string
}
declare global {
interface Window {