Skip to content

Commit a2d0ea7

Browse files
authored
Merge pull request #122 from nrotstan/issue-85-busy-indicator-when-tasks-building
Indicate in admin if tasks building. Closes #85
2 parents 0d91e5e + bacc09a commit a2d0ea7

File tree

9 files changed

+192
-80
lines changed

9 files changed

+192
-80
lines changed

src/components/AdminPane/HOCs/WithCurrentChallenge/WithCurrentChallenge.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ const WithCurrentChallenge = function(WrappedComponent,
3333
currentChallengeId = () =>
3434
parseInt(_get(this.props, 'match.params.challengeId'), 10)
3535

36-
componentDidMount() {
36+
loadChallenge = () => {
3737
const challengeId = this.currentChallengeId()
3838

3939
if (!isNaN(challengeId)) {
40+
this.setState({loadingChallenge: true})
4041
const timelineStartDate = subMonths(new Date(), historicalMonths)
4142

4243
Promise.all([
@@ -57,6 +58,10 @@ const WithCurrentChallenge = function(WrappedComponent,
5758
}
5859
}
5960

61+
componentDidMount() {
62+
this.loadChallenge()
63+
}
64+
6065
render() {
6166
const challengeId = this.currentChallengeId()
6267
let challenge = null
@@ -79,6 +84,7 @@ const WithCurrentChallenge = function(WrappedComponent,
7984
clusteredTasks={clusteredTasks}
8085
loadingChallenge={this.state.loadingChallenge}
8186
loadingTasks={this.state.loadingTasks}
87+
refreshStatus={this.loadChallenge}
8288
{..._omit(this.props, ['entities',
8389
'fetchChallenge',
8490
'fetchChallengeComments',

src/components/AdminPane/Manage/ViewChallenge/Messages.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ export default defineMessages({
1919
defaultMessage: "Metrics",
2020
},
2121

22+
asOf: {
23+
id: "Admin.Challenge.status.asOf.label",
24+
defaultMessage: "as of",
25+
},
26+
27+
tasksBuilding: {
28+
id: "Admin.Challenge.tasksBuilding",
29+
defaultMessage: "Tasks Building...",
30+
},
31+
32+
tasksFailed: {
33+
id: "Admin.Challenge.tasksFailed",
34+
defaultMessage: "Tasks Failed to Build",
35+
},
36+
37+
refreshStatusLabel: {
38+
id: "Admin.Challenge.controls.refreshStatus.label",
39+
defaultMessage: "Refresh Status",
40+
},
41+
2242
tasksHeader: {
2343
id: "Admin.Tasks.header",
2444
defaultMessage: "Tasks",

src/components/AdminPane/Manage/ViewChallenge/ViewChallenge.js

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,32 @@
11
import React, { Component } from 'react'
22
import PropTypes from 'prop-types'
3-
import classNames from 'classnames'
43
import { FormattedMessage, injectIntl } from 'react-intl'
54
import _get from 'lodash/get'
6-
import _map from 'lodash/map'
75
import { Link } from 'react-router-dom'
8-
import { TaskStatus,
9-
keysByStatus,
10-
messagesByStatus } from '../../../../services/Task/TaskStatus/TaskStatus'
11-
import { MAPBOX_LIGHT,
12-
layerSourceWithName }
13-
from '../../../../services/VisibleLayer/LayerSources'
146
import WithCurrentChallenge
157
from '../../HOCs/WithCurrentChallenge/WithCurrentChallenge'
168
import WithFilteredClusteredTasks
179
from '../../HOCs/WithFilteredClusteredTasks/WithFilteredClusteredTasks'
18-
import WithBoundedTasks
19-
from '../../HOCs/WithBoundedTasks/WithBoundedTasks'
2010
import Sidebar from '../../../Sidebar/Sidebar'
2111
import CommentList from '../../../CommentList/CommentList'
22-
import MapPane from '../../../EnhancedMap/MapPane/MapPane'
23-
import ChallengeTaskMap from '../ChallengeTaskMap/ChallengeTaskMap'
24-
import TaskAnalysisTable from '../TaskAnalysisTable/TaskAnalysisTable'
2512
import ChallengeOverview from '../ManageChallenges/ChallengeOverview'
2613
import BusySpinner from '../../../BusySpinner/BusySpinner'
2714
import SvgSymbol from '../../../SvgSymbol/SvgSymbol'
2815
import Tabs from '../../../Bulma/Tabs'
2916
import ChallengeMetrics from '../ChallengeMetrics/ChallengeMetrics'
3017
import ConfirmAction from '../../../ConfirmAction/ConfirmAction'
18+
import ViewChallengeTasks from './ViewChallengeTasks'
3119
import manageMessages from '../Messages'
3220
import messages from './Messages'
3321
import './ViewChallenge.css'
3422

35-
const BoundedTaskTable =
36-
WithBoundedTasks(TaskAnalysisTable, 'filteredClusteredTasks', 'taskInfo')
37-
3823
/**
3924
* ViewChallenge displays various challenge details and metrics of interest
4025
* to challenge owners, along with a list of the challenge tasks.
4126
*
4227
* @author [Neil Rotstan](https://github.com/nrotstan)
4328
*/
4429
export class ViewChallenge extends Component {
45-
state = {
46-
mapBounds: null,
47-
mapZoom: null,
48-
renderingProgress: null,
49-
}
50-
51-
/** Invoked by the map when the user pans or zooms */
52-
updateMapBounds = (challengeId, bounds, zoom) =>
53-
this.setState({mapBounds: bounds, mapZoom: zoom})
54-
5530
deleteChallenge = () => {
5631
this.props.deleteChallenge(this.props.challenge.parent.id,
5732
this.props.challenge.id)
@@ -74,36 +49,6 @@ export class ViewChallenge extends Component {
7449
<ChallengeMetrics challenges={[this.props.challenge]} />,
7550
}
7651

77-
// Use CSS Modules once supported by create-react-app
78-
const statusColors = {
79-
[TaskStatus.created]: '#0082C8', // $status-created-color
80-
[TaskStatus.fixed]: '#3CB44B', // $status-fixed-color
81-
[TaskStatus.falsePositive]: '#F58231', // $status-falsePositive-color
82-
[TaskStatus.skipped]: '#FFE119', // $status-skipped-color
83-
[TaskStatus.deleted]: '#46F0F0', // $status-deleted-color
84-
[TaskStatus.alreadyFixed]: '#911EB4', // $status-alreadyFixed-color
85-
[TaskStatus.tooHard]: '#E6194B', // $status-tooHard-color
86-
}
87-
88-
const statusFilters = _map(TaskStatus, status => (
89-
<div key={status} className="status-filter is-narrow">
90-
<div className={classNames("field", keysByStatus[status])}
91-
onClick={() => this.props.toggleIncludedStatus(status)}>
92-
<input className="is-checkradio is-circle has-background-color is-success" type="checkbox"
93-
checked={this.props.includeStatuses[status]}
94-
onChange={() => null} />
95-
<label>
96-
<FormattedMessage {...messagesByStatus[status]} />
97-
</label>
98-
</div>
99-
</div>
100-
))
101-
102-
const filterOptions = {
103-
includeStatuses: this.props.includeStatuses,
104-
withinBounds: this.state.mapBounds,
105-
}
106-
10752
return (
10853
<div className="admin__manage view-challenge">
10954
<div className="admin__manage__header">
@@ -153,26 +98,7 @@ export class ViewChallenge extends Component {
15398
</Sidebar>
15499

155100
<div className="admin__manage__primary-content">
156-
<div className='admin__manage-tasks'>
157-
<div className="status-filter-options">
158-
{statusFilters}
159-
</div>
160-
161-
<MapPane>
162-
<ChallengeTaskMap taskInfo={this.props.filteredClusteredTasks}
163-
setChallengeMapBounds={this.updateMapBounds}
164-
lastBounds={this.state.mapBounds}
165-
lastZoom={this.state.mapZoom}
166-
statusColors={statusColors}
167-
filterOptions={filterOptions}
168-
monochromaticClusters
169-
defaultLayer={layerSourceWithName(MAPBOX_LIGHT)}
170-
{...this.props} />
171-
</MapPane>
172-
<BoundedTaskTable filterOptions={filterOptions}
173-
totalTaskCount={_get(this.props, 'clusteredTasks.tasks.length')}
174-
{...this.props} />
175-
</div>
101+
<ViewChallengeTasks {...this.props} />
176102
</div>
177103
</div>
178104
</div>

src/components/AdminPane/Manage/ViewChallenge/ViewChallenge.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
.admin__manage.view-challenge {
44
.admin__manage__primary-content {
55
padding-top: 0;
6+
7+
.challenge-tasks-status {
8+
display: flex;
9+
flex-direction: column;
10+
justify-content: flex-start;
11+
align-items: center;
12+
13+
h3 {
14+
font-size: $size-3;
15+
16+
&.is-danger {
17+
color: $coral;
18+
}
19+
}
20+
21+
.refresh-control {
22+
margin-top: 50px;
23+
}
24+
}
625
}
726

827
section {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import React, { Component } from 'react'
2+
import classNames from 'classnames'
3+
import { FormattedMessage,
4+
FormattedRelative } from 'react-intl'
5+
import _get from 'lodash/get'
6+
import _map from 'lodash/map'
7+
import { ChallengeStatus }
8+
from '../../../../services/Challenge/ChallengeStatus/ChallengeStatus'
9+
import { TaskStatus,
10+
keysByStatus,
11+
messagesByStatus } from '../../../../services/Task/TaskStatus/TaskStatus'
12+
import { MAPBOX_LIGHT,
13+
layerSourceWithName }
14+
from '../../../../services/VisibleLayer/LayerSources'
15+
import WithBoundedTasks
16+
from '../../HOCs/WithBoundedTasks/WithBoundedTasks'
17+
import MapPane from '../../../EnhancedMap/MapPane/MapPane'
18+
import ChallengeTaskMap from '../ChallengeTaskMap/ChallengeTaskMap'
19+
import TaskAnalysisTable from '../TaskAnalysisTable/TaskAnalysisTable'
20+
import SvgSymbol from '../../../SvgSymbol/SvgSymbol'
21+
import messages from './Messages'
22+
23+
const BoundedTaskTable =
24+
WithBoundedTasks(TaskAnalysisTable, 'filteredClusteredTasks', 'taskInfo')
25+
26+
export default class ViewChallengeTasks extends Component {
27+
state = {
28+
mapBounds: null,
29+
mapZoom: null,
30+
renderingProgress: null,
31+
}
32+
33+
/** Invoked by the map when the user pans or zooms */
34+
updateMapBounds = (challengeId, bounds, zoom) =>
35+
this.setState({mapBounds: bounds, mapZoom: zoom})
36+
37+
render() {
38+
if (this.props.challenge.status === ChallengeStatus.building) {
39+
return (
40+
<div>
41+
<div className="challenge-tasks-status">
42+
<h3><FormattedMessage {...messages.tasksBuilding} /></h3>
43+
44+
<div className="since-when">
45+
<FormattedMessage {...messages.asOf} /> <FormattedRelative value={new Date(this.props.challenge._meta.fetchedAt)} />
46+
</div>
47+
48+
<button className={classNames("button is-primary is-outlined has-svg-icon refresh-control",
49+
{"is-loading": this.props.loadingChallenge})}
50+
onClick={this.props.refreshStatus}>
51+
<SvgSymbol viewBox='0 0 20 20' sym="refresh-icon" />
52+
<FormattedMessage {...messages.refreshStatusLabel} />
53+
</button>
54+
</div>
55+
</div>
56+
)
57+
}
58+
59+
if (this.props.challenge.status === ChallengeStatus.failed) {
60+
return (
61+
<div className="challenge-tasks-status title has-centered-children">
62+
<h3 className="is-danger">
63+
<FormattedMessage {...messages.tasksFailed} />
64+
</h3>
65+
</div>
66+
)
67+
}
68+
69+
// Use CSS Modules once supported by create-react-app
70+
const statusColors = {
71+
[TaskStatus.created]: '#0082C8', // $status-created-color
72+
[TaskStatus.fixed]: '#3CB44B', // $status-fixed-color
73+
[TaskStatus.falsePositive]: '#F58231', // $status-falsePositive-color
74+
[TaskStatus.skipped]: '#FFE119', // $status-skipped-color
75+
[TaskStatus.deleted]: '#46F0F0', // $status-deleted-color
76+
[TaskStatus.alreadyFixed]: '#911EB4', // $status-alreadyFixed-color
77+
[TaskStatus.tooHard]: '#E6194B', // $status-tooHard-color
78+
}
79+
80+
const statusFilters = _map(TaskStatus, status => (
81+
<div key={status} className="status-filter is-narrow">
82+
<div className={classNames("field", keysByStatus[status])}
83+
onClick={() => this.props.toggleIncludedStatus(status)}>
84+
<input className="is-checkradio is-circle has-background-color is-success"
85+
type="checkbox"
86+
checked={this.props.includeStatuses[status]}
87+
onChange={() => null} />
88+
<label>
89+
<FormattedMessage {...messagesByStatus[status]} />
90+
</label>
91+
</div>
92+
</div>
93+
))
94+
95+
const filterOptions = {
96+
includeStatuses: this.props.includeStatuses,
97+
withinBounds: this.state.mapBounds,
98+
}
99+
100+
return (
101+
<div className='admin__manage-tasks'>
102+
<div className="status-filter-options">
103+
{statusFilters}
104+
</div>
105+
106+
<MapPane>
107+
<ChallengeTaskMap taskInfo={this.props.filteredClusteredTasks}
108+
setChallengeMapBounds={this.updateMapBounds}
109+
lastBounds={this.state.mapBounds}
110+
lastZoom={this.state.mapZoom}
111+
statusColors={statusColors}
112+
filterOptions={filterOptions}
113+
monochromaticClusters
114+
defaultLayer={layerSourceWithName(MAPBOX_LIGHT)}
115+
{...this.props} />
116+
</MapPane>
117+
<BoundedTaskTable filterOptions={filterOptions}
118+
totalTaskCount={_get(this.props, 'clusteredTasks.tasks.length')}
119+
{...this.props} />
120+
</div>
121+
)
122+
}
123+
}

src/components/EnhancedMap/MapPane/MapPane.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@
4949
border-radius: $radius-medium;
5050

5151
.leaflet-popup-content {
52-
width: 300px;
52+
width: 350px;
5353
overflow-y: auto;
5454
margin: 0;
5555

5656
.feature-properties {
5757
color: $grey;
5858
font-size: $size-7;
5959
margin: 19px 0px;
60+
max-height: 70vh;
61+
overflow-y: auto;
6062

6163
h3 {
6264
color: $primary;

src/components/Sprites/Sprites.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/TaskPane/TaskMap/TaskMap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MIN_ZOOM,
1313
MAX_ZOOM,
1414
DEFAULT_ZOOM }
1515
from '../../../services/Challenge/ChallengeZoom/ChallengeZoom'
16+
import BusySpinner from '../../BusySpinner/BusySpinner'
1617
import './TaskMap.css'
1718

1819
const VisibleLayerToggle = WithVisibleLayer(WithLayerSources(LayerToggle))
@@ -53,7 +54,7 @@ export default class TaskMap extends Component {
5354

5455
render() {
5556
if (!this.props.task || !_isObject(this.props.task.parent)) {
56-
return null
57+
return <BusySpinner />
5758
}
5859

5960
const zoom = _get(this.props.task, "parent.defaultZoom", DEFAULT_ZOOM)

0 commit comments

Comments
 (0)