github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/settings/components/cluster-details/cluster-details.tsx (about) 1 import * as classNames from 'classnames'; 2 import * as moment from 'moment'; 3 import * as React from 'react'; 4 import {FieldApi, FormField as ReactFormField, Text} from 'react-form'; 5 import {RouteComponentProps, Link} from 'react-router-dom'; 6 import {from, timer} from 'rxjs'; 7 import {mergeMap} from 'rxjs/operators'; 8 9 import {FormField, Ticker} from 'argo-ui'; 10 import {ConnectionStateIcon, DataLoader, EditablePanel, Page, Timestamp, MapInputField} from '../../../shared/components'; 11 import {Cluster} from '../../../shared/models'; 12 import {services} from '../../../shared/services'; 13 import {formatClusterQueryParam} from '../../../shared/utils'; 14 15 function isRefreshRequested(cluster: Cluster): boolean { 16 return cluster.info.connectionState.attemptedAt && cluster.refreshRequestedAt && moment(cluster.info.connectionState.attemptedAt).isBefore(moment(cluster.refreshRequestedAt)); 17 } 18 19 export const NamespacesEditor = ReactFormField((props: {fieldApi: FieldApi; className: string}) => { 20 const val = (props.fieldApi.getValue() || []).join(','); 21 return <input className={props.className} value={val} onChange={event => props.fieldApi.setValue(event.target.value.split(','))} />; 22 }); 23 24 export const ClusterDetails = (props: RouteComponentProps<{server: string}>) => { 25 const server = decodeURIComponent(props.match.params.server); 26 const loaderRef = React.useRef<DataLoader>(); 27 const [updating, setUpdating] = React.useState(false); 28 return ( 29 <DataLoader ref={loaderRef} input={server} load={(url: string) => timer(0, 1000).pipe(mergeMap(() => from(services.clusters.get(url, ''))))}> 30 {(cluster: Cluster) => ( 31 <Page 32 title='Clusters' 33 toolbar={{ 34 breadcrumbs: [{title: 'Settings', path: '/settings'}, {title: 'Clusters', path: '/settings/clusters'}, {title: server}], 35 actionMenu: { 36 items: [ 37 { 38 iconClassName: classNames('fa fa-redo', {'status-icon--spin': isRefreshRequested(cluster)}), 39 title: 'Invalidate Cache', 40 disabled: isRefreshRequested(cluster) || updating, 41 action: async () => { 42 setUpdating(true); 43 try { 44 const updated = await services.clusters.invalidateCache(props.match.params.server); 45 loaderRef.current.setData(updated); 46 } finally { 47 setUpdating(false); 48 } 49 } 50 } 51 ] 52 } 53 }}> 54 <p /> 55 56 <div className='argo-container'> 57 <EditablePanel 58 values={cluster} 59 save={async updated => { 60 const item = await services.clusters.get(updated.server, ''); 61 item.name = updated.name; 62 item.namespaces = updated.namespaces; 63 item.labels = updated.labels; 64 item.annotations = updated.annotations; 65 loaderRef.current.setData(await services.clusters.update(item, 'name', 'namespaces', 'labels', 'annotations')); 66 }} 67 title='GENERAL' 68 items={[ 69 { 70 title: 'SERVER', 71 view: cluster.server 72 }, 73 { 74 title: 'CREDENTIALS TYPE', 75 view: 76 (cluster.config.awsAuthConfig && `IAM AUTH (cluster name: ${cluster.config.awsAuthConfig.clusterName})`) || 77 (cluster.config.execProviderConfig && `External provider (command: ${cluster.config.execProviderConfig.command})`) || 78 'Token/Basic Auth' 79 }, 80 { 81 title: 'NAME', 82 view: cluster.name, 83 edit: formApi => <FormField formApi={formApi} field='name' component={Text} /> 84 }, 85 { 86 title: 'NAMESPACES', 87 view: ((cluster.namespaces || []).length === 0 && 'All namespaces') || cluster.namespaces.join(', '), 88 edit: formApi => <FormField formApi={formApi} field='namespaces' component={NamespacesEditor} /> 89 }, 90 { 91 title: 'APPLICATIONS', 92 view: ( 93 <div> 94 <DataLoader load={() => services.applications.list([])}> 95 {apps => ( 96 <Link to={`/applications?cluster=${formatClusterQueryParam(cluster)}`}> 97 { 98 apps.items.filter(app => app.spec.destination.name === cluster.name || app.spec.destination.server === cluster.server) 99 .length 100 } 101 </Link> 102 )} 103 </DataLoader> 104 </div> 105 ) 106 }, 107 { 108 title: 'LABELS', 109 view: Object.keys(cluster.labels || []) 110 .map(label => `${label}=${cluster.labels[label]}`) 111 .join(' '), 112 edit: formApi => <FormField formApi={formApi} field='labels' component={MapInputField} /> 113 }, 114 { 115 title: 'ANNOTATIONS', 116 view: Object.keys(cluster.annotations || []) 117 .map(annotation => `${annotation}=${cluster.annotations[annotation]}`) 118 .join(' '), 119 edit: formApi => <FormField formApi={formApi} field='annotations' component={MapInputField} /> 120 } 121 ]} 122 /> 123 <div className='white-box'> 124 <p>CONNECTION STATE</p> 125 <div className='white-box__details'> 126 <div className='row white-box__details-row'> 127 <div className='columns small-3'>STATUS:</div> 128 <div className='columns small-9'> 129 <ConnectionStateIcon state={cluster.info.connectionState} /> {cluster.info.connectionState.status} 130 </div> 131 </div> 132 <div className='row white-box__details-row'> 133 <div className='columns small-3'>VERSION:</div> 134 <div className='columns small-9'> {cluster.info.serverVersion}</div> 135 </div> 136 <div className='row white-box__details-row'> 137 <div className='columns small-3'>DETAILS:</div> 138 <div className='columns small-9'> {cluster.info.connectionState.message} </div> 139 </div> 140 <div className='row white-box__details-row'> 141 <div className='columns small-3'>MODIFIED AT:</div> 142 <div className='columns small-9'> 143 <Ticker> 144 {now => { 145 if (!cluster.info.connectionState.attemptedAt) { 146 return <span>Never (next refresh in few seconds)</span>; 147 } 148 const secondsBeforeRefresh = Math.round( 149 Math.max(10 - moment(now).diff(moment(cluster.info.connectionState.attemptedAt)) / 1000, 1) 150 ); 151 return ( 152 <React.Fragment> 153 <Timestamp date={cluster.info.connectionState.attemptedAt} /> (next refresh in {secondsBeforeRefresh} seconds) 154 </React.Fragment> 155 ); 156 }} 157 </Ticker> 158 </div> 159 </div> 160 </div> 161 </div> 162 163 <div className='white-box'> 164 <p>CACHE INFO</p> 165 <div className='white-box__details'> 166 <Ticker> 167 {() => ( 168 <div className='row white-box__details-row'> 169 <div className='columns small-3'>RE-SYNCHRONIZED:</div> 170 <div className='columns small-9'> 171 <Timestamp date={cluster.info.cacheInfo.lastCacheSyncTime} /> 172 </div> 173 </div> 174 )} 175 </Ticker> 176 <div className='row white-box__details-row'> 177 <div className='columns small-3'>APIs COUNT:</div> 178 <div className='columns small-9'> {cluster.info.cacheInfo.apisCount} </div> 179 </div> 180 <div className='row white-box__details-row'> 181 <div className='columns small-3'>RESOURCES COUNT:</div> 182 <div className='columns small-9'> {cluster.info.cacheInfo.resourcesCount} </div> 183 </div> 184 <div className='row white-box__details-row'> 185 <div className='columns small-3'>APPLICATIONS COUNT:</div> 186 <div className='columns small-9'> {cluster.info.applicationsCount} </div> 187 </div> 188 </div> 189 </div> 190 </div> 191 </Page> 192 )} 193 </DataLoader> 194 ); 195 };