From 7f7ce6ce47955ac065e2b5a089f895e8b6308192 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Thu, 3 Apr 2025 17:51:02 +0530 Subject: [PATCH 01/28] feat: add environment column in namespace list Show Add environment button against unmapped namespaces --- .../ResourceBrowser.service.tsx | 36 +++++++++++++++++++ .../ResourceList/BaseResourceList.tsx | 27 +++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/components/ResourceBrowser/ResourceBrowser.service.tsx b/src/components/ResourceBrowser/ResourceBrowser.service.tsx index 466cde86d8..cf23dd7ef2 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.service.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.service.tsx @@ -24,6 +24,8 @@ import { getUrlWithSearchParams, getK8sResourceListPayload, stringComparatorBySortOrder, + Nodes, + getNamespaceListMin, } from '@devtron-labs/devtron-fe-common-lib' import { RefObject } from 'react' import { Routes } from '../../config' @@ -77,6 +79,40 @@ export const getResourceData = async ({ return parseNodeList(response) } + if (selectedResource.gvk.Kind.toLowerCase() === Nodes.Namespace.toLowerCase()) { + const { result } = await getNamespaceListMin(clusterId) + const [{ environments }] = result + + const response = await getK8sResourceList( + getK8sResourceListPayload(clusterId, selectedNamespace.value.toLowerCase(), selectedResource, filters), + abortControllerRef.current.signal, + ) + + const namespaceToEnvironmentMap = environments.reduce( + (acc, { environmentName, namespace, environmentId }) => { + if (environmentId === 0) { + return acc + } + + acc[namespace] = environmentName + return acc + }, + {}, + ) + + return { + ...response, + result: { + ...response.result, + headers: [...response.result.headers, 'environment'], + data: response.result.data.map((data) => ({ + ...data, + environment: namespaceToEnvironmentMap[data.name as string], + })), + }, + } + } + return await getK8sResourceList( getK8sResourceListPayload(clusterId, selectedNamespace.value.toLowerCase(), selectedResource, filters), abortControllerRef.current.signal, diff --git a/src/components/ResourceBrowser/ResourceList/BaseResourceList.tsx b/src/components/ResourceBrowser/ResourceList/BaseResourceList.tsx index f37bf336d6..60c79ce80f 100644 --- a/src/components/ResourceBrowser/ResourceList/BaseResourceList.tsx +++ b/src/components/ResourceBrowser/ResourceList/BaseResourceList.tsx @@ -39,6 +39,9 @@ import { Nodes, ALL_NAMESPACE_OPTION, useResizableTableConfig, + Button, + ButtonVariantType, + ButtonComponentType, } from '@devtron-labs/devtron-fe-common-lib' import DOMPurify from 'dompurify' import { useHistory, useLocation, useRouteMatch } from 'react-router-dom' @@ -52,6 +55,7 @@ import { getManifestResource, updateManifestResourceHelmApps, } from '@Components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.api' +import { ADD_ENVIRONMENT_FORM_LOCAL_STORAGE_KEY } from '@Components/cluster/constants' import ResourceListEmptyState from './ResourceListEmptyState' import { DEFAULT_K8SLIST_PAGE_SIZE, @@ -129,7 +133,7 @@ const BaseResourceListContent = ({ const { searchParams } = useSearchString() - const isNodeListing = selectedResource?.gvk.Kind === SIDEBAR_KEYS.nodeGVK.Kind + const isNodeListing = selectedResource?.gvk.Kind.toLowerCase() === SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase() const { selectedIdentifiers: bulkSelectionState, @@ -382,6 +386,14 @@ const BaseResourceListContent = ({ } } + const getAddEnvironmentClickHandler = (namespace: string) => () => { + const environmentFormData = { + namespace, + } + + localStorage.setItem(ADD_ENVIRONMENT_FORM_LOCAL_STORAGE_KEY, JSON.stringify(environmentFormData)) + } + const renderResourceRow = (resourceData: K8sResourceDetailDataType): JSX.Element => { const lowercaseKind = (resourceData.kind as string)?.toLowerCase() // Redirection and actions are not possible for Events since the required data for the same is not available @@ -516,6 +528,19 @@ const BaseResourceListContent = ({ }} /> + {columnName === 'environment' && !resourceData.environment && ( + +
@@ -215,6 +237,15 @@ const ChartHeaderFilter = ({ >
Show deprecated charts
+ {ChartCategoryFilters && ( + <> +
+ + + )}
void + chartCategoryIds: string[] + setChartCategoryIds: Dispatch> } export interface DeleteInstalledChartParamsType { diff --git a/src/components/charts/charts.util.tsx b/src/components/charts/charts.util.tsx index be27a9d554..13c5ed7c35 100644 --- a/src/components/charts/charts.util.tsx +++ b/src/components/charts/charts.util.tsx @@ -117,6 +117,7 @@ export const QueryParams = { AppStoreName: 'appStoreName', RegistryId: 'registryId', SearchKey: 'searchKey', + ChartCategoryId: 'chartCategoryId', } export const PaginationParams = { diff --git a/src/components/charts/constants.ts b/src/components/charts/constants.ts index bd38f5063c..8c38cb6012 100644 --- a/src/components/charts/constants.ts +++ b/src/components/charts/constants.ts @@ -34,7 +34,7 @@ export const APP_NAME_TAKEN = 'App name already taken' export enum CHART_KEYS { CHART_REPO = 'chart-repo', + CHART_CATEGORY = 'chart-category', DEPRECATED = 'deprecated', SEARCH = 'search', - CLEAR = 'clear', } diff --git a/src/components/charts/list/DiscoverCharts.tsx b/src/components/charts/list/DiscoverCharts.tsx index 8c1eae5504..be1d1bd769 100644 --- a/src/components/charts/list/DiscoverCharts.tsx +++ b/src/components/charts/list/DiscoverCharts.tsx @@ -26,6 +26,9 @@ import { FeatureTitleWithInfo, ToastVariantType, ToastManager, + Button, + ComponentSizeType, + ButtonVariantType, } from '@devtron-labs/devtron-fe-common-lib' import { Switch, Route, NavLink, useHistory, useLocation, useRouteMatch, Prompt } from 'react-router-dom' import Tippy from '@tippyjs/react' @@ -111,6 +114,7 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { const [appStoreName, setAppStoreName] = useState('') const [searchApplied, setSearchApplied] = useState(false) const [includeDeprecated, setIncludeDeprecated] = useState(0) + const [chartCategoryIds, setChartCategoryIds] = useState([]) const projectsMap = mapByKey(state.projects, 'id') const chartList: Chart[] = Array.from(state.availableCharts.values()) const isLeavingPageNotAllowed = useRef(false) @@ -262,12 +266,14 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { toggleDeployModal(false) } + // should be removed and use useUrlFilters instead function initialiseFromQueryParams(chartRepoList): void { const searchParams = new URLSearchParams(location.search) const allChartRepoIds: string = searchParams.get(QueryParams.ChartRepoId) const allRegistryIds: string = searchParams.get(QueryParams.RegistryId) const deprecated: string = searchParams.get(QueryParams.IncludeDeprecated) const appStoreName: string = searchParams.get(QueryParams.AppStoreName) + const chartCategoryCsv: string = searchParams.get(QueryParams.ChartCategoryId) let chartRepoIdArray = [] let ociRegistryArray = [] if (allChartRepoIds) { @@ -297,6 +303,8 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { } if (deprecated) { setIncludeDeprecated(parseInt(deprecated)) + } else { + setIncludeDeprecated(0) } if (appStoreName) { setSearchApplied(true) @@ -305,6 +313,14 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { setSearchApplied(false) setAppStoreName('') } + if (chartCategoryCsv) { + const idsArray = chartCategoryCsv.split(',') + if (idsArray) { + setChartCategoryIds(idsArray) + } + } else { + setChartCategoryIds([]) + } } async function callApplyFilterOnCharts(resetPage?: boolean) { @@ -369,24 +385,25 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { / )} - +
{state.charts.length === 0 ? ( <> - Chart Store + Chart Store {isSuperAdmin && ( - + size={ComponentSizeType.xs} + variant={ButtonVariantType.secondary} + /> )} ) : ( 'Deploy multiple charts' )} - +
{showSourcePopoUp && ( @@ -429,9 +446,7 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { return ( <> -
0 ? 'summary-show' : ''} chart-store-header`} - > +
0 ? 'summary-show' : ''}`}> 0} wrap={(children) =>
{children}
}>
@@ -451,6 +466,8 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { selectedChartRepo={selectedChartRepo} isGrid={isGrid} setIsGrid={setIsGrid} + chartCategoryIds={chartCategoryIds} + setChartCategoryIds={setChartCategoryIds} /> )} {state.loading || chartListLoading ? ( @@ -495,7 +512,8 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => {
{serverMode == SERVER_MODE.FULL && !searchApplied && - selectedChartRepo.length === 0 && ( + !selectedChartRepo.length && + !chartCategoryIds.length && ( { - let url = `${Routes.CHART_AVAILABLE}/discover/` + let url = `${Routes.CHART_STORE}/discover/` if (pageOffset >= 0 && pageSize) { queryString = `${queryString || '?'}&offset=${pageOffset}&size=${pageSize}` From d4c3bb182096c84eebca63944f9e470ca28bdef4 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Tue, 8 Apr 2025 14:42:30 +0530 Subject: [PATCH 08/28] feat: add RESOURCE_BROWSER_INSTALLATION_CLUSTER to RB router --- src/components/ResourceBrowser/ResourceBrowser.tsx | 1 + src/components/ResourceBrowser/ResourceBrowserRouter.tsx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/components/ResourceBrowser/ResourceBrowser.tsx b/src/components/ResourceBrowser/ResourceBrowser.tsx index 9eceb62b4e..292f31b512 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.tsx @@ -23,6 +23,7 @@ import { ClusterDetail, } from '@devtron-labs/devtron-fe-common-lib' import { DEFAULT_CLUSTER_ID } from '@Components/cluster/cluster.type' + import { sortObjectArrayAlphabetically } from '../common' import ClusterSelectionList from '../ClusterNodes/ClusterSelectionList' import { AddClusterButton } from './PageHeader.buttons' diff --git a/src/components/ResourceBrowser/ResourceBrowserRouter.tsx b/src/components/ResourceBrowser/ResourceBrowserRouter.tsx index 6c250b480b..107c3616b6 100644 --- a/src/components/ResourceBrowser/ResourceBrowserRouter.tsx +++ b/src/components/ResourceBrowser/ResourceBrowserRouter.tsx @@ -18,6 +18,8 @@ import React from 'react' import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' import { URLS as COMMON_URLS } from '@devtron-labs/devtron-fe-common-lib' import { importComponentFromFELibrary } from '@Components/common' +import { URLS } from '@Config/routes' + import ResourceBrowser from './ResourceBrowser' import ResourceList from './ResourceList' import './ResourceBrowser.scss' @@ -29,6 +31,10 @@ const ResourceBrowserRouter: React.FC = () => { return ( + +
+ + From 126d0695e9bca400f548e80644d4dd2cc724d848 Mon Sep 17 00:00:00 2001 From: arunjaindev Date: Tue, 8 Apr 2025 18:15:02 +0530 Subject: [PATCH 09/28] feat: add cluster installation page --- .../ResourceBrowser/ResourceBrowser.service.tsx | 2 +- src/components/ResourceBrowser/ResourceBrowserRouter.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/ResourceBrowser/ResourceBrowser.service.tsx b/src/components/ResourceBrowser/ResourceBrowser.service.tsx index adc5a20e06..3fcc5b7e75 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.service.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.service.tsx @@ -145,7 +145,7 @@ export const getClusterListing = async ( installationClustersList .filter( ({ status }) => - status !== InstallationClusterStatus.Succeeded && status !== InstallationClusterStatus.Updating, + status !== InstallationClusterStatus.Installed && status !== InstallationClusterStatus.Updating, ) .map(({ installationId, name }) => ({ id: installationId, diff --git a/src/components/ResourceBrowser/ResourceBrowserRouter.tsx b/src/components/ResourceBrowser/ResourceBrowserRouter.tsx index 107c3616b6..d9b295b46b 100644 --- a/src/components/ResourceBrowser/ResourceBrowserRouter.tsx +++ b/src/components/ResourceBrowser/ResourceBrowserRouter.tsx @@ -25,15 +25,18 @@ import ResourceList from './ResourceList' import './ResourceBrowser.scss' const CompareClusterViewWrapper = importComponentFromFELibrary('CompareClusterViewWrapper', null, 'function') +const ClusterInstallationPage = importComponentFromFELibrary('ClusterInstallationPage', null, 'function') const ResourceBrowserRouter: React.FC = () => { const { path } = useRouteMatch() return ( - -
- + {ClusterInstallationPage && ( + + + + )} From 5272ac47457b7d8e1e9fff8d51e20b93280f7b98 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Tue, 8 Apr 2025 18:41:33 +0530 Subject: [PATCH 10/28] feat: incorporate connect cluster & isolated cluster --- .../CreateCluster/CreateCluster.component.tsx | 39 +++++- .../CreateCluster/types.ts | 7 ++ .../ClusterNodes/ClusterOverview.tsx | 2 +- .../ResourceBrowser/PageHeader.buttons.tsx | 4 +- .../ResourceBrowser.service.tsx | 2 +- src/components/cluster/Cluster.tsx | 32 ++++- src/components/cluster/ClusterForm.tsx | 114 +++++++++++++----- 7 files changed, 161 insertions(+), 39 deletions(-) diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx index 90d8ddce87..d6585ca329 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx @@ -13,16 +13,18 @@ import { useState } from 'react' import { ReactComponent as ICClose } from '@Icons/ic-close.svg' import { URLS } from '@Config/routes' import { importComponentFromFELibrary } from '@Components/common' +import ClusterForm from '@Components/cluster/ClusterForm' -import { CreateClusterParams, CreateClusterTypeEnum } from './types' +import { CreateClusterParams, CreateClusterProps, CreateClusterTypeEnum } from './types' import Sidebar from './Sidebar' import FooterComponent from './FooterComponent' import './styles.scss' +// TODO: show a default component that says its a enterprise feature const CreateClusterForm = importComponentFromFELibrary('CreateClusterForm', null, 'function') -const CreateCluster = () => { +const CreateCluster = ({ handleReloadClusterList, clusterFormProps }: CreateClusterProps) => { const { type } = useParams() const [apiCallInProgress, setApiCallInProgress] = useState(false) @@ -31,23 +33,50 @@ const CreateCluster = () => { const handleRedirectToClusterList = () => { push(URLS.GLOBAL_CONFIG_CLUSTER) + handleReloadClusterList() + } + + const handleRedirectToResourceBrowser = () => { + push(URLS.RESOURCE_BROWSER) } const renderContent = () => { switch (type) { case CreateClusterTypeEnum.CONNECT_CLUSTER: - return 'connect cluster' + return ( + + ) case CreateClusterTypeEnum.CREATE_EKS_CLUSTER: return ( ) case CreateClusterTypeEnum.ADD_ISOLATED_CLUSTER: - return 'create isolated cluster' + return ( + + ) default: return } diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/types.ts b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/types.ts index d603c90613..89b5d6eea1 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/types.ts +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/types.ts @@ -1,3 +1,5 @@ +import { ClusterFormProps } from '@Components/cluster/cluster.type' + export enum CreateClusterTypeEnum { CONNECT_CLUSTER = 'connect-cluster', CREATE_EKS_CLUSTER = 'create-eks-cluster', @@ -18,3 +20,8 @@ export type SidebarConfigType = Record< export interface CreateClusterParams { type: CreateClusterTypeEnum } + +export interface CreateClusterProps { + handleReloadClusterList: () => void + clusterFormProps: ClusterFormProps +} diff --git a/src/components/ClusterNodes/ClusterOverview.tsx b/src/components/ClusterNodes/ClusterOverview.tsx index e1dc187b37..3ee393b6d4 100644 --- a/src/components/ClusterNodes/ClusterOverview.tsx +++ b/src/components/ClusterNodes/ClusterOverview.tsx @@ -508,7 +508,7 @@ function ClusterOverview({ selectedCluster, addTab }: ClusterOverviewProps) {
{renderCardDetails()} {renderClusterError()} - {ClusterConfig && clusterCapacityData.installationId && } + {ClusterConfig && } {Catalog && } ( size={ComponentSizeType.small} startIcon={} linkProps={{ - to: URLS.GLOBAL_CONFIG_CREATE_CLUSTER, + to: generatePath(URLS.GLOBAL_CONFIG_CREATE_CLUSTER, { type: CreateClusterTypeEnum.CREATE_EKS_CLUSTER }), target: '_blank', }} /> diff --git a/src/components/ResourceBrowser/ResourceBrowser.service.tsx b/src/components/ResourceBrowser/ResourceBrowser.service.tsx index adc5a20e06..3fcc5b7e75 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.service.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.service.tsx @@ -145,7 +145,7 @@ export const getClusterListing = async ( installationClustersList .filter( ({ status }) => - status !== InstallationClusterStatus.Succeeded && status !== InstallationClusterStatus.Updating, + status !== InstallationClusterStatus.Installed && status !== InstallationClusterStatus.Updating, ) .map(({ installationId, name }) => ({ id: installationId, diff --git a/src/components/cluster/Cluster.tsx b/src/components/cluster/Cluster.tsx index 6d37ecfc45..b3d22202e2 100644 --- a/src/components/cluster/Cluster.tsx +++ b/src/components/cluster/Cluster.tsx @@ -252,7 +252,9 @@ class ClusterList extends Component { -
+
)}
) @@ -1222,7 +1230,7 @@ export default function ClusterForm({ {isKubeConfigFile && (
@@ -1361,7 +1369,7 @@ export default function ClusterForm({
-
+ {id &&
-
+
} + + + +
)} {isClusterDetails && !isKubeConfigFile && saveClusterDetails()} @@ -1431,7 +1451,7 @@ export default function ClusterForm({ return null } - if (isKubeConfigFile) { + if (isKubeConfigFile && id) { return (
+ + ) + } + return ( -
+
{id && (
+ {id ? ( +
+
+ ) : ( + +
) } @@ -1496,9 +1548,9 @@ export default function ClusterForm({ return getClusterVar ? ( displayClusterDetails() ) : ( -
+
-
+
{VirtualClusterSelectionTab && ( )} {!isVirtual && ( From 85ae37eecdf9afa4490e8442f1d5640e0e1dc823 Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Tue, 8 Apr 2025 21:00:52 +0530 Subject: [PATCH 11/28] chore(text): change connect cluster to new cluster in RB page header --- src/components/ClusterNodes/ClusterSelectionList.tsx | 4 ++-- src/components/ResourceBrowser/PageHeader.buttons.tsx | 4 ++-- src/components/ResourceBrowser/ResourceBrowser.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ClusterNodes/ClusterSelectionList.tsx b/src/components/ClusterNodes/ClusterSelectionList.tsx index d28e03d745..a4011e154c 100644 --- a/src/components/ClusterNodes/ClusterSelectionList.tsx +++ b/src/components/ClusterNodes/ClusterSelectionList.tsx @@ -31,7 +31,7 @@ import dayjs, { Dayjs } from 'dayjs' import { importComponentFromFELibrary } from '@Components/common' import Timer from '@Components/common/DynamicTabs/DynamicTabs.timer' import NoClusterEmptyState from '@Images/no-cluster-empty-state.png' -import { AddClusterButton } from '@Components/ResourceBrowser/PageHeader.buttons' +import { NewClusterButton } from '@Components/ResourceBrowser/PageHeader.buttons' import { ReactComponent as Error } from '@Icons/ic-error-exclamation.svg' import { ReactComponent as TerminalIcon } from '@Icons/ic-terminal-fill.svg' import ClusterNodeEmptyState from './ClusterNodeEmptyStates' @@ -222,7 +222,7 @@ const ClusterSelectionList: React.FC = ({ image={NoClusterEmptyState} title="No clusters found" subTitle="Add a cluster to view and debug Kubernetes resources in the cluster" - renderButton={AddClusterButton} + renderButton={NewClusterButton} /> ) } diff --git a/src/components/ResourceBrowser/PageHeader.buttons.tsx b/src/components/ResourceBrowser/PageHeader.buttons.tsx index b925328fec..21dfd01922 100644 --- a/src/components/ResourceBrowser/PageHeader.buttons.tsx +++ b/src/components/ResourceBrowser/PageHeader.buttons.tsx @@ -51,11 +51,11 @@ export const renderCreateResourceButton = (clusterId: string, callback: CreateRe ) -export const AddClusterButton = (): JSX.Element => ( +export const NewClusterButton = (): JSX.Element => (
From 7f420c444b1303a845553ab1970f6e7b6463ebae Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Wed, 9 Apr 2025 12:10:11 +0530 Subject: [PATCH 12/28] fix: show all installation clusters not found in cluster list --- .../ResourceBrowser/ResourceBrowser.service.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/ResourceBrowser/ResourceBrowser.service.tsx b/src/components/ResourceBrowser/ResourceBrowser.service.tsx index 3fcc5b7e75..cb23fd8525 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.service.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.service.tsx @@ -29,7 +29,6 @@ import { getClusterListRaw, ClusterDetail, InstallationClusterConfigType, - InstallationClusterStatus, ClusterStatusType, APIOptions, } from '@devtron-labs/devtron-fe-common-lib' @@ -135,6 +134,8 @@ export const getClusterListing = async ( try { const { result: rawClusterList } = await (minified ? getClusterListRaw : getClusterList)(abortControllerRef) + const clusterListNameSet = new Set(rawClusterList.map(({ name }) => name)) + const installationClustersList: InstallationClusterConfigType[] = await getInstallationClusterConfigs() if (!installationClustersList.length) { @@ -143,10 +144,13 @@ export const getClusterListing = async ( return rawClusterList.concat( installationClustersList - .filter( - ({ status }) => - status !== InstallationClusterStatus.Installed && status !== InstallationClusterStatus.Updating, - ) + .filter(({ name }) => { + if (!clusterListNameSet.has(name)) { + return true + } + + return false + }) .map(({ installationId, name }) => ({ id: installationId, name, From e8b73db9d3aea2834148d6958fd72445699cfc5c Mon Sep 17 00:00:00 2001 From: arunjaindev Date: Wed, 9 Apr 2025 12:23:10 +0530 Subject: [PATCH 13/28] feat: add page header on cluster installation status page --- .../ClusterInstallationStatus.tsx | 68 +++++++++++++++++++ .../ResourceBrowser/ResourceBrowserRouter.tsx | 6 +- .../ResourceList/ResourceList.tsx | 49 +++++-------- .../ResourceList/ResourcePageHeader.tsx | 17 +++++ .../ResourceBrowser/ResourceList/types.ts | 6 ++ .../ResourceBrowser/ResourceList/utils.tsx | 18 ++++- src/components/ResourceBrowser/Types.ts | 2 +- src/components/ResourceBrowser/Utils.tsx | 8 +++ 8 files changed, 136 insertions(+), 38 deletions(-) create mode 100644 src/components/ResourceBrowser/ClusterInstallationStatus.tsx create mode 100644 src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx diff --git a/src/components/ResourceBrowser/ClusterInstallationStatus.tsx b/src/components/ResourceBrowser/ClusterInstallationStatus.tsx new file mode 100644 index 0000000000..cb8a184071 --- /dev/null +++ b/src/components/ResourceBrowser/ClusterInstallationStatus.tsx @@ -0,0 +1,68 @@ +import { useHistory, useParams } from 'react-router-dom' +import { useAsync, useBreadcrumb } from '@devtron-labs/devtron-fe-common-lib' +import { importComponentFromFELibrary } from '@Components/common' +import { useMemo } from 'react' +import ResourcePageHeader from './ResourceList/ResourcePageHeader' +import ClusterSelector from './ResourceList/ClusterSelector' +import { getClusterListing } from './ResourceBrowser.service' +import { getClusterOptions } from './ResourceList/utils' +import { ClusterOptionType } from './Types' +import { getClusterChangeRedirectionUrl } from './Utils' + +const ClusterInstallationStatusDialog = importComponentFromFELibrary( + 'ClusterInstallationStatusDialog', + null, + 'function', +) + +const ClusterInstallationStatus = () => { + const { replace } = useHistory() + const { installationId } = useParams<{ installationId: string }>() + + const [, clusterList] = useAsync(() => getClusterListing(true)) + + const clusterOptions = useMemo(() => getClusterOptions(clusterList), [clusterList, installationId]) + + const onClusterChange = ({ value, isInstallationCluster }: ClusterOptionType) => { + const path = getClusterChangeRedirectionUrl(isInstallationCluster, value) + + replace({ + pathname: path, + }) + } + + const { breadcrumbs } = useBreadcrumb( + { + alias: { + 'resource-browser': { + component: Resource Browser, + linked: true, + }, + 'installation-cluster': { + component: ( + + ), + }, + ':installationId': { + component: null, + }, + }, + }, + [clusterOptions, installationId], + ) + + return ( +
+ +
+ +
+
+ ) +} + +export default ClusterInstallationStatus diff --git a/src/components/ResourceBrowser/ResourceBrowserRouter.tsx b/src/components/ResourceBrowser/ResourceBrowserRouter.tsx index d9b295b46b..9be8ce6f55 100644 --- a/src/components/ResourceBrowser/ResourceBrowserRouter.tsx +++ b/src/components/ResourceBrowser/ResourceBrowserRouter.tsx @@ -23,18 +23,18 @@ import { URLS } from '@Config/routes' import ResourceBrowser from './ResourceBrowser' import ResourceList from './ResourceList' import './ResourceBrowser.scss' +import ClusterInstallationStatus from './ClusterInstallationStatus' const CompareClusterViewWrapper = importComponentFromFELibrary('CompareClusterViewWrapper', null, 'function') -const ClusterInstallationPage = importComponentFromFELibrary('ClusterInstallationPage', null, 'function') const ResourceBrowserRouter: React.FC = () => { const { path } = useRouteMatch() return ( - {ClusterInstallationPage && ( + {CompareClusterViewWrapper && ( - + )} diff --git a/src/components/ResourceBrowser/ResourceList/ResourceList.tsx b/src/components/ResourceBrowser/ResourceList/ResourceList.tsx index 7d0ae430cb..b8956ffd4f 100644 --- a/src/components/ResourceBrowser/ResourceList/ResourceList.tsx +++ b/src/components/ResourceBrowser/ResourceList/ResourceList.tsx @@ -17,13 +17,11 @@ import { useState, useEffect, useMemo, useRef } from 'react' import { useHistory, useParams, useRouteMatch, useLocation } from 'react-router-dom' import { - BreadCrumb, useBreadcrumb, ErrorScreenManager, DevtronProgressing, useAsync, useEffectAfterMount, - PageHeader, getResourceGroupListRaw, noop, ALL_NAMESPACE_OPTION, @@ -49,12 +47,11 @@ import { UPGRADE_CLUSTER_CONSTANTS, } from '../Constants' import { URLS } from '../../../config' -import { convertToOptionsList, importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../common' +import { importComponentFromFELibrary } from '../../common' import { AppDetailsTabs } from '../../v2/appDetails/appDetails.store' import NodeDetailComponent from '../../v2/appDetails/k8Resource/nodeDetail/NodeDetail.component' import { DynamicTabs, useTabs } from '../../common/DynamicTabs' -import { getTabsBasedOnRole } from '../Utils' -import { getClusterListMin } from '../../ClusterNodes/clusterNodes.service' +import { getClusterChangeRedirectionUrl, getTabsBasedOnRole } from '../Utils' import ClusterSelector from './ClusterSelector' import ClusterOverview from '../../ClusterNodes/ClusterOverview' import NodeDetails from '../../ClusterNodes/NodeDetails' @@ -64,8 +61,15 @@ import AdminTerminal from './AdminTerminal' import { renderRefreshBar } from './ResourceList.component' import { renderCreateResourceButton } from '../PageHeader.buttons' import ClusterUpgradeCompatibilityInfo from './ClusterUpgradeCompatibilityInfo' -import { getFirstResourceFromKindResourceMap, getUpgradeCompatibilityTippyConfig, parseSearchParams } from './utils' +import { + getClusterOptions, + getFirstResourceFromKindResourceMap, + getUpgradeCompatibilityTippyConfig, + parseSearchParams, +} from './utils' import { ResourceListUrlFiltersType } from './types' +import ResourcePageHeader from './ResourcePageHeader' +import { getClusterListing } from '../ResourceBrowser.service' const EventsAIResponseWidget = importComponentFromFELibrary('EventsAIResponseWidget', null, 'function') const MonitoringDashboard = importComponentFromFELibrary('MonitoringDashboard', null, 'function') @@ -102,22 +106,9 @@ const ResourceList = () => { [clusterId], ) - const [loading, clusterListData, error] = useAsync(() => getClusterListMin()) + const [loading, clusterList, error] = useAsync(() => getClusterListing(true)) - const clusterList = clusterListData?.result || null - - const clusterOptions = useMemo( - () => - clusterList && - (convertToOptionsList( - sortObjectArrayAlphabetically(clusterList, 'name').filter(({ isVirtualCluster }) => !isVirtualCluster), - 'name', - 'id', - 'nodeErrors', - 'isProd', - ) as ClusterOptionType[]), - [clusterList], - ) + const clusterOptions: ClusterOptionType[] = useMemo(() => getClusterOptions(clusterList), [clusterList]) /* NOTE: this is being used as dependency in useEffect down the tree */ const selectedCluster = useMemo( @@ -125,8 +116,8 @@ const ResourceList = () => { clusterOptions?.find((cluster) => String(cluster.value) === clusterId) || { label: '', value: clusterId, - errorInConnecting: '', isProd: false, + isInstallationCluster: false, }, [clusterId, clusterOptions], ) @@ -296,9 +287,7 @@ const ResourceList = () => { return } - const path = `${URLS.RESOURCE_BROWSER}/${selected.value}/${ - ALL_NAMESPACE_OPTION.value - }/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}` + const path = getClusterChangeRedirectionUrl(selected.isInstallationCluster, selected.value) replace({ pathname: path, @@ -361,8 +350,6 @@ const ResourceList = () => { } } - const renderBreadcrumbs = () => - const updateTerminalTabUrl = (queryParams: string) => { const terminalTab = getTabById(ResourceBrowserTabsId.terminal) if (!terminalTab || terminalTab.name !== AppDetailsTabs.terminal) { @@ -584,11 +571,9 @@ const ResourceList = () => { return (
- {renderMainBody()}
diff --git a/src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx b/src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx new file mode 100644 index 0000000000..87481732fe --- /dev/null +++ b/src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx @@ -0,0 +1,17 @@ +import { BreadCrumb, noop, PageHeader } from '@devtron-labs/devtron-fe-common-lib' +import { ResourcePageHeaderProps } from './types' + +const ResourcePageHeader = ({ breadcrumbs, renderPageHeaderActionButtons }: ResourcePageHeaderProps) => { + const renderBreadcrumbs = () => + + return ( + + ) +} + +export default ResourcePageHeader diff --git a/src/components/ResourceBrowser/ResourceList/types.ts b/src/components/ResourceBrowser/ResourceList/types.ts index 354b28659e..3c4fac0426 100644 --- a/src/components/ResourceBrowser/ResourceList/types.ts +++ b/src/components/ResourceBrowser/ResourceList/types.ts @@ -19,6 +19,7 @@ import { ServerErrors, ALL_NAMESPACE_OPTION, RBBulkOperationType, + useBreadcrumb, } from '@devtron-labs/devtron-fe-common-lib' import { Dispatch, ReactNode, SetStateAction } from 'react' import { ClusterListType } from '@Components/ClusterNodes/types' @@ -79,3 +80,8 @@ export interface ResourceListUrlFiltersType { } export type BulkOperationsModalState = RBBulkOperationType | 'closed' + +export interface ResourcePageHeaderProps { + breadcrumbs: ReturnType['breadcrumbs'] + renderPageHeaderActionButtons?: () => JSX.Element +} diff --git a/src/components/ResourceBrowser/ResourceList/utils.tsx b/src/components/ResourceBrowser/ResourceList/utils.tsx index b67b1365ad..3e6dffafa2 100644 --- a/src/components/ResourceBrowser/ResourceList/utils.tsx +++ b/src/components/ResourceBrowser/ResourceList/utils.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { logExceptionToSentry, noop } from '@devtron-labs/devtron-fe-common-lib' +import { ClusterDetail, logExceptionToSentry, noop } from '@devtron-labs/devtron-fe-common-lib' +import { sortObjectArrayAlphabetically } from '@Components/common' import { TARGET_K8S_VERSION_SEARCH_KEY, LOCAL_STORAGE_EXISTS, @@ -22,7 +23,7 @@ import { OPTIONAL_NODE_LIST_HEADERS, } from '../Constants' import { ResourceListUrlFiltersType } from './types' -import { K8SResourceListType } from '../Types' +import { ClusterOptionType, K8SResourceListType } from '../Types' export const parseSearchParams = (searchParams: URLSearchParams) => ({ targetK8sVersion: searchParams.get(TARGET_K8S_VERSION_SEARCH_KEY), @@ -87,3 +88,16 @@ export const getFirstResourceFromKindResourceMap = ( (resourceGroup) => resourceGroup.gvk.Kind?.toLowerCase() === kind?.toLowerCase(), ) } + +export const getClusterOptions = (clusterList: ClusterDetail[]): ClusterOptionType[] => + clusterList + ? sortObjectArrayAlphabetically(clusterList, 'name') + .filter(({ isVirtualCluster }) => !isVirtualCluster) + .map(({ name, id, nodeErrors, isProd, isInstallationCluster }) => ({ + label: name, + value: id.toString(), + description: nodeErrors, + isProd, + isInstallationCluster, + })) + : [] diff --git a/src/components/ResourceBrowser/Types.ts b/src/components/ResourceBrowser/Types.ts index 9747a05282..3b0b26ab46 100644 --- a/src/components/ResourceBrowser/Types.ts +++ b/src/components/ResourceBrowser/Types.ts @@ -94,8 +94,8 @@ export interface SidebarType { } export interface ClusterOptionType extends OptionType { - errorInConnecting: string isProd: boolean + isInstallationCluster: boolean } export interface ResourceFilterOptionsProps extends Pick { diff --git a/src/components/ResourceBrowser/Utils.tsx b/src/components/ResourceBrowser/Utils.tsx index e695c725fd..5fc9843ab8 100644 --- a/src/components/ResourceBrowser/Utils.tsx +++ b/src/components/ResourceBrowser/Utils.tsx @@ -18,6 +18,7 @@ import React from 'react' import queryString from 'query-string' import { useLocation } from 'react-router-dom' import { + ALL_NAMESPACE_OPTION, ApiResourceGroupType, DATE_TIME_FORMAT_STRING, GVKType, @@ -406,3 +407,10 @@ export const parseNodeList = (response: ResponseType): Response }), }, }) + +export const getClusterChangeRedirectionUrl = (isInstallationCluster: boolean, clusterId: string) => + isInstallationCluster + ? `${URLS.RESOURCE_BROWSER}/installation-cluster/${clusterId}` + : `${URLS.RESOURCE_BROWSER}/${clusterId}/${ + ALL_NAMESPACE_OPTION.value + }/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}` From 34361a0a64f6f06e462d1d27605883a47096b4ae Mon Sep 17 00:00:00 2001 From: arunjaindev Date: Wed, 9 Apr 2025 17:18:36 +0530 Subject: [PATCH 14/28] feat: update filter empty state --- src/components/charts/list/DiscoverCharts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/charts/list/DiscoverCharts.tsx b/src/components/charts/list/DiscoverCharts.tsx index be1d1bd769..5adfb66f15 100644 --- a/src/components/charts/list/DiscoverCharts.tsx +++ b/src/components/charts/list/DiscoverCharts.tsx @@ -129,7 +129,7 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { const [isLoading, setIsLoading] = useState(false) const [filteredChartList, setFilteredChartList] = useState([]) - const noChartAvailable: boolean = chartList.length > 0 || searchApplied || selectedChartRepo.length > 0 + const noChartAvailable: boolean = chartList.length > 0 || searchApplied || selectedChartRepo.length > 0 || !!chartCategoryIds isLeavingPageNotAllowed.current = !state.charts.reduce((acc: boolean, chart: ChartGroupEntry) => { return (acc = acc && chart.originalValuesYaml === chart.valuesYaml) }, true) From 9b15d0f2af8c288bc975d2b5e50c209816ebec8b Mon Sep 17 00:00:00 2001 From: Amrit Kashyap Borah Date: Wed, 9 Apr 2025 17:37:26 +0530 Subject: [PATCH 15/28] fix: redirect to installation status after cluster creation --- .../CreateCluster/CreateCluster.component.tsx | 9 ++-- .../ClusterNodes/ClusterOverview.tsx | 47 +++++++++++++++++-- src/components/ClusterNodes/constants.ts | 2 + 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx index d6585ca329..6a160199bb 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component.tsx @@ -8,7 +8,7 @@ import { Drawer, stopPropagation, } from '@devtron-labs/devtron-fe-common-lib' -import { Prompt, Redirect, useHistory, useParams } from 'react-router-dom' +import { generatePath, Prompt, Redirect, useHistory, useParams } from 'react-router-dom' import { useState } from 'react' import { ReactComponent as ICClose } from '@Icons/ic-close.svg' import { URLS } from '@Config/routes' @@ -36,8 +36,8 @@ const CreateCluster = ({ handleReloadClusterList, clusterFormProps }: CreateClus handleReloadClusterList() } - const handleRedirectToResourceBrowser = () => { - push(URLS.RESOURCE_BROWSER) + const handleRedirectToClusterInstallationStatus = (installationId: string) => { + push(generatePath(URLS.RESOURCE_BROWSER_INSTALLATION_CLUSTER, { installationId })) } const renderContent = () => { @@ -60,7 +60,8 @@ const CreateCluster = ({ handleReloadClusterList, clusterFormProps }: CreateClus ) diff --git a/src/components/ClusterNodes/ClusterOverview.tsx b/src/components/ClusterNodes/ClusterOverview.tsx index e3800bbf41..4b66fdccdf 100644 --- a/src/components/ClusterNodes/ClusterOverview.tsx +++ b/src/components/ClusterNodes/ClusterOverview.tsx @@ -26,6 +26,9 @@ import { getUrlWithSearchParams, showError, ClusterCapacityType, + InstallationClusterConfigType, + noop, + StatusComponent, } from '@devtron-labs/devtron-fe-common-lib' import { ClusterErrorType, @@ -40,7 +43,7 @@ import { getURLBasedOnSidebarGVK } from '@Components/ResourceBrowser/Utils' import { ReactComponent as Error } from '../../assets/icons/ic-error-exclamation.svg' import { getClusterCapacity, getClusterDetails, updateClusterShortDescription } from './clusterNodes.service' import GenericDescription from '../common/Description/GenericDescription' -import { defaultClusterNote, defaultClusterShortDescription } from './constants' +import { CLUSTER_CONFIG_POLLING_INTERVAL, defaultClusterNote, defaultClusterShortDescription } from './constants' import { Moment12HourFormat, URLS } from '../../config' import { K8S_EMPTY_GROUP, @@ -60,6 +63,7 @@ const Catalog = importComponentFromFELibrary('Catalog', null, 'function') const ClusterConfig = importComponentFromFELibrary('ClusterConfig', null, 'function') const ClusterAddOns = importComponentFromFELibrary('ClusterAddOns', null, 'function') const MigrateClusterVersionInfoBar = importComponentFromFELibrary('MigrateClusterVersionInfoBar', null, 'function') +const getInstallationClusterConfig = importComponentFromFELibrary('getInstallationClusterConfig', null, 'function') /* TODO: move into utils */ const metricsApiTippyContent = () => ( @@ -114,8 +118,10 @@ function ClusterOverview({ selectedCluster, addTab }: ClusterOverviewProps) { const [clusterErrorList, setClusterErrorList] = useState([]) const [clusterDetails, setClusterDetails] = useState({} as ClusterDetailsType) const [clusterCapacityData, setClusterCapacityData] = useState(null) + const [clusterConfig, setClusterConfig] = useState(null) const requestAbortControllerRef = useRef(null) + const clusterConfigPollTimeoutRef = useRef(-1) const handleRetry = async () => { abortRequestAndResetError(true) @@ -176,9 +182,32 @@ function ClusterOverview({ selectedCluster, addTab }: ClusterOverviewProps) { } } + const fetchClusterConfig = async (clusterName: string) => { + if (!getInstallationClusterConfig) { + return + } + + const config = await (getInstallationClusterConfig({ clusterName }) as Promise) + setClusterConfig(config) + } + + const pollClusterConfig = async (clusterName: string) => { + if (clusterCapacityData?.name && !clusterConfigPollTimeoutRef.current) { + setTimeout(() => { + fetchClusterConfig(clusterCapacityData.name) + .then(() => pollClusterConfig(clusterName)) + .catch(noop) + }, CLUSTER_CONFIG_POLLING_INTERVAL) + } + } + const setClusterCapacityDetails = (clusterCapacityResponse: PromiseSettledResult) => { if (clusterCapacityResponse.status === 'fulfilled') { - setClusterCapacityData(clusterCapacityResponse.value.result) + const clusterCapacity = clusterCapacityResponse.value.result + setClusterCapacityData(clusterCapacity) + if (clusterCapacity?.name) { + fetchClusterConfig(clusterCapacity.name).catch(noop) + } const _errorList = [] const _nodeErrors = Object.keys(clusterCapacityResponse.value.result.nodeErrors || {}) const _nodeK8sVersions = clusterCapacityResponse.value.result.nodeK8sVersions || [] @@ -410,6 +439,8 @@ function ClusterOverview({ selectedCluster, addTab }: ClusterOverviewProps) { }).then(() => history.push(URL)) } + const creationPrefix = clusterConfig ? 'Created' : 'Added' + const renderSideInfoData = () => { return (