|
| 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 | +} |
0 commit comments