argo-cd

Форк
0
656 строк · 42.1 Кб
1
import {Autocomplete, ErrorNotification, MockupList, NotificationType, SlidingPanel, Toolbar, Tooltip} from 'argo-ui';
2
import * as classNames from 'classnames';
3
import * as React from 'react';
4
import * as ReactDOM from 'react-dom';
5
import {Key, KeybindingContext, KeybindingProvider} from 'argo-ui/v2';
6
import {RouteComponentProps} from 'react-router';
7
import {combineLatest, from, merge, Observable} from 'rxjs';
8
import {bufferTime, delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators';
9
import {AddAuthToToolbar, ClusterCtx, DataLoader, EmptyState, ObservableQuery, Page, Paginate, Query, Spinner} from '../../../shared/components';
10
import {AuthSettingsCtx, Consumer, Context, ContextApis} from '../../../shared/context';
11
import * as models from '../../../shared/models';
12
import {AppsListViewKey, AppsListPreferences, AppsListViewType, HealthStatusBarPreferences, services} from '../../../shared/services';
13
import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel';
14
import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel';
15
import {ApplicationsSyncPanel} from '../applications-sync-panel/applications-sync-panel';
16
import * as AppUtils from '../utils';
17
import {ApplicationsFilter, FilteredApp, getFilterResults} from './applications-filter';
18
import {ApplicationsStatusBar} from './applications-status-bar';
19
import {ApplicationsSummary} from './applications-summary';
20
import {ApplicationsTable} from './applications-table';
21
import {ApplicationTiles} from './applications-tiles';
22
import {ApplicationsRefreshPanel} from '../applications-refresh-panel/applications-refresh-panel';
23
import {useSidebarTarget} from '../../../sidebar/sidebar';
24

25
import './applications-list.scss';
26
import './flex-top-bar.scss';
27

28
const EVENTS_BUFFER_TIMEOUT = 500;
29
const WATCH_RETRY_TIMEOUT = 500;
30

31
// The applications list/watch API supports only selected set of fields.
32
// Make sure to register any new fields in the `appFields` map of `pkg/apiclient/application/forwarder_overwrite.go`.
33
const APP_FIELDS = [
34
    'metadata.name',
35
    'metadata.namespace',
36
    'metadata.annotations',
37
    'metadata.labels',
38
    'metadata.creationTimestamp',
39
    'metadata.deletionTimestamp',
40
    'spec',
41
    'operation.sync',
42
    'status.sync.status',
43
    'status.sync.revision',
44
    'status.health',
45
    'status.operationState.phase',
46
    'status.operationState.finishedAt',
47
    'status.operationState.operation.sync',
48
    'status.summary',
49
    'status.resources'
50
];
51
const APP_LIST_FIELDS = ['metadata.resourceVersion', ...APP_FIELDS.map(field => `items.${field}`)];
52
const APP_WATCH_FIELDS = ['result.type', ...APP_FIELDS.map(field => `result.application.${field}`)];
53

54
function loadApplications(projects: string[], appNamespace: string): Observable<models.Application[]> {
55
    return from(services.applications.list(projects, {appNamespace, fields: APP_LIST_FIELDS})).pipe(
56
        mergeMap(applicationsList => {
57
            const applications = applicationsList.items;
58
            return merge(
59
                from([applications]),
60
                services.applications
61
                    .watch({projects, resourceVersion: applicationsList.metadata.resourceVersion}, {fields: APP_WATCH_FIELDS})
62
                    .pipe(repeat())
63
                    .pipe(retryWhen(errors => errors.pipe(delay(WATCH_RETRY_TIMEOUT))))
64
                    // batch events to avoid constant re-rendering and improve UI performance
65
                    .pipe(bufferTime(EVENTS_BUFFER_TIMEOUT))
66
                    .pipe(
67
                        map(appChanges => {
68
                            appChanges.forEach(appChange => {
69
                                const index = applications.findIndex(item => AppUtils.appInstanceName(item) === AppUtils.appInstanceName(appChange.application));
70
                                switch (appChange.type) {
71
                                    case 'DELETED':
72
                                        if (index > -1) {
73
                                            applications.splice(index, 1);
74
                                        }
75
                                        break;
76
                                    default:
77
                                        if (index > -1) {
78
                                            applications[index] = appChange.application;
79
                                        } else {
80
                                            applications.unshift(appChange.application);
81
                                        }
82
                                        break;
83
                                }
84
                            });
85
                            return {applications, updated: appChanges.length > 0};
86
                        })
87
                    )
88
                    .pipe(filter(item => item.updated))
89
                    .pipe(map(item => item.applications))
90
            );
91
        })
92
    );
93
}
94

95
const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: number; search: string}) => React.ReactNode}) => (
96
    <ObservableQuery>
97
        {q => (
98
            <DataLoader
99
                load={() =>
100
                    combineLatest([services.viewPreferences.getPreferences().pipe(map(item => item.appList)), q]).pipe(
101
                        map(items => {
102
                            const params = items[1];
103
                            const viewPref: AppsListPreferences = {...items[0]};
104
                            if (params.get('proj') != null) {
105
                                viewPref.projectsFilter = params
106
                                    .get('proj')
107
                                    .split(',')
108
                                    .filter(item => !!item);
109
                            }
110
                            if (params.get('sync') != null) {
111
                                viewPref.syncFilter = params
112
                                    .get('sync')
113
                                    .split(',')
114
                                    .filter(item => !!item);
115
                            }
116
                            if (params.get('autoSync') != null) {
117
                                viewPref.autoSyncFilter = params
118
                                    .get('autoSync')
119
                                    .split(',')
120
                                    .filter(item => !!item);
121
                            }
122
                            if (params.get('health') != null) {
123
                                viewPref.healthFilter = params
124
                                    .get('health')
125
                                    .split(',')
126
                                    .filter(item => !!item);
127
                            }
128
                            if (params.get('namespace') != null) {
129
                                viewPref.namespacesFilter = params
130
                                    .get('namespace')
131
                                    .split(',')
132
                                    .filter(item => !!item);
133
                            }
134
                            if (params.get('cluster') != null) {
135
                                viewPref.clustersFilter = params
136
                                    .get('cluster')
137
                                    .split(',')
138
                                    .filter(item => !!item);
139
                            }
140
                            if (params.get('showFavorites') != null) {
141
                                viewPref.showFavorites = params.get('showFavorites') === 'true';
142
                            }
143
                            if (params.get('view') != null) {
144
                                viewPref.view = params.get('view') as AppsListViewType;
145
                            }
146
                            if (params.get('labels') != null) {
147
                                viewPref.labelsFilter = params
148
                                    .get('labels')
149
                                    .split(',')
150
                                    .map(decodeURIComponent)
151
                                    .filter(item => !!item);
152
                            }
153
                            return {...viewPref, page: parseInt(params.get('page') || '0', 10), search: params.get('search') || ''};
154
                        })
155
                    )
156
                }>
157
                {pref => children(pref)}
158
            </DataLoader>
159
        )}
160
    </ObservableQuery>
161
);
162

