Skip to content

Commit

Permalink
fix: Coalesce UI filtering menus (#4972)
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Behar <simbeh7@gmail.com>
  • Loading branch information
simster7 committed Feb 2, 2021
1 parent 7710a2c commit ccd669e
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,10 @@ export const EventFlowPage = ({history, location, match}: RouteComponentProps<an
storageScope='events'
classNames='events'
graph={graph}
nodeGenresTitle={'Type'}
nodeGenres={genres}
nodeClassNames={{'': true, 'Pending': true, 'Ready': true, 'Running': true, 'Failed': true, 'Succeeded': true, 'Error': true}}
nodeClassNamesTitle={'Status'}
nodeClassNames={{Pending: true, Ready: true, Running: true, Failed: true, Succeeded: true, Error: true}}
iconShapes={{workflow: 'circle', collapsed: 'circle', conditions: 'circle'}}
horizontal={true}
selectedNode={selectedNode}
Expand Down
67 changes: 67 additions & 0 deletions ui/src/app/shared/components/dropdown/dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@import 'node_modules/argo-ui/src/styles/config';

.argo-dropdown {
display: inline-block;

&__anchor {
cursor: pointer;
}

&__content {
position: fixed;
z-index: $notifications-z-index;
padding: 1rem;
background-color: $white-color;
box-shadow: 0 0 4px rgba(#000, .2);
transition: opacity .2s, transform .2s, visibility .2s;

&:not(.opened) {
transform: translateY(-30%);
opacity: 0;
visibility: hidden;
transition: opacity .2s, transform .2s .2s, visibility .2s;
}

&.is-menu {
overflow: auto;
max-height: 600px;
padding: 10px;
border: 0;

ul {
margin: 0;
list-style-type: none;
white-space: nowrap;
text-align: left;
cursor: pointer;
min-width: 150px;
column-count: 3;
column-gap: 10px;

li {
padding: 0.5em 1em;
font-size: 14px;
color: $argo-color-gray-6;
cursor: pointer;
border-bottom: none !important;

i {
margin-right: 2px;
}

&.title {
font-weight: bolder;
}

&:hover {
background-color: $argo-color-gray-1;
}

&:last-child {
border-bottom: none;
}
}
}
}
}
}
127 changes: 127 additions & 0 deletions ui/src/app/shared/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as classNames from 'classnames';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {BehaviorSubject, Observable, Subscription} from 'rxjs';

export interface DropDownProps {
isMenu?: boolean;
anchor: React.ComponentType;
children: React.ReactNode | (() => React.ReactNode);
qeId?: string;
}

export interface DropDownState {
opened: boolean;
left: number;
top: number;
}

require('./dropdown.scss');

const dropDownOpened = new BehaviorSubject<DropDown>(null);

export class DropDown extends React.Component<DropDownProps, DropDownState> {
private el: HTMLDivElement;
private content: HTMLDivElement;
private subscriptions: Subscription[];

constructor(props: DropDownProps) {
super(props);
this.state = {opened: false, left: 0, top: 0};
}

public render() {
let children: React.ReactNode = null;
if (typeof this.props.children === 'function') {
if (this.state.opened) {
const fun = this.props.children as () => React.ReactNode;
children = fun();
}
} else {
children = this.props.children as React.ReactNode;
}

return (
<div className='argo-dropdown' ref={el => (this.el = el)}>
<div
qe-id={this.props.qeId}
className='argo-dropdown__anchor'
onClick={event => {
this.open();
event.stopPropagation();
}}>
<this.props.anchor />
</div>
{ReactDOM.createPortal(
<div
className={classNames('argo-dropdown__content', {'opened': this.state.opened, 'is-menu': this.props.isMenu})}
style={{top: this.state.top, left: this.state.left}}
ref={el => (this.content = el)}>
{children}
</div>,
document.body
)}
</div>
);
}

public componentWillMount() {
this.subscriptions = [
Observable.merge(
dropDownOpened.filter(dropdown => dropdown !== this),
Observable.fromEvent(document, 'click').filter((event: Event) => {
return this.content && this.state.opened && !this.content.contains(event.target as Node) && !this.el.contains(event.target as Node);
})
).subscribe(() => {
this.close();
}),
Observable.fromEvent(document, 'scroll', true).subscribe(() => {
if (this.state.opened && this.content && this.el) {
this.setState(this.refreshState());
}
})
];
}

public componentWillUnmount() {
(this.subscriptions || []).forEach(s => s.unsubscribe());
this.subscriptions = [];
}

public close() {
this.setState({opened: false});
}

private refreshState() {
const anchor = this.el.querySelector('.argo-dropdown__anchor') as HTMLElement;
const {top, left} = anchor.getBoundingClientRect();
const anchorHeight = anchor.offsetHeight + 2;

const newState = {left: this.state.left, top: this.state.top, opened: this.state.opened};
// Set top position
if (this.content.offsetHeight + top + anchorHeight > window.innerHeight) {
newState.top = top - this.content.offsetHeight - 2;
} else {
newState.top = top + anchorHeight;
}

// Set left position
if (this.content.offsetWidth + left > window.innerWidth) {
newState.left = left - this.content.offsetWidth + anchor.offsetWidth;
} else {
newState.left = left;
}
return newState;
}

private open() {
if (!this.content || !this.el) {
return;
}

const newState = this.refreshState();
newState.opened = true;
this.setState(newState);
dropDownOpened.next(this);
}
}
61 changes: 47 additions & 14 deletions ui/src/app/shared/components/filter-drop-down.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import {DropDown} from 'argo-ui/src/components/dropdown/dropdown';
import {Checkbox} from 'argo-ui';
import * as classNames from 'classnames';
import * as React from 'react';
import {CheckboxList} from './checkbox-list';
import {DropDown} from './dropdown/dropdown';

export const FilterDropDown = (props: {values: {[label: string]: boolean}; onChange: (label: string, checked: boolean) => void}) => (
<DropDown
isMenu={true}
anchor={() => (
<div className={'top-bar__filter' + (props.values.size > props.values.size ? ' top-bar__filter--selected' : '')}>
<i className='argo-icon-filter' />
<i className='fa fa-angle-down' />
</div>
)}>
<CheckboxList values={props.values} onChange={(label, checked) => props.onChange(label, checked)} />
</DropDown>
);
interface FilterDropDownProps {
sections: FilterDropSection[];
}

export interface FilterDropSection {
title: string;
values: {[label: string]: boolean};
onChange: (label: string, checked: boolean) => void;
}

export const FilterDropDown = (props: FilterDropDownProps) => {
return (
<DropDown
isMenu={true}
anchor={() => (
<div className={classNames('top-bar__filter', {'top-bar__filter--selected': true})} title='Filter'>
<i className='argo-icon-filter' aria-hidden='true' />
<i className='fa fa-angle-down' aria-hidden='true' />
</div>
)}>
<ul>
{props.sections
.filter(item => item.values)
.map((item, i) => (
<div key={i}>
<li className={classNames('top-bar__filter-item', {title: true})}>
<span>{item.title}</span>
</li>
{Object.entries(item.values)
.sort()
.map(([label, checked]) => (
<li className={classNames('top-bar__filter-item')}>
<React.Fragment>
<Checkbox id={`filter__${label}`} checked={checked} onChange={v => item.onChange(label, v)} />
<label htmlFor={`filter__${label}`}>{label}</label>
</React.Fragment>
</li>
))}
</div>
))}
</ul>
</DropDown>
);
};
68 changes: 36 additions & 32 deletions ui/src/app/shared/components/graph/graph-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface NodeGenres {
interface NodeClassNames {
[type: string]: boolean;
}

interface NodeTags {
[key: string]: boolean;
}
Expand All @@ -28,8 +29,11 @@ interface Props {
storageScope: string; // the scope of storage, similar graphs should use the same vaulue
options?: React.ReactNode; // add to the option panel
classNames?: string;
nodeGenresTitle: string;
nodeGenres: NodeGenres;
nodeClassNamesTitle?: string;
nodeClassNames?: NodeClassNames;
nodeTagsTitle?: string;
nodeTags?: NodeTags;
nodeSize?: number; // default "64"
horizontal?: boolean; // default "false"
Expand Down Expand Up @@ -82,39 +86,39 @@ export const GraphPanel = (props: Props) => {
{!props.hideOptions && (
<div className='graph-options-panel'>
<FilterDropDown
key='types'
values={nodeGenres}
onChange={(label, checked) => {
setNodeGenres(v => {
v[label] = checked;
return Object.assign({}, v);
});
}}
sections={[
{
title: props.nodeGenresTitle,
values: nodeGenres,
onChange: (label, checked) => {
setNodeGenres(v => {
v[label] = checked;
return Object.assign({}, v);
});
}
},
{
title: props.nodeClassNamesTitle,
values: nodeClassNames,
onChange: (label, checked) => {
setNodeClassNames(v => {
v[label] = checked;
return Object.assign({}, v);
});
}
},
{
title: props.nodeTagsTitle,
values: nodeTags,
onChange: (label, checked) => {
setNodeTags(v => {
v[label] = checked;
return Object.assign({}, v);
});
}
}
]}
/>
{nodeClassNames && (
<FilterDropDown
key='class-names'
values={nodeClassNames}
onChange={(label, checked) => {
setNodeClassNames(v => {
v[label] = checked;
return Object.assign({}, v);
});
}}
/>
)}
{nodeTags && (
<FilterDropDown
key='annotations'
values={nodeTags}
onChange={(label, checked) => {
setNodeTags(v => {
v[label] = checked;
return Object.assign({}, v);
});
}}
/>
)}
<a onClick={() => setHorizontal(s => !s)} title='Horizontal/vertical layout'>
<i className={`fa ${horizontal ? 'fa-long-arrow-alt-right' : 'fa-long-arrow-alt-down'}`} />
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const WorkflowEventBindings = ({match, location, history}: RouteComponent
<GraphPanel
storageScope='workflow-event-bindings'
graph={g}
nodeGenresTitle={'Type'}
nodeGenres={{'event': true, 'template': true, 'cluster-template': true}}
horizontal={true}
onNodeSelect={id => {
Expand Down
3 changes: 3 additions & 0 deletions ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ export class WorkflowDag extends React.Component<WorkflowDagProps, WorkflowDagRe
<GraphPanel
storageScope='workflow-dag'
graph={this.graph}
nodeGenresTitle={'Node Type'}
nodeGenres={genres}
nodeClassNamesTitle={'Node Phase'}
nodeClassNames={classNames}
nodeTagsTitle={'Template'}
nodeTags={tags}
nodeSize={this.props.nodeSize || 32}
defaultIconShape='circle'
Expand Down

0 comments on commit ccd669e

Please sign in to comment.