Skip to content

Commit d5bb692

Browse files
author
David Emory
committed
feat(common): Clean up JobMonitor display and functionality. Addresses #85
1 parent 2332198 commit d5bb692

File tree

5 files changed

+165
-103
lines changed

5 files changed

+165
-103
lines changed

lib/common/components/JobMonitor.js

Lines changed: 117 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,18 @@ export default class JobMonitor extends Pure {
2424
// }
2525
}
2626

27+
componentDidUpdate () {
28+
const jobList = this.refs.jobList
29+
jobList.scrollTop = jobList.scrollHeight
30+
}
31+
32+
// Remove any retired jobs from the monitor
2733
removeAll = () => {
28-
this.props.jobMonitor.retired.forEach(job => this.props.removeRetiredJob(job))
34+
const { jobMonitor } = this.props
35+
jobMonitor.retired.forEach(job => this.props.removeRetiredJob(job))
36+
37+
// If no active jobs left, then close the popover
38+
if (jobMonitor.jobs.length === 0) this.props.close()
2939
}
3040

3141
sortByDate = (a, b) => new Date(b.status.initialized) - new Date(a.status.initialized)
@@ -37,41 +47,59 @@ export default class JobMonitor extends Pure {
3747
<SidebarPopover
3848
ref={(SidebarPopover) => { this.popover = SidebarPopover }}
3949
title='Server Jobs'
40-
{...this.props}>
41-
<ul className='list-unstyled job-list'>
42-
{retired.sort(this.sortByDate).map(job => (
43-
<RetiredJob
44-
key={`retired-${job.jobId}`}
45-
job={job}
46-
removeRetiredJob={removeRetiredJob} />
47-
))}
48-
{jobs.sort(this.sortByDate).map(job => (
49-
<li key={job.jobId} className='job-container'>
50-
<div className='job-spinner-div'>
51-
<Icon type='spinner' className='fa-pulse' />
52-
</div>
53-
<div className='job-container-inner'>
54-
<div>
55-
<strong>{job.name}</strong>
56-
</div>
57-
<ProgressBar
58-
label={`${job.status ? job.status.percentComplete : 0}%`}
59-
active now={job.status ? job.status.percentComplete : 0}
60-
className='job-status-progress-bar' />
61-
<div className='job-status-message' >
62-
{job.status ? job.status.message : 'waiting'}
63-
</div>
64-
</div>
65-
</li>
66-
))}
67-
</ul>
68-
<p className='lead text-center'>{jobs.length ? jobs.length : 'No'} active jobs.</p>
69-
<Button
70-
block
71-
disabled={retired.length === 0}
72-
onClick={this.removeAll}>
73-
<Icon type='times-circle' /> Clear completed
74-
</Button>
50+
fixedHeight={300}
51+
minMarginBottom={75}
52+
{...this.props}
53+
>
54+
<div className='job-monitor'>
55+
{/* The main list of jobs */}
56+
<div className='job-list' ref='jobList'> {/* TODO: replace w/ React 16 createRef() */}
57+
<ul className='list-unstyled'>
58+
{retired.sort(this.sortByDate).map(job => (
59+
<RetiredJob
60+
key={`retired-${job.jobId}`}
61+
job={job}
62+
removeRetiredJob={removeRetiredJob} />
63+
))}
64+
{jobs.sort(this.sortByDate).map(job => {
65+
const pctComplete = Math.round(job.status ? job.status.percentComplete : 0)
66+
return (
67+
<li key={job.jobId} className='job-container'>
68+
<div className='job-spinner-div'>
69+
<Icon type='spinner' className='fa-pulse' />
70+
</div>
71+
<div className='job-container-inner'>
72+
<div>
73+
<strong>{job.name}</strong>
74+
</div>
75+
<ProgressBar active
76+
style={{ width: 190 }}
77+
label={`${pctComplete}%`}
78+
now={pctComplete}
79+
className='job-status-progress-bar' />
80+
<div className='job-status-message' >
81+
{job.status ? job.status.message : 'waiting'}
82+
</div>
83+
</div>
84+
</li>
85+
)
86+
})}
87+
</ul>
88+
</div>
89+
{/* Lower panel job count and clear-all button */}
90+
<div style={{ marginTop: 8 }}>
91+
<Button
92+
style={{ float: 'right' }}
93+
bsSize='small'
94+
disabled={retired.length === 0}
95+
onClick={this.removeAll}>
96+
<Icon type='times-circle' /> Clear completed
97+
</Button>
98+
<div style={{ paddingTop: 6, fontSize: 13 }}>
99+
{jobs.length ? jobs.length : 'No'} active job{!jobs.length || jobs.length > 1 ? 's' : ''}
100+
</div>
101+
</div>
102+
</div>
75103
</SidebarPopover>
76104
)
77105
}
@@ -98,61 +126,67 @@ class RetiredJob extends Pure {
98126
}
99127
</div>
100128
<div className='job-container-inner'>
101-
<div style={{
102-
overflow: 'hidden',
103-
textOverflow: 'ellipsis',
104-
width: '220px',
105-
whiteSpace: 'nowrap'
106-
}}>
129+
<div style={{ float: 'right' }}>
107130
<Button
131+
className='close-job-button'
108132
bsStyle='link'
109-
className='pull-right'
133+
style={{ padding: 'none' }}
110134
onClick={this.removeJob}>
111135
<Icon className='pull-right' type='times-circle' />
112136
</Button>
113-
<strong
114-
title={job.name}>
115-
{job.name}
116-
</strong>
117137
</div>
118-
<div className='job-status-message'>
119-
{job.status.message}
120-
{job.status.exceptionDetails
121-
? <OverlayTrigger
122-
trigger='click'
123-
placement='right'
124-
overlay={
125-
<Popover
126-
id='job-exception-detail'
127-
style={{
128-
minWidth: '400px'
129-
}}
130-
title={
131-
<span>
132-
<Icon type='bug' /> Oh no! Looks like an error has occurred.
138+
<div>
139+
<div style={{
140+
display: 'inline-block',
141+
overflow: 'hidden',
142+
textOverflow: 'ellipsis',
143+
width: 170,
144+
whiteSpace: 'nowrap'
145+
}}>
146+
<strong
147+
title={job.name}>
148+
{job.name}
149+
</strong>
150+
</div>
151+
<div className='job-status-message'>
152+
{job.status.message}
153+
{job.status.exceptionDetails
154+
? <OverlayTrigger
155+
trigger='click'
156+
placement='right'
157+
overlay={
158+
<Popover
159+
id='job-exception-detail'
160+
style={{
161+
minWidth: '400px'
162+
}}
163+
title={
164+
<span>
165+
<Icon type='bug' /> Oh no! Looks like an error has occurred.
166+
</span>
167+
}>
168+
<p>
169+
To submit an error report email a screenshot of your browser
170+
window, the following text (current URL and error details),
171+
and a detailed description of the steps you followed
172+
to <a href='mailto:support@conveyal.com'>support@conveyal.com</a>.
173+
</p>
174+
<p>{window.location.href}</p>
175+
<span style={{whiteSpace: 'pre', fontSize: 'xx-small'}}>
176+
{job.status.exceptionDetails}
133177
</span>
178+
</Popover>
134179
}>
135-
<p>
136-
To submit an error report email a screenshot of your browser
137-
window, the following text (current URL and error details),
138-
and a detailed description of the steps you followed
139-
to <a href='mailto:support@conveyal.com'>support@conveyal.com</a>.
140-
</p>
141-
<p>{window.location.href}</p>
142-
<span style={{whiteSpace: 'pre', fontSize: 'xx-small'}}>
143-
{job.status.exceptionDetails}
144-
</span>
145-
</Popover>
146-
}>
147-
<Button
148-
bsSize='small'
149-
style={{padding: '0px'}}
150-
bsStyle='link'>
151-
<Icon type='bug' />
152-
</Button>
153-
</OverlayTrigger>
154-
: null
155-
}
180+
<Button
181+
bsSize='small'
182+
style={{padding: '0px'}}
183+
bsStyle='link'>
184+
<Icon type='bug' />
185+
</Button>
186+
</OverlayTrigger>
187+
: null
188+
}
189+
</div>
156190
</div>
157191
</div>
158192
</li>

lib/common/components/SidebarPopover.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {Popover} from 'react-bootstrap'
77
export default class SidebarPopover extends Pure {
88
static propTypes = {
99
children: PropTypes.node,
10-
expanded: PropTypes.bool,
11-
target: PropTypes.object,
10+
expanded: PropTypes.bool, // whether the sidebar is expanded, i.e. wider with labels and icons
11+
target: PropTypes.object, // the on-screen element (e.g. an icon) that the popover is triggered by
1212
title: PropTypes.string,
1313
close: PropTypes.func,
1414
visible: PropTypes.bool.isRequired
@@ -18,26 +18,41 @@ export default class SidebarPopover extends Pure {
1818
top: 0,
1919
arrowOffset: 0
2020
}
21+
2122
_onResize = () => {
2223
this.setState({width: window.innerWidth, height: window.innerHeight})
2324
this.reposition()
2425
}
26+
2527
componentWillMount () {
2628
this._onResize()
2729
}
30+
2831
componentDidMount () {
2932
window.addEventListener('resize', this._onResize)
3033
}
34+
3135
componentWillUnmount () {
3236
window.removeEventListener('resize', this._onResize)
3337
}
38+
3439
componentWillReceiveProps (nextProps) {
3540
if (nextProps.visible) this.reposition()
3641
}
42+
3743
reposition () {
3844
const padding = 10 // minimum space between popover and top/bottom of screen
45+
const minMarginBottom = this.props.minMarginBottom || 10
3946

40-
const height = ReactDOM.findDOMNode(this.refs.popover) ? ReactDOM.findDOMNode(this.refs.popover).offsetHeight : 0
47+
// Get the height of the popover itself. Is either a fixed, property-defined
48+
// value or is the default height (i.e. scaled to fit content )
49+
const height = this.props.fixedHeight
50+
? this.props.fixedHeight
51+
: ReactDOM.findDOMNode(this.refs.popover)
52+
? ReactDOM.findDOMNode(this.refs.popover).offsetHeight
53+
: 0
54+
55+
// Get location and height of the popover's target (i.e. trigger) element
4156
const target = ReactDOM.findDOMNode(this.props.target)
4257
const targetTop = target ? target.getBoundingClientRect().top : 0
4358
const targetHeight = target ? target.getBoundingClientRect().bottom - target.getBoundingClientRect().top : 0
@@ -50,9 +65,9 @@ export default class SidebarPopover extends Pure {
5065
top = padding
5166
}
5267

53-
const maxTop = window.innerHeight - padding - height
68+
const maxTop = window.innerHeight - minMarginBottom - height
5469
if (top > maxTop) {
55-
arrowOffset = Math.min(window.innerHeight - padding - 20, arrowOffset + (top - maxTop))
70+
arrowOffset = Math.min(window.innerHeight - minMarginBottom - 20, arrowOffset + (top - maxTop))
5671
top = maxTop
5772
}
5873

@@ -64,14 +79,17 @@ export default class SidebarPopover extends Pure {
6479
}
6580

6681
render () {
82+
const { expanded, fixedHeight, visible } = this.props
6783
const style = {
6884
position: 'fixed',
69-
marginLeft: this.props.expanded ? 160 : 60,
85+
marginLeft: expanded ? 140 : 60,
7086
width: 276, // max from bootstrap
7187
top: this.state.top,
72-
visibility: this.props.visible ? 'visible' : 'hidden'
88+
visibility: visible ? 'visible' : 'hidden'
7389
}
7490

91+
if (fixedHeight) style.height = fixedHeight
92+
7593
const title = (
7694
<div>
7795
<span>{this.props.title}</span>

lib/common/components/StatusMessage.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,23 @@ export default class StatusMessage extends React.Component {
1919
}
2020

2121
render () {
22+
const { message, sidebarExpanded } = this.props
2223
const styles = {
2324
position: 'fixed',
24-
left: '15px',
25+
left: sidebarExpanded ? '140px' : '60px',
2526
bottom: '0px',
2627
height: '60px',
2728
zIndex: 1000
2829
}
2930

3031
return (
3132
<div style={styles}>
32-
{this.props.message && this.state.visible
33+
{message && this.state.visible
3334
? <Button
3435
bsStyle='info'
3536
bsSize='large'
3637
onClick={this.clear}>
37-
{this.props.message}
38+
{message}
3839
</Button>
3940
: null
4041
}

lib/common/containers/CurrentStatusMessage.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import StatusMessage from '../components/StatusMessage'
44

55
const mapStateToProps = (state, ownProps) => {
66
return {
7-
message: state.status.message
7+
message: state.status.message,
8+
sidebarExpanded: state.ui.sidebarExpanded
89
}
910
}
1011

0 commit comments

Comments
 (0)