163
function filterApps(applications: models.Application[], pref: AppsListPreferences, search: string): {filteredApps: models.Application[]; filterResults: FilteredApp[]} {
164
    applications = applications.map(app => {
165
        let isAppOfAppsPattern = false;
166
        for (const resource of app.status.resources) {
167
            if (resource.kind === 'Application') {
168
                isAppOfAppsPattern = true;
169
                break;
170
            }
171
        }
172
        return {...app, isAppOfAppsPattern};
173
    });
174
    const filterResults = getFilterResults(applications, pref);
175
    return {
176
        filterResults,
177
        filteredApps: filterResults.filter(
178
            app => (search === '' || app.metadata.name.includes(search) || app.metadata.namespace.includes(search)) && Object.values(app.filterResult).every(val => val)
179
        )
180
    };
181
}
182

183
function tryJsonParse(input: string) {
184
    try {
185
        return (input && JSON.parse(input)) || null;
186
    } catch {
187
        return null;
188
    }
189
}
190

191
const SearchBar = (props: {content: string; ctx: ContextApis; apps: models.Application[]}) => {
192
    const {content, ctx, apps} = {...props};
193

194
    const searchBar = React.useRef<HTMLDivElement>(null);
195

196
    const query = new URLSearchParams(window.location.search);
197
    const appInput = tryJsonParse(query.get('new'));
198

199
    const {useKeybinding} = React.useContext(KeybindingContext);
200
    const [isFocused, setFocus] = React.useState(false);
201
    const useAuthSettingsCtx = React.useContext(AuthSettingsCtx);
202

203
    useKeybinding({
204
        keys: Key.SLASH,
205
        action: () => {
206
            if (searchBar.current && !appInput) {
207
                searchBar.current.querySelector('input').focus();
208
                setFocus(true);
209
                return true;
210
            }
211
            return false;
212
        }
213
    });
214

215
    useKeybinding({
216
        keys: Key.ESCAPE,
217
        action: () => {
218
            if (searchBar.current && !appInput && isFocused) {
219
                searchBar.current.querySelector('input').blur();
220
                setFocus(false);
221
                return true;
222
            }
223
            return false;
224
        }
225
    });
226

227
    return (
228
        <Autocomplete
229
            filterSuggestions={true}
230
            renderInput={inputProps => (
231
                <div className='applications-list__search' ref={searchBar}>
232
                    <i
233
                        className='fa fa-search'
234
                        style={{marginRight: '9px', cursor: 'pointer'}}
235
                        onClick={() => {
236
                            if (searchBar.current) {
237
                                searchBar.current.querySelector('input').focus();
238
                            }
239
                        }}
240
                    />
241
                    <input
242
                        {...inputProps}
243
                        onFocus={e => {
244
                            e.target.select();
245
                            if (inputProps.onFocus) {
246
                                inputProps.onFocus(e);
247
                            }
248
                        }}
249
                        style={{fontSize: '14px'}}
250
                        className='argo-field'
251
                        placeholder='Search applications...'
252
                    />
253
                    <div className='keyboard-hint'>/</div>
254
                    {content && (
255
                        <i className='fa fa-times' onClick={() => ctx.navigation.goto('.', {search: null}, {replace: true})} style={{cursor: 'pointer', marginLeft: '5px'}} />
256
                    )}
257
                </div>
258
            )}
259
            wrapperProps={{className: 'applications-list__search-wrapper'}}
260
            renderItem={item => (
261
                <React.Fragment>
262
                    <i className='icon argo-icon-application' /> {item.label}
263
                </React.Fragment>
264
            )}
265
            onSelect={val => {
266
                ctx.navigation.goto(`./${val}`);
267
            }}
268
            onChange={e => ctx.navigation.goto('.', {search: e.target.value}, {replace: true})}
269
            value={content || ''}
270
            items={apps.map(app => AppUtils.appQualifiedName(app, useAuthSettingsCtx?.appsInAnyNamespaceEnabled))}
271
        />
272
    );
273
};
274

