github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/application-details/application-resource-list.tsx (about) 1 import {DropDown, Tooltip} from 'argo-ui'; 2 import * as React from 'react'; 3 import * as classNames from 'classnames'; 4 import * as models from '../../../shared/models'; 5 import {ResourceIcon} from '../resource-icon'; 6 import {ResourceLabel} from '../resource-label'; 7 import {ComparisonStatusIcon, HealthStatusIcon, nodeKey, isSameNode, createdOrNodeKey} from '../utils'; 8 import {AppDetailsPreferences} from '../../../shared/services'; 9 import {Consumer} from '../../../shared/context'; 10 import Moment from 'react-moment'; 11 import {format} from 'date-fns'; 12 import {HealthPriority, ResourceNode, SyncPriority, SyncStatusCode} from '../../../shared/models'; 13 import './application-resource-list.scss'; 14 15 export interface ApplicationResourceListProps { 16 pref: AppDetailsPreferences; 17 resources: models.ResourceStatus[]; 18 onNodeClick?: (fullName: string) => any; 19 nodeMenu?: (node: models.ResourceNode) => React.ReactNode; 20 tree?: models.ApplicationTree; 21 } 22 23 export const ApplicationResourceList = (props: ApplicationResourceListProps) => { 24 const nodeByKey = new Map<string, models.ResourceNode>(); 25 props.tree?.nodes?.forEach(res => nodeByKey.set(nodeKey(res), res)); 26 27 const [sortConfig, setSortConfig] = React.useState<{key: string; direction: 'asc' | 'desc'}>({key: 'createdAt', direction: 'desc'}); 28 29 const handleSort = (key: string) => { 30 setSortConfig(prevConfig => { 31 if (prevConfig.key !== key) { 32 return {key, direction: 'asc'}; 33 } 34 return {key, direction: prevConfig.direction === 'asc' ? 'desc' : 'asc'}; 35 }); 36 }; 37 38 const getSortArrow = (key: string) => { 39 if (sortConfig.key !== key) { 40 return null; 41 } 42 43 const isAsc = sortConfig.direction === 'asc'; 44 const style: React.CSSProperties = { 45 position: 'relative', 46 top: isAsc ? '2px' : '-2px' 47 }; 48 return ( 49 <span style={style}> 50 <i className={isAsc ? 'fa fa-sort-up' : 'fa fa-sort-down'} /> 51 </span> 52 ); 53 }; 54 55 const sortedResources = React.useMemo(() => { 56 const resourcesToSort = [...props.resources]; 57 resourcesToSort.sort((a, b) => { 58 let compare = 0; 59 switch (sortConfig.key) { 60 case 'name': 61 compare = a.name.localeCompare(b.name); 62 break; 63 64 case 'group-kind': 65 { 66 const groupKindA = [a.group, a.kind].filter(item => !!item).join('/'); 67 const groupKindB = [b.group, b.kind].filter(item => !!item).join('/'); 68 compare = groupKindA.localeCompare(groupKindB); 69 } 70 break; 71 72 case 'syncOrder': 73 { 74 const waveA = a.syncWave ?? 0; 75 const waveB = b.syncWave ?? 0; 76 compare = waveA - waveB; 77 } 78 break; 79 case 'namespace': 80 { 81 const namespaceA = a.namespace ?? ''; 82 const namespaceB = b.namespace ?? ''; 83 compare = namespaceA.localeCompare(namespaceB); 84 } 85 break; 86 case 'createdAt': 87 { 88 compare = createdOrNodeKey(a).localeCompare(createdOrNodeKey(b), undefined, {numeric: true}); 89 } 90 break; 91 case 'status': 92 { 93 const healthA = a.health?.status ?? 'Unknown'; 94 const healthB = b.health?.status ?? 'Unknown'; 95 const syncA = (a.status as SyncStatusCode) ?? 'Unknown'; 96 const syncB = (b.status as SyncStatusCode) ?? 'Unknown'; 97 98 compare = HealthPriority[healthA] - HealthPriority[healthB]; 99 if (compare === 0) { 100 compare = SyncPriority[syncA] - SyncPriority[syncB]; 101 } 102 } 103 break; 104 } 105 return sortConfig.direction === 'asc' ? compare : -compare; 106 }); 107 return resourcesToSort; 108 }, [props.resources, sortConfig]); 109 110 const firstParentNode = props.resources.length > 0 && (nodeByKey.get(nodeKey(props.resources[0])) as ResourceNode)?.parentRefs?.[0]; 111 const isSameParent = firstParentNode && props.resources?.every(x => (nodeByKey.get(nodeKey(x)) as ResourceNode)?.parentRefs?.every(p => isSameNode(p, firstParentNode))); 112 const isSameKind = props.resources?.every(x => x.group === props.resources[0].group && x.kind === props.resources[0].kind); 113 const view = props.pref.view; 114 115 const ParentRefDetails = () => { 116 return isSameParent ? ( 117 <div className='resource-parent-node-info-title'> 118 <div>Parent Node Info</div> 119 <div className='resource-parent-node-info-title__label'> 120 <div>Name:</div> 121 <div>{firstParentNode.name}</div> 122 </div> 123 <div className='resource-parent-node-info-title__label'> 124 <div>Kind:</div> 125 <div>{firstParentNode.kind}</div> 126 </div> 127 </div> 128 ) : ( 129 <div /> 130 ); 131 }; 132 return ( 133 props.resources.length > 0 && ( 134 <div> 135 {/* Display only when the view is set to or network */} 136 {(view === 'tree' || view === 'network') && ( 137 <div className='resource-details__header' style={{paddingTop: '20px'}}> 138 <ParentRefDetails /> 139 </div> 140 )} 141 <div className='argo-table-list argo-table-list--clickable'> 142 <div className='argo-table-list__head'> 143 <div className='row'> 144 <div className='columns small-1 xxxlarge-1' /> 145 <div className='columns small-2 xxxlarge-2' onClick={() => handleSort('name')} style={{cursor: 'pointer'}}> 146 NAME {getSortArrow('name')} 147 </div> 148 <div className='columns small-1 xxxlarge-1' onClick={() => handleSort('group-kind')} style={{cursor: 'pointer'}}> 149 GROUP/KIND {getSortArrow('group-kind')} 150 </div> 151 <div className='columns small-1 xxxlarge-1' onClick={() => handleSort('syncOrder')} style={{cursor: 'pointer'}}> 152 SYNC ORDER {getSortArrow('syncOrder')} 153 </div> 154 <div className='columns small-2 xxxlarge-1' onClick={() => handleSort('namespace')} style={{cursor: 'pointer'}}> 155 NAMESPACE {getSortArrow('namespace')} 156 </div> 157 {isSameKind && props.resources[0].kind === 'ReplicaSet' && <div className='columns small-1 xxxlarge-1'>REVISION</div>} 158 <div className='columns small-2 xxxlarge-2' onClick={() => handleSort('createdAt')} style={{cursor: 'pointer'}}> 159 CREATED AT {getSortArrow('createdAt')} 160 </div> 161 <div className='columns small-2 xxxlarge-1' onClick={() => handleSort('status')} style={{cursor: 'pointer'}}> 162 STATUS {getSortArrow('status')} 163 </div> 164 </div> 165 </div> 166 {sortedResources.map(res => { 167 const groupkindjoin = [res.group, res.kind].filter(item => !!item).join('/'); 168 return ( 169 <div 170 key={nodeKey(res)} 171 className={classNames('argo-table-list__row', { 172 'application-resource-tree__node--orphaned': res.orphaned 173 })} 174 onClick={() => props.onNodeClick && props.onNodeClick(nodeKey(res))}> 175 <div className='row'> 176 <div className='columns small-1 xxxlarge-1'> 177 <div className='application-details__resource-icon'> 178 <ResourceIcon kind={res.kind} /> 179 <br /> 180 <div>{ResourceLabel({kind: res.kind})}</div> 181 </div> 182 </div> 183 <Tooltip content={res.name} enabled={!!res.name}> 184 <div className='columns small-2 xxxlarge-2 application-details__item'> 185 <span className='application-details__item_text'>{res.name}</span> 186 {res.kind === 'Application' && ( 187 <Consumer> 188 {ctx => ( 189 <span className='application-details__external_link'> 190 <a 191 href={ctx.baseHref + 'applications/' + res.namespace + '/' + res.name} 192 onClick={e => e.stopPropagation()} 193 title='Open application'> 194 <i className='fa fa-external-link-alt' /> 195 </a> 196 </span> 197 )} 198 </Consumer> 199 )} 200 </div> 201 </Tooltip> 202 <Tooltip content={groupkindjoin}> 203 <div className='columns small-1 xxxlarge-1'>{groupkindjoin}</div> 204 </Tooltip> 205 <Tooltip content={res.syncWave} enabled={!!res.syncWave}> 206 <div className='columns small-1 xxxlarge-1'>{res.syncWave || '-'}</div> 207 </Tooltip> 208 <Tooltip content={res.namespace} enabled={!!res.namespace}> 209 <div className='columns small-2 xxxlarge-1'>{res.namespace}</div> 210 </Tooltip> 211 {isSameKind && 212 res.kind === 'ReplicaSet' && 213 ((nodeByKey.get(nodeKey(res)) as ResourceNode).info || []) 214 .filter(tag => !tag.name.includes('Node')) 215 .slice(0, 4) 216 .map((tag, i) => { 217 return ( 218 <div key={i} className='columns small-1 xxxlarge-1'> 219 {tag?.value?.split(':')[1] || '-'} 220 </div> 221 ); 222 })} 223 <Tooltip content={res.createdAt} enabled={!!res.createdAt}> 224 <div className='columns small-2 xxxlarge-2'> 225 {res.createdAt && ( 226 <span> 227 <Moment fromNow={true} ago={true}> 228 {res.createdAt} 229 </Moment> 230 ago {format(new Date(res.createdAt), 'MM/dd/yy')} 231 </span> 232 )} 233 </div> 234 </Tooltip> 235 <div className='columns small-2 xxxlarge-1'> 236 {res.health && ( 237 <React.Fragment> 238 <HealthStatusIcon state={res.health} /> {res.health.status} 239 </React.Fragment> 240 )} 241 {res.status && <ComparisonStatusIcon status={res.status} resource={res} label={true} />} 242 {res.hook && <i title='Resource lifecycle hook' className='fa fa-anchor' />} 243 {props.nodeMenu && ( 244 <div className='application-details__node-menu'> 245 <DropDown 246 isMenu={true} 247 anchor={() => ( 248 <button className='argo-button argo-button--light argo-button--lg argo-button--short'> 249 <i className='fa fa-ellipsis-v' /> 250 </button> 251 )}> 252 {() => props.nodeMenu(nodeByKey.get(nodeKey(res)))} 253 </DropDown> 254 </div> 255 )} 256 </div> 257 </div> 258 </div> 259 ); 260 })} 261 </div> 262 </div> 263 ) 264 ); 265 };