Web UI: allow a privileged user to request autohold
Add a "autohold" command on a build page, displayed only if the currently logged in user is an admin on the tenant. By clicking the command, the user can display a form and set custom parameters for an autohold request. Change-Id: I0f9f069d4ad36d4961eb6925146f67fe4910cb2e
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
|
||||
export const ADMIN_DEQUEUE_FAIL = 'ADMIN_DEQUEUE_FAIL'
|
||||
export const ADMIN_ENQUEUE_FAIL = 'ADMIN_ENQUEUE_FAIL'
|
||||
export const ADMIN_AUTOHOLD_FAIL = 'ADMIN_AUTOHOLD_FAIL'
|
||||
|
||||
export const addDequeueError = error => ({
|
||||
type: ADMIN_DEQUEUE_FAIL,
|
||||
@@ -24,3 +25,8 @@ export const addEnqueueError = error => ({
|
||||
type: ADMIN_ENQUEUE_FAIL,
|
||||
notification: error
|
||||
})
|
||||
|
||||
export const addAutoholdError = error => ({
|
||||
type: ADMIN_AUTOHOLD_FAIL,
|
||||
notification: error
|
||||
})
|
||||
|
||||
@@ -240,6 +240,26 @@ function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev, toke
|
||||
)
|
||||
return res
|
||||
}
|
||||
function autohold (apiPrefix, projectName, job, change, ref,
|
||||
reason, count, node_hold_expiration, token) {
|
||||
const instance = Axios.create({
|
||||
baseURL: apiUrl
|
||||
})
|
||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
||||
let res = instance.post(
|
||||
apiPrefix + 'project/' + projectName + '/autohold',
|
||||
{
|
||||
change: change,
|
||||
job: job,
|
||||
ref: ref,
|
||||
reason: reason,
|
||||
count: count,
|
||||
node_hold_expiration: node_hold_expiration,
|
||||
}
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
apiUrl,
|
||||
@@ -264,6 +284,7 @@ export {
|
||||
fetchComponents,
|
||||
fetchTenantInfo,
|
||||
fetchUserAuthorizations,
|
||||
autohold,
|
||||
dequeue,
|
||||
dequeue_ref,
|
||||
enqueue,
|
||||
|
||||
@@ -12,11 +12,19 @@
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import * as React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { connect, useDispatch } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Flex, FlexItem, List, ListItem, Title } from '@patternfly/react-core'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Form,
|
||||
FormGroup,
|
||||
TextInput
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
BookIcon,
|
||||
BuildIcon,
|
||||
@@ -30,6 +38,7 @@ import {
|
||||
OutlinedClockIcon,
|
||||
StreamIcon,
|
||||
ThumbtackIcon,
|
||||
LockIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import * as moment from 'moment'
|
||||
import 'moment-duration-format'
|
||||
@@ -37,9 +46,129 @@ import 'moment-duration-format'
|
||||
import { BuildResultBadge, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { buildExternalLink, ExternalLink } from '../../Misc'
|
||||
|
||||
function Build({ build, tenant, timezone }) {
|
||||
import { autohold } from '../../api'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
|
||||
|
||||
function Build({ build, tenant, timezone, user }) {
|
||||
const [showAutoholdModal, setShowAutoholdModal] = useState(false)
|
||||
let default_form_reason = user.data ? 'Requested from the web UI by ' + user.data.profile.preferred_username : '-'
|
||||
const [ahFormReason, setAhFormReason] = useState(default_form_reason)
|
||||
const [ahFormCount, setAhFormCount] = useState(1)
|
||||
const [ahFormNodeHoldExpiration, setAhFormNodeHoldExpiration] = useState(86400)
|
||||
|
||||
const build_link = buildExternalLink(build)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
function handleConfirm() {
|
||||
let change = build.change ? build.change : null
|
||||
let ref = change ? null : build.ref
|
||||
autohold(tenant.apiPrefix, build.project, build.job_name, change, ref, ahFormReason, parseInt(ahFormCount), parseInt(ahFormNodeHoldExpiration), user.token)
|
||||
.then(() => {
|
||||
dispatch(addNotification(
|
||||
{
|
||||
text: 'Autohold request set successfully.',
|
||||
type: 'success',
|
||||
status: '',
|
||||
url: '',
|
||||
}))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(addApiError(error))
|
||||
})
|
||||
}
|
||||
|
||||
function renderAutoholdButton() {
|
||||
const value = (
|
||||
<span style={{
|
||||
cursor: 'pointer',
|
||||
color: 'var(--pf-global--primary-color--100)'
|
||||
}}
|
||||
title="Hold nodes next time this job ends in failure for this specific change"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
setShowAutoholdModal(true)
|
||||
}}
|
||||
>
|
||||
Autohold future build failure(s)
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<LockIcon />}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderAutoholdModal() {
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
titleIconVariant={LockIcon}
|
||||
isOpen={showAutoholdModal}
|
||||
title='Create an Autohold Request'
|
||||
onClose={() => { setShowAutoholdModal(false) }}
|
||||
actions={[
|
||||
<Button
|
||||
key="autohold_confirm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
handleConfirm()
|
||||
setShowAutoholdModal()
|
||||
}}>Create</Button>,
|
||||
<Button
|
||||
key="autohold_cancel"
|
||||
variant="link"
|
||||
onClick={() => { setShowAutoholdModal(false) }}>Cancel</Button>
|
||||
]}>
|
||||
<Form isHorizontal>
|
||||
<FormGroup
|
||||
label="Reason"
|
||||
isRequired
|
||||
fieldId="ah-form-reason"
|
||||
helperText="A descriptive reason for holding the next failing build">
|
||||
<TextInput
|
||||
value={ahFormReason}
|
||||
isRequired
|
||||
type="text"
|
||||
id="ah-form-reason"
|
||||
name="ahFormReason"
|
||||
onChange={(value) => { setAhFormReason(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Count"
|
||||
isRequired
|
||||
fieldId="ah-form-count"
|
||||
helperText="How many times a failing build should be held">
|
||||
<TextInput
|
||||
value={ahFormCount}
|
||||
isRequired
|
||||
type="number"
|
||||
id="ah-form-count"
|
||||
name="ahFormCount"
|
||||
onChange={(value) => { setAhFormCount(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Node Hold Expires in (s)"
|
||||
isRequired
|
||||
fieldId="ah-form-nhe"
|
||||
helperText="How long nodes should be kept in HELD state (seconds)">
|
||||
<TextInput
|
||||
value={ahFormNodeHoldExpiration}
|
||||
isRequired
|
||||
type="number"
|
||||
id="ah-form-count"
|
||||
name="ahFormNodeHoldExpiration"
|
||||
onChange={(value) => { setAhFormNodeHoldExpiration(value) }} />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title
|
||||
@@ -230,10 +359,12 @@ function Build({ build, tenant, timezone }) {
|
||||
)
|
||||
}
|
||||
/>
|
||||
{(user.isAdmin && user.scope.indexOf(tenant.name) !== -1) && renderAutoholdButton()}
|
||||
</List>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{renderAutoholdModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -243,9 +374,11 @@ Build.propTypes = {
|
||||
tenant: PropTypes.object,
|
||||
hash: PropTypes.array,
|
||||
timezone: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
timezone: state.timezone,
|
||||
user: state.user,
|
||||
}))(Build)
|
||||
|
||||
@@ -77,7 +77,7 @@ class Change extends React.Component {
|
||||
url: null,
|
||||
status: 'Invalid change ' + changeRef + ' on project ' + projectName,
|
||||
text: '',
|
||||
type: 'error'
|
||||
type: 'error',
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user