275
const FlexTopBar = (props: {toolbar: Toolbar | Observable<Toolbar>}) => {
276
    const ctx = React.useContext(Context);
277
    const loadToolbar = AddAuthToToolbar(props.toolbar, ctx);
278
    return (
279
        <React.Fragment>
280
            <div className='top-bar row flex-top-bar' key='tool-bar'>
281
                <DataLoader load={() => loadToolbar}>
282
                    {toolbar => (
283
                        <React.Fragment>
284
                            <div className='flex-top-bar__actions'>
285
                                {toolbar.actionMenu && (
286
                                    <React.Fragment>
287
                                        {toolbar.actionMenu.items.map((item, i) => (
288
                                            <button
289
                                                disabled={!!item.disabled}
290
                                                qe-id={item.qeId}
291
                                                className='argo-button argo-button--base'
292
                                                onClick={() => item.action()}
293
                                                style={{marginRight: 2}}
294
                                                key={i}>
295
                                                {item.iconClassName && <i className={item.iconClassName} style={{marginLeft: '-5px', marginRight: '5px'}} />}
296
                                                <span className='show-for-large'>{item.title}</span>
297
                                            </button>
298
                                        ))}
299
                                    </React.Fragment>
300
                                )}
301
                            </div>
302
                            <div className='flex-top-bar__tools'>{toolbar.tools}</div>
303
                        </React.Fragment>
304
                    )}
305
                </DataLoader>
306
            </div>
307
            <div className='flex-top-bar__padder' />
308
        </React.Fragment>
309
    );
310
};
311

