Web UI: Add "Create Autohold Request" form, improve API error messages
Add a "create autohold request" button on the autoholds page for admin users. Refactor autohold form modal so that it can be reused in autoholds and build pages. Improve error messages when using admin actions: if the API call returns an error description, display it in the toast. Otherwise return the HTTP error code. Change-Id: I9d1cc8a1d751e7b4ce32e7abecc2ad9f3e96f676
This commit is contained in:
parent
54635a4de7
commit
41d6008a3d
|
@ -17,22 +17,33 @@ export const ADMIN_ENQUEUE_FAIL = 'ADMIN_ENQUEUE_FAIL'
|
|||
export const ADMIN_AUTOHOLD_FAIL = 'ADMIN_AUTOHOLD_FAIL'
|
||||
export const ADMIN_PROMOTE_FAIL = 'ADMIN_PROMOTE_FAIL'
|
||||
|
||||
function parseAPIerror(error) {
|
||||
if (error.response) {
|
||||
let parser = new DOMParser()
|
||||
let htmlError = parser.parseFromString(error.response.data, 'text/html')
|
||||
let error_description = htmlError.getElementsByTagName('p')[0].innerText
|
||||
return (error_description)
|
||||
} else {
|
||||
return (error)
|
||||
}
|
||||
}
|
||||
|
||||
export const addDequeueError = error => ({
|
||||
type: ADMIN_DEQUEUE_FAIL,
|
||||
notification: error
|
||||
notification: parseAPIerror(error)
|
||||
})
|
||||
|
||||
export const addEnqueueError = error => ({
|
||||
type: ADMIN_ENQUEUE_FAIL,
|
||||
notification: error
|
||||
notification: parseAPIerror(error)
|
||||
})
|
||||
|
||||
export const addAutoholdError = error => ({
|
||||
type: ADMIN_AUTOHOLD_FAIL,
|
||||
notification: error
|
||||
notification: parseAPIerror(error)
|
||||
})
|
||||
|
||||
export const addPromoteError = error => ({
|
||||
type: ADMIN_PROMOTE_FAIL,
|
||||
notification: error
|
||||
notification: parseAPIerror(error)
|
||||
})
|
|
@ -43,8 +43,9 @@ import { Link } from 'react-router-dom'
|
|||
import * as moment from 'moment'
|
||||
|
||||
import { autohold_delete } from '../../api'
|
||||
import { addNotification } from '../../actions/notifications'
|
||||
import { addAutoholdError } from '../../actions/adminActions'
|
||||
import { fetchAutoholds } from '../../actions/autoholds'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
|
||||
import { IconProperty } from '../../Misc'
|
||||
|
||||
|
@ -98,7 +99,7 @@ function AutoholdTable(props) {
|
|||
dispatch(fetchAutoholds(tenant))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(addApiError(error))
|
||||
dispatch(addAutoholdError(error))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2021 Red Hat, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect, useDispatch } from 'react-redux'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Form,
|
||||
FormGroup,
|
||||
TextInput
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
import { autohold } from '../../api'
|
||||
import { addAutoholdError } from '../../actions/adminActions'
|
||||
import { addNotification } from '../../actions/notifications'
|
||||
import { fetchAutoholds } from '../../actions/autoholds'
|
||||
|
||||
const AutoholdModal = props => {
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const { tenant, user, showAutoholdModal, setShowAutoholdModal } = props
|
||||
|
||||
const [change, setChange] = useState('')
|
||||
const [changeRef, setChangeRef] = useState('')
|
||||
const [project, setProject] = useState('some project')
|
||||
const [job_name, setJob_name] = useState('some job')
|
||||
const [reason, setReason] = useState('-')
|
||||
const [count, setCount] = useState(1)
|
||||
const [nodeHoldExpiration, setNodeHoldExpiration] = useState(86400)
|
||||
|
||||
// Override defaults if optional parameters were passed
|
||||
useEffect(() => {
|
||||
if (props.change) { setChange(props.change) }
|
||||
if (props.changeRef) { setChangeRef(props.changeRef) }
|
||||
if (props.project) { setProject(props.project) }
|
||||
if (props.jobName) { setJob_name(props.jobName) }
|
||||
if (props.reason) {
|
||||
setReason(props.reason)
|
||||
} else {
|
||||
setReason(
|
||||
user.data
|
||||
? 'Requested from the web UI by ' + user.data.profile.preferred_username
|
||||
: '-'
|
||||
)
|
||||
}
|
||||
}, [props.change, props.changeRef, props.project, props.jobName, props.reason, user.data])
|
||||
|
||||
function handleConfirm() {
|
||||
let ah_change = change === '' ? null : change
|
||||
let ah_ref = changeRef === '' ? null : changeRef
|
||||
|
||||
autohold(tenant.apiPrefix, project, job_name, ah_change, ah_ref, reason, parseInt(count), parseInt(nodeHoldExpiration), user.token)
|
||||
.then(() => {
|
||||
/* TODO it looks like there is a delay in the registering of the autohold request
|
||||
by the backend, meaning we sometimes do not get the newly created request after
|
||||
the dispatch. A solution could be to make the autoholds page auto-refreshing
|
||||
like the status page.*/
|
||||
dispatch(fetchAutoholds(tenant))
|
||||
dispatch(addNotification(
|
||||
{
|
||||
text: 'Autohold request set successfully.',
|
||||
type: 'success',
|
||||
status: '',
|
||||
url: '',
|
||||
}))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(addAutoholdError(error))
|
||||
})
|
||||
setShowAutoholdModal(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
isOpen={showAutoholdModal}
|
||||
title='Create an Autohold Request'
|
||||
onClose={() => { setShowAutoholdModal(false) }}
|
||||
actions={[
|
||||
<Button
|
||||
key="autohold_confirm"
|
||||
variant="primary"
|
||||
onClick={() => handleConfirm()}>Create</Button>,
|
||||
<Button
|
||||
key="autohold_cancel"
|
||||
variant="link"
|
||||
onClick={() => { setShowAutoholdModal(false) }}>Cancel</Button>
|
||||
]}>
|
||||
<Form isHorizontal>
|
||||
<FormGroup
|
||||
label="Project"
|
||||
isRequired
|
||||
fieldId="ah-form-project"
|
||||
helperText="The project for which to hold the next failing build">
|
||||
<TextInput
|
||||
value={project}
|
||||
isRequired
|
||||
type="text"
|
||||
id="ah-form-ref"
|
||||
name="project"
|
||||
onChange={(value) => { setProject(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Job"
|
||||
isRequired
|
||||
fieldId="ah-form-job-name"
|
||||
helperText="The job for which to hold the next failing build">
|
||||
<TextInput
|
||||
value={job_name}
|
||||
isRequired
|
||||
type="text"
|
||||
id="ah-form-job-name"
|
||||
name="job_name"
|
||||
onChange={(value) => { setJob_name(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Change"
|
||||
fieldId="ah-form-change"
|
||||
helperText="The change for which to hold the next failing build">
|
||||
<TextInput
|
||||
value={change}
|
||||
type="text"
|
||||
id="ah-form-change"
|
||||
name="change"
|
||||
onChange={(value) => { setChange(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Ref"
|
||||
fieldId="ah-form-ref"
|
||||
helperText="The ref for which to hold the next failing build">
|
||||
<TextInput
|
||||
value={changeRef}
|
||||
type="text"
|
||||
id="ah-form-ref"
|
||||
name="change"
|
||||
onChange={(value) => { setChangeRef(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Reason"
|
||||
isRequired
|
||||
fieldId="ah-form-reason"
|
||||
helperText="A descriptive reason for holding the next failing build">
|
||||
<TextInput
|
||||
value={reason}
|
||||
isRequired
|
||||
type="text"
|
||||
id="ah-form-reason"
|
||||
name="reason"
|
||||
onChange={(value) => { setReason(value) }} />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label="Count"
|
||||
isRequired
|
||||
fieldId="ah-form-count"
|
||||
helperText="How many times a failing build should be held">
|
||||
<TextInput
|
||||
value={count}
|
||||
isRequired
|
||||
type="number"
|
||||
id="ah-form-count"
|
||||
name="count"
|
||||
onChange={(value) => { setCount(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={nodeHoldExpiration}
|
||||
isRequired
|
||||
type="number"
|
||||
id="ah-form-count"
|
||||
name="nodeHoldExpiration"
|
||||
onChange={(value) => { setNodeHoldExpiration(value) }} />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
AutoholdModal.propTypes = {
|
||||
tenant: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
change: PropTypes.string,
|
||||
changeRef: PropTypes.string,
|
||||
project: PropTypes.string,
|
||||
jobName: PropTypes.string,
|
||||
reason: PropTypes.string,
|
||||
showAutoholdModal: PropTypes.bool,
|
||||
setShowAutoholdModal: PropTypes.func,
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
user: state.user,
|
||||
}))(AutoholdModal)
|
|
@ -14,17 +14,10 @@
|
|||
|
||||
import React, { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect, useDispatch } from 'react-redux'
|
||||
import { connect } 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,
|
||||
|
@ -46,39 +39,17 @@ import 'moment-duration-format'
|
|||
import { BuildResultBadge, BuildResultWithIcon } from './Misc'
|
||||
import { buildExternalLink, ExternalLink, IconProperty } from '../../Misc'
|
||||
|
||||
import { autohold } from '../../api'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
|
||||
import AutoholdModal from '../autohold/autoholdModal'
|
||||
|
||||
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 change = build.change ? build.change : ''
|
||||
const ref = build.change ? '' : build.ref
|
||||
const project = build.project
|
||||
const job_name = build.job_name
|
||||
|
||||
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={{
|
||||
|
@ -103,71 +74,6 @@ function Build({ build, tenant, timezone, user }) {
|
|||
)
|
||||
}
|
||||
|
||||
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(false)
|
||||
}}>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 (
|
||||
<>
|
||||
|
@ -364,7 +270,14 @@ function Build({ build, tenant, timezone, user }) {
|
|||
</FlexItem>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{renderAutoholdModal()}
|
||||
{<AutoholdModal
|
||||
showAutoholdModal={showAutoholdModal}
|
||||
setShowAutoholdModal={setShowAutoholdModal}
|
||||
change={change}
|
||||
changeRef={ref}
|
||||
project={project}
|
||||
jobName={job_name}
|
||||
/>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,47 +15,103 @@
|
|||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
import { Flex, FlexItem, PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
|
||||
import { fetchAutoholdsIfNeeded } from '../actions/autoholds'
|
||||
import AutoholdTable from '../containers/autohold/AutoholdTable'
|
||||
|
||||
import {
|
||||
AddCircleOIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import AutoholdModal from '../containers/autohold/autoholdModal'
|
||||
import { IconProperty } from '../Misc'
|
||||
|
||||
|
||||
class AutoholdsPage extends React.Component {
|
||||
static propTypes = {
|
||||
tenant: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
remoteData: PropTypes.object,
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
showAutoholdModal: false,
|
||||
}
|
||||
}
|
||||
|
||||
setShowAutoholdModal = (value) => {
|
||||
this.setState(() => ({ showAutoholdModal: value }))
|
||||
}
|
||||
|
||||
renderAutoholdButton() {
|
||||
const value = (<span style={{
|
||||
cursor: 'pointer',
|
||||
color: 'var(--pf-global--primary-color--100)'
|
||||
}}
|
||||
title="Create a new autohold request to hold nodes in case of a build failure"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
this.setShowAutoholdModal(true)
|
||||
}}
|
||||
>
|
||||
Create Request
|
||||
</span>)
|
||||
return (
|
||||
<IconProperty
|
||||
icon={<AddCircleOIcon />}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
updateData = (force) => {
|
||||
this.props.dispatch(fetchAutoholdsIfNeeded(this.props.tenant, force))
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
document.title = 'Zuul Autoholds'
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.tenant.name !== prevProps.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { remoteData } = this.props
|
||||
render() {
|
||||
const { tenant, user, remoteData } = this.props
|
||||
const autoholds = remoteData.autoholds
|
||||
const { showAutoholdModal } = this.state
|
||||
|
||||
return (
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<AutoholdTable
|
||||
autoholds={autoholds}
|
||||
fetching={remoteData.isFetching} />
|
||||
</PageSection>
|
||||
<>
|
||||
{(user.isAdmin && user.scope.indexOf(tenant.name) !== -1) && (
|
||||
<PageSection>
|
||||
<Flex flex={{ default: 'flex_1' }}>
|
||||
<FlexItem>
|
||||
{this.renderAutoholdButton()}
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</PageSection>
|
||||
)}
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<AutoholdTable
|
||||
autoholds={autoholds}
|
||||
fetching={remoteData.isFetching} />
|
||||
</PageSection>
|
||||
{<AutoholdModal
|
||||
showAutoholdModal={showAutoholdModal}
|
||||
setShowAutoholdModal={this.setShowAutoholdModal}
|
||||
/>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -63,4 +119,5 @@ class AutoholdsPage extends React.Component {
|
|||
export default connect(state => ({
|
||||
tenant: state.tenant,
|
||||
remoteData: state.autoholds,
|
||||
user: state.user,
|
||||
}))(AutoholdsPage)
|
||||
|
|
Loading…
Reference in New Issue