Skip to content

Commit 223d093

Browse files
committed
feat(auto-deploy): Add OTP auto-deploy project setting
corresponds to changes in ibi-group/datatools-server#361
1 parent efeae58 commit 223d093

File tree

7 files changed

+113
-44
lines changed

7 files changed

+113
-44
lines changed

i18n/english.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ components:
5757
success: Success!
5858
warning: Warning!
5959
DeploymentSettings:
60+
autoDeploy:
61+
description: >
62+
Once a pinned deployment has been configured and deployed to a
63+
server (at least once), auto-deployment can be enabled so that whenever
64+
a new version is processed for an associated feed source, a deployment
65+
will be kicked off (assuming there are no critical errors with the new
66+
version).
67+
label: Enable auto-deploy
68+
noPinnedDeployment: None selected
69+
pinnedDeploymentHelp: >
70+
Tip: Under the Deployments tab, click the thumbtack icon next to the
71+
deployment you would like to target for auto-deployments.
72+
title: Auto-deployment
6073
boundsPlaceholder: 'min_lon, min_lat, max_lon, max_lat'
6174
buildConfig:
6275
elevationBucket:
@@ -69,6 +82,8 @@ components:
6982
stationTransfers: Sta. Transfers
7083
subwayAccessTime: Subway Access Time
7184
title: Build Config
85+
clear: Clear
86+
manageServers: Manage deployment servers
7287
osm:
7388
bounds: Custom Extract Bounds
7489
custom: Use Custom Extract Bounds