312
export const ApplicationsList = (props: RouteComponentProps<{}>) => {
313
    const query = new URLSearchParams(props.location.search);
314
    const appInput = tryJsonParse(query.get('new'));
315
    const syncAppsInput = tryJsonParse(query.get('syncApps'));
316
    const refreshAppsInput = tryJsonParse(query.get('refreshApps'));
317
    const [createApi, setCreateApi] = React.useState(null);
318
    const clusters = React.useMemo(() => services.clusters.list(), []);
319
    const [isAppCreatePending, setAppCreatePending] = React.useState(false);
320
    const loaderRef = React.useRef<DataLoader>();
321
    const {List, Summary, Tiles} = AppsListViewKey;
322

323
    function refreshApp(appName: string, appNamespace: string) {
324
        // app refreshing might be done too quickly so that UI might miss it due to event batching
325
        // add refreshing annotation in the UI to improve user experience
326
        if (loaderRef.current) {
327
            const applications = loaderRef.current.getData() as models.Application[];
328
            const app = applications.find(item => item.metadata.name === appName && item.metadata.namespace === appNamespace);
329
            if (app) {
330
                AppUtils.setAppRefreshing(app);
331
                loaderRef.current.setData(applications);
332
            }
333
        }
334
        services.applications.get(appName, appNamespace, 'normal');
335
    }
336

337
    function onFilterPrefChanged(ctx: ContextApis, newPref: AppsListPreferences) {
338
        services.viewPreferences.updatePreferences({appList: newPref});
339
        ctx.navigation.goto(
340
            '.',
341
            {
342
                proj: newPref.projectsFilter.join(','),
343
                sync: newPref.syncFilter.join(','),
344
                autoSync: newPref.autoSyncFilter.join(','),
345
                health: newPref.healthFilter.join(','),
346
                namespace: newPref.namespacesFilter.join(','),
347
                cluster: newPref.clustersFilter.join(','),
348
                labels: newPref.labelsFilter.map(encodeURIComponent).join(',')
349
            },
350
            {replace: true}
351
        );
352
    }
353

354
    function getPageTitle(view: string) {
355
        switch (view) {
356
            case List:
357
                return 'Applications List';
358
            case Tiles:
359
                return 'Applications Tiles';
360
            case Summary:
361
                return 'Applications Summary';
362
        }
363
        return '';
364
    }
365

366
    const sidebarTarget = useSidebarTarget();
367

368
    return (
369
        <ClusterCtx.Provider value={clusters}>
370
            <KeybindingProvider>
371
                <Consumer>
372
                    {ctx => (
373
                        <ViewPref>
374
                            {pref => (
375
                                <Page
376
                                    key={pref.view}
377
                                    title={getPageTitle(pref.view)}
378
                                    useTitleOnly={true}
379
                                    toolbar={{breadcrumbs: [{title: 'Applications', path: '/applications'}]}}
380
                                    hideAuth={true}>
381
                                    <DataLoader
382
                                        input={pref.projectsFilter?.join(',')}
383
                                        ref={loaderRef}
384
                                        load={() => AppUtils.handlePageVisibility(() => loadApplications(pref.projectsFilter, query.get('appNamespace')))}
385
                                        loadingRenderer={() => (
386
                                            <div className='argo-container'>
387
                                                <MockupList height={100} marginTop={30} />
388
                                            </div>
389
                                        )}>
390
                                        {(applications: models.Application[]) => {
391
                                            const healthBarPrefs = pref.statusBarView || ({} as HealthStatusBarPreferences);
392
                                            const {filteredApps, filterResults} = filterApps(applications, pref, pref.search);
393
                                            return (
394
                                                <React.Fragment>
395
                                                    <FlexTopBar
396
                                                        toolbar={{
397
                                                            tools: (
398
                                                                <React.Fragment key='app-list-tools'>
399
                                                                    <Query>{q => <SearchBar content={q.get('search')} apps={applications} ctx={ctx} />}</Query>
400
                                                                    <Tooltip content='Toggle Health Status Bar'>
401
                                                                        <button
402
                                                                            className={`applications-list__accordion argo-button argo-button--base${
403
                                                                                healthBarPrefs.showHealthStatusBar ? '-o' : ''
404
                                                                            }`}
405
                                                                            style={{border: 'none'}}
406
                                                                            onClick={() => {
407
                                                                                healthBarPrefs.showHealthStatusBar = !healthBarPrefs.showHealthStatusBar;
408
                                                                                services.viewPreferences.updatePreferences({
409
                                                                                    appList: {
410
                                                                                        ...pref,
411
                                                                                        statusBarView: {
412
                                                                                            ...healthBarPrefs,
413
                                                                                            showHealthStatusBar: healthBarPrefs.showHealthStatusBar
414
                                                                                        }
415
                                                                                    }
416
                                                                                });
417
                                                                            }}>
418
                                                                            <i className={`fas fa-ruler-horizontal`} />
419
                                                                        </button>
420
                                                                    </Tooltip>
421
                                                                    <div className='applications-list__view-type' style={{marginLeft: 'auto'}}>
422
                                                                        <i
423
                                                                            className={classNames('fa fa-th', {selected: pref.view === Tiles}, 'menu_icon')}
424
                                                                            title='Tiles'
425
                                                                            onClick={() => {
426
                                                                                ctx.navigation.goto('.', {view: Tiles});
427
                                                                                services.viewPreferences.updatePreferences({appList: {...pref, view: Tiles}});
428
                                                                            }}
429
                                                                        />
430
                                                                        <i
431
                                                                            className={classNames('fa fa-th-list', {selected: pref.view === List}, 'menu_icon')}
432
                                                                            title='List'
433
                                                                            onClick={() => {
434
                                                                                ctx.navigation.goto('.', {view: List});
435
                                                                                services.viewPreferences.updatePreferences({appList: {...pref, view: List}});
436
                                                                            }}
437
                                                                        />
438
                                                                        <i
439
                                                                            className={classNames('fa fa-chart-pie', {selected: pref.view === Summary}, 'menu_icon')}
440
                                                                            title='Summary'
441
                                                                            onClick={() => {
442
                                                                                ctx.navigation.goto('.', {view: Summary});
443
                                                                                services.viewPreferences.updatePreferences({appList: {...pref, view: Summary}});
444
                                                                            }}
445
                                                                        />
446
                                                                    </div>
447
                                                                </React.Fragment>
448
                                                            ),
449
                                                            actionMenu: {
450
                                                                items: [
451
                                                                    {
452
                                                                        title: 'New App',
453
                                                                        iconClassName: 'fa fa-plus',
454
                                                                        qeId: 'applications-list-button-new-app',
455
                                                                        action: () => ctx.navigation.goto('.', {new: '{}'}, {replace: true})
456
                                                                    },
457
                                                                    {
458
                                                                        title: 'Sync Apps',
459
                                                                        iconClassName: 'fa fa-sync',
460
                                                                        action: () => ctx.navigation.goto('.', {syncApps: true}, {replace: true})
461
                                                                    },
462
                                                                    {
463
                                                                        title: 'Refresh Apps',
464
                                                                        iconClassName: 'fa fa-redo',
465
                                                                        action: () => ctx.navigation.goto('.', {refreshApps: true}, {replace: true})
466
                                                                    }
467
                                                                ]
468
                                                            }
469
                                                        }}
470
                                                    />
471
                                                    <div className='applications-list'>
472
                                                        {applications.length === 0 && pref.projectsFilter?.length === 0 && (pref.labelsFilter || []).length === 0 ? (
473
                                                            <EmptyState icon='argo-icon-application'>
474
                                                                <h4>No applications available to you just yet</h4>
475
                                                                <h5>Create new application to start managing resources in your cluster</h5>
476
                                                                <button
477
                                                                    qe-id='applications-list-button-create-application'
478
                                                                    className='argo-button argo-button--base'
479
                                                                    onClick={() => ctx.navigation.goto('.', {new: JSON.stringify({})}, {replace: true})}>
480
                                                                    Create application
481
                                                                </button>
482
                                                            </EmptyState>
483
                                                        ) : (
484
                                                            <>
485
                                                                {ReactDOM.createPortal(
486
                                                                    <DataLoader load={() => services.viewPreferences.getPreferences()}>
487
                                                                        {allpref => (
488
                                                                            <ApplicationsFilter
489
                                                                                apps={filterResults}
490
                                                                                onChange={newPrefs => onFilterPrefChanged(ctx, newPrefs)}
491
                                                                                pref={pref}
492
                                                                                collapsed={allpref.hideSidebar}
493
                                                                            />
494
                                                                        )}
495
                                                                    </DataLoader>,
496
                                                                    sidebarTarget?.current
497
                                                                )}
498

499
                                                                {(pref.view === 'summary' && <ApplicationsSummary applications={filteredApps} />) || (
500
                                                                    <Paginate
501
                                                                        header={filteredApps.length > 1 && <ApplicationsStatusBar applications={filteredApps} />}
502
                                                                        showHeader={healthBarPrefs.showHealthStatusBar}
503
                                                                        preferencesKey='applications-list'
504
                                                                        page={pref.page}
505
                                                                        emptyState={() => (
506
                                                                            <EmptyState icon='fa fa-search'>
507
                                                                                <h4>No matching applications found</h4>
508
                                                                                <h5>
509
                                                                                    Change filter criteria or&nbsp;
510
                                                                                    <a
511
                                                                                        onClick={() => {
512
                                                                                            AppsListPreferences.clearFilters(pref);
513
                                                                                            onFilterPrefChanged(ctx, pref);
514
                                                                                        }}>
515
                                                                                        clear filters
516
                                                                                    </a>
517
                                                                                </h5>
518
                                                                            </EmptyState>
519
                                                                        )}
520
                                                                        sortOptions={[
521
                                                                            {title: 'Name', compare: (a, b) => a.metadata.name.localeCompare(b.metadata.name)},
522
                                                                            {
523
                                                                                title: 'Created At',
524
                                                                                compare: (b, a) => a.metadata.creationTimestamp.localeCompare(b.metadata.creationTimestamp)
525
                                                                            },
526
                                                                            {
527
                                                                                title: 'Synchronized',
528
                                                                                compare: (b, a) =>
529
                                                                                    a.status.operationState?.finishedAt?.localeCompare(b.status.operationState?.finishedAt)
530
                                                                            }
531
                                                                        ]}
532
                                                                        data={filteredApps}
533
                                                                        onPageChange={page => ctx.navigation.goto('.', {page})}>
534
                                                                        {data =>
535
                                                                            (pref.view === 'tiles' && (
536
                                                                                <ApplicationTiles
537
                                                                                    applications={data}
538
                                                                                    syncApplication={(appName, appNamespace) =>
539
                                                                                        ctx.navigation.goto('.', {syncApp: appName, appNamespace}, {replace: true})
540
                                                                                    }
541
                                                                                    refreshApplication={refreshApp}
542
                                                                                    deleteApplication={(appName, appNamespace) =>
543
                                                                                        AppUtils.deleteApplication(appName, appNamespace, ctx)
544
                                                                                    }
545
                                                                                />
546
                                                                            )) || (
547
                                                                                <ApplicationsTable
548
                                                                                    applications={data}
549
                                                                                    syncApplication={(appName, appNamespace) =>
550
                                                                                        ctx.navigation.goto('.', {syncApp: appName, appNamespace}, {replace: true})
551
                                                                                    }
552
                                                                                    refreshApplication={refreshApp}
553
                                                                                    deleteApplication={(appName, appNamespace) =>
554
                                                                                        AppUtils.deleteApplication(appName, appNamespace, ctx)
555
                                                                                    }
556
                                                                                />
557
                                                                            )
558
                                                                        }
559
                                                                    </Paginate>
560
                                                                )}
561
                                                            </>
562
                                                        )}
563
                                                        <ApplicationsSyncPanel
564
                                                            key='syncsPanel'
565
                                                            show={syncAppsInput}
566
                                                            hide={() => ctx.navigation.goto('.', {syncApps: null}, {replace: true})}
567
                                                            apps={filteredApps}
568
                                                        />
569
                                                        <ApplicationsRefreshPanel
570
                                                            key='refreshPanel'
571
                                                            show={refreshAppsInput}
572
                                                            hide={() => ctx.navigation.goto('.', {refreshApps: null}, {replace: true})}
573
                                                            apps={filteredApps}
574
                                                        />
575
                                                    </div>
576
                                                    <ObservableQuery>
577
                                                        {q => (
578
                                                            <DataLoader
579
                                                                load={() =>
580
                                                                    q.pipe(
581
                                                                        mergeMap(params => {
582
                                                                            const syncApp = params.get('syncApp');
583
                                                                            const appNamespace = params.get('appNamespace');
584
                                                                            return (syncApp && from(services.applications.get(syncApp, appNamespace))) || from([null]);
585
                                                                        })
586
                                                                    )
587
                                                                }>
588
                                                                {app => (
589
                                                                    <ApplicationSyncPanel
590
                                                                        key='syncPanel'
591
                                                                        application={app}
592
                                                                        selectedResource={'all'}
593
                                                                        hide={() => ctx.navigation.goto('.', {syncApp: null}, {replace: true})}
594
                                                                    />
595
                                                                )}
596
                                                            </DataLoader>
597
                                                        )}
598
                                                    </ObservableQuery>
599
                                                    <SlidingPanel
600
                                                        isShown={!!appInput}
601
                                                        onClose={() => ctx.navigation.goto('.', {new: null}, {replace: true})}
602
                                                        header={
603
                                                            <div>
604
                                                                <button
605
                                                                    qe-id='applications-list-button-create'
606
                                                                    className='argo-button argo-button--base'
607
                                                                    disabled={isAppCreatePending}
608
                                                                    onClick={() => createApi && createApi.submitForm(null)}>
609
                                                                    <Spinner show={isAppCreatePending} style={{marginRight: '5px'}} />
610
                                                                    Create
611
                                                                </button>{' '}
612
                                                                <button
613
                                                                    qe-id='applications-list-button-cancel'
614
                                                                    onClick={() => ctx.navigation.goto('.', {new: null}, {replace: true})}
615
                                                                    className='argo-button argo-button--base-o'>
616
                                                                    Cancel
617
                                                                </button>
618
                                                            </div>
619
                                                        }>
620
                                                        {appInput && (
621
                                                            <ApplicationCreatePanel
622
                                                                getFormApi={api => {
623
                                                                    setCreateApi(api);
624
                                                                }}
625
                                                                createApp={async app => {
626
                                                                    setAppCreatePending(true);
627
                                                                    try {
628
                                                                        await services.applications.create(app);
629
                                                                        ctx.navigation.goto('.', {new: null}, {replace: true});
630
                                                                    } catch (e) {
631
                                                                        ctx.notifications.show({
632
                                                                            content: <ErrorNotification title='Unable to create application' e={e} />,
633
                                                                            type: NotificationType.Error
634
                                                                        });
635
                                                                    } finally {
636
                                                                        setAppCreatePending(false);
637
                                                                    }
638
                                                                }}
639
                                                                app={appInput}
640
                                                                onAppChanged={app => ctx.navigation.goto('.', {new: JSON.stringify(app)}, {replace: true})}
641
                                                            />
642
                                                        )}
643
                                                    </SlidingPanel>
644
                                                </React.Fragment>
645
                                            );
646
                                        }}
647
                                    </DataLoader>
648
                                </Page>
649
                            )}
650
                        </ViewPref>
651
                    )}
652
                </Consumer>
653
            </KeybindingProvider>
654
        </ClusterCtx.Provider>
655
    );
656
};
657

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.