Skip to content

Commit f3b99b7

Browse files
authored
Merge pull request #2004 from devtron-labs/refactor/application-group-overview-table
refactor: Application Group Overview Table Revamp, feat: create EnvironmentOverviewTable
2 parents 5ef4f5b + de90346 commit f3b99b7

14 files changed

+578
-314
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const EnvironmentOverviewTableHeaderFixedKeys = {
2+
STATUS: 'status',
3+
NAME: 'name',
4+
} as const
5+
6+
export const EnvironmentOverviewTableHeaderVariableKeys = {
7+
DEPLOYMENT_STATUS: 'deploymentStatus',
8+
LAST_DEPLOYED_IMAGE: 'lastDeployedImage',
9+
COMMITS: 'commits',
10+
DEPLOYED_AT: 'deployedAt',
11+
DEPLOYED_BY: 'deployedBy',
12+
} as const
13+
14+
export const EnvironmentOverviewTableHeaderKeys = {
15+
...EnvironmentOverviewTableHeaderFixedKeys,
16+
...EnvironmentOverviewTableHeaderVariableKeys,
17+
} as const
18+
19+
export const EnvironmentOverviewTableSortableKeys = (({ NAME, DEPLOYED_AT }) => ({ NAME, DEPLOYED_AT }))(
20+
EnvironmentOverviewTableHeaderKeys,
21+
)
22+
23+
export const EnvironmentOverviewTableHeaderValues: Record<keyof typeof EnvironmentOverviewTableHeaderKeys, string> = {
24+
NAME: 'APPLICATION',
25+
DEPLOYMENT_STATUS: 'DEPLOYMENT STATUS',
26+
LAST_DEPLOYED_IMAGE: 'LAST DEPLOYED IMAGE',
27+
COMMITS: 'COMMIT',
28+
DEPLOYED_AT: 'DEPLOYED AT',
29+
DEPLOYED_BY: 'DEPLOYED BY',
30+
STATUS: null,
31+
}
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { ChangeEvent, Fragment, useMemo, useState } from 'react'
2+
import { Link } from 'react-router-dom'
3+
4+
import {
5+
AppStatus,
6+
Checkbox,
7+
ImageChipCell,
8+
SortableTableHeaderCell,
9+
useUrlFilters,
10+
RegistryType,
11+
CommitChipCell,
12+
getRandomColor,
13+
processDeployedTime,
14+
PopupMenu,
15+
stringComparatorBySortOrder,
16+
handleRelativeDateSorting,
17+
Tooltip,
18+
} from '@devtron-labs/devtron-fe-common-lib'
19+
20+
import { ReactComponent as DevtronIcon } from '@Icons/ic-devtron-app.svg'
21+
import { ReactComponent as ICActivity } from '@Icons/ic-activity.svg'
22+
import { ReactComponent as ICArrowLineDown } from '@Icons/ic-arrow-line-down.svg'
23+
import { ReactComponent as ICMoreOption } from '@Icons/ic-more-option.svg'
24+
import { StatusConstants } from '@Components/app/list-new/Constants'
25+
26+
import {
27+
EnvironmentOverviewTableHeaderFixedKeys,
28+
EnvironmentOverviewTableHeaderValues,
29+
EnvironmentOverviewTableHeaderVariableKeys,
30+
EnvironmentOverviewTableSortableKeys,
31+
} from './EnvironmentOverview.constants'
32+
import {
33+
EnvironmentOverviewTableProps,
34+
EnvironmentOverviewTableRow,
35+
EnvironmentOverviewTableRowData,
36+
} from './EnvironmentOverviewTable.types'
37+
import './EnvironmentOverviewTable.scss'
38+
39+
const renderPopUpMenu = (items: EnvironmentOverviewTableRow['popUpMenuItems']) => (
40+
<PopupMenu autoClose>
41+
<PopupMenu.Button isKebab rootClassName="p-4 flex dc__no-border cursor">
42+
<ICMoreOption className="icon-dim-16 fcn-6 rotateBy--90" />
43+
</PopupMenu.Button>
44+
<PopupMenu.Body rootClassName="dc__border py-4 w-180">
45+
{items.map((popUpMenuItem) => {
46+
if ('label' in popUpMenuItem) {
47+
const { label, onClick, disabled, Icon, iconType = 'fill' } = popUpMenuItem
48+
49+
return (
50+
<button
51+
key={label}
52+
type="button"
53+
className={`dc__transparent w-100 py-6 px-8 flexbox dc__align-items-center dc__gap-8 ${disabled ? ' dc__opacity-0_5 cursor-not-allowed' : 'dc__hover-n50'}`}
54+
onClick={onClick}
55+
disabled={disabled}
56+
>
57+
{Icon && (
58+
<Icon
59+
className={`icon-dim-16 ${iconType === 'fill' ? 'fcn-7' : ''} ${iconType === 'stroke' ? 'scn-7' : ''}`}
60+
/>
61+
)}
62+
<span className="dc__truncate cn-9 fs-13 lh-20">{label}</span>
63+
</button>
64+
)
65+
}
66+
67+
return popUpMenuItem
68+
})}
69+
</PopupMenu.Body>
70+
</PopupMenu>
71+
)
72+
73+
export const EnvironmentOverviewTable = ({
74+
rows = [],
75+
isVirtualEnv,
76+
onCheckboxSelect,
77+
}: EnvironmentOverviewTableProps) => {
78+
// STATES
79+
const [isLastDeployedExpanded, setIsLastDeployedExpanded] = useState(false)
80+
81+
// HOOKS
82+
const { sortBy, sortOrder, handleSorting } = useUrlFilters<
83+
(typeof EnvironmentOverviewTableSortableKeys)[keyof typeof EnvironmentOverviewTableSortableKeys]
84+
>({
85+
initialSortKey: EnvironmentOverviewTableSortableKeys.NAME,
86+
})
87+
88+
// ROWS
89+
const sortedRows = useMemo(
90+
() =>
91+
rows.sort((a, b) => {
92+
if (sortBy === EnvironmentOverviewTableSortableKeys.DEPLOYED_AT) {
93+
return handleRelativeDateSorting(a.environment.deployedAt, b.environment.deployedAt, sortOrder)
94+
}
95+
96+
return stringComparatorBySortOrder(a.environment.name, b.environment.name, sortOrder)
97+
}),
98+
[rows, sortBy, sortOrder],
99+
)
100+
101+
// CONSTANTS
102+
const isAllChecked = sortedRows.every(({ isChecked }) => isChecked)
103+
const isPartialChecked = sortedRows.some(({ isChecked }) => isChecked)
104+
105+
// CLASSES
106+
const isCheckedRowClassName = 'bcb-50 no-hover'
107+
const isVirtualEnvRowClassName = isVirtualEnv ? 'environment-overview-table__fixed-cell--no-status' : ''
108+
const isLastDeployedExpandedRowClassName = isLastDeployedExpanded
109+
? 'environment-overview-table__variable-cell--last-deployed-expanded'
110+
: ''
111+
112+
// METHODS
113+
const toggleLastDeployedExpanded = () => setIsLastDeployedExpanded(!isLastDeployedExpanded)
114+
115+
const handleCheckboxChange = (id: EnvironmentOverviewTableRowData['id']) => (e: ChangeEvent<HTMLInputElement>) => {
116+
const { checked } = e.target
117+
// if id is null, then it denotes 'ALL CHECKBOX' is checked
118+
onCheckboxSelect(id, checked, !id)
119+
}
120+
121+
// RENDERERS
122+
const renderHeaderValue = (key: string) => {
123+
const headerValue = EnvironmentOverviewTableHeaderValues[key]
124+
125+
if (EnvironmentOverviewTableSortableKeys[key]) {
126+
return (
127+
<SortableTableHeaderCell
128+
title={headerValue}
129+
sortOrder={sortOrder}
130+
isSorted={sortBy === EnvironmentOverviewTableSortableKeys[key]}
131+
triggerSorting={() => handleSorting(EnvironmentOverviewTableSortableKeys[key])}
132+
isSortable
133+
disabled={false}
134+
/>
135+
)
136+
}
137+
138+
if (
139+
EnvironmentOverviewTableHeaderVariableKeys[key] ===
140+
EnvironmentOverviewTableHeaderVariableKeys.LAST_DEPLOYED_IMAGE
141+
) {
142+
return (
143+
<button
144+
type="button"
145+
className="dc__transparent flexbox dc__align-items-center dc__gap-4 p-0"
146+
onClick={toggleLastDeployedExpanded}
147+
>
148+
{headerValue}
149+
<ICArrowLineDown
150+
className="icon-dim-14 scn-7 rotate"
151+
style={{ ['--rotateBy' as string]: isLastDeployedExpanded ? '90deg' : '-90deg' }}
152+
/>
153+
</button>
154+
)
155+
}
156+
157+
return headerValue ? <span>{headerValue}</span> : null
158+
}
159+
160+
const renderHeaderRow = () => (
161+
<div className="environment-overview-table__row bcn-0 dc__border-bottom-n1 no-hover">
162+
<div
163+
className={`environment-overview-table__fixed-cell bcn-0 pl-16 pr-15 py-8 cn-7 fw-6 fs-12 lh-20 ${isVirtualEnvRowClassName}`}
164+
>
165+
<Checkbox
166+
isChecked={isPartialChecked}
167+
onChange={handleCheckboxChange(null)}
168+
value={isAllChecked ? 'CHECKED' : 'INTERMEDIATE'}
169+
rootClassName="mb-0 ml-2"
170+
/>
171+
{!isVirtualEnv && <ICActivity className="icon-dim-16" />}
172+
{Object.keys(EnvironmentOverviewTableHeaderFixedKeys).map((key) => (
173+
<Fragment key={key}>{renderHeaderValue(key)}</Fragment>
174+
))}
175+
</div>
176+
<div
177+
className={`environment-overview-table__variable-cell px-16 py-8 cn-7 fw-6 fs-12 lh-20 ${isLastDeployedExpandedRowClassName}`}
178+
>
179+
{Object.keys(EnvironmentOverviewTableHeaderVariableKeys).map((key) => (
180+
<Fragment key={key}>{renderHeaderValue(key)}</Fragment>
181+
))}
182+
</div>
183+
</div>
184+
)
185+
186+
const renderRow = ({
187+
environment,
188+
isChecked,
189+
deployedAtLink,
190+
redirectLink,
191+
onCommitClick,
192+
onLastDeployedImageClick,
193+
popUpMenuItems = [],
194+
}: EnvironmentOverviewTableProps['rows'][0]) => {
195+
const { id, name, status, commits, deployedAt, deployedBy, deploymentStatus, lastDeployedImage } = environment
196+
197+
return (
198+
<div className={`environment-overview-table__row ${isChecked ? isCheckedRowClassName : ''}`}>
199+
<div
200+
className={`environment-overview-table__fixed-cell pl-16 pr-7 py-8 cn-9 fs-13 lh-20 dc__visible-hover dc__visible-hover--parent ${isVirtualEnvRowClassName} ${isChecked ? isCheckedRowClassName : 'bcn-0'}`}
201+
>
202+
{!isPartialChecked && <DevtronIcon className="icon-dim-24 dc__visible-hover--hide-child" />}
203+
<Checkbox
204+
isChecked={isChecked}
205+
onChange={handleCheckboxChange(id)}
206+
value="CHECKED"
207+
rootClassName={`mb-0 ml-2 ${!isPartialChecked ? 'dc__visible-hover--child' : ''}`}
208+
/>
209+
{!isVirtualEnv && (
210+
<AppStatus
211+
appStatus={deployedAt ? status : StatusConstants.NOT_DEPLOYED.noSpaceLower}
212+
hideStatusMessage
213+
/>
214+
)}
215+
<div className="flexbox dc__align-items-center dc__content-space dc__gap-8">
216+
<Tooltip content={name}>
217+
<Link className="py-2 dc__truncate dc__no-decor" to={redirectLink}>
218+
{name}
219+
</Link>
220+
</Tooltip>
221+
{!!popUpMenuItems?.length && renderPopUpMenu(popUpMenuItems)}
222+
</div>
223+
</div>
224+
<div
225+
className={`environment-overview-table__variable-cell px-16 py-8 cn-9 fs-13 lh-20 ${isLastDeployedExpandedRowClassName}`}
226+
>
227+
<AppStatus
228+
appStatus={deployedAt ? deploymentStatus : StatusConstants.NOT_DEPLOYED.noSpaceLower}
229+
isDeploymentStatus
230+
isVirtualEnv={isVirtualEnv}
231+
/>
232+
{lastDeployedImage && (
233+
<ImageChipCell
234+
imagePath={lastDeployedImage}
235+
isExpanded={isLastDeployedExpanded}
236+
registryType={RegistryType.DOCKER}
237+
handleClick={onLastDeployedImageClick}
238+
/>
239+
)}
240+
<CommitChipCell handleClick={onCommitClick} commits={commits} />
241+
{deployedBy && (
242+
<>
243+
{deployedAt ? (
244+
<Link className="dc__no-decor" to={deployedAtLink}>
245+
{processDeployedTime(deployedAt, true)}
246+
</Link>
247+
) : (
248+
<span />
249+
)}
250+
<div className="flexbox dc__align-items-center dc__gap-8">
251+
<span
252+
className="icon-dim-20 mw-20 flex dc__border-radius-50-per dc__uppercase cn-0 fw-4"
253+
style={{
254+
backgroundColor: getRandomColor(deployedBy),
255+
}}
256+
>
257+
{deployedBy[0]}
258+
</span>
259+
<Tooltip content={deployedBy}>
260+
<span className="dc__truncate">{deployedBy}</span>
261+
</Tooltip>
262+
</div>
263+
</>
264+
)}
265+
</div>
266+
</div>
267+
)
268+
}
269+
270+
return (
271+
<div className="environment-overview-table dc__border br-4 bcn-0 w-100">
272+
{renderHeaderRow()}
273+
{sortedRows.map((row) => (
274+
<Fragment key={row.environment.id}>{renderRow(row)}</Fragment>
275+
))}
276+
</div>
277+
)
278+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.environment-overview-table {
2+
$parent-selector: &;
3+
4+
overflow-x: auto;
5+
6+
&__row {
7+
display: grid;
8+
grid-template-columns: 300px 1fr;
9+
min-width: fit-content;
10+
11+
&:hover:not(.no-hover) {
12+
background-color: var(--N50);
13+
14+
#{$parent-selector}__fixed-cell:not(.no-hover) {
15+
background-color: var(--N50);
16+
}
17+
}
18+
}
19+
20+
&__fixed-cell {
21+
position: sticky;
22+
left: 0;
23+
display: grid;
24+
grid-template-columns: 24px 16px 1fr;
25+
column-gap: 12px;
26+
align-items: center;
27+
border-right: 1px solid var(--N100);
28+
z-index: 3;
29+
30+
&--no-status {
31+
grid-template-columns: 24px 1fr;
32+
}
33+
}
34+
35+
&__variable-cell {
36+
display: grid;
37+
grid-template-columns: 150px 200px 110px 120px minmax(200px, 1fr);
38+
column-gap: 16px;
39+
align-items: center;
40+
transition: grid-template-columns 0.2s ease-in-out;
41+
42+
&--last-deployed-expanded {
43+
grid-template-columns: 150px 280px 110px 120px minmax(200px, 1fr);
44+
}
45+
}
46+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { FunctionComponent, MouseEvent, ReactElement, SVGProps } from 'react'
2+
import { EnvironmentOverviewTableHeaderKeys } from './EnvironmentOverview.constants'
3+
4+
export interface EnvironmentOverviewTableRowData {
5+
id: number
6+
[EnvironmentOverviewTableHeaderKeys.NAME]: string
7+
[EnvironmentOverviewTableHeaderKeys.STATUS]: string
8+
[EnvironmentOverviewTableHeaderKeys.DEPLOYMENT_STATUS]: string
9+
[EnvironmentOverviewTableHeaderKeys.LAST_DEPLOYED_IMAGE]: string
10+
[EnvironmentOverviewTableHeaderKeys.COMMITS]: string[]
11+
[EnvironmentOverviewTableHeaderKeys.DEPLOYED_AT]: string
12+
[EnvironmentOverviewTableHeaderKeys.DEPLOYED_BY]: string
13+
}
14+
15+
export type EnvironmentOverviewTablePopUpMenuItem =
16+
| {
17+
label: string
18+
Icon?: FunctionComponent<SVGProps<SVGSVGElement>>
19+
iconType?: 'fill' | 'stroke'
20+
disabled?: boolean
21+
onClick?: (event: MouseEvent<HTMLButtonElement>) => void
22+
}
23+
| ReactElement
24+
25+
export interface EnvironmentOverviewTableRow {
26+
environment: EnvironmentOverviewTableRowData
27+
isChecked?: boolean
28+
onLastDeployedImageClick: (event: MouseEvent<HTMLButtonElement>) => void
29+
onCommitClick: (event: MouseEvent<HTMLButtonElement>) => void
30+
deployedAtLink: string
31+
redirectLink: string
32+
popUpMenuItems?: EnvironmentOverviewTablePopUpMenuItem[]
33+
}
34+
35+
export interface EnvironmentOverviewTableProps {
36+
rows: EnvironmentOverviewTableRow[]
37+
isVirtualEnv?: boolean
38+
onCheckboxSelect: (id: EnvironmentOverviewTableRowData['id'], isChecked: boolean, isAllChecked: boolean) => void
39+
}

0 commit comments

Comments
 (0)