Skip to content

Commit 908b905

Browse files
authored
Merge pull request #377 from devtron-labs/feat/resizable-columns
feat: add support for resizable columns in resource browser
2 parents fbd1c9d + 0d92225 commit 908b905

File tree

8 files changed

+205
-40
lines changed

8 files changed

+205
-40
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "0.6.3",
3+
"version": "0.6.4",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
*/
1616

1717
import { Tooltip } from '@Common/Tooltip'
18-
import { ReactComponent as SortIcon } from '../../Assets/Icon/ic-arrow-up-down.svg'
19-
import { ReactComponent as SortArrowDown } from '../../Assets/Icon/ic-sort-arrow-down.svg'
18+
import Draggable, { DraggableProps } from 'react-draggable'
19+
import { ReactComponent as SortIcon } from '@Icons/ic-arrow-up-down.svg'
20+
import { ReactComponent as SortArrowDown } from '@Icons/ic-sort-arrow-down.svg'
2021
import { SortingOrder } from '../Constants'
2122
import { noop } from '../Helper'
2223
import { SortableTableHeaderCellProps } from './types'
24+
import './sortableTableHeaderCell.scss'
2325

2426
/**
2527
* Reusable component for the table header cell with support for sorting icons
@@ -34,6 +36,23 @@ import { SortableTableHeaderCellProps } from './types'
3436
* disabled={isDisabled}
3537
* />
3638
* ```
39+
*
40+
* @example Non-sortable cell
41+
* ```tsx
42+
* <SortableTableHeaderCell
43+
* isSortable={false}
44+
* title="Header Cell"
45+
* />
46+
* ```
47+
*
48+
* * @example Resizable cell (Layout to be controlled externally using useResizableTableConfig)
49+
* ```tsx
50+
* <SortableTableHeaderCell
51+
* isSortable={false}
52+
* isResizable
53+
* title="Header Cell"
54+
* />
55+
* ```
3756
*/
3857
const SortableTableHeaderCell = ({
3958
isSorted,
@@ -43,7 +62,12 @@ const SortableTableHeaderCell = ({
4362
disabled,
4463
isSortable = true,
4564
showTippyOnTruncate = false,
65+
id,
66+
handleResize,
67+
isResizable,
4668
}: SortableTableHeaderCellProps) => {
69+
const isCellResizable = !!(isResizable && id && handleResize)
70+
4771
const renderSortIcon = () => {
4872
if (!isSortable) {
4973
return null
@@ -60,18 +84,46 @@ const SortableTableHeaderCell = ({
6084
return <SortIcon className="icon-dim-12 mw-12 scn-7 dc__no-shrink" />
6185
}
6286

87+
const handleDrag: DraggableProps['onDrag'] = (_, data) => {
88+
if (isCellResizable) {
89+
handleResize(id, data.deltaX)
90+
}
91+
}
92+
6393
return (
64-
<button
65-
type="button"
66-
className={`dc__transparent p-0 cn-7 flex dc__content-start dc__gap-4 dc__select-text ${!isSortable ? 'cursor-default' : ''}`}
67-
onClick={isSortable ? triggerSorting : noop}
68-
disabled={disabled}
69-
>
70-
<Tooltip showOnTruncate={showTippyOnTruncate} content={title}>
71-
<span className="dc__uppercase dc__ellipsis-right">{title}</span>
72-
</Tooltip>
73-
{renderSortIcon()}
74-
</button>
94+
<div className="flex dc__content-space dc__gap-6 dc__position-rel">
95+
<button
96+
type="button"
97+
className={`dc__transparent p-0 cn-7 flex dc__content-start dc__gap-4 dc__select-text ${!isSortable ? 'cursor-default' : ''} dc__position-rel`}
98+
onClick={isSortable ? triggerSorting : noop}
99+
disabled={disabled}
100+
>
101+
<Tooltip showOnTruncate={showTippyOnTruncate} content={title}>
102+
<span className="dc__uppercase dc__truncate">{title}</span>
103+
</Tooltip>
104+
{renderSortIcon()}
105+
</button>
106+
{isCellResizable && (
107+
<Draggable
108+
handle=".sortable-table-header__resize-btn"
109+
defaultClassNameDragging="sortable-table-header__resize-btn--dragging"
110+
position={{
111+
x: 0,
112+
y: 0,
113+
}}
114+
axis="none"
115+
onDrag={handleDrag}
116+
bounds={{
117+
top: 0,
118+
bottom: 0,
119+
}}
120+
>
121+
<div className="sortable-table-header__resize-btn flex h-100 dc__no-shrink px-2 dc__position-abs dc__cursor-col-resize dc__right-3--neg">
122+
<div className="dc__divider h-16" />
123+
</div>
124+
</Draggable>
125+
)}
126+
</div>
75127
)
76128
}
77129

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const DEFAULT_MINIMUM_HEADER_WIDTH = 70
2+
3+
export const DEFAULT_MAXIMUM_HEADER_WIDTH = 600

src/Common/SortableTableHeaderCell/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
*/
1616

1717
export { default as SortableTableHeaderCell } from './SortableTableHeaderCell'
18+
export { default as useResizableTableConfig } from './useResizableTableConfig'
1819
export type { SortableTableHeaderCellProps } from './types'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.sortable-table-header {
2+
&__resize-btn {
3+
> div {
4+
transition: all 0.1s ease-out;
5+
}
6+
7+
&:hover, &--dragging {
8+
> div {
9+
height: 100% !important;
10+
background-color: var(--B500);
11+
}
12+
}
13+
}
14+
}

src/Common/SortableTableHeaderCell/types.ts

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,78 @@
1616

1717
import { SortingOrder } from '../Constants'
1818

19-
export interface SortableTableHeaderCellProps {
20-
/**
21-
* If true, the cell is sorted
22-
*/
23-
isSorted: boolean
24-
/**
25-
* Callback for handling the sorting of the cell
26-
*/
27-
triggerSorting: () => void
28-
/**
29-
* Current sort order
30-
*
31-
* Note: On click, the sort order should be updated as required
32-
*/
33-
sortOrder: SortingOrder
19+
export type SortableTableHeaderCellProps = {
3420
/**
3521
* Label for the cell
3622
*/
3723
title: string
38-
/**
39-
* If true, the cell is disabled
40-
*/
41-
disabled: boolean
42-
/**
43-
* If false, the cell acts like normal table header cell
44-
* @default true
45-
*/
46-
isSortable?: boolean
4724
/**
4825
* If true, the tippy is shown on Sortable header if text is truncated
4926
* @default false
5027
*/
5128
showTippyOnTruncate?: boolean
29+
} & (
30+
| {
31+
/**
32+
* Unique identifier for the column
33+
*/
34+
id: string | number
35+
/**
36+
* If true, the cell is resizable
37+
*
38+
* @default false
39+
*/
40+
isResizable: true | boolean
41+
/**
42+
* Resize handler for the table
43+
*/
44+
handleResize: (id: string | number, deltaChange: number) => void
45+
}
46+
| {
47+
id?: never
48+
isResizable?: false | undefined
49+
handleResize?: never
50+
}
51+
) &
52+
(
53+
| {
54+
/**
55+
* If false, the cell acts like normal table header cell
56+
* @default true
57+
*/
58+
isSortable?: boolean | undefined
59+
/**
60+
* If true, the cell is disabled
61+
*/
62+
disabled: boolean
63+
/**
64+
* If true, the cell is sorted
65+
*/
66+
isSorted: boolean
67+
/**
68+
* Callback for handling the sorting of the cell
69+
*/
70+
triggerSorting: () => void
71+
/**
72+
* Current sort order
73+
*
74+
* Note: On click, the sort order should be updated as required
75+
*/
76+
sortOrder: SortingOrder
77+
}
78+
| {
79+
isSortable: false
80+
disabled?: never
81+
isSorted?: never
82+
triggerSorting?: never
83+
sortOrder?: never
84+
}
85+
)
86+
87+
export interface UseResizableTableConfigProps {
88+
headersConfig: (Pick<SortableTableHeaderCellProps, 'id'> & {
89+
width: number | string
90+
maxWidth?: number
91+
minWidth?: number
92+
})[]
5293
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useEffect, useState } from 'react'
2+
import { UseResizableTableConfigProps } from './types'
3+
import { DEFAULT_MAXIMUM_HEADER_WIDTH, DEFAULT_MINIMUM_HEADER_WIDTH } from './constants'
4+
5+
const useResizableTableConfig = ({ headersConfig }: UseResizableTableConfigProps) => {
6+
const [headerDimensionsConfig, setHeaderDimensionsConfig] = useState<
7+
UseResizableTableConfigProps['headersConfig'][number]['width'][]
8+
>([])
9+
10+
useEffect(() => {
11+
setHeaderDimensionsConfig(headersConfig.map((config) => config.width))
12+
}, [JSON.stringify(headersConfig)])
13+
14+
const handleResize = (
15+
headerCellId: UseResizableTableConfigProps['headersConfig'][number]['id'],
16+
deltaChange: number,
17+
) => {
18+
const headerCellIndexInConfig = headersConfig.findIndex((config) => config.id === headerCellId)
19+
20+
if (headerCellIndexInConfig < 0) {
21+
return
22+
}
23+
24+
setHeaderDimensionsConfig((prev) => {
25+
const updatedHeaderDimensionsConfig = structuredClone(prev)
26+
// Only numbers are supported for v1
27+
if (typeof updatedHeaderDimensionsConfig[headerCellIndexInConfig] !== 'number') {
28+
return prev
29+
}
30+
31+
const updatedCellDimension = updatedHeaderDimensionsConfig[headerCellIndexInConfig] + deltaChange
32+
const currentHeaderCellConfig = headersConfig[headerCellIndexInConfig]
33+
34+
if (
35+
updatedCellDimension < (currentHeaderCellConfig.minWidth ?? DEFAULT_MINIMUM_HEADER_WIDTH) ||
36+
updatedCellDimension > (currentHeaderCellConfig.maxWidth ?? DEFAULT_MAXIMUM_HEADER_WIDTH)
37+
) {
38+
return prev
39+
}
40+
41+
updatedHeaderDimensionsConfig[headerCellIndexInConfig] = updatedCellDimension
42+
return updatedHeaderDimensionsConfig
43+
})
44+
}
45+
46+
return {
47+
gridTemplateColumns: headerDimensionsConfig
48+
.map((config) => (typeof config === 'number' ? `${config}px` : config))
49+
.join(' '),
50+
handleResize,
51+
}
52+
}
53+
54+
export default useResizableTableConfig

0 commit comments

Comments
 (0)