github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/applications-list/applications-tiles.tsx (about) 1 import {DataLoader, Tooltip} from 'argo-ui'; 2 import * as classNames from 'classnames'; 3 import * as React from 'react'; 4 import {Key, KeybindingContext, NumKey, NumKeyToNumber, NumPadKey, useNav} from 'argo-ui/v2'; 5 import {Cluster} from '../../../shared/components'; 6 import {Consumer, Context, AuthSettingsCtx} from '../../../shared/context'; 7 import * as models from '../../../shared/models'; 8 import {ApplicationURLs} from '../application-urls'; 9 import * as AppUtils from '../utils'; 10 import {getAppDefaultSource, OperationState} from '../utils'; 11 import {services} from '../../../shared/services'; 12 13 import './applications-tiles.scss'; 14 15 export interface ApplicationTilesProps { 16 applications: models.Application[]; 17 syncApplication: (appName: string, appNamespace: string) => any; 18 refreshApplication: (appName: string, appNamespace: string) => any; 19 deleteApplication: (appName: string, appNamespace: string) => any; 20 } 21 22 const useItemsPerContainer = (itemRef: any, containerRef: any): number => { 23 const [itemsPer, setItemsPer] = React.useState(0); 24 25 React.useEffect(() => { 26 const handleResize = () => { 27 let timeoutId: any; 28 clearTimeout(timeoutId); 29 timeoutId = setTimeout(() => { 30 timeoutId = null; 31 const itemWidth = itemRef.current ? itemRef.current.offsetWidth : -1; 32 const containerWidth = containerRef.current ? containerRef.current.offsetWidth : -1; 33 const curItemsPer = containerWidth > 0 && itemWidth > 0 ? Math.floor(containerWidth / itemWidth) : 1; 34 if (curItemsPer !== itemsPer) { 35 setItemsPer(curItemsPer); 36 } 37 }, 1000); 38 }; 39 window.addEventListener('resize', handleResize); 40 handleResize(); 41 return () => { 42 window.removeEventListener('resize', handleResize); 43 }; 44 }, []); 45 46 return itemsPer || 1; 47 }; 48 49 export const ApplicationTiles = ({applications, syncApplication, refreshApplication, deleteApplication}: ApplicationTilesProps) => { 50 const [selectedApp, navApp, reset] = useNav(applications.length); 51 52 const ctxh = React.useContext(Context); 53 const appRef = {ref: React.useRef(null), set: false}; 54 const appContainerRef = React.useRef(null); 55 const appsPerRow = useItemsPerContainer(appRef.ref, appContainerRef); 56 const useAuthSettingsCtx = React.useContext(AuthSettingsCtx); 57 58 const {useKeybinding} = React.useContext(KeybindingContext); 59 60 useKeybinding({keys: Key.RIGHT, action: () => navApp(1)}); 61 useKeybinding({keys: Key.LEFT, action: () => navApp(-1)}); 62 useKeybinding({keys: Key.DOWN, action: () => navApp(appsPerRow)}); 63 useKeybinding({keys: Key.UP, action: () => navApp(-1 * appsPerRow)}); 64 65 useKeybinding({ 66 keys: Key.ENTER, 67 action: () => { 68 if (selectedApp > -1) { 69 ctxh.navigation.goto(`/${AppUtils.getAppUrl(applications[selectedApp])}`); 70 return true; 71 } 72 return false; 73 } 74 }); 75 76 useKeybinding({ 77 keys: Key.ESCAPE, 78 action: () => { 79 if (selectedApp > -1) { 80 reset(); 81 return true; 82 } 83 return false; 84 } 85 }); 86 87 useKeybinding({ 88 keys: Object.values(NumKey) as NumKey[], 89 action: n => { 90 reset(); 91 return navApp(NumKeyToNumber(n)); 92 } 93 }); 94 useKeybinding({ 95 keys: Object.values(NumPadKey) as NumPadKey[], 96 action: n => { 97 reset(); 98 return navApp(NumKeyToNumber(n)); 99 } 100 }); 101 return ( 102 <Consumer> 103 {ctx => ( 104 <DataLoader load={() => services.viewPreferences.getPreferences()}> 105 {pref => { 106 const favList = pref.appList.favoritesAppList || []; 107 return ( 108 <div className='applications-tiles argo-table-list argo-table-list--clickable' ref={appContainerRef}> 109 {applications.map((app, i) => { 110 const source = getAppDefaultSource(app); 111 const isOci = source?.repoURL?.startsWith('oci://'); 112 const targetRevision = source ? source.targetRevision || 'HEAD' : 'Unknown'; 113 return ( 114 <div 115 key={AppUtils.appInstanceName(app)} 116 ref={appRef.set ? null : appRef.ref} 117 className={`argo-table-list__row applications-list__entry applications-list__entry--health-${app.status.health.status} ${ 118 selectedApp === i ? 'applications-tiles__selected' : '' 119 }`}> 120 <div 121 className='row applications-tiles__wrapper' 122 onClick={e => ctx.navigation.goto(`/${AppUtils.getAppUrl(app)}`, {view: pref.appDetails.view}, {event: e})}> 123 <div 124 className={`columns small-12 applications-list__info qe-applications-list-${AppUtils.appInstanceName( 125 app 126 )} applications-tiles__item`}> 127 <div className='row '> 128 <div className={app.status.summary.externalURLs?.length > 0 ? 'columns small-10' : 'columns small-11'}> 129 <i 130 className={ 131 'icon argo-icon-' + (source?.chart != null ? 'helm' : isOci ? 'oci applications-tiles__item__small' : 'git') 132 } 133 /> 134 <Tooltip content={AppUtils.appInstanceName(app)}> 135 <span className='applications-list__title'> 136 {AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled)} 137 </span> 138 </Tooltip> 139 </div> 140 <div className={app.status.summary.externalURLs?.length > 0 ? 'columns small-2' : 'columns small-1'}> 141 <div className='applications-list__external-link'> 142 <ApplicationURLs urls={app.status.summary.externalURLs} /> 143 <Tooltip content={favList?.includes(app.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}> 144 <button 145 className='large-text-height' 146 onClick={e => { 147 e.stopPropagation(); 148 favList?.includes(app.metadata.name) 149 ? favList.splice(favList.indexOf(app.metadata.name), 1) 150 : favList.push(app.metadata.name); 151 services.viewPreferences.updatePreferences({appList: {...pref.appList, favoritesAppList: favList}}); 152 }}> 153 <i 154 className={favList?.includes(app.metadata.name) ? 'fas fa-star fa-lg' : 'far fa-star fa-lg'} 155 style={{ 156 cursor: 'pointer', 157 marginLeft: '7px', 158 color: favList?.includes(app.metadata.name) ? '#FFCE25' : '#8fa4b1' 159 }} 160 /> 161 </button> 162 </Tooltip> 163 </div> 164 </div> 165 </div> 166 <div className='row'> 167 <div className='columns small-3' title='Project:'> 168 Project: 169 </div> 170 <div className='columns small-9'>{app.spec.project}</div> 171 </div> 172 <div className='row'> 173 <div className='columns small-3' title='Labels:'> 174 Labels: 175 </div> 176 <div className='columns small-9'> 177 <Tooltip 178 zIndex={4} 179 content={ 180 <div> 181 {Object.keys(app.metadata.labels || {}) 182 .map(label => ({label, value: app.metadata.labels[label]})) 183 .map(item => ( 184 <div key={item.label}> 185 {item.label}={item.value} 186 </div> 187 ))} 188 </div> 189 }> 190 <span> 191 {Object.keys(app.metadata.labels || {}) 192 .map(label => `${label}=${app.metadata.labels[label]}`) 193 .join(', ')} 194 </span> 195 </Tooltip> 196 </div> 197 </div> 198 <div className='row'> 199 <div className='columns small-3' title='Status:'> 200 Status: 201 </div> 202 <div className='columns small-9' qe-id='applications-tiles-health-status'> 203 <AppUtils.HealthStatusIcon state={app.status.health} /> {app.status.health.status} 204 205 {app.status.sourceHydrator?.currentOperation && ( 206 <> 207 <AppUtils.HydrateOperationPhaseIcon operationState={app.status.sourceHydrator.currentOperation} />{' '} 208 {app.status.sourceHydrator.currentOperation.phase} 209 210 </> 211 )} 212 <AppUtils.ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status} 213 214 <OperationState app={app} quiet={true} /> 215 </div> 216 </div> 217 <div className='row'> 218 <div className='columns small-3' title='Repository:'> 219 Repository: 220 </div> 221 <div className='columns small-9'> 222 <Tooltip content={source?.repoURL} zIndex={4}> 223 <span>{source?.repoURL}</span> 224 </Tooltip> 225 </div> 226 </div> 227 <div className='row'> 228 <div className='columns small-3' title='Target Revision:'> 229 Target Revision: 230 </div> 231 <div className='columns small-9'>{targetRevision}</div> 232 </div> 233 {source?.path && ( 234 <div className='row'> 235 <div className='columns small-3' title='Path:'> 236 Path: 237 </div> 238 <div className='columns small-9'>{source?.path}</div> 239 </div> 240 )} 241 {source?.chart && ( 242 <div className='row'> 243 <div className='columns small-3' title='Chart:'> 244 Chart: 245 </div> 246 <div className='columns small-9'>{source?.chart}</div> 247 </div> 248 )} 249 <div className='row'> 250 <div className='columns small-3' title='Destination:'> 251 Destination: 252 </div> 253 <div className='columns small-9'> 254 <Cluster server={app.spec.destination.server} name={app.spec.destination.name} /> 255 </div> 256 </div> 257 <div className='row'> 258 <div className='columns small-3' title='Namespace:'> 259 Namespace: 260 </div> 261 <div className='columns small-9'>{app.spec.destination.namespace}</div> 262 </div> 263 <div className='row'> 264 <div className='columns small-3' title='Age:'> 265 Created At: 266 </div> 267 <div className='columns small-9'>{AppUtils.formatCreationTimestamp(app.metadata.creationTimestamp)}</div> 268 </div> 269 {app.status.operationState && ( 270 <div className='row'> 271 <div className='columns small-3' title='Last sync:'> 272 Last Sync: 273 </div> 274 <div className='columns small-9'> 275 {AppUtils.formatCreationTimestamp(app.status.operationState.finishedAt || app.status.operationState.startedAt)} 276 </div> 277 </div> 278 )} 279 <div className='row applications-tiles__actions'> 280 <div className='columns applications-list__entry--actions'> 281 <a 282 className='argo-button argo-button--base' 283 qe-id='applications-tiles-button-sync' 284 onClick={e => { 285 e.stopPropagation(); 286 syncApplication(app.metadata.name, app.metadata.namespace); 287 }}> 288 <i className='fa fa-sync' /> Sync 289 </a> 290 291 <Tooltip className='custom-tooltip' content={'Refresh'}> 292 <a 293 className='argo-button argo-button--base' 294 qe-id='applications-tiles-button-refresh' 295 {...AppUtils.refreshLinkAttrs(app)} 296 onClick={e => { 297 e.stopPropagation(); 298 refreshApplication(app.metadata.name, app.metadata.namespace); 299 }}> 300 <i className={classNames('fa fa-redo', {'status-icon--spin': AppUtils.isAppRefreshing(app)})} />{' '} 301 <span className='show-for-xxlarge'>Refresh</span> 302 </a> 303 </Tooltip> 304 305 <Tooltip className='custom-tooltip' content={'Delete'}> 306 <a 307 className='argo-button argo-button--base' 308 qe-id='applications-tiles-button-delete' 309 onClick={e => { 310 e.stopPropagation(); 311 deleteApplication(app.metadata.name, app.metadata.namespace); 312 }}> 313 <i className='fa fa-times-circle' /> <span className='show-for-xxlarge'>Delete</span> 314 </a> 315 </Tooltip> 316 </div> 317 </div> 318 </div> 319 </div> 320 </div> 321 ); 322 })} 323 </div> 324 ); 325 }} 326 </DataLoader> 327 )} 328 </Consumer> 329 ); 330 };