lib/common/util/permissions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function checkEntitiesForFeeds (
4343

4444
/**
4545
* Checks whether it is possible for a user in this application to analyze
46-
* projet deployments
46+
* project deployments
4747
*/
4848
export function deploymentsEnabledAndAccessAllowedForProject (
4949
project: ?Project,

lib/manager/actions/projects.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {createAction, type ActionType} from 'redux-actions'
44
import {browserHistory} from 'react-router'
55

66
import {createVoidPayloadAction, secureFetch} from '../../common/actions'
7-
import {getConfigProperty} from '../../common/util/config'
7+
import {getConfigProperty, isModuleEnabled} from '../../common/util/config'
88
import {deploymentsEnabledAndAccessAllowedForProject} from '../../common/util/permissions'
99
import {fetchProjectDeployments} from './deployments'
1010
import {fetchProjectFeeds} from './feeds'
@@ -139,15 +139,16 @@ export function deleteProject (project: Project) {
139139
export function updateProject (
140140
projectId: string,
141141
changes: {[string]: any},
142-
fetchFeeds: ?boolean = false
142+
fetchFeedsAndDeployments: ?boolean = false
143143
) {
144144
return function (dispatch: dispatchFn, getState: getStateFn) {
145145
dispatch(savingProject())
146146
const url = `/api/manager/secure/project/${projectId}`
147147
return dispatch(secureFetch(url, 'put', changes))
148148
.then((res) => {
149-
if (fetchFeeds) {
150-
return dispatch(fetchProjectWithFeeds(projectId))
149+
if (fetchFeedsAndDeployments) {
150+
dispatch(fetchProjectWithFeeds(projectId))
151+
isModuleEnabled('deployment') && dispatch(fetchProjectDeployments(projectId))
151152
} else {
152153
return dispatch(fetchProject(projectId))
153154
}

lib/manager/components/DeploymentSettings.js

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
// @flow
22

33
import Icon from '@conveyal/woonerf/components/icon'
4-
import objectPath from 'object-path'
4+
// $FlowFixMe coalesce method is missing in flow type
5+
import {coalesce, get, set} from 'object-path'
56
import React, {Component} from 'react'
6-
import {Row, Col, Button, Panel, Glyphicon, Radio, FormGroup, ControlLabel, FormControl} from 'react-bootstrap'
7+
import {
8+
Button,
9+
Checkbox,
10+
Col,
11+
ControlLabel,
12+
FormControl,
13+
FormGroup,
14+
Glyphicon,
15+
Panel,
16+
Radio,
17+
Row
18+
} from 'react-bootstrap'
719
import update from 'react-addons-update'
820
import {shallowEqual} from 'react-pure-render'
921
import {withRouter} from 'react-router'
@@ -25,6 +37,7 @@ type Props = {
2537
}
2638

2739
type State = {
40+
autoDeploy?: boolean,
2841
buildConfig: Object,
2942
routerConfig: Object,
3043
useCustomOsmBounds?: boolean
@@ -34,27 +47,29 @@ class DeploymentSettings extends Component<Props, State> {
3447
messages = getComponentMessages('DeploymentSettings')
3548

3649
state = {
37-
buildConfig: objectPath.get(this.props, 'project.buildConfig') || {},
38-
routerConfig: objectPath.get(this.props, 'project.routerConfig') || {}
50+
buildConfig: get(this.props, 'project.buildConfig') || {},
51+
routerConfig: get(this.props, 'project.routerConfig') || {}
3952
}
4053

4154
componentWillReceiveProps (nextProps) {
4255
if (nextProps.project.lastUpdated !== this.props.project.lastUpdated) {
4356
// Reset state using project data if it is updated.
4457
this.setState({
45-
buildConfig: objectPath.get(nextProps, 'project.buildConfig') || {},
46-
routerConfig: objectPath.get(nextProps, 'project.routerConfig') || {}
58+
buildConfig: get(nextProps, 'project.buildConfig') || {},
59+
routerConfig: get(nextProps, 'project.routerConfig') || {}
4760
})
4861
}
4962
}
5063

5164
componentDidMount () {
52-
// FIXME: This is broken. Check for edits does not always return correct value.
53-
// this.props.router.setRouteLeaveHook(this.props.route, () => {
54-
// if (!this._noEdits()) {
55-
// return 'You have unsaved information, are you sure you want to leave this page?'
56-
// }
57-
// })
65+
// $FlowFixMe react-router 3.x is not available in flow-typed.
66+
const {routes, router} = this.props
67+
// Check for unsaved edits and warn user if they attempt to navigate away.
68+
router.setRouteLeaveHook(routes[0], () => {
69+
if (!this._noEdits()) {
70+
return 'You have unsaved information, are you sure you want to leave this page?'
71+
}
72+
})
5873
}
5974

6075
_clearBuildConfig = () => {
@@ -72,7 +87,7 @@ class DeploymentSettings extends Component<Props, State> {
7287
if (item) {
7388
const stateUpdate = {}
7489
item.effects && item.effects.forEach(e => {
75-
objectPath.set(stateUpdate, `${e.key}.$set`, e.value)
90+
set(stateUpdate, `${e.key}.$set`, e.value)
7691
})
7792
switch (item.type) {
7893
case 'checkbox':
@@ -96,19 +111,19 @@ class DeploymentSettings extends Component<Props, State> {
96111

97112
_onChangeCheckbox = (evt, stateUpdate = {}, index = null) => {
98113
const name = index !== null ? evt.target.name.replace('$index', `${index}`) : evt.target.name
99-
objectPath.set(stateUpdate, `${name}.$set`, evt.target.checked)
114+
set(stateUpdate, `${name}.$set`, evt.target.checked)
100115
this.setState(update(this.state, stateUpdate))
101116
}
102117

103118
_onChangeSplit = (evt, stateUpdate = {}, index = null) => {
104119
const name = index !== null ? evt.target.name.replace('$index', `${index}`) : evt.target.name
105-
objectPath.set(stateUpdate, `${name}.$set`, evt.target.value.split(','))
120+
set(stateUpdate, `${name}.$set`, evt.target.value.split(','))
106121
this.setState(update(this.state, stateUpdate))
107122
}
108123

109124
_onAddUpdater = () => {
110125
const stateUpdate = {}
111-
objectPath.set(stateUpdate,
126+
set(stateUpdate,
112127
`routerConfig.updaters.$${this.state.routerConfig.updaters ? 'push' : 'set'}`,
113128
[{type: '', url: '', frequencySec: 30, sourceType: '', defaultAgencyId: ''}]
114129
)
@@ -117,27 +132,27 @@ class DeploymentSettings extends Component<Props, State> {
117132

118133
_onRemoveUpdater = (index) => {
119134
const stateUpdate = {}
120-
objectPath.set(stateUpdate, `routerConfig.updaters.$splice`, [[index, 1]])
135+
set(stateUpdate, `routerConfig.updaters.$splice`, [[index, 1]])
121136
this.setState(update(this.state, stateUpdate))
122137
}
123138

124139
_onChange = (evt, stateUpdate = {}, index = null) => {
125140
const name = index !== null ? evt.target.name.replace('$index', `${index}`) : evt.target.name
126141
// If value is empty string or undefined, set to null in settings object.
127142
// Otherwise, certain fields (such as 'fares') would cause issues with OTP.
128-
objectPath.set(stateUpdate, `${name}.$set`, evt.target.value || null)
143+
set(stateUpdate, `${name}.$set`, evt.target.value || null)
129144
this.setState(update(this.state, stateUpdate))
130145
}
131146

132147
_onChangeNumber = (evt, stateUpdate = {}, index = null) => {
133148
const name = index !== null ? evt.target.name.replace('$index', `${index}`) : evt.target.name
134-
objectPath.set(stateUpdate, `${name}.$set`, +evt.target.value)
149+
set(stateUpdate, `${name}.$set`, +evt.target.value)
135150
this.setState(update(this.state, stateUpdate))
136151
}
137152

138153
_onSelectBool = (evt, stateUpdate = {}, index = null) => {
139154
const name = index !== null ? evt.target.name.replace('$index', `${index}`) : evt.target.name
140-
objectPath.set(stateUpdate, `${name}.$set`, (evt.target.value === 'true'))
155+
set(stateUpdate, `${name}.$set`, (evt.target.value === 'true'))
141156
this.setState(update(this.state, stateUpdate))
142157
}
143158

@@ -149,15 +164,15 @@ class DeploymentSettings extends Component<Props, State> {
149164
// check for conditional render, e.g. elevationBucket is dependent on fetchElevationUS
150165
if (f.condition) {
151166
const {key, value} = f.condition
152-
const val = objectPath.get(state, `${key}`)
167+
const val = get(state, `${key}`)
153168
if (val !== value) return false
154169
}
155170
return true
156171
}
157172
return (
158173
<FormInput
159174
key={`${index}`}
160-
value={objectPath.get(state, `${f.name}`)}
175+
value={get(state, `${f.name}`)}
161176
field={f}
162177
onChange={this._getOnChange}
163178
data={state}
@@ -166,7 +181,13 @@ class DeploymentSettings extends Component<Props, State> {
166181
})
167182
}
168183

169-
_onSave = (evt) => this.props.updateProject(this.props.project.id, this.state)
184+
_onSave = (evt) => this.props.updateProject(this.props.project.id, this.state, true)
185+
186+
_onToggleAutoDeploy = (evt) => {
187+
console.log(evt.target.checked)
188+
const stateUpdate = { autoDeploy: { $set: evt.target.checked } }
189+
this.setState(update(this.state, stateUpdate))
190+
}
170191

171192
_onToggleCustomBounds = (evt) => {
172193
const stateUpdate = { useCustomOsmBounds: { $set: (evt.target.value === 'true') } }
@@ -189,6 +210,12 @@ class DeploymentSettings extends Component<Props, State> {
189210
}
190211
}
191212

213+
/**
214+
* Get value for key from state or, if undefined, default to project property
215+
* from props.
216+
*/
217+
_getValue = (key) => coalesce(this.state, [key], this.props.project[key])
218+
192219
/**
193220
* Determine if deployment settings have been modified by checking that every
194221
* item in the state matches the original object found in the project object.
@@ -198,22 +225,55 @@ class DeploymentSettings extends Component<Props, State> {
198225
.every(key => shallowEqual(this.state[key], this.props.project[key]))
199226

200227
render () {
201-
const updaters = objectPath.get(this.state, 'routerConfig.updaters') || []
228+
const updaters = get(this.state, 'routerConfig.updaters') || []
202229
const {project, editDisabled} = this.props
230+
const {pinnedDeploymentId} = project
231+
const pinnedDeployment = pinnedDeploymentId && project.deployments && project.deployments.find(d => d.id === pinnedDeploymentId)
203232
return (
204233
<div key={project.lastUpdated} className='deployment-settings-panel'>
205234
<LinkContainer to={`/admin/servers`} style={{marginBottom: '20px'}}>
206235
<Button block bsStyle='primary' bsSize='large'>
207-
<Icon type='server' /> Manage deployment servers
236+
<Icon type='server' /> {this.messages('manageServers')}
208237
</Button>
209238
</LinkContainer>
239+
{/* Auto-deploy settings */}
240+
<Panel header={
241+
<h4>
242+
<Icon type='rocket' />{' '}
243+
{this.messages('autoDeploy.title')}
244+
</h4>
245+
}>
246+
<p>{this.messages('autoDeploy.description')}</p>
247+
<p>
248+
Pinned Deployment:{' '}
249+
{pinnedDeployment
250+
? pinnedDeployment.name
251+
: <span className='text-muted'>
252+
{this.messages('autoDeploy.noPinnedDeployment')}
253+
</span>
254+
}
255+
</p>
256+
{!pinnedDeployment &&
257+
<small className='text-danger'>
258+
{this.messages('autoDeploy.pinnedDeploymentHelp')}
259+
</small>
260+
}
261+
<Checkbox
262+
checked={this._getValue('autoDeploy')}
263+
disabled={!pinnedDeployment}
264+
name={'autoDeploy'}
265+
onChange={this._onToggleAutoDeploy}
266+
>
267+
{this.messages('autoDeploy.label')}
268+
</Checkbox>
269+
</Panel>
210270
{/* Build config settings */}
211271
<Panel header={
212272
<h4>
213273
<Button
214274
bsSize='xsmall'
215275
onClick={this._clearBuildConfig}
216-
className='pull-right'>Clear
276+
className='pull-right'>{this.messages('clear')}
217277
</Button>
218278
<Icon type='cog' />{' '}
219279
{this.messages('buildConfig.title')}
@@ -227,7 +287,7 @@ class DeploymentSettings extends Component<Props, State> {
227287
<Button
228288
bsSize='xsmall'
229289
onClick={this._clearRouterConfig}
230-
className='pull-right'>Clear
290+
className='pull-right'>{this.messages('clear')}
231291
</Button>
232292
<Icon type='cog' />{' '}
233293
{this.messages('routerConfig.title')}
@@ -278,14 +338,14 @@ class DeploymentSettings extends Component<Props, State> {
278338
<FormGroup
279339
onChange={this._onToggleCustomBounds}>
280340
<Radio
341+
checked={!this._getValue('useCustomOsmBounds')}
281342
name='osm-extract'
282-
checked={typeof this.state.useCustomOsmBounds !== 'undefined' ? !this.state.useCustomOsmBounds : !project.useCustomOsmBounds}
283343
value={false}>
284344
{this.messages('osm.gtfs')}
285345
</Radio>
286346
<Radio
347+
checked={this._getValue('useCustomOsmBounds')}
287348
name='osm-extract'
288-
checked={typeof this.state.useCustomOsmBounds !== 'undefined' ? this.state.useCustomOsmBounds : project.useCustomOsmBounds}
289349
value>
290350
{this.messages('osm.custom')}
291351
</Radio>

lib/types/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ export type RouterConfig = {
690690
}
691691

692692
export type Project = {
693+
autoDeploy: boolean,
693694
autoFetchFeeds: boolean,
694695
autoFetchHour: number,
695696
autoFetchMinute: number,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"lodash": "^4.17.10",
6262
"moment": "^2.11.2",
6363
"numeral": "2.0.4",
64-
"object-path": "^0.11.1",
64+
"object-path": "^0.11.5",
6565
"polyline": "^0.2.0",
6666
"prop-types": "^15.6.0",
6767
"qs": "^6.2.1",

yarn.lock

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9695,7 +9695,6 @@ npm@^6.14.8:
96959695
cmd-shim "^3.0.3"
96969696
columnify "~1.5.4"
96979697
config-chain "^1.1.12"
9698-
debuglog "*"
96999698
detect-indent "~5.0.0"
97009699
detect-newline "^2.1.0"
97019700
dezalgo "~1.0.3"
@@ -9710,7 +9709,6 @@ npm@^6.14.8:
97109709
has-unicode "~2.0.1"
97119710
hosted-git-info "^2.8.8"
97129711
iferr "^1.0.2"
9713-
imurmurhash "*"
97149712
infer-owner "^1.0.4"
97159713
inflight "~1.0.6"
97169714
inherits "^2.0.4"
@@ -9729,14 +9727,8 @@ npm@^6.14.8:
97299727
libnpx "^10.2.4"
97309728
lock-verify "^2.1.0"
97319729
lockfile "^1.0.4"
9732-
lodash._baseindexof "*"
97339730
lodash._baseuniq "~4.6.0"
9734-
lodash._bindcallback "*"
9735-
lodash._cacheindexof "*"
9736-
lodash._createcache "*"
9737-
lodash._getnative "*"
97389731
lodash.clonedeep "~4.5.0"
9739-
lodash.restparam "*"
97409732
lodash.union "~4.6.0"
97419733
lodash.uniq "~4.5.0"
97429734
lodash.without "~4.4.0"
@@ -9872,7 +9864,7 @@ object-keys@^1.0.11, object-keys@^1.0.12:
98729864
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
98739865
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
98749866

9875-
object-path@^0.11.1:
9867+
object-path@^0.11.5:
98769868
version "0.11.5"
98779869
resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.5.tgz#d4e3cf19601a5140a55a16ad712019a9c50b577a"
98789870
integrity sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg==

0 commit comments

Comments
 (0)