Skip to content

Commit 526be24

Browse files
Merge pull request #2808 from devtron-labs/feat/tree-view
feat: replace resource browser sidebar by tree view
2 parents f694d02 + 05c0a79 commit 526be24

File tree

10 files changed

+167
-324
lines changed

10 files changed

+167
-324
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.17.0-pre-1",
7+
"@devtron-labs/devtron-fe-common-lib": "1.17.0-pre-5",
88
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
99
"@rjsf/core": "^5.13.3",
1010
"@rjsf/utils": "^5.13.3",

src/components/ResourceBrowser/ResourceList/ResourceList.component.tsx

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@
1717
import React, { MouseEventHandler, useEffect, useState } from 'react'
1818
import { ClearIndicatorProps, components, ValueContainerProps } from 'react-select'
1919

20-
import { Button, ButtonVariantType, ComponentSizeType, Tooltip } from '@devtron-labs/devtron-fe-common-lib'
20+
import { Button, ButtonVariantType, ComponentSizeType } from '@devtron-labs/devtron-fe-common-lib'
2121

2222
import { ReactComponent as ClearIcon } from '@Icons/ic-error.svg'
2323
import { ReactComponent as SearchIcon } from '@Icons/ic-search.svg'
2424
import { ReactComponent as Warning } from '@Icons/ic-warning.svg'
2525

2626
import { handleUTCTime } from '../../common'
2727
import { ShortcutKeyBadge } from '../../common/formFields/Widgets/Widgets'
28-
import { SidebarChildButtonPropsType } from '../Types'
2928

3029
export const KindSearchValueContainer = (props: ValueContainerProps) => {
3130
const { selectProps, children } = props
@@ -96,38 +95,3 @@ export const renderRefreshBar =
9695
(show: boolean, lastSyncTime: string, callback: () => void): (() => JSX.Element) =>
9796
() =>
9897
!show ? null : <WarningStrip lastSyncTime={lastSyncTime} callback={callback} />
99-
100-
export const SidebarChildButton: React.FC<SidebarChildButtonPropsType> = ({
101-
parentRef,
102-
group,
103-
version,
104-
text,
105-
kind,
106-
namespaced,
107-
isSelected,
108-
onClick,
109-
}) => (
110-
<button
111-
type="button"
112-
className="dc__unset-button-styles"
113-
key={text}
114-
ref={parentRef}
115-
data-group={group}
116-
data-version={version}
117-
data-kind={kind}
118-
data-namespaced={namespaced}
119-
data-selected={isSelected}
120-
onClick={onClick}
121-
aria-label={`Select ${text}`}
122-
>
123-
<Tooltip content={text} placement="right">
124-
<div
125-
className={`fs-13 pointer dc__ellipsis-right dc__align-left dc__border-radius-4-imp fw-4 pt-6 lh-20 pr-8 pb-6 pl-8 ${
126-
isSelected ? 'bcb-1 cb-5' : 'cn-9 dc__hover-n50'
127-
}`}
128-
>
129-
{text}
130-
</div>
131-
</Tooltip>
132-
</button>
133-
)

src/components/ResourceBrowser/ResourceList/Sidebar.tsx

Lines changed: 30 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,32 @@
1414
* limitations under the License.
1515
*/
1616

17-
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'
17+
import React, { useEffect, useMemo, useRef, useState } from 'react'
1818
import { generatePath, useHistory, useLocation, useParams } from 'react-router-dom'
1919
import ReactSelect, { GroupBase, InputActionMeta } from 'react-select'
2020
import Select, { FormatOptionLabelMeta } from 'react-select/base'
2121
import DOMPurify from 'dompurify'
2222

2323
import {
24-
ApiResourceGroupType,
24+
capitalizeFirstLetter,
2525
highlightSearchText,
2626
K8S_EMPTY_GROUP,
2727
Nodes,
28+
NodeType,
2829
ReactSelectInputAction,
2930
RESOURCE_BROWSER_ROUTES,
31+
TreeNode,
32+
TreeView,
3033
URL_FILTER_KEYS,
3134
useRegisterShortcut,
3235
} from '@devtron-labs/devtron-fe-common-lib'
3336

34-
import { ReactComponent as ICExpand } from '../../../assets/icons/ic-expand.svg'
35-
import { AggregationKeys } from '../../app/types'
36-
import { KIND_SEARCH_COMMON_STYLES, ResourceBrowserTabsId, SIDEBAR_KEYS } from '../Constants'
37-
import { K8SObjectChildMapType, K8SObjectMapType, K8sObjectOptionType, SidebarType } from '../Types'
38-
import {
39-
convertK8sObjectMapToOptionsList,
40-
convertResourceGroupListToK8sObjectList,
41-
getK8SObjectMapAfterGroupHeadingClick,
42-
} from '../Utils'
43-
import { KindSearchClearIndicator, KindSearchValueContainer, SidebarChildButton } from './ResourceList.component'
37+
import { KIND_SEARCH_COMMON_STYLES, ResourceBrowserTabsId } from '../Constants'
38+
import { K8sObjectOptionType, RBResourceSidebarDataAttributeType, SidebarType } from '../Types'
39+
import { convertK8sObjectMapToOptionsList, convertResourceGroupListToK8sObjectList } from '../Utils'
40+
import { KindSearchClearIndicator, KindSearchValueContainer } from './ResourceList.component'
4441
import { K8sResourceListURLParams } from './types'
42+
import { getRBSidebarTreeViewNodeId, getRBSidebarTreeViewNodes } from './utils'
4543

4644
const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateTabLastSyncMoment }: SidebarType) => {
4745
const { registerShortcut, unregisterShortcut } = useRegisterShortcut()
@@ -50,8 +48,7 @@ const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateT
5048
const { clusterId, kind, group } = useParams<K8sResourceListURLParams>()
5149
const [searchText, setSearchText] = useState('')
5250
/* NOTE: apiResources prop will only change after a component mount/dismount */
53-
const [list, setList] = useState(convertResourceGroupListToK8sObjectList(apiResources || null, kind))
54-
const preventScrollRef = useRef(false)
51+
const list = convertResourceGroupListToK8sObjectList(apiResources || null, kind)
5552
const searchInputRef = useRef<Select<K8sObjectOptionType, false, GroupBase<K8sObjectOptionType>>>(null)
5653
const k8sObjectOptionsList = useMemo(
5754
() => convertK8sObjectMapToOptionsList(convertResourceGroupListToK8sObjectList(apiResources || null, kind)),
@@ -96,62 +93,29 @@ const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateT
9693
}
9794
}, [])
9895

