mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 17:48:47 +01:00
Compare commits
2 Commits
master
...
top-bar-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0198e756d5 | ||
|
|
dcdc2d6255 |
@@ -1,4 +1,4 @@
|
||||
import {DropDownMenu, NotificationType, SlidingPanel, Tooltip} from 'argo-ui';
|
||||
import {DropDownMenu, NotificationType, SlidingPanel} from 'argo-ui';
|
||||
import * as classNames from 'classnames';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
@@ -30,7 +30,7 @@ import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown
|
||||
import {useSidebarTarget} from '../../../sidebar/sidebar';
|
||||
|
||||
import './application-details.scss';
|
||||
import {AppViewExtension, ExtensionComponentProps} from '../../../shared/services/extensions-service';
|
||||
import {AppViewExtension} from '../../../shared/services/extensions-service';
|
||||
|
||||
interface ApplicationDetailsState {
|
||||
page: number;
|
||||
@@ -683,11 +683,11 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
|
||||
iconClassName: 'fa fa-history',
|
||||
title: hasMultipleSources ? (
|
||||
<React.Fragment>
|
||||
<ActionMenuItem actionLabel=' History and rollback' />
|
||||
<ActionMenuItem actionLabel=' History and Rollback' />
|
||||
{helpTip('Rollback is not supported for apps with multiple sources')}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<ActionMenuItem actionLabel='History and rollback' />
|
||||
<ActionMenuItem actionLabel='History and Rollback' />
|
||||
),
|
||||
action: () => {
|
||||
this.setRollbackPanelVisible(0);
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
&__empty-state {
|
||||
text-align: center;
|
||||
}
|
||||
&__tools {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__entry {
|
||||
padding-left: 1em;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Autocomplete, ErrorNotification, MockupList, NotificationType, SlidingPanel, Toolbar, Tooltip} from 'argo-ui';
|
||||
import {Autocomplete, ErrorNotification, MockupList, NotificationType, SlidingPanel, Tooltip} from 'argo-ui';
|
||||
import * as classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
@@ -6,8 +6,8 @@ import {Key, KeybindingContext, KeybindingProvider} from 'argo-ui/v2';
|
||||
import {RouteComponentProps} from 'react-router';
|
||||
import {combineLatest, from, merge, Observable} from 'rxjs';
|
||||
import {bufferTime, delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators';
|
||||
import {AddAuthToToolbar, ClusterCtx, DataLoader, EmptyState, ObservableQuery, Page, Paginate, Query, Spinner} from '../../../shared/components';
|
||||
import {AuthSettingsCtx, Consumer, Context, ContextApis} from '../../../shared/context';
|
||||
import {ClusterCtx, DataLoader, EmptyState, ObservableQuery, Page, Paginate, Query, Spinner} from '../../../shared/components';
|
||||
import {AuthSettingsCtx, Consumer, ContextApis} from '../../../shared/context';
|
||||
import * as models from '../../../shared/models';
|
||||
import {AppsListViewKey, AppsListPreferences, AppsListViewType, HealthStatusBarPreferences, services} from '../../../shared/services';
|
||||
import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel';
|
||||
@@ -272,43 +272,6 @@ const SearchBar = (props: {content: string; ctx: ContextApis; apps: models.Appli
|
||||
);
|
||||
};
|
||||
|
||||
const FlexTopBar = (props: {toolbar: Toolbar | Observable<Toolbar>}) => {
|
||||
const ctx = React.useContext(Context);
|
||||
const loadToolbar = AddAuthToToolbar(props.toolbar, ctx);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='top-bar row flex-top-bar' key='tool-bar'>
|
||||
<DataLoader load={() => loadToolbar}>
|
||||
{toolbar => (
|
||||
<React.Fragment>
|
||||
<div className='flex-top-bar__actions'>
|
||||
{toolbar.actionMenu && (
|
||||
<React.Fragment>
|
||||
{toolbar.actionMenu.items.map((item, i) => (
|
||||
<button
|
||||
disabled={!!item.disabled}
|
||||
qe-id={item.qeId}
|
||||
className='argo-button argo-button--base'
|
||||
onClick={() => item.action()}
|
||||
style={{marginRight: 2}}
|
||||
key={i}>
|
||||
{item.iconClassName && <i className={item.iconClassName} style={{marginLeft: '-5px', marginRight: '5px'}} />}
|
||||
<span className='show-for-large'>{item.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-top-bar__tools'>{toolbar.tools}</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DataLoader>
|
||||
</div>
|
||||
<div className='flex-top-bar__padder' />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const ApplicationsList = (props: RouteComponentProps<{}>) => {
|
||||
const query = new URLSearchParams(props.location.search);
|
||||
const appInput = tryJsonParse(query.get('new'));
|
||||
@@ -372,102 +335,98 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
|
||||
{ctx => (
|
||||
<ViewPref>
|
||||
{pref => (
|
||||
<Page
|
||||
key={pref.view}
|
||||
title={getPageTitle(pref.view)}
|
||||
useTitleOnly={true}
|
||||
toolbar={{breadcrumbs: [{title: 'Applications', path: '/applications'}]}}
|
||||
hideAuth={true}>
|
||||
<DataLoader
|
||||
input={pref.projectsFilter?.join(',')}
|
||||
ref={loaderRef}
|
||||
load={() => AppUtils.handlePageVisibility(() => loadApplications(pref.projectsFilter, query.get('appNamespace')))}
|
||||
loadingRenderer={() => (
|
||||
<div className='argo-container'>
|
||||
<MockupList height={100} marginTop={30} />
|
||||
</div>
|
||||
)}>
|
||||
{(applications: models.Application[]) => {
|
||||
const healthBarPrefs = pref.statusBarView || ({} as HealthStatusBarPreferences);
|
||||
const {filteredApps, filterResults} = filterApps(applications, pref, pref.search);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FlexTopBar
|
||||
toolbar={{
|
||||
tools: (
|
||||
<React.Fragment key='app-list-tools'>
|
||||
<Query>{q => <SearchBar content={q.get('search')} apps={applications} ctx={ctx} />}</Query>
|
||||
<Tooltip content='Toggle Health Status Bar'>
|
||||
<button
|
||||
className={`applications-list__accordion argo-button argo-button--base${
|
||||
healthBarPrefs.showHealthStatusBar ? '-o' : ''
|
||||
}`}
|
||||
style={{border: 'none'}}
|
||||
onClick={() => {
|
||||
healthBarPrefs.showHealthStatusBar = !healthBarPrefs.showHealthStatusBar;
|
||||
services.viewPreferences.updatePreferences({
|
||||
appList: {
|
||||
...pref,
|
||||
statusBarView: {
|
||||
...healthBarPrefs,
|
||||
showHealthStatusBar: healthBarPrefs.showHealthStatusBar
|
||||
}
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<i className={`fas fa-ruler-horizontal`} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className='applications-list__view-type' style={{marginLeft: 'auto'}}>
|
||||
<i
|
||||
className={classNames('fa fa-th', {selected: pref.view === Tiles}, 'menu_icon')}
|
||||
title='Tiles'
|
||||
onClick={() => {
|
||||
ctx.navigation.goto('.', {view: Tiles});
|
||||
services.viewPreferences.updatePreferences({appList: {...pref, view: Tiles}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-th-list', {selected: pref.view === List}, 'menu_icon')}
|
||||
title='List'
|
||||
onClick={() => {
|
||||
ctx.navigation.goto('.', {view: List});
|
||||
services.viewPreferences.updatePreferences({appList: {...pref, view: List}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-chart-pie', {selected: pref.view === Summary}, 'menu_icon')}
|
||||
title='Summary'
|
||||
onClick={() => {
|
||||
ctx.navigation.goto('.', {view: Summary});
|
||||
services.viewPreferences.updatePreferences({appList: {...pref, view: Summary}});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
),
|
||||
actionMenu: {
|
||||
items: [
|
||||
{
|
||||
title: 'New App',
|
||||
iconClassName: 'fa fa-plus',
|
||||
qeId: 'applications-list-button-new-app',
|
||||
action: () => ctx.navigation.goto('.', {new: '{}'}, {replace: true})
|
||||
},
|
||||
{
|
||||
title: 'Sync Apps',
|
||||
iconClassName: 'fa fa-sync',
|
||||
action: () => ctx.navigation.goto('.', {syncApps: true}, {replace: true})
|
||||
},
|
||||
{
|
||||
title: 'Refresh Apps',
|
||||
iconClassName: 'fa fa-redo',
|
||||
action: () => ctx.navigation.goto('.', {refreshApps: true}, {replace: true})
|
||||
}
|
||||
]
|
||||
<DataLoader
|
||||
input={pref.projectsFilter?.join(',')}
|
||||
ref={loaderRef}
|
||||
load={() => AppUtils.handlePageVisibility(() => loadApplications(pref.projectsFilter, query.get('appNamespace')))}
|
||||
loadingRenderer={() => (
|
||||
<div className='argo-container'>
|
||||
<MockupList height={100} marginTop={30} />
|
||||
</div>
|
||||
)}>
|
||||
{(applications: models.Application[]) => {
|
||||
const healthBarPrefs = pref.statusBarView || ({} as HealthStatusBarPreferences);
|
||||
const {filteredApps, filterResults} = filterApps(applications, pref, pref.search);
|
||||
return (
|
||||
<Page
|
||||
key={pref.view}
|
||||
title={getPageTitle(pref.view)}
|
||||
toolbar={{
|
||||
tools: (
|
||||
<div className='applications-list__tools'>
|
||||
<Query>{q => <SearchBar content={q.get('search')} apps={applications} ctx={ctx} />}</Query>
|
||||
<Tooltip content='Toggle Health Status Bar'>
|
||||
<button
|
||||
className={`applications-list__accordion argo-button argo-button--base${
|
||||
healthBarPrefs.showHealthStatusBar ? '-o' : ''
|
||||
}`}
|
||||
style={{border: 'none'}}
|
||||
onClick={() => {
|
||||
healthBarPrefs.showHealthStatusBar = !healthBarPrefs.showHealthStatusBar;
|
||||
services.viewPreferences.updatePreferences({
|
||||
appList: {
|
||||
...pref,
|
||||
statusBarView: {
|
||||
...healthBarPrefs,
|
||||
showHealthStatusBar: healthBarPrefs.showHealthStatusBar
|
||||
}
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<i className={`fas fa-ruler-horizontal`} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className='applications-list__view-type' style={{marginLeft: 'auto'}}>
|
||||
<i
|
||||
className={classNames('fa fa-th', {selected: pref.view === Tiles}, 'menu_icon')}
|
||||
title='Tiles'
|
||||
onClick={() => {
|
||||
ctx.navigation.goto('.', {view: Tiles});
|
||||
services.viewPreferences.updatePreferences({appList: {...pref, view: Tiles}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-th-list', {selected: pref.view === List}, 'menu_icon')}
|
||||
title='List'
|
||||
onClick={() => {
|
||||
ctx.navigation.goto('.', {view: List});
|
||||
services.viewPreferences.updatePreferences({appList: {...pref, view: List}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-chart-pie', {selected: pref.view === Summary}, 'menu_icon')}
|
||||
title='Summary'
|
||||
onClick={() => {
|
||||
ctx.navigation.goto('.', {view: Summary});
|
||||
services.viewPreferences.updatePreferences({appList: {...pref, view: Summary}});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
breadcrumbs: [{title: 'Applications', path: '/applications'}],
|
||||
actionMenu: {
|
||||
items: [
|
||||
{
|
||||
title: 'New App',
|
||||
iconClassName: 'fa fa-plus',
|
||||
qeId: 'applications-list-button-new-app',
|
||||
action: () => ctx.navigation.goto('.', {new: '{}'}, {replace: true})
|
||||
},
|
||||
{
|
||||
title: 'Sync Apps',
|
||||
iconClassName: 'fa fa-sync',
|
||||
action: () => ctx.navigation.goto('.', {syncApps: true}, {replace: true})
|
||||
},
|
||||
{
|
||||
title: 'Refresh Apps',
|
||||
iconClassName: 'fa fa-redo',
|
||||
action: () => ctx.navigation.goto('.', {refreshApps: true}, {replace: true})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
]
|
||||
}
|
||||
}}>
|
||||
<React.Fragment>
|
||||
<div className='applications-list'>
|
||||
{applications.length === 0 && pref.projectsFilter?.length === 0 && (pref.labelsFilter || []).length === 0 ? (
|
||||
<EmptyState icon='argo-icon-application'>
|
||||
@@ -642,10 +601,10 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
|
||||
)}
|
||||
</SlidingPanel>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
</Page>
|
||||
</Page>
|
||||
);
|
||||
}}
|
||||
</DataLoader>
|
||||
)}
|
||||
</ViewPref>
|
||||
)}
|
||||
|
||||
@@ -48,30 +48,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.login-logout-button {
|
||||
border-radius: 3px;
|
||||
@include themify($themes) {
|
||||
color: themed('light-argo-teal-7');
|
||||
}
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
color: $argo-color-teal-5;
|
||||
box-shadow: 0 0 0 0.1em $argo-color-teal-5;
|
||||
}
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
color: themed('light-argo-teal-5');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-left: 0 !important;
|
||||
&__top-bar {
|
||||
left: $sidebar-width !important;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background-color: $argo-color-gray-6;
|
||||
&:hover {
|
||||
background-color: $argo-success-color;
|
||||
}
|
||||
|
||||
.fa-right-to-bracket {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sb-page-wrapper {
|
||||
@@ -94,3 +93,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import {DataLoader, Page as ArgoPage, Toolbar, Utils} from 'argo-ui';
|
||||
import {DataLoader, Toolbar, Utils} from 'argo-ui';
|
||||
import * as classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import {Context, ContextApis} from '../../context';
|
||||
import {services} from '../../services';
|
||||
@@ -23,17 +26,19 @@ export const AddAuthToToolbar = (init: Toolbar | Observable<Toolbar>, ctx: Conte
|
||||
toolbar.tools = [
|
||||
toolbar.tools,
|
||||
<DataLoader key='loginPanel' load={() => isLoggedIn()}>
|
||||
{loggedIn =>
|
||||
loggedIn ? (
|
||||
<button className='login-logout-button' key='logout' onClick={() => (window.location.href = requests.toAbsURL('/auth/logout'))}>
|
||||
Log out
|
||||
</button>
|
||||
) : (
|
||||
<button className='login-logout-button' key='login' onClick={() => ctx.navigation.goto(`/login?return_url=${encodeURIComponent(location.href)}`)}>
|
||||
Log in
|
||||
</button>
|
||||
)
|
||||
}
|
||||
{loggedIn => (
|
||||
<div style={{marginLeft: '5px', display: 'inline', flexShrink: 0}}>
|
||||
{loggedIn ? (
|
||||
<button className='page__button' key='logout' onClick={() => (window.location.href = requests.toAbsURL('/auth/logout'))}>
|
||||
<i className='fa fa-right-to-bracket' /> Log Out
|
||||
</button>
|
||||
) : (
|
||||
<button className='page__button' key='login' onClick={() => ctx.navigation.goto(`/login?return_url=${encodeURIComponent(location.href)}`)}>
|
||||
<i className='fa fa-right-to-bracket' /> Log In
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DataLoader>
|
||||
];
|
||||
return toolbar;
|
||||
@@ -67,3 +72,130 @@ export const Page = (props: PageProps) => {
|
||||
</DataLoader>
|
||||
);
|
||||
};
|
||||
|
||||
interface ArgoPageProps extends React.Props<any> {
|
||||
title: string;
|
||||
toolbar?: Toolbar | Observable<Toolbar>;
|
||||
topBarTitle?: string;
|
||||
useTitleOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface PageContextProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const PageContext = React.createContext<PageContextProps>({title: 'Argo'});
|
||||
|
||||
export const ArgoPage = (props: ArgoPageProps) => {
|
||||
const toolbarObservable = props.toolbar && Utils.toObservable(props.toolbar);
|
||||
|
||||
return (
|
||||
<div className={classNames('page', {'page--has-toolbar': !!props.toolbar})}>
|
||||
<React.Fragment>
|
||||
{toolbarObservable && (
|
||||
<DataLoader input={new Date()} load={() => toolbarObservable}>
|
||||
{(toolbar: Toolbar) => (
|
||||
<React.Fragment>
|
||||
<PageContext.Consumer>
|
||||
{ctx => {
|
||||
let titleParts = [ctx.title];
|
||||
if (!props.useTitleOnly && toolbar && toolbar.breadcrumbs && toolbar.breadcrumbs.length > 0) {
|
||||
titleParts = [
|
||||
toolbar.breadcrumbs
|
||||
.map(item => item.title)
|
||||
.reverse()
|
||||
.join(' / ')
|
||||
].concat(titleParts);
|
||||
} else if (props.title) {
|
||||
titleParts = [props.title].concat(titleParts);
|
||||
}
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{titleParts.join(' - ')}</title>
|
||||
</Helmet>
|
||||
);
|
||||
}}
|
||||
</PageContext.Consumer>
|
||||
<div className='page__top-bar'>
|
||||
<TopBar title={props.topBarTitle ? props.topBarTitle : props.title} toolbar={toolbar} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DataLoader>
|
||||
)}
|
||||
<div className='page__content-wrapper'>{props.children}</div>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TopBarProps extends React.Props<any> {
|
||||
title: string;
|
||||
toolbar?: Toolbar;
|
||||
}
|
||||
export interface ActionMenu {
|
||||
className?: string;
|
||||
items: {
|
||||
action: () => any;
|
||||
title: string | React.ReactElement;
|
||||
iconClassName?: string;
|
||||
qeId?: string;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
const renderActionMenu = (actionMenu: ActionMenu) => (
|
||||
<div>
|
||||
{actionMenu.items.map((item, i) => (
|
||||
<button disabled={!!item.disabled} qe-id={item.qeId} className='page__button' onClick={() => item.action()} style={{marginRight: 4}} key={i}>
|
||||
{item.iconClassName && <i className={item.iconClassName} style={{marginLeft: '-5px', marginRight: '5px'}} />}
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const RenderToolbar = (toolbar: Toolbar) => (
|
||||
<div className='top-bar row toolbar' key='tool-bar'>
|
||||
<div className='top-bar__left-side'>{toolbar.actionMenu && renderActionMenu(toolbar.actionMenu)}</div>
|
||||
<div style={{marginLeft: 'auto', marginRight: '1em', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', flexGrow: 1}}>{toolbar.tools}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderBreadcrumbs = (breadcrumbs: {title: string | React.ReactNode; path?: string}[]) => (
|
||||
<div className='top-bar__breadcrumbs'>
|
||||
{(breadcrumbs || []).map((breadcrumb, i) => {
|
||||
const nodes = [];
|
||||
if (i === breadcrumbs.length - 1) {
|
||||
nodes.push(
|
||||
<span key={i} className='top-bar__breadcrumbs-last-item'>
|
||||
{breadcrumb.title}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
nodes.push(
|
||||
<Link key={i} to={breadcrumb.path}>
|
||||
{' '}
|
||||
{breadcrumb.title}{' '}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (i < breadcrumbs.length - 1) {
|
||||
nodes.push(<span key={`${i}_sep`} className='top-bar__sep' />);
|
||||
}
|
||||
return nodes;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const TopBar = (props: TopBarProps) => (
|
||||
<div>
|
||||
<div className='top-bar' key='top-bar'>
|
||||
<div className='row'>
|
||||
<div className='columns top-bar__left-side'>{props.toolbar && props.toolbar.breadcrumbs && renderBreadcrumbs(props.toolbar.breadcrumbs)}</div>
|
||||
<div className='top-bar__title text-truncate top-bar__right-side'>{props.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.toolbar && RenderToolbar(props.toolbar)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user