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:
Matthieu Huin
2020-12-21 18:50:14 +01:00
parent c82619174c
commit 84ae08fb39
5 changed files with 166 additions and 4 deletions

View File

@@ -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
})

View File

@@ -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,

View File

@@ -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)

View File

@@ -77,7 +77,7 @@ class Change extends React.Component {
url: null,
status: 'Invalid change ' + changeRef + ' on project ' + projectName,
text: '',
type: 'error'
type: 'error',
}))
}
}