99-
const getGroupHeadingClickHandler =
100-
(preventCollapse = false, preventScroll = false) =>
101-
(e: React.MouseEvent<HTMLButtonElement> | { currentTarget: { dataset: { groupName: string } } }) => {
102-
preventScrollRef.current = preventScroll
103-
setList(getK8SObjectMapAfterGroupHeadingClick(e, list, preventCollapse))
104-
}
105-
106-
const selectNode = (
107-
e: React.MouseEvent<HTMLButtonElement> | { currentTarget: Pick<K8sObjectOptionType, 'dataset'> },
108-
groupName?: string,
109-
): void => {
110-
const _selectedKind = e.currentTarget.dataset.kind.toLowerCase()
111-
const _selectedGroup = e.currentTarget.dataset.group.toLowerCase()
112-
96+
const selectNode = (selectedKind: string, selectedGroup: string): void => {
11397
const params = new URLSearchParams(location.search)
11498
params.delete(URL_FILTER_KEYS.PAGE_NUMBER)
11599
params.delete(URL_FILTER_KEYS.SORT_BY)
116100
params.delete(URL_FILTER_KEYS.SORT_ORDER)
117-
if (_selectedKind !== Nodes.Event.toLowerCase()) {
101+
if (selectedKind !== Nodes.Event.toLowerCase()) {
118102
params.delete('eventType')
119103
}
120104
const path = generatePath(RESOURCE_BROWSER_ROUTES.K8S_RESOURCE_LIST, {
121105
clusterId,
122-
kind: _selectedKind,
123-
group: _selectedGroup || K8S_EMPTY_GROUP,
106+
kind: selectedKind,
107+
group: selectedGroup || K8S_EMPTY_GROUP,
124108
})
125109

126110
if (path === location.pathname) {
127111
return
128112
}
129113

130114
const _url = `${path}?${params.toString()}`
131-
132-
updateK8sResourceTab({ url: _url, dynamicTitle: e.currentTarget.dataset.kind, retainSearchParams: true })
115+
updateK8sResourceTab({ url: _url, dynamicTitle: capitalizeFirstLetter(selectedKind), retainSearchParams: true })
133116
updateTabLastSyncMoment(ResourceBrowserTabsId.k8s_Resources)
134117

135118
push(_url)
136-
137-
/**
138-
* If groupName present then kind selection is from search dropdown,
139-
* - Expand parent group if not already expanded
140-
* - Auto scroll to selection
141-
* Else reset prevent scroll to true
142-
*/
143-
if (groupName) {
144-
getGroupHeadingClickHandler(
145-
true,
146-
false,
147-
)({
148-
currentTarget: {
149-
dataset: {
150-
groupName,
151-
},
152-
},
153-
})
154-
}
155119
}
156120

157121
useEffect(() => {
@@ -169,89 +133,9 @@ const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateT
169133
(option.dataset.group || K8S_EMPTY_GROUP).toLowerCase() === lowercasedGroup,
170134
) ?? k8sObjectOptionsList[0]
171135
/* NOTE: if nodeType doesn't match the selectedResource kind, set it accordingly */
172-
selectNode(
173-
{
174-
currentTarget: {
175-
dataset: match.dataset,
176-
},
177-
},
178-
match.groupName,
179-
)
136+
selectNode(match.dataset.kind.toLowerCase(), match.dataset.group.toLowerCase())
180137
}, [kind, group, k8sObjectOptionsList])
181138

182-
const selectedChildRef: React.Ref<HTMLButtonElement> = (node) => {
183-
/**
184-
* NOTE: all list items will be passed this ref callback
185-
* The correct node will get scrolled into view */
186-
if (node?.dataset.selected !== 'true' || preventScrollRef.current) {
187-
return
188-
}
189-
node?.scrollIntoView({ block: 'center' })
190-
}
191-
192-
const renderChild = (childData: ApiResourceGroupType, useGroupName = false) => {
193-
const nodeName = useGroupName && childData.gvk.Group ? childData.gvk.Group : childData.gvk.Kind
194-
const isSelected =
195-
useGroupName && childData.gvk.Group
196-
? selectedResource?.gvk?.Group === childData.gvk.Group &&
197-
selectedResource?.gvk?.Kind === childData.gvk.Kind
198-
: selectedResource?.gvk?.Kind === childData.gvk.Kind &&
199-
(selectedResource?.gvk?.Group === childData.gvk.Group ||
200-
selectedResource?.gvk?.Group === K8S_EMPTY_GROUP)
201-
return (
202-
<SidebarChildButton
203-
parentRef={selectedChildRef}
204-
text={nodeName}
205-
group={childData.gvk.Group}
206-
version={childData.gvk.Version}
207-
kind={childData.gvk.Kind}
208-
namespaced={childData.namespaced}
209-
isSelected={isSelected}
210-
onClick={selectNode}
211-
/>
212-
)
213-
}
214-
215-
const renderK8sResourceChildren = (key: string, value: K8SObjectChildMapType, k8sObject: K8SObjectMapType) => {
216-
const keyLowerCased = key.toLowerCase()
217-
if (
218-
keyLowerCased === 'node' ||
219-
keyLowerCased === SIDEBAR_KEYS.namespaceGVK.Kind.toLowerCase() ||
220-
keyLowerCased === SIDEBAR_KEYS.eventGVK.Kind.toLowerCase()
221-
) {
222-
return null
223-
}
224-
if (value.data.length === 1) {
225-
return renderChild(value.data[0])
226-
}
227-
return (
228-
<Fragment key={`${k8sObject.name}/${key}-child`}>
229-
<button
230-
type="button"
231-
className="dc__unset-button-styles"
232-
data-group-name={`${k8sObject.name}/${key}`}
233-
onClick={getGroupHeadingClickHandler(false, true) as React.MouseEventHandler<HTMLButtonElement>}
234-
>
235-
<div className="flex pointer dc__align-left">
236-
<ICExpand
237-
className={`${value.isExpanded ? 'fcn-9' : 'fcn-5'} rotate icon-dim-24 pointer`}
238-
style={{
239-
['--rotateBy' as string]: value.isExpanded ? '90deg' : '0deg',
240-
}}
241-
/>
242-
<span className="fs-13 cn-9 fw-6 pointer w-100 pt-6 pb-6">{key}</span>
243-
</div>
244-
</button>
245-
<div className="pl-20 flexbox-col">
246-
{value.isExpanded &&
247-
value.data.map((_child) => (
248-
<React.Fragment key={_child.gvk.Group}>{renderChild(_child, true)}</React.Fragment>
249-
))}
250-
</div>
251-
</Fragment>
252-
)
253-
}
254-
255139
const handleInputChange = (newValue: string, actionMeta: InputActionMeta): void => {
256140
if (actionMeta.action !== ReactSelectInputAction.inputChange) {
257141
return
@@ -268,14 +152,11 @@ const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateT
268152
if (!option) {
269153
return
270154
}
271-
selectNode(
272-
{
273-
currentTarget: {
274-
dataset: option.dataset,
275-
},
276-
},
277-
option.groupName,
278-
)
155+
selectNode(option.dataset.kind.toLowerCase(), option.dataset.group.toLowerCase())
156+
}
157+
158+
const handleTreeViewNodeSelect = (node: TreeNode<RBResourceSidebarDataAttributeType>): void => {
159+
selectNode(node.dataAttributes?.['data-kind'], node.dataAttributes?.['data-group'])
279160
}
280161

281162
const formatOptionLabel = (
@@ -315,6 +196,8 @@ const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateT
315196

316197
const noOptionsMessage = () => 'No matching kind'
317198

199+
const treeViewNodes = getRBSidebarTreeViewNodes(list)
200+
318201
return (
319202
<div className="w-250 dc__no-shrink dc__overflow-hidden flexbox-col">
320203
<div className="k8s-object-kind-search bg__primary pt-16 pb-8 px-10 w-100 cursor">
@@ -348,82 +231,13 @@ const Sidebar = ({ apiResources, selectedResource, updateK8sResourceTab, updateT
348231
</div>
349232

350233
<div className="dc__overflow-auto flexbox-col flex-grow-1 dc__border-top-n1 p-8 dc__user-select-none">
351-
<div className="pb-8 flexbox-col">
352-
{!!list?.size && !!list.get(AggregationKeys.Nodes) && (
353-
<SidebarChildButton
354-
parentRef={selectedChildRef}
355-
text={SIDEBAR_KEYS.nodes}
356-
group={SIDEBAR_KEYS.nodeGVK.Group}
357-
version={SIDEBAR_KEYS.nodeGVK.Version}
358-
kind={SIDEBAR_KEYS.nodeGVK.Kind}
359-
namespaced={false}
360-
isSelected={kind === SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}
361-
onClick={selectNode}
362-
/>
363-
)}
364-
{!!list?.size && !!list.get(AggregationKeys.Events) && (
365-
<SidebarChildButton
366-
parentRef={selectedChildRef}
367-
text={SIDEBAR_KEYS.events}
368-
group={SIDEBAR_KEYS.eventGVK.Group}
369-
version={SIDEBAR_KEYS.eventGVK.Version}
370-
kind={SIDEBAR_KEYS.eventGVK.Kind}
371-
namespaced
372-
isSelected={kind === SIDEBAR_KEYS.eventGVK.Kind.toLowerCase()}
373-
onClick={selectNode}
374-
/>
375-
)}
376-
{!!list?.size && !!list.get(AggregationKeys.Namespaces) && (
377-
<SidebarChildButton
378-
parentRef={selectedChildRef}
379-
text={SIDEBAR_KEYS.namespaces}
380-
group={SIDEBAR_KEYS.namespaceGVK.Group}
381-
version={SIDEBAR_KEYS.namespaceGVK.Version}
382-
kind={SIDEBAR_KEYS.namespaceGVK.Kind}
383-
namespaced={false}
384-
isSelected={kind === SIDEBAR_KEYS.namespaceGVK.Kind.toLowerCase()}
385-
onClick={selectNode}
386-
/>
387-
)}
388-
</div>
389-
{!!list?.size &&
390-
[...list.values()].map((k8sObject) =>
391-
k8sObject.name === AggregationKeys.Events ||
392-
k8sObject.name === AggregationKeys.Namespaces ||
393-
k8sObject.name === AggregationKeys.Nodes ? null : (
394-
<div key={`${k8sObject.name}-parent`}>
395-
<button
396-
type="button"
397-
className={`dc__unset-button-styles dc__zi-1 bg__primary w-100 ${k8sObject.isExpanded ? 'dc__position-sticky' : ''}`}
398-
style={{ top: '-8px' }}
399-
data-group-name={k8sObject.name}
400-
onClick={getGroupHeadingClickHandler(false, true)}
401-
>
402-
<div className="flex pointer dc__align-left">
403-
<ICExpand
404-
className={`${k8sObject.isExpanded ? 'fcn-9' : 'fcn-5'} rotate icon-dim-24 pointer`}
405-
style={{
406-
['--rotateBy' as string]: !k8sObject.isExpanded ? '0deg' : '90deg',
407-
}}
408-
/>
409-
<span
410-
className="fs-13 cn-9 fw-6 pointer w-100 pt-6 pb-6"
411-
data-testid={`k8sObject-${k8sObject.name}`}
412-
>
413-
{k8sObject.name}
414-
</span>
415-
</div>
416-
</button>
417-
{k8sObject.isExpanded && (
418-
<div className="pl-20 flexbox-col">
419-
{[...k8sObject.child.entries()].map(([key, value]) =>
420-
renderK8sResourceChildren(key, value, k8sObject),
421-
)}
422-
</div>
423-
)}
424-
</div>
425-
),
234+
<TreeView<RBResourceSidebarDataAttributeType>
235+
nodes={treeViewNodes}
236+
selectedId={getRBSidebarTreeViewNodeId(
237+
selectedResource?.gvk || { Group: '', Version: '', Kind: '' as NodeType },
426238
)}
239+
onSelect={handleTreeViewNodeSelect}
240+
/>
427241
</div>
428242
</div>
429243
)

0 commit comments

Comments
 (0)