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