web UI: allow a privileged user to promote a change

Add a "promote" button in the actions menu of a change, if the
currently logged in user is an admin on the tenant and if the pipeline
is dependent.
Clicking the button opens a modal, so that the user can confirm her decision.

Change-Id: I8262888aef9ba1a106e0b321cc4cf2e14465b90c
This commit is contained in:
Matthieu Huin 2021-03-19 19:31:25 +01:00
parent 40dd00bc26
commit 54635a4de7
6 changed files with 104 additions and 11 deletions

View File

@ -15,6 +15,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 ADMIN_PROMOTE_FAIL = 'ADMIN_PROMOTE_FAIL'
export const addDequeueError = error => ({
type: ADMIN_DEQUEUE_FAIL,
@ -30,3 +31,8 @@ export const addAutoholdError = error => ({
type: ADMIN_AUTOHOLD_FAIL,
notification: error
})
export const addPromoteError = error => ({
type: ADMIN_PROMOTE_FAIL,
notification: error
})

View File

@ -278,6 +278,21 @@ function autohold_delete(apiPrefix, requestId, token) {
return res
}
function promote(apiPrefix, pipeline, changes, token) {
const instance = Axios.create({
baseURL: apiUrl
})
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
let res = instance.post(
apiPrefix + '/promote',
{
pipeline: pipeline,
changes: changes,
}
)
return res
}
export {
apiUrl,
@ -310,4 +325,5 @@ export {
dequeue_ref,
enqueue,
enqueue_ref,
promote,
}

View File

@ -26,12 +26,14 @@ import {
ModalVariant
} from '@patternfly/react-core'
import {
AngleDoubleUpIcon,
BanIcon,
} from '@patternfly/react-icons'
import { dequeue, dequeue_ref } from '../../api'
import { addDequeueError } from '../../actions/adminActions'
import { dequeue, dequeue_ref, promote } from '../../api'
import { addDequeueError, addPromoteError } from '../../actions/adminActions'
import { addNotification } from '../../actions/notifications'
import { fetchStatusIfNeeded } from '../../actions/status'
import LineAngleImage from '../../images/line-angle.png'
import LineTImage from '../../images/line-t.png'
@ -43,7 +45,7 @@ class Change extends React.Component {
change: PropTypes.object.isRequired,
queue: PropTypes.object.isRequired,
expanded: PropTypes.bool.isRequired,
pipeline: PropTypes.string,
pipeline: PropTypes.object,
tenant: PropTypes.object,
user: PropTypes.object,
dispatch: PropTypes.func
@ -51,6 +53,7 @@ class Change extends React.Component {
state = {
showDequeueModal: false,
showPromoteModal: false,
showAdminActions: false,
}
@ -63,12 +66,18 @@ class Change extends React.Component {
// post-merge
if (/^[0-9a-f]{40}$/.test(changeId)) {
dequeue_ref(tenant.apiPrefix, projectName, pipeline.name, changeRef, user.token)
.then(() => {
this.props.dispatch(fetchStatusIfNeeded(tenant))
})
.catch(error => {
this.props.dispatch(addDequeueError(error))
})
// pre-merge, ie we have a change id
} else if (changeId !== 'N/A') {
dequeue(tenant.apiPrefix, projectName, pipeline.name, changeId, user.token)
.then(() => {
this.props.dispatch(fetchStatusIfNeeded(tenant))
})
.catch(error => {
this.props.dispatch(addDequeueError(error))
})
@ -108,13 +117,59 @@ class Change extends React.Component {
)
}
promoteConfirm = () => {
const { tenant, user, change, pipeline } = this.props
let changeId = change.id || 'NA'
this.setState(() => ({ showPromoteModal: false }))
if (changeId !== 'N/A') {
promote(tenant.apiPrefix, pipeline.name, [changeId,], user.token)
.then(() => {
this.props.dispatch(fetchStatusIfNeeded(tenant))
})
.catch(error => {
this.props.dispatch(addPromoteError(error))
})
} else {
this.props.dispatch(addNotification({
url: null,
status: 'Invalid change ' + changeId + ' for promotion',
text: '',
type: 'error'
}))
}
}
promoteCancel = () => {
this.setState(() => ({ showPromoteModal: false }))
}
renderPromoteModal() {
const { showPromoteModal } = this.state
const { change } = this.props
let changeId = change.id || 'N/A'
const title = 'You are about to promote a change'
return (
<Modal
variant={ModalVariant.small}
// titleIconVariant={BullhornIcon}
isOpen={showPromoteModal}
title={title}
onClose={this.promoteCancel}
actions={[
<Button key="prom_confirm" variant="primary" onClick={this.promoteConfirm}>Confirm</Button>,
<Button key="prom_cancel" variant="link" onClick={this.promoteCancel}>Cancel</Button>,
]}>
<p>Please confirm that you want to promote change <strong>{changeId}</strong>.</p>
</Modal>
)
}
renderAdminCommands(idx) {
const { showAdminActions } = this.state
const { queue } = this.props
const { queue, pipeline } = this.props
const dropdownCommands = [
<DropdownItem
key="dequeue"
icon={<BanIcon style={{
color: 'var(--pf-global--danger-color--100)',
}} />}
@ -125,6 +180,21 @@ class Change extends React.Component {
}}
>Dequeue</DropdownItem>,
]
if (pipeline.manager === 'dependent') {
dropdownCommands.push(
<DropdownItem
key="promote"
icon={<AngleDoubleUpIcon style={{
color: 'var(--pf-global--default-color--200)',
}} />}
description="Promote this change to the top of the queue"
onClick={(event) => {
event.preventDefault()
this.setState(() => ({ showPromoteModal: true }))
}}
>Promote</DropdownItem>
)
}
return (
<Dropdown
title='Actions'
@ -238,6 +308,7 @@ class Change extends React.Component {
</tbody>
</table>
{this.renderDequeueModal()}
{this.renderPromoteModal()}
</>
)
}

View File

@ -339,7 +339,7 @@ class ChangePanel extends React.Component {
</div>
</div>
{expand ? this.renderJobList(change.jobs) : ''}
</div>
</div >
)
return (
<React.Fragment>

View File

@ -20,15 +20,15 @@ import Change from './Change'
class ChangeQueue extends React.Component {
static propTypes = {
pipeline: PropTypes.string.isRequired,
pipeline: PropTypes.object.isRequired,
queue: PropTypes.object.isRequired,
expanded: PropTypes.bool.isRequired
}
render () {
render() {
const { queue, pipeline, expanded } = this.props
let fullName = queue.name
if(queue.branch) {
if (queue.branch) {
fullName = `${fullName} (${queue.branch})`
}
let shortName = fullName
@ -49,7 +49,7 @@ class ChangeQueue extends React.Component {
})
})
return (
<div className="change-queue" data-zuul-pipeline={pipeline}>
<div className="change-queue" data-zuul-pipeline={pipeline.name}>
<p>Queue: <abbr title={fullName}>{shortName}</abbr></p>
{changesList}
</div>)

View File

@ -214,7 +214,7 @@ class Pipeline extends React.Component {
<ChangeQueue
queue={changeQueue}
expanded={expanded}
pipeline={pipeline.name}
pipeline={pipeline}
key={changeQueue.uuid}
/>
))}