Compare commits

...

2 Commits

Author SHA1 Message Date
Remington Breeze
0198e756d5 remove comment block
Signed-off-by: Remington Breeze <remington@breeze.software>
2023-02-27 16:18:39 -08:00
Remington Breeze
dcdc2d6255 feat(ui): improve top bar button design
Signed-off-by: Remington Breeze <remington@breeze.software>
2023-02-24 15:26:02 -08:00
5 changed files with 275 additions and 174 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>
)}

View File

@@ -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%;
}

View File

@@ -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>
);