diff --git a/web/src/App.jsx b/web/src/App.jsx index 68e7ca3790..207417c966 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -64,7 +64,7 @@ import { Fetching } from './containers/Fetching' import SelectTz from './containers/timezone/SelectTz' import ConfigModal from './containers/config/Config' import logo from './images/logo.svg' -import { clearError } from './actions/errors' +import { clearNotification } from './actions/notifications' import { fetchConfigErrorsAction } from './actions/configErrors' import { routes } from './routes' import { setTenantAction } from './actions/tenant' @@ -72,7 +72,7 @@ import { configureAuthFromTenant, configureAuthFromInfo } from './actions/auth' class App extends React.Component { static propTypes = { - errors: PropTypes.array, + notifications: PropTypes.array, configErrors: PropTypes.array, info: PropTypes.object, tenant: PropTypes.object, @@ -235,21 +235,34 @@ class App extends React.Component { }) } - renderErrors = (errors) => { + renderNotifications = (notifications) => { return ( - {errors.map(error => ( - { this.props.dispatch(clearError(error.id)) }} - > - - {error.text} ({error.status})  - {error.url} - - - ))} + {notifications.map(notification => { + let notificationBody + if (notification.type === 'error') { + notificationBody = ( + <> + {notification.text} {notification.status}   + {notification.url} + + ) + } else { + notificationBody = ({notification.text}) + } + return ( + { this.props.dispatch(clearNotification(notification.id)) }} + > + + {notificationBody} + + + ) + } + )} ) } @@ -318,7 +331,7 @@ class App extends React.Component { render() { const { isKebabDropdownOpen } = this.state - const { errors, configErrors, tenant } = this.props + const { notifications, configErrors, tenant } = this.props const nav = this.renderMenu() @@ -440,7 +453,7 @@ class App extends React.Component { return ( - {errors.length > 0 && this.renderErrors(errors)} + {notifications.length > 0 && this.renderNotifications(notifications)} {this.renderConfigErrors(configErrors)} @@ -455,7 +468,7 @@ class App extends React.Component { // This connect the info state from the store to the info property of the App. export default withRouter(connect( state => ({ - errors: state.errors, + notifications: state.notifications, configErrors: state.configErrors, info: state.info, tenant: state.tenant, diff --git a/web/src/actions/adminActions.js b/web/src/actions/adminActions.js index a3cbc1a045..f18014cb8b 100644 --- a/web/src/actions/adminActions.js +++ b/web/src/actions/adminActions.js @@ -13,9 +13,14 @@ // under the License. export const ADMIN_DEQUEUE_FAIL = 'ADMIN_DEQUEUE_FAIL' - +export const ADMIN_ENQUEUE_FAIL = 'ADMIN_ENQUEUE_FAIL' export const addDequeueError = error => ({ type: ADMIN_DEQUEUE_FAIL, - error: error + notification: error +}) + +export const addEnqueueError = error => ({ + type: ADMIN_ENQUEUE_FAIL, + notification: error }) diff --git a/web/src/actions/errors.js b/web/src/actions/notifications.js similarity index 62% rename from web/src/actions/errors.js rename to web/src/actions/notifications.js index 3464dcb9c1..f4b9757ba7 100644 --- a/web/src/actions/errors.js +++ b/web/src/actions/notifications.js @@ -12,38 +12,39 @@ // License for the specific language governing permissions and limitations // under the License. -export const ADD_ERROR = 'ADD_ERROR' -export const CLEAR_ERROR = 'CLEAR_ERROR' -export const CLEAR_ERRORS = 'CLEAR_ERRORS' +export const ADD_NOTIFICATION = 'ADD_NOTIFICATION' +export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION' +export const CLEAR_NOTIFICATIONS = 'CLEAR_NOTIFICATIONS' -let errorId = 0 +let notificationId = 0 -export const addError = error => ({ - type: ADD_ERROR, - id: errorId++, - error +export const addNotification = notification => ({ + type: ADD_NOTIFICATION, + id: notificationId++, + notification }) export const addApiError = error => { const d = { - url: (error && error.request && error.request.responseURL) || error.url + url: (error && error.request && error.request.responseURL) || error.url, + type: 'error', } if (error.response) { d.text = error.response.statusText d.status = error.response.status } else { d.status = 'Unable to fetch URL, check your network connectivity,' - + ' browser plugins, ad-blockers, or try to refresh this page' + + ' browser plugins, ad-blockers, or try to refresh this page' d.text = error.message } - return addError(d) + return addNotification(d) } -export const clearError = id => ({ - type: CLEAR_ERROR, +export const clearNotification = id => ({ + type: CLEAR_NOTIFICATION, id }) -export const clearErrors = () => ({ - type: CLEAR_ERRORS +export const clearNotifications = () => ({ + type: CLEAR_NOTIFICATIONS }) diff --git a/web/src/api.js b/web/src/api.js index 5bf8b5b06e..88ac01955a 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -181,7 +181,7 @@ function fetchUserAuthorizations(apiPrefix, token) { return res } -function dequeue (apiPrefix, projectName, pipeline, change, token) { +function dequeue(apiPrefix, projectName, pipeline, change, token) { const instance = Axios.create({ baseURL: apiUrl }) @@ -195,7 +195,7 @@ function dequeue (apiPrefix, projectName, pipeline, change, token) { ) return res } -function dequeue_ref (apiPrefix, projectName, pipeline, ref, token) { +function dequeue_ref(apiPrefix, projectName, pipeline, ref, token) { const instance = Axios.create({ baseURL: apiUrl }) @@ -210,6 +210,37 @@ function dequeue_ref (apiPrefix, projectName, pipeline, ref, token) { return res } +function enqueue(apiPrefix, projectName, pipeline, change, token) { + const instance = Axios.create({ + baseURL: apiUrl + }) + instance.defaults.headers.common['Authorization'] = 'Bearer ' + token + let res = instance.post( + apiPrefix + 'project/' + projectName + '/enqueue', + { + pipeline: pipeline, + change: change, + } + ) + return res +} +function enqueue_ref(apiPrefix, projectName, pipeline, ref, oldrev, newrev, token) { + const instance = Axios.create({ + baseURL: apiUrl + }) + instance.defaults.headers.common['Authorization'] = 'Bearer ' + token + let res = instance.post( + apiPrefix + 'project/' + projectName + '/enqueue', + { + pipeline: pipeline, + ref: ref, + oldrev: oldrev, + newrev: newrev, + } + ) + return res +} + export { apiUrl, getHomepageUrl, @@ -235,4 +266,6 @@ export { fetchUserAuthorizations, dequeue, dequeue_ref, + enqueue, + enqueue_ref, } diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx index a980af705d..3b14f9785e 100644 --- a/web/src/containers/build/Buildset.jsx +++ b/web/src/containers/build/Buildset.jsx @@ -14,7 +14,7 @@ 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 { Button, @@ -23,6 +23,8 @@ import { List, ListItem, Title, + Modal, + ModalVariant, } from '@patternfly/react-core' import { CodeIcon, @@ -33,16 +35,19 @@ import { StreamIcon, OutlinedCalendarAltIcon, OutlinedClockIcon, + RedoAltIcon, } from '@patternfly/react-icons' import * as moment from 'moment' import 'moment-duration-format' import { buildExternalLink } from '../../Misc' import { BuildResultBadge, BuildResultWithIcon, IconProperty } from './Misc' +import { enqueue, enqueue_ref } from '../../api' +import { addNotification, addApiError } from '../../actions/notifications' import { ChartModal } from '../charts/ChartModal' import BuildsetGanttChart from '../charts/GanttChart' -function Buildset({ buildset, timezone, tenant }) { +function Buildset({ buildset, timezone, tenant, user }) { const buildset_link = buildExternalLink(buildset) const [isGanttChartModalOpen, setIsGanttChartModalOpen] = useState(false) @@ -127,13 +132,92 @@ function Buildset({ buildset, timezone, tenant }) { ) } + const [showEnqueueModal, setShowEnqueueModal] = useState(false) + const dispatch = useDispatch() + + function renderEnqueueButton() { + const value = ( { + event.preventDefault() + setShowEnqueueModal(true) + }} + > + Re-enqueue buildset + ) + return ( + } + value={value} + /> + ) + } + + function enqueueConfirm() { + let changeId = buildset.change ? buildset.change + ',' + buildset.patchset : buildset.newrev + setShowEnqueueModal(false) + if (/^[0-9a-f]{40}$/.test(changeId)) { + const oldrev = '0000000000000000000000000000000000000000' + enqueue_ref(tenant.apiPrefix, buildset.project, buildset.pipeline, buildset.ref, oldrev, changeId, user.token) + .then(() => { + dispatch(addNotification( + { + text: 'Change queued successfully.', + type: 'success', + status: '', + url: '', + })) + }) + .catch(error => { + dispatch(addApiError(error)) + }) + } else { + enqueue(tenant.apiPrefix, buildset.project, buildset.pipeline, changeId, user.token) + .then(() => { + dispatch(addNotification( + { + text: 'Change queued successfully.', + type: 'success', + status: '', + url: '', + })) + }) + .catch(error => { + dispatch(addApiError(error)) + }) + } + } + + function renderEnqueueModal() { + let changeId = buildset.change ? buildset.change + ',' + buildset.patchset : buildset.newrev + const title = 'You are about to re-enqueue a change' + return ( + { setShowEnqueueModal(false) }} + actions={[ + , + , + ]}> +

Please confirm that you want to re-enqueue all jobs for change {changeId} (project {buildset.project}) on pipeline {buildset.pipeline}.

+
+ ) + } + return ( <> <BuildResultWithIcon result={buildset.result} size="md"> Buildset result </BuildResultWithIcon> - <BuildResultBadge result={buildset.result} /> + <BuildResultBadge result={buildset.result} />   {/* We handle the spacing for the body and the flex items by ourselves so they go hand in hand. By default, the flex items' spacing only @@ -216,6 +300,10 @@ function Buildset({ buildset, timezone, tenant }) { } /> + {(user.isAdmin && user.scope.indexOf(tenant.name) !== -1) && + <> + {renderEnqueueButton()} + } @@ -228,6 +316,7 @@ function Buildset({ buildset, timezone, tenant }) { setIsGanttChartModalOpen(false) }} /> + {renderEnqueueModal()} ) } @@ -236,9 +325,11 @@ Buildset.propTypes = { buildset: PropTypes.object, tenant: PropTypes.object, timezone: PropTypes.string, + user: PropTypes.object, } export default connect((state) => ({ tenant: state.tenant, timezone: state.timezone, + user: state.user, }))(Buildset) diff --git a/web/src/containers/status/Change.jsx b/web/src/containers/status/Change.jsx index ce5cb5ab64..8039a0c455 100644 --- a/web/src/containers/status/Change.jsx +++ b/web/src/containers/status/Change.jsx @@ -31,7 +31,7 @@ import { import { dequeue, dequeue_ref } from '../../api' import { addDequeueError } from '../../actions/adminActions' -import { addError } from '../../actions/errors' +import { addNotification } from '../../actions/notifications' import LineAngleImage from '../../images/line-angle.png' import LineTImage from '../../images/line-t.png' @@ -73,10 +73,11 @@ class Change extends React.Component { this.props.dispatch(addDequeueError(error)) }) } else { - this.props.dispatch(addError({ + this.props.dispatch(addNotification({ url: null, status: 'Invalid change ' + changeRef + ' on project ' + projectName, - text: '' + text: '', + type: 'error' })) } } diff --git a/web/src/reducers/index.js b/web/src/reducers/index.js index 2e47320cdf..0f357c55af 100644 --- a/web/src/reducers/index.js +++ b/web/src/reducers/index.js @@ -18,7 +18,7 @@ import auth from './auth' import configErrors from './configErrors' import change from './change' import component from './component' -import errors from './errors' +import notifications from './notifications' import build from './build' import info from './info' import job from './job' @@ -42,7 +42,7 @@ const reducers = { change, component, configErrors, - errors, + notifications, info, job, jobs, diff --git a/web/src/reducers/errors.js b/web/src/reducers/notifications.js similarity index 58% rename from web/src/reducers/errors.js rename to web/src/reducers/notifications.js index ae9632487c..71e0341db2 100644 --- a/web/src/reducers/errors.js +++ b/web/src/reducers/notifications.js @@ -13,34 +13,34 @@ // under the License. import { - ADD_ERROR, - CLEAR_ERROR, - CLEAR_ERRORS, + ADD_NOTIFICATION, + CLEAR_NOTIFICATION, + CLEAR_NOTIFICATIONS, addApiError, -} from '../actions/errors' +} from '../actions/notifications' export default (state = [], action) => { // Intercept API failure - if (action.error && action.type.match(/.*_FETCH_FAIL$/)) { - action = addApiError(action.error) + if (action.notification && action.type.match(/.*_FETCH_FAIL$/)) { + action = addApiError(action.notification) } // Intercept Admin API failures - if (action.error && action.type.match(/ADMIN_.*_FAIL$/)) { - action = addApiError(action.error) + if (action.notification && action.type.match(/ADMIN_.*_FAIL$/)) { + action = addApiError(action.notification) } switch (action.type) { - case ADD_ERROR: - if (state.filter(error => ( - error.url === action.error.url && - error.status === action.error.status)).length > 0) + case ADD_NOTIFICATION: + if (state.filter(notification => ( + notification.url === action.notification.url && + notification.status === action.notification.status)).length > 0) return state return [ ...state, - { ...action.error, id: action.id, date: Date.now() }] - case CLEAR_ERROR: + { ...action.notification, id: action.id, date: Date.now() }] + case CLEAR_NOTIFICATION: return state.filter(item => (item.id !== action.id)) - case CLEAR_ERRORS: + case CLEAR_NOTIFICATIONS: return [] default: return state