From b46304fa5e3b3ba9b163c80a8779c5ee06be7827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:03:54 +0000 Subject: [PATCH 1/5] Initial plan From 6f2644f43b5bc56004ddd50805ed65cc3ffdf5c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:09:39 +0000 Subject: [PATCH 2/5] Refactor HelmAppList to use Table component Co-authored-by: Elessar1802 <66767648+Elessar1802@users.noreply.github.com> --- src/components/app/list-new/AppListType.ts | 11 + src/components/app/list-new/HelmAppList.tsx | 551 +++++++----------- .../list-new/HelmAppListCellComponents.tsx | 82 +++ .../app/list-new/HelmAppListViewWrapper.tsx | 103 ++++ src/components/app/list-new/list.utils.ts | 66 +++ src/components/app/list-new/styles.scss | 21 + 6 files changed, 483 insertions(+), 351 deletions(-) create mode 100644 src/components/app/list-new/HelmAppListCellComponents.tsx create mode 100644 src/components/app/list-new/HelmAppListViewWrapper.tsx diff --git a/src/components/app/list-new/AppListType.ts b/src/components/app/list-new/AppListType.ts index 42643d8a05..7237ce0fb7 100644 --- a/src/components/app/list-new/AppListType.ts +++ b/src/components/app/list-new/AppListType.ts @@ -248,3 +248,14 @@ export interface ExportAppListDataType { export type GenericAppListRowType = { detail: GenericAppType } & Record + +export type HelmAppListRowType = { + detail: HelmApp +} & Record + +export interface HelmAppListAdditionalProps { + showGuidedContentCards: boolean + externalHelmListFetchErrors: string[] + clusterIdsCsv: string + removeExternalAppFetchError: (index: number) => void +} diff --git a/src/components/app/list-new/HelmAppList.tsx b/src/components/app/list-new/HelmAppList.tsx index 57ec62e22b..d647c32ce9 100644 --- a/src/components/app/list-new/HelmAppList.tsx +++ b/src/components/app/list-new/HelmAppList.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState, useCallback } from 'react' import { AppStatus, showError, @@ -22,39 +22,31 @@ import { ServerErrors, Host, GenericEmptyState, - DEFAULT_BASE_PAGE_SIZE, - Pagination, handleUTCTime, DATE_TIME_FORMATS, - SortableTableHeaderCell, stringComparatorBySortOrder, - useStickyEvent, - getClassNameForStickyHeaderWithShadow, DocLink, URLS as CommonURLS, ComponentSizeType, + Table, + PaginationEnum, + FiltersTypeEnum, + TableProps, } from '@devtron-labs/devtron-fe-common-lib' -import { Link } from 'react-router-dom' +import { Link, useHistory } from 'react-router-dom' import Tippy from '@tippyjs/react' import moment from 'moment' import { getDevtronInstalledHelmApps } from './AppListService' import { LazyImage } from '../../common' import { SERVER_MODE, URLS, checkIfDevtronOperatorHelmRelease, ModuleNameMap } from '../../../config' import { AppListViewType } from '../config' -import { ReactComponent as ICHelpOutline } from '../../../assets/icons/ic-help-outline.svg' import NoClusterSelectImage from '../../../assets/icons/ic-select-cluster.svg' import defaultChartImage from '../../../assets/icons/ic-default-chart.svg' import HelmCluster from '../../../assets/img/guided-helm-cluster.png' import DeployCICD from '../../../assets/img/guide-onboard.png' import { AllCheckModal } from '../../checkList/AllCheckModal' -import { ReactComponent as InfoFillPurple } from '../../../assets/icons/ic-info-filled-purple.svg' -import { ReactComponent as ErrorExclamationIcon } from '../../../assets/icons/ic-error-exclamation.svg' -import { ReactComponent as CloseIcon } from '../../../assets/icons/ic-close.svg' import { ReactComponent as AlertTriangleIcon } from '../../../assets/icons/ic-alert-triangle.svg' -import { ReactComponent as ArrowRight } from '../../../assets/icons/ic-arrow-right.svg' import noChartInClusterImage from '../../../assets/img/ic-no-chart-in-clusters@2x.png' -import ContentCard from '../../common/ContentCard/ContentCard' -import { CardContentDirection, CardLinkIconPlacement } from '../../common/ContentCard/ContentCard.types' import '../list/list.scss' import { APP_LIST_EMPTY_STATE_MESSAGING, @@ -65,11 +57,14 @@ import { APP_LIST_HEADERS, HELM_PERMISSION_MESSAGE, SELECT_CLUSTER_FROM_FILTER_NOTE, - appListLoadingArray, } from './Constants' import { HELM_GUIDED_CONTENT_CARDS_TEXTS } from '../../onboardingGuide/OnboardingGuide.constants' -import { HelmAppListResponse, HelmApp, AppListSortableKeys, HelmAppListProps } from './AppListType' +import { HelmAppListResponse, HelmApp, AppListSortableKeys, HelmAppListProps, HelmAppListRowType } from './AppListType' import AskToClearFilters from './AppListComponents' +import { getHelmAppListColumns } from './list.utils' +import { HelmAppListViewWrapper } from './HelmAppListViewWrapper' + +import './styles.scss' const HelmAppList = ({ serverMode, @@ -96,53 +91,104 @@ const HelmAppList = ({ const [externalHelmListFetchErrors, setExternalHelmListFetchErrors] = useState([]) const [showGuidedContentCards, setShowGuidedContentCards] = useState(false) + const { push } = useHistory() + const { appStatus, environment, cluster, namespace, project, searchKey, sortBy, sortOrder, offset, pageSize } = filterConfig - const handleAppListSorting = (a: HelmApp, b: HelmApp) => - sortBy === AppListSortableKeys.APP_NAME - ? stringComparatorBySortOrder(a.appName, b.appName, sortOrder) - : stringComparatorBySortOrder(a.lastDeployedAt, b.lastDeployedAt, sortOrder) - - const { filteredHelmAppList, filteredListTotalSize } = useMemo(() => { - let filteredHelmAppList: HelmApp[] = [...devtronInstalledHelmAppsList, ...externalHelmAppsList] - if (searchKey) { - const searchLowerCase = searchKey.toLowerCase() - filteredHelmAppList = filteredHelmAppList.filter( - (app) => app.appName.includes(searchLowerCase) || app.chartName.includes(searchLowerCase), - ) - } - if (project.length) { - const projectMap = new Map(project.map((projectId) => [projectId, true])) - filteredHelmAppList = filteredHelmAppList.filter((app) => projectMap.get(String(app.projectId)) ?? false) - } - if (environment.length) { - const environmentMap = new Map(environment.map((envId) => [envId, true])) - filteredHelmAppList = filteredHelmAppList.filter( - (app) => environmentMap.get(String(app.environmentDetail.environmentId)) ?? false, - ) - } - if (namespace.length) { - const namespaceMap = new Map(namespace.map((namespaceItem) => [namespaceItem, true])) - filteredHelmAppList = filteredHelmAppList.filter( - (app) => - namespaceMap.get(`${app.environmentDetail.clusterId}_${app.environmentDetail.namespace}`) ?? false, - ) + // Transform helm apps into table rows + const rows = useMemo( + () => + [...devtronInstalledHelmAppsList, ...externalHelmAppsList].map((app) => ({ + id: app.appId, + data: { + detail: app, + [AppListSortableKeys.APP_NAME]: app.appName, + [APP_LIST_HEADERS.AppStatus]: app.appStatus, + [APP_LIST_HEADERS.Environment]: app.environmentDetail.environmentName + ? app.environmentDetail.environmentName + : `${app.environmentDetail.clusterName}__${app.environmentDetail.namespace}`, + [APP_LIST_HEADERS.Cluster]: app.environmentDetail.clusterName, + [APP_LIST_HEADERS.Namespace]: app.environmentDetail.namespace, + [AppListSortableKeys.LAST_DEPLOYED]: app.lastDeployedAt, + }, + })) as TableProps['rows'], + [devtronInstalledHelmAppsList, externalHelmAppsList], + ) + + const columns = useMemo(() => getHelmAppListColumns(isArgoInstalled), [isArgoInstalled]) + + // Filter function for the table + const filter = useCallback( + ({ data: app }) => { + let isMatch = true + + if (searchKey) { + const searchLowerCase = searchKey.toLowerCase() + isMatch = + isMatch && + (app.detail.appName.toLowerCase().includes(searchLowerCase) || + app.detail.chartName.toLowerCase().includes(searchLowerCase)) + } + + if (project.length) { + const projectMap = new Map(project.map((projectId) => [projectId, true])) + isMatch = isMatch && (projectMap.get(String(app.detail.projectId)) ?? false) + } + + if (environment.length) { + const environmentMap = new Map(environment.map((envId) => [envId, true])) + isMatch = isMatch && (environmentMap.get(String(app.detail.environmentDetail.environmentId)) ?? false) + } + + if (namespace.length) { + const namespaceMap = new Map(namespace.map((namespaceItem) => [namespaceItem, true])) + isMatch = + isMatch && + (namespaceMap.get( + `${app.detail.environmentDetail.clusterId}_${app.detail.environmentDetail.namespace}`, + ) ?? + false) + } + + return isMatch + }, + [searchKey, project, environment, namespace], + ) + + const _buildAppDetailUrl = (app: HelmApp) => { + if (app.isExternal) { + return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.EXTERNAL_APPS}/${app.appId}/${app.appName}` } - filteredHelmAppList = filteredHelmAppList.sort((a, b) => handleAppListSorting(a, b)) + return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.DEVTRON_CHARTS}/deployments/${app.appId}/env/${app.environmentDetail.environmentId}` + } - const filteredListTotalSize = filteredHelmAppList.length + const onRowClick = useCallback(({ data: app }) => { + push(_buildAppDetailUrl(app.detail)) + }, []) - filteredHelmAppList = filteredHelmAppList.slice(offset, offset + pageSize) + const _removeExternalAppFetchError = (index: number) => { + const _externalHelmListFetchErrors = [...externalHelmListFetchErrors] + _externalHelmListFetchErrors.splice(index, 1) + setExternalHelmListFetchErrors(_externalHelmListFetchErrors) + } - return { filteredHelmAppList, filteredListTotalSize } - }, [devtronInstalledHelmAppsList, externalHelmAppsList, filterConfig]) + function _isAnyFilterationAppliedExceptClusterAndNs() { + return project.length || searchKey.length || environment.length || appStatus.length + } - const { stickyElementRef, isStuck: isHeaderStuck } = useStickyEvent({ - identifier: 'helm-app-list', - containerRef: appListContainerRef, - isStickyElementMounted: dataStateType === AppListViewType.LIST && filteredListTotalSize > 0, - }) + function _isAnyFilterationApplied() { + return _isAnyFilterationAppliedExceptClusterAndNs() || cluster.length || namespace.length + } + + function _isOnlyAllClusterFilterationApplied() { + const _isAllClusterSelected = cluster.length === clusterList?.length + const _isAnyNamespaceSelected = !!namespace.length + return !_isAnyFilterationAppliedExceptClusterAndNs() && _isAllClusterSelected && !_isAnyNamespaceSelected + } + + const isAnyFilterApplied = _isAnyFilterationApplied() + const isOnlyAllClusterFilterApplied = _isOnlyAllClusterFilterationApplied() // component load useEffect(() => { @@ -295,237 +341,6 @@ const HelmAppList = ({ setSseConnection(_sseConnection) } - function _isAnyFilterationAppliedExceptClusterAndNs() { - return project.length || searchKey.length || environment.length || appStatus.length - } - - function _isAnyFilterationApplied() { - return _isAnyFilterationAppliedExceptClusterAndNs() || cluster.length || namespace.length - } - - function _isOnlyAllClusterFilterationApplied() { - const _isAllClusterSelected = cluster.length === clusterList?.length - const _isAnyNamespaceSelected = !!namespace.length - return !_isAnyFilterationAppliedExceptClusterAndNs() && _isAllClusterSelected && !_isAnyNamespaceSelected - } - - function handleImageError(e) { - const target = e.target as HTMLImageElement - target.onerror = null - target.src = defaultChartImage - } - - function _buildAppDetailUrl(app: HelmApp) { - if (app.isExternal) { - return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.EXTERNAL_APPS}/${app.appId}/${app.appName}` - } - return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.DEVTRON_CHARTS}/deployments/${app.appId}/env/${app.environmentDetail.environmentId}` - } - - function _removeExternalAppFetchError(e) { - const index = Number(e.currentTarget.dataset.id) - const _externalHelmListFetchErrors = [...externalHelmListFetchErrors] - _externalHelmListFetchErrors.splice(index, 1) - setExternalHelmListFetchErrors(_externalHelmListFetchErrors) - } - - const handleAppNameSorting = () => handleSorting(AppListSortableKeys.APP_NAME) - - const handleLastDeployedSorting = () => handleSorting(AppListSortableKeys.LAST_DEPLOYED) - - function renderHeaders() { - return ( -
-
-
- {sseConnection && {APP_LIST_HEADERS.ReleaseName}} - {!sseConnection && ( - - )} -
- {isArgoInstalled && ( -
- {APP_LIST_HEADERS.AppStatus} -
- )} -
- {APP_LIST_HEADERS.Environment} - -
- -
-
-
-
- {APP_LIST_HEADERS.Cluster} -
-
- {APP_LIST_HEADERS.Namespace} -
-
- -
-
- ) - } - - const renderFetchError = (externalHelmListFetchError: string, index: number) => ( -
-
-
- - - - {externalHelmListFetchError} - -
-
- ) - - const renderHelmAppLink = (app: HelmApp): JSX.Element => ( - -
- -
-
-
{app.appName}
-
{app.chartName}
-
- {isArgoInstalled && ( -
- -
- )} -
-

- {app.environmentDetail.environmentName - ? app.environmentDetail.environmentName - : `${app.environmentDetail.clusterName}__${app.environmentDetail.namespace}`} -

-
-
-

- {app.environmentDetail.clusterName} -

-
-
-

- {app.environmentDetail.namespace} -

-
-
- {app.lastDeployedAt && ( - -

{handleUTCTime(app.lastDeployedAt, true)}

-
- )} -
- - ) - - function renderApplicationList() { - return ( -
- {!clusterIdsCsv && ( -
-
-
- - - -
- {SELECT_CLUSTER_FROM_FILTER_NOTE}  - -
-
-
- )} - {externalHelmListFetchErrors.map((externalHelmListFetchError, index) => - renderFetchError(externalHelmListFetchError, index), - )} - {filteredListTotalSize > 0 && renderHeaders()} - {filteredHelmAppList.map((app) => renderHelmAppLink(app))} - {showGuidedContentCards && ( -
- - -
- )} -
- ) - } - - function renderAllCheckModal() { - return ( -
- -
- ) - } - function askToSelectClusterId() { return (
@@ -550,17 +365,13 @@ const HelmAppList = ({ ) - return ( -
- -
- ) + return { + imgName: 'img-no-chart-in-clusters', + title: APP_LIST_EMPTY_STATE_MESSAGING.noHelmChartsFound, + subTitle: APP_LIST_EMPTY_STATE_MESSAGING.connectClusterInfoText, + isButtonAvailable: true, + renderButton: handleButton, + } } function renderHelmPermissionMessageStrip() { @@ -577,49 +388,51 @@ const HelmAppList = ({ ) } - function renderNoApplicationState() { + function renderAllCheckModal() { + return ( +
+ +
+ ) + } + + function renderAllCheckModal() { + return ( +
+ +
+ ) + } + + // Determine empty state config + const getNoRowsConfig = () => { if (_isAnyFilterationAppliedExceptClusterAndNs() && !clusterIdsCsv) { - return askToClearFiltersWithSelectClusterTip() - } - if (_isOnlyAllClusterFilterationApplied()) { - return askToConnectAClusterForNoResult() - } - if (_isAnyFilterationApplied()) { - return + // Return null to render custom component + return null } if (!clusterIdsCsv) { - return askToSelectClusterId() + return null } - return renderAllCheckModal() + // Return allCheckModal config + return null } - function renderFullModeApplicationListContainer() { - if (!sseConnection && filteredListTotalSize === 0) { - return ( - <> - {serverMode === SERVER_MODE.FULL && renderHelmPermissionMessageStrip()} - {renderNoApplicationState()} - - ) + const getNoRowsForFilterConfig = () => { + if (isOnlyAllClusterFilterApplied) { + return askToConnectAClusterForNoResult() + } + return { + title: APP_LIST_EMPTY_STATE_MESSAGING.noHelmChartsFound, + subTitle: APP_LIST_EMPTY_STATE_MESSAGING.connectClusterInfoText, } - return renderApplicationList() } - function renderPagination(): JSX.Element { - return ( - filteredListTotalSize > DEFAULT_BASE_PAGE_SIZE && - !fetchingExternalApps && ( - - ) - ) - } if (dataStateType === AppListViewType.ERROR) { return (
@@ -627,28 +440,64 @@ const HelmAppList = ({
) } - return ( - <> - {dataStateType === AppListViewType.LOADING && ( + + // Handle edge cases where we don't show the table + if (!sseConnection && rows.length === 0 && dataStateType === AppListViewType.LIST) { + if (_isAnyFilterationAppliedExceptClusterAndNs() && !clusterIdsCsv) { + return ( <> - {renderHeaders()} -
- {appListLoadingArray.map((eachRow) => ( -
- {Object.keys(eachRow).map((eachKey) => ( -
- ))} -
- ))} -
+ {serverMode === SERVER_MODE.FULL && renderHelmPermissionMessageStrip()} + {askToClearFiltersWithSelectClusterTip()} - )} - {dataStateType === AppListViewType.LIST && ( -
- {renderFullModeApplicationListContainer()} - {renderPagination()} -
- )} + ) + } + if (!clusterIdsCsv) { + return ( + <> + {serverMode === SERVER_MODE.FULL && renderHelmPermissionMessageStrip()} + {askToSelectClusterId()} + + ) + } + if (!_isAnyFilterationApplied()) { + return ( + <> + {serverMode === SERVER_MODE.FULL && renderHelmPermissionMessageStrip()} + {renderAllCheckModal()} + + ) + } + } + + return ( + <> + {serverMode === SERVER_MODE.FULL && rows.length === 0 && dataStateType === AppListViewType.LIST && renderHelmPermissionMessageStrip()} + + id="table__helm-app-list" + columns={columns} + loading={dataStateType === AppListViewType.LOADING} + rows={rows} + filter={filter} + filtersVariant={FiltersTypeEnum.URL} + paginationVariant={PaginationEnum.PAGINATED} + emptyStateConfig={{ + noRowsConfig: getNoRowsConfig(), + noRowsForFilterConfig: getNoRowsForFilterConfig(), + }} + clearFilters={clearAllFilters} + onRowClick={onRowClick} + additionalFilterProps={{ + initialSortKey: AppListSortableKeys.APP_NAME, + }} + areFiltersApplied={isAnyFilterApplied} + ViewWrapper={HelmAppListViewWrapper} + additionalProps={{ + showGuidedContentCards, + externalHelmListFetchErrors, + clusterIdsCsv, + removeExternalAppFetchError: _removeExternalAppFetchError, + }} + /> ) } diff --git a/src/components/app/list-new/HelmAppListCellComponents.tsx b/src/components/app/list-new/HelmAppListCellComponents.tsx new file mode 100644 index 0000000000..fa15360c73 --- /dev/null +++ b/src/components/app/list-new/HelmAppListCellComponents.tsx @@ -0,0 +1,82 @@ +/* + * 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 { AppStatus, DATE_TIME_FORMATS, FiltersTypeEnum, handleUTCTime, TableCellProps } from '@devtron-labs/devtron-fe-common-lib' +import Tippy from '@tippyjs/react' +import moment from 'moment' +import { LazyImage } from '../../common' +import defaultChartImage from '../../../assets/icons/ic-default-chart.svg' +import { HelmAppListRowType } from './AppListType' + +const handleImageError = (e) => { + const target = e.target as HTMLImageElement + target.onerror = null + target.src = defaultChartImage +} + +export const HelmAppNameCellComponent = ({ rowData }: TableCellProps) => { + const app = rowData.data.detail + return ( + <> +
+ +
+
+
{app.appName}
+
{app.chartName}
+
+ + ) +} + +export const HelmAppStatusCellComponent = ({ rowData }: TableCellProps) => { + const app = rowData.data.detail + return +} + +export const HelmAppEnvironmentCellComponent = ({ rowData }: TableCellProps) => { + const app = rowData.data.detail + return ( +

+ {app.environmentDetail.environmentName + ? app.environmentDetail.environmentName + : `${app.environmentDetail.clusterName}__${app.environmentDetail.namespace}`} +

+ ) +} + +export const HelmAppLastDeployedCellComponent = ({ rowData }: TableCellProps) => { + const app = rowData.data.detail + + if (!app.lastDeployedAt) { + return null + } + + return ( + +

{handleUTCTime(app.lastDeployedAt, true)}

+
+ ) +} diff --git a/src/components/app/list-new/HelmAppListViewWrapper.tsx b/src/components/app/list-new/HelmAppListViewWrapper.tsx new file mode 100644 index 0000000000..59ae01e0d7 --- /dev/null +++ b/src/components/app/list-new/HelmAppListViewWrapper.tsx @@ -0,0 +1,103 @@ +/* + * 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 { ComponentSizeType, DocLink, FiltersTypeEnum, TableViewWrapperProps, URLS as CommonURLS } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as InfoFillPurple } from '../../../assets/icons/ic-info-filled-purple.svg' +import { ReactComponent as ErrorExclamationIcon } from '../../../assets/icons/ic-error-exclamation.svg' +import { ReactComponent as CloseIcon } from '../../../assets/icons/ic-close.svg' +import { ReactComponent as ArrowRight } from '../../../assets/icons/ic-arrow-right.svg' +import HelmCluster from '../../../assets/img/guided-helm-cluster.png' +import DeployCICD from '../../../assets/img/guide-onboard.png' +import ContentCard from '../../common/ContentCard/ContentCard' +import { CardContentDirection, CardLinkIconPlacement } from '../../common/ContentCard/ContentCard.types' +import { HELM_GUIDED_CONTENT_CARDS_TEXTS } from '../../onboardingGuide/OnboardingGuide.constants' +import { SELECT_CLUSTER_FROM_FILTER_NOTE } from './Constants' +import { HelmAppListAdditionalProps, HelmAppListRowType } from './AppListType' +import { ModuleNameMap, URLS } from '../../../config' + +export const HelmAppListViewWrapper = ({ + children, + additionalProps, +}: TableViewWrapperProps) => { + const { showGuidedContentCards, externalHelmListFetchErrors, clusterIdsCsv, removeExternalAppFetchError } = additionalProps + + const renderFetchError = (externalHelmListFetchError: string, index: number) => ( +
+
+
+ + + + {externalHelmListFetchError} + removeExternalAppFetchError(index)} + /> +
+
+ ) + + return ( +
+ {!clusterIdsCsv && ( +
+
+
+ + + +
+ {SELECT_CLUSTER_FROM_FILTER_NOTE}  + +
+
+
+ )} + {externalHelmListFetchErrors.map((externalHelmListFetchError, index) => + renderFetchError(externalHelmListFetchError, index), + )} + {children} + {showGuidedContentCards && ( +
+ + +
+ )} +
+ ) +} diff --git a/src/components/app/list-new/list.utils.ts b/src/components/app/list-new/list.utils.ts index 172fbe6e70..ee934fb0b2 100644 --- a/src/components/app/list-new/list.utils.ts +++ b/src/components/app/list-new/list.utils.ts @@ -45,8 +45,15 @@ import { GenericAppListRowType, GetAppListFiltersParams, useFilterOptionsProps, + HelmAppListRowType, } from './AppListType' import { APP_LIST_HEADERS, ENVIRONMENT_HEADER_TIPPY_CONTENT, SELECT_CLUSTER_TIPPY } from './Constants' +import { + HelmAppNameCellComponent, + HelmAppStatusCellComponent, + HelmAppEnvironmentCellComponent, + HelmAppLastDeployedCellComponent, +} from './HelmAppListCellComponents' export const getAppTabNameFromAppType = (appType: InfrastructureManagementAppListType) => { switch (appType) { @@ -341,3 +348,62 @@ export const getGenericAppListColumns = (isFluxCDAppList: boolean) => }, }, ] as TableColumnType[] + +export const getHelmAppListColumns = (isArgoInstalled: boolean) => + [ + { + field: AppListSortableKeys.APP_NAME, + label: APP_LIST_HEADERS.ReleaseName, + isSortable: true, + size: { + fixed: 250, + }, + comparator: stringComparatorBySortOrder, + CellComponent: HelmAppNameCellComponent, + }, + ...(isArgoInstalled + ? [ + { + field: APP_LIST_HEADERS.AppStatus, + label: APP_LIST_HEADERS.AppStatus, + size: { + fixed: 164, + }, + CellComponent: HelmAppStatusCellComponent, + } as TableColumnType, + ] + : []), + { + field: APP_LIST_HEADERS.Environment, + label: APP_LIST_HEADERS.Environment, + size: { + fixed: 200, + }, + infoTooltipText: ENVIRONMENT_HEADER_TIPPY_CONTENT, + CellComponent: HelmAppEnvironmentCellComponent, + }, + { + field: APP_LIST_HEADERS.Cluster, + label: APP_LIST_HEADERS.Cluster, + size: { + fixed: 150, + }, + }, + { + field: APP_LIST_HEADERS.Namespace, + label: APP_LIST_HEADERS.Namespace, + size: { + fixed: 150, + }, + }, + { + field: AppListSortableKeys.LAST_DEPLOYED, + label: APP_LIST_HEADERS.LastDeployedAt, + isSortable: true, + size: { + fixed: 180, + }, + comparator: stringComparatorBySortOrder, + CellComponent: HelmAppLastDeployedCellComponent, + }, + ] as TableColumnType[] diff --git a/src/components/app/list-new/styles.scss b/src/components/app/list-new/styles.scss index ab1a58e1f3..eda3922e5a 100644 --- a/src/components/app/list-new/styles.scss +++ b/src/components/app/list-new/styles.scss @@ -18,3 +18,24 @@ padding: 0 0 0 20px; } } + +#table-wrapper-table__helm-app-list { + .generic-table__row { + padding-block: 10px; + padding-inline-end: 0px; + + & > div.generic-table__cell:nth-of-type(2) { + span { + color: var(--B500); + } + } + } + + .generic-table__row--expanded-row { + padding-block: 0px; + } + + .generic-table__header { + padding: 0 0 0 20px; + } +} From 8d2a1011117e45087e16d3e87dfbe91562a28c94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:10:59 +0000 Subject: [PATCH 3/5] Fix unused imports and duplicate function Co-authored-by: Elessar1802 <66767648+Elessar1802@users.noreply.github.com> --- src/components/app/list-new/HelmAppList.tsx | 40 +++++++-------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/components/app/list-new/HelmAppList.tsx b/src/components/app/list-new/HelmAppList.tsx index d647c32ce9..ba446d1af5 100644 --- a/src/components/app/list-new/HelmAppList.tsx +++ b/src/components/app/list-new/HelmAppList.tsx @@ -16,49 +16,34 @@ import { useEffect, useMemo, useState, useCallback } from 'react' import { - AppStatus, showError, ErrorScreenManager, ServerErrors, Host, GenericEmptyState, - handleUTCTime, - DATE_TIME_FORMATS, - stringComparatorBySortOrder, - DocLink, URLS as CommonURLS, - ComponentSizeType, Table, PaginationEnum, FiltersTypeEnum, TableProps, } from '@devtron-labs/devtron-fe-common-lib' import { Link, useHistory } from 'react-router-dom' -import Tippy from '@tippyjs/react' -import moment from 'moment' import { getDevtronInstalledHelmApps } from './AppListService' -import { LazyImage } from '../../common' -import { SERVER_MODE, URLS, checkIfDevtronOperatorHelmRelease, ModuleNameMap } from '../../../config' +import { SERVER_MODE, URLS, checkIfDevtronOperatorHelmRelease } from '../../../config' import { AppListViewType } from '../config' import NoClusterSelectImage from '../../../assets/icons/ic-select-cluster.svg' -import defaultChartImage from '../../../assets/icons/ic-default-chart.svg' -import HelmCluster from '../../../assets/img/guided-helm-cluster.png' -import DeployCICD from '../../../assets/img/guide-onboard.png' import { AllCheckModal } from '../../checkList/AllCheckModal' import { ReactComponent as AlertTriangleIcon } from '../../../assets/icons/ic-alert-triangle.svg' import noChartInClusterImage from '../../../assets/img/ic-no-chart-in-clusters@2x.png' import '../list/list.scss' import { APP_LIST_EMPTY_STATE_MESSAGING, - ENVIRONMENT_HEADER_TIPPY_CONTENT, EXTERNAL_HELM_APP_FETCH_CLUSTER_ERROR, EXTERNAL_HELM_APP_FETCH_ERROR, EXTERNAL_HELM_SSE_CONNECTION_ERROR, APP_LIST_HEADERS, HELM_PERMISSION_MESSAGE, - SELECT_CLUSTER_FROM_FILTER_NOTE, } from './Constants' -import { HELM_GUIDED_CONTENT_CARDS_TEXTS } from '../../onboardingGuide/OnboardingGuide.constants' import { HelmAppListResponse, HelmApp, AppListSortableKeys, HelmAppListProps, HelmAppListRowType } from './AppListType' import AskToClearFilters from './AppListComponents' import { getHelmAppListColumns } from './list.utils' @@ -116,7 +101,17 @@ const HelmAppList = ({ [devtronInstalledHelmAppsList, externalHelmAppsList], ) - const columns = useMemo(() => getHelmAppListColumns(isArgoInstalled), [isArgoInstalled]) + const columns = useMemo(() => { + const cols = getHelmAppListColumns(isArgoInstalled) + // Disable sorting when SSE is fetching external apps + if (sseConnection) { + return cols.map(col => ({ + ...col, + isSortable: false, + })) + } + return cols + }, [isArgoInstalled, sseConnection]) // Filter function for the table const filter = useCallback( @@ -399,17 +394,6 @@ const HelmAppList = ({ ) } - function renderAllCheckModal() { - return ( -
- -
- ) - } - // Determine empty state config const getNoRowsConfig = () => { if (_isAnyFilterationAppliedExceptClusterAndNs() && !clusterIdsCsv) { From 7da68984dffdd2d37f4c2c6b721e755e2a7ddc1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:12:38 +0000 Subject: [PATCH 4/5] Address code review feedback - add types and fix dependencies Co-authored-by: Elessar1802 <66767648+Elessar1802@users.noreply.github.com> --- src/components/app/list-new/HelmAppList.tsx | 21 +++++++++---------- .../list-new/HelmAppListCellComponents.tsx | 5 +++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/app/list-new/HelmAppList.tsx b/src/components/app/list-new/HelmAppList.tsx index ba446d1af5..79ca36b689 100644 --- a/src/components/app/list-new/HelmAppList.tsx +++ b/src/components/app/list-new/HelmAppList.tsx @@ -105,8 +105,8 @@ const HelmAppList = ({ const cols = getHelmAppListColumns(isArgoInstalled) // Disable sorting when SSE is fetching external apps if (sseConnection) { - return cols.map(col => ({ - ...col, + return cols.map(column => ({ + ...column, isSortable: false, })) } @@ -151,16 +151,15 @@ const HelmAppList = ({ [searchKey, project, environment, namespace], ) - const _buildAppDetailUrl = (app: HelmApp) => { - if (app.isExternal) { - return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.EXTERNAL_APPS}/${app.appId}/${app.appName}` - } - return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.DEVTRON_CHARTS}/deployments/${app.appId}/env/${app.environmentDetail.environmentId}` - } - const onRowClick = useCallback(({ data: app }) => { - push(_buildAppDetailUrl(app.detail)) - }, []) + const buildAppDetailUrl = (helmApp: HelmApp) => { + if (helmApp.isExternal) { + return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.EXTERNAL_APPS}/${helmApp.appId}/${helmApp.appName}` + } + return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.DEVTRON_CHARTS}/deployments/${helmApp.appId}/env/${helmApp.environmentDetail.environmentId}` + } + push(buildAppDetailUrl(app.detail)) + }, [push]) const _removeExternalAppFetchError = (index: number) => { const _externalHelmListFetchErrors = [...externalHelmListFetchErrors] diff --git a/src/components/app/list-new/HelmAppListCellComponents.tsx b/src/components/app/list-new/HelmAppListCellComponents.tsx index fa15360c73..da180829cb 100644 --- a/src/components/app/list-new/HelmAppListCellComponents.tsx +++ b/src/components/app/list-new/HelmAppListCellComponents.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import React from 'react' import { AppStatus, DATE_TIME_FORMATS, FiltersTypeEnum, handleUTCTime, TableCellProps } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import moment from 'moment' @@ -21,8 +22,8 @@ import { LazyImage } from '../../common' import defaultChartImage from '../../../assets/icons/ic-default-chart.svg' import { HelmAppListRowType } from './AppListType' -const handleImageError = (e) => { - const target = e.target as HTMLImageElement +const handleImageError = (event: React.SyntheticEvent) => { + const target = event.target as HTMLImageElement target.onerror = null target.src = defaultChartImage } From 686ed99120cab2b429b0018cc77f75b6933b030a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 05:13:29 +0000 Subject: [PATCH 5/5] Final code review fixes - safer column mapping and better docs Co-authored-by: Elessar1802 <66767648+Elessar1802@users.noreply.github.com> --- src/components/app/list-new/HelmAppList.tsx | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/app/list-new/HelmAppList.tsx b/src/components/app/list-new/HelmAppList.tsx index 79ca36b689..2e92bcd701 100644 --- a/src/components/app/list-new/HelmAppList.tsx +++ b/src/components/app/list-new/HelmAppList.tsx @@ -107,7 +107,7 @@ const HelmAppList = ({ if (sseConnection) { return cols.map(column => ({ ...column, - isSortable: false, + isSortable: column.isSortable ? false : undefined, })) } return cols @@ -151,14 +151,14 @@ const HelmAppList = ({ [searchKey, project, environment, namespace], ) - const onRowClick = useCallback(({ data: app }) => { - const buildAppDetailUrl = (helmApp: HelmApp) => { - if (helmApp.isExternal) { - return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.EXTERNAL_APPS}/${helmApp.appId}/${helmApp.appName}` + const onRowClick = useCallback(({ data: helmApp }) => { + const buildAppDetailUrl = (app: HelmApp) => { + if (app.isExternal) { + return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.EXTERNAL_APPS}/${app.appId}/${app.appName}` } - return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.DEVTRON_CHARTS}/deployments/${helmApp.appId}/env/${helmApp.environmentDetail.environmentId}` + return `${CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}/${URLS.DEVTRON_CHARTS}/deployments/${app.appId}/env/${app.environmentDetail.environmentId}` } - push(buildAppDetailUrl(app.detail)) + push(buildAppDetailUrl(helmApp.detail)) }, [push]) const _removeExternalAppFetchError = (index: number) => { @@ -393,7 +393,11 @@ const HelmAppList = ({ ) } - // Determine empty state config + /** + * Determines the empty state configuration when no rows are present (before any filters are applied). + * Returns null to indicate that the empty state should be handled externally (outside the Table component). + * The external handling is done in the conditional rendering below. + */ const getNoRowsConfig = () => { if (_isAnyFilterationAppliedExceptClusterAndNs() && !clusterIdsCsv) { // Return null to render custom component @@ -402,10 +406,13 @@ const HelmAppList = ({ if (!clusterIdsCsv) { return null } - // Return allCheckModal config + // Return null for allCheckModal - handled externally return null } + /** + * Determines the empty state configuration when filters are applied but no rows match. + */ const getNoRowsForFilterConfig = () => { if (isOnlyAllClusterFilterApplied) { return askToConnectAClusterForNoResult()