Skip to content

Commit 2dcb316

Browse files
authored
Merge pull request #2643 from devtron-labs/feat/bulk-kubeconfig
feat: Support for bulk kubeconfig in rb
2 parents 133b812 + ee81c5b commit 2dcb316

16 files changed

+543
-313
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"homepage": "/dashboard",
66
"dependencies": {
7-
"@devtron-labs/devtron-fe-common-lib": "1.11.0-pre-2",
7+
"@devtron-labs/devtron-fe-common-lib": "1.11.0-pre-3",
88
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
99
"@rjsf/core": "^5.13.3",
1010
"@rjsf/utils": "^5.13.3",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react'
2+
3+
import {
4+
BulkSelection,
5+
ClusterFiltersType,
6+
SortableTableHeaderCell,
7+
useUrlFilters,
8+
} from '@devtron-labs/devtron-fe-common-lib'
9+
10+
import { importComponentFromFELibrary } from '@Components/common'
11+
12+
import { ClusterMapListSortableKeys, ClusterMapListSortableTitle } from '../constants'
13+
import { parseSearchParams } from '../utils'
14+
import ClusterListRow from './ClusterListRow'
15+
import { ClusterListTypes } from './types'
16+
17+
const KubeConfigRowCheckbox = importComponentFromFELibrary('KubeConfigRowCheckbox', null, 'function')
18+
19+
const ClusterList = ({
20+
filteredList,
21+
clusterListLoader,
22+
showKubeConfigModal,
23+
onChangeShowKubeConfigModal,
24+
setSelectedClusterName,
25+
}: ClusterListTypes) => {
26+
const { sortBy, sortOrder, handleSorting } = useUrlFilters<
27+
ClusterMapListSortableKeys,
28+
{ clusterFilter: ClusterFiltersType }
29+
>({
30+
parseSearchParams,
31+
initialSortKey: ClusterMapListSortableKeys.CLUSTER_NAME,
32+
})
33+
34+
const handleCellSorting = (cellToSort: ClusterMapListSortableKeys) => () => {
35+
handleSorting(cellToSort)
36+
}
37+
38+
return (
39+
<div data-testid="cluster-list-container" className="flexbox-col flex-grow-1">
40+
<div className="cluster-list-row fw-6 cn-7 fs-12 dc__border-bottom pt-8 pb-8 pr-20 pl-20 dc__uppercase bg__primary dc__position-sticky dc__top-0 dc__zi-3">
41+
{Object.entries(ClusterMapListSortableKeys).map(([cellName, cellKey]) => (
42+
<React.Fragment key={cellName}>
43+
{KubeConfigRowCheckbox && cellKey === ClusterMapListSortableKeys.CLUSTER_NAME && (
44+
<BulkSelection showPagination={false} />
45+
)}
46+
<SortableTableHeaderCell
47+
key={cellName}
48+
title={ClusterMapListSortableTitle[cellName]}
49+
isSorted={sortBy === cellKey}
50+
sortOrder={sortOrder}
51+
isSortable
52+
disabled={false}
53+
triggerSorting={handleCellSorting(cellKey)}
54+
/>
55+
</React.Fragment>
56+
))}
57+
</div>
58+
{filteredList.map((clusterData) => (
59+
<ClusterListRow
60+
key={clusterData.id}
61+
clusterData={clusterData}
62+
clusterListLoader={clusterListLoader}
63+
showKubeConfigModal={showKubeConfigModal}
64+
onChangeShowKubeConfigModal={onChangeShowKubeConfigModal}
65+
setSelectedClusterName={setSelectedClusterName}
66+
/>
67+
))}
68+
</div>
69+
)
70+
}
71+
72+
export default ClusterList
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Link } from 'react-router-dom'
2+
3+
import {
4+
ALL_NAMESPACE_OPTION,
5+
BulkSelectionIdentifiersType,
6+
Button,
7+
ButtonComponentType,
8+
ButtonStyleType,
9+
ButtonVariantType,
10+
ClusterDetail,
11+
ClusterStatusType,
12+
ComponentSizeType,
13+
Icon,
14+
Tooltip,
15+
URLS,
16+
useBulkSelection,
17+
} from '@devtron-labs/devtron-fe-common-lib'
18+
19+
import { ReactComponent as Error } from '@Icons/ic-error-exclamation.svg'
20+
import { importComponentFromFELibrary } from '@Components/common'
21+
import { K8S_EMPTY_GROUP, SIDEBAR_KEYS } from '@Components/ResourceBrowser/Constants'
22+
import { AppDetailsTabs } from '@Components/v2/appDetails/appDetails.store'
23+
24+
import { ClusterMapInitialStatus } from '../ClusterMapInitialStatus'
25+
import { CLUSTER_PROD_TYPE } from '../constants'
26+
import { ClusterListRowTypes } from './types'
27+
28+
const CompareClusterButton = importComponentFromFELibrary('CompareClusterButton', null, 'function')
29+
const ClusterStatusCell = importComponentFromFELibrary('ClusterStatus', null, 'function')
30+
const KubeConfigButton = importComponentFromFELibrary('KubeConfigButton', null, 'function')
31+
const KubeConfigRowCheckbox = importComponentFromFELibrary('KubeConfigRowCheckbox', null, 'function')
32+
33+
const ClusterListRow = ({
34+
clusterData,
35+
clusterListLoader,
36+
onChangeShowKubeConfigModal,
37+
setSelectedClusterName,
38+
}: ClusterListRowTypes) => {
39+
const { selectedIdentifiers: bulkSelectionState, getSelectedIdentifiersCount } =
40+
useBulkSelection<BulkSelectionIdentifiersType<ClusterDetail>>()
41+
const errorCount = clusterData.nodeErrors ? Object.keys(clusterData.nodeErrors).length : 0
42+
const identifierCount = getSelectedIdentifiersCount()
43+
44+
const hideDataOnLoad = (value) => {
45+
if (clusterListLoader) {
46+
return null
47+
}
48+
return value
49+
}
50+
51+
const renderClusterStatus = ({ errorInNodeListing, status }: ClusterDetail) => {
52+
if (ClusterStatusCell && status) {
53+
return <ClusterStatusCell status={status} errorInNodeListing={errorInNodeListing} />
54+
}
55+
56+
return <ClusterMapInitialStatus errorInNodeListing={errorInNodeListing} />
57+
}
58+
const isIdentifierSelected = !!bulkSelectionState[clusterData.name]
59+
60+
// TODO: @Elessar1802 will be replacing all terminal url with new utils
61+
62+
return (
63+
<div
64+
key={`cluster-${clusterData.id}`}
65+
className={`cluster-list-row fw-4 cn-9 fs-13 dc__border-bottom-n1 py-12 px-20 hover-class dc__visible-hover dc__visible-hover--parent
66+
${clusterListLoader ? 'show-shimmer-loading dc__align-items-center' : ''}`}
67+
>
68+
{KubeConfigRowCheckbox && <KubeConfigRowCheckbox clusterData={clusterData} />}
69+
{!isIdentifierSelected && identifierCount === 0 && (
70+
<div className="dc__visible-hover--hide-child flex left">
71+
<Icon name="ic-bg-cluster" color={null} size={24} />
72+
</div>
73+
)}
74+
<div data-testid={`cluster-row-${clusterData.name}`} className="flex left dc__overflow-hidden">
75+
<Link
76+
className="dc__ellipsis-right dc__no-decor lh-24"
77+
to={`${URLS.RESOURCE_BROWSER}/${clusterData.id}/${ALL_NAMESPACE_OPTION.value}/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}`}
78+
>
79+
{clusterData.name}
80+
</Link>
81+
{/* NOTE: visible-hover plays with display prop; therefore need to set display: flex on a new div */}
82+
<div className="cursor dc__visible-hover--child ml-8">
83+
<div className="flexbox dc__align-items-center dc__gap-4">
84+
{!!clusterData.nodeCount && !clusterListLoader && (
85+
<Button
86+
icon={<Icon name="ic-terminal-fill" color={null} size={16} />}
87+
ariaLabel="View terminal"
88+
size={ComponentSizeType.xs}
89+
dataTestId={`cluster-terminal-${clusterData.name}`}
90+
style={ButtonStyleType.neutral}
91+
variant={ButtonVariantType.borderLess}
92+
component={ButtonComponentType.link}
93+
linkProps={{
94+
to: `${URLS.RESOURCE_BROWSER}/${clusterData.id}/${ALL_NAMESPACE_OPTION.value}/${AppDetailsTabs.terminal}/${K8S_EMPTY_GROUP}`,
95+
}}
96+
/>
97+
)}
98+
{CompareClusterButton && clusterData.status !== ClusterStatusType.CONNECTION_FAILED && (
99+
<CompareClusterButton sourceClusterId={clusterData.id} isIconButton />
100+
)}
101+
{KubeConfigButton && (
102+
<KubeConfigButton
103+
onChangeShowKubeConfigModal={onChangeShowKubeConfigModal}
104+
clusterName={clusterData.name}
105+
setSelectedClusterName={setSelectedClusterName}
106+
/>
107+
)}
108+
</div>
109+
</div>
110+
</div>
111+
{renderClusterStatus(clusterData)}
112+
<div className="child-shimmer-loading">
113+
{hideDataOnLoad(clusterData.isProd ? CLUSTER_PROD_TYPE.PRODUCTION : CLUSTER_PROD_TYPE.NON_PRODUCTION)}
114+
</div>
115+
<div className="child-shimmer-loading">{hideDataOnLoad(clusterData.nodeCount)}</div>
116+
<div className="child-shimmer-loading">
117+
{errorCount > 0 &&
118+
hideDataOnLoad(
119+
<>
120+
<Error className="mr-3 icon-dim-16 dc__position-rel top-3" />
121+
<span className="cr-5">{errorCount}</span>
122+
</>,
123+
)}
124+
</div>
125+
<div className="flexbox child-shimmer-loading">
126+
{hideDataOnLoad(
127+
<Tooltip content={clusterData.serverVersion}>
128+
<span className="dc__truncate">{clusterData.serverVersion}</span>
129+
</Tooltip>,
130+
)}
131+
</div>
132+
<div className="child-shimmer-loading">{hideDataOnLoad(clusterData.cpu?.capacity)}</div>
133+
<div className="child-shimmer-loading">{hideDataOnLoad(clusterData.memory?.capacity)}</div>
134+
</div>
135+
)
136+
}
137+
138+
export default ClusterListRow
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { useMemo, useState } from 'react'
2+
import dayjs, { Dayjs } from 'dayjs'
3+
4+
import {
5+
BulkSelectionEvents,
6+
BulkSelectionIdentifiersType,
7+
BulkSelectionProvider,
8+
Button,
9+
ButtonVariantType,
10+
ClusterDetail,
11+
ClusterFiltersType,
12+
SearchBar,
13+
SelectAllDialogStatus,
14+
useBulkSelection,
15+
useUrlFilters,
16+
} from '@devtron-labs/devtron-fe-common-lib'
17+
18+
import { importComponentFromFELibrary } from '@Components/common'
19+
import Timer from '@Components/common/DynamicTabs/DynamicTabs.timer'
20+
21+
import { ClusterMapListSortableKeys, ClusterStatusByFilter } from '../constants'
22+
import { getSortedClusterList, parseSearchParams } from '../utils'
23+
import ClusterSelectionBody from './ClusterSelectionBody'
24+
import { ClusterViewType } from './types'
25+
26+
const ClusterFilters = importComponentFromFELibrary('ClusterFilters', null, 'function')
27+
28+
const getSelectAllDialogStatus = () => SelectAllDialogStatus.CLOSED
29+
30+
const ClusterListView = (props: ClusterViewType) => {
31+
const [lastSyncTime, setLastSyncTime] = useState<Dayjs>(dayjs())
32+
33+
const { searchKey, clusterFilter, updateSearchParams, handleSearch, sortBy, sortOrder } = useUrlFilters<
34+
ClusterMapListSortableKeys,
35+
{ clusterFilter: ClusterFiltersType }
36+
>({
37+
parseSearchParams,
38+
initialSortKey: ClusterMapListSortableKeys.CLUSTER_NAME,
39+
})
40+
const { handleBulkSelection } = useBulkSelection<BulkSelectionIdentifiersType<ClusterDetail>>()
41+
42+
const { clusterOptions, initialLoading, clusterListLoader, refreshData } = props
43+
44+
const setClusterFilter = (_clusterFilter: ClusterFiltersType) => {
45+
updateSearchParams({ clusterFilter: _clusterFilter })
46+
}
47+
48+
const filteredList: ClusterDetail[] = useMemo(() => {
49+
const loweredSearchKey = searchKey.toLowerCase()
50+
const updatedClusterOptions = [...clusterOptions]
51+
// Sort the cluster list based on the selected sorting key
52+
getSortedClusterList(updatedClusterOptions, sortBy, sortOrder)
53+
54+
// Filter the cluster list based on the search key and cluster filter
55+
return updatedClusterOptions.filter((option) => {
56+
const filterCondition =
57+
clusterFilter === ClusterFiltersType.ALL_CLUSTERS ||
58+
!option.status ||
59+
option.status === ClusterStatusByFilter[clusterFilter]
60+
61+
return (!searchKey || option.name.toLowerCase().includes(loweredSearchKey)) && filterCondition
62+
})
63+
}, [searchKey, clusterOptions, `${clusterFilter}`, sortBy, sortOrder])
64+
65+
const handleFilterKeyPress = (value: string) => {
66+
handleSearch(value)
67+
}
68+
69+
const allOnThisPageIdentifiers = useMemo(
70+
() =>
71+
filteredList?.reduce((acc, cluster) => {
72+
acc[cluster.name] = cluster
73+
return acc
74+
}, {} as ClusterDetail) ?? {},
75+
[filteredList],
76+
)
77+
78+
const handleClearBulkSelection = () => {
79+
handleBulkSelection({
80+
action: BulkSelectionEvents.CLEAR_ALL_SELECTIONS,
81+
})
82+
}
83+
84+
const handleRefresh = () => {
85+
refreshData()
86+
setLastSyncTime(dayjs())
87+
handleClearBulkSelection()
88+
}
89+
90+
return (
91+
<BulkSelectionProvider<BulkSelectionIdentifiersType<ClusterDetail>>
92+
identifiers={allOnThisPageIdentifiers}
93+
getSelectAllDialogStatus={getSelectAllDialogStatus}
94+
>
95+
<div className="flexbox dc__content-space pl-20 pr-20 pt-16 pb-16 dc__zi-4">
96+
<div className="flex dc__gap-12">
97+
<SearchBar
98+
initialSearchText={searchKey}
99+
handleEnter={handleFilterKeyPress}
100+
containerClassName="w-250-imp"
101+
inputProps={{
102+
placeholder: 'Search clusters',
103+
autoFocus: true,
104+
disabled: initialLoading,
105+
}}
106+
/>
107+
{ClusterFilters && (
108+
<ClusterFilters clusterFilter={clusterFilter} setClusterFilter={setClusterFilter} />
109+
)}
110+
</div>
111+
{clusterListLoader ? (
112+
<span className="dc__loading-dots mr-20">Syncing</span>
113+
) : (
114+
<div className="flex left dc__gap-8">
115+
<span>
116+
Last refreshed&nbsp;
117+
<Timer start={lastSyncTime} />
118+
&nbsp;ago
119+
</span>
120+
<Button
121+
text="Refresh"
122+
dataTestId="cluster-list-refresh-button"
123+
onClick={handleRefresh}
124+
variant={ButtonVariantType.text}
125+
/>
126+
</div>
127+
)}
128+
</div>
129+
130+
<ClusterSelectionBody {...props} filteredList={filteredList} />
131+
</BulkSelectionProvider>
132+
)
133+
}
134+
135+
export default ClusterListView

0 commit comments

Comments
 (0)