Web UI: add Autoholds, Autohold page
A user can list active autoholds for a given tenant. If the user is a tenant admin, she can also discard a autohold request from the autoholds page. Add a autohold page holding information about a single autohold request, in particular links to held builds if any. Change-Id: I4f5f2cfdf8e46ce8fb7ac69e330d6e51bd8b19fe
This commit is contained in:
parent
84ae08fb39
commit
491cf439fa
|
@ -63,7 +63,7 @@ function buildExternalLink(buildish) {
|
|||
return (
|
||||
<ExternalLink target={buildish.ref_url}>
|
||||
<strong>Revision </strong>
|
||||
{buildish.newrev.slice(0,7)}
|
||||
{buildish.newrev.slice(0, 7)}
|
||||
</ExternalLink>
|
||||
)
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ function buildExternalTableLink(buildish) {
|
|||
} else if (buildish.ref_url && buildish.newrev) {
|
||||
return (
|
||||
<ExternalLink target={buildish.ref_url}>
|
||||
{buildish.newrev.slice(0,7)}
|
||||
{buildish.newrev.slice(0, 7)}
|
||||
</ExternalLink>
|
||||
)
|
||||
}
|
||||
|
@ -92,9 +92,32 @@ function buildExternalTableLink(buildish) {
|
|||
return null
|
||||
}
|
||||
|
||||
function IconProperty(props) {
|
||||
const { icon, value, WrapElement = 'span' } = props
|
||||
return (
|
||||
<WrapElement style={{ marginLeft: '25px' }}>
|
||||
<span
|
||||
style={{
|
||||
marginRight: 'var(--pf-global--spacer--sm)',
|
||||
marginLeft: '-25px',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span>{value}</span>
|
||||
</WrapElement>
|
||||
)
|
||||
}
|
||||
|
||||
IconProperty.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
WrapElement: PropTypes.func,
|
||||
}
|
||||
|
||||
// https://github.com/kitze/conditional-wrap
|
||||
// appears to be the first implementation of this pattern
|
||||
const ConditionalWrapper = ({ condition, wrapper, children }) =>
|
||||
condition ? wrapper(children) : children
|
||||
|
||||
export { removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, ConditionalWrapper }
|
||||
export { IconProperty, removeHash, ExternalLink, buildExternalLink, buildExternalTableLink, ConditionalWrapper }
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2020 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 * as API from '../api'
|
||||
|
||||
export const AUTOHOLDS_FETCH_REQUEST = 'AUTOHOLDS_FETCH_REQUEST'
|
||||
export const AUTOHOLDS_FETCH_SUCCESS = 'AUTOHOLDS_FETCH_SUCCESS'
|
||||
export const AUTOHOLDS_FETCH_FAIL = 'AUTOHOLDS_FETCH_FAIL'
|
||||
|
||||
export const AUTOHOLD_FETCH_REQUEST = 'AUTOHOLD_FETCH_REQUEST'
|
||||
export const AUTOHOLD_FETCH_SUCCESS = 'AUTOHOLD_FETCH_SUCCESS'
|
||||
export const AUTOHOLD_FETCH_FAIL = 'AUTOHOLD_FETCH_FAIL'
|
||||
|
||||
export const requestAutoholds = () => ({
|
||||
type: AUTOHOLDS_FETCH_REQUEST
|
||||
})
|
||||
|
||||
export const receiveAutoholds = (tenant, json) => ({
|
||||
type: AUTOHOLDS_FETCH_SUCCESS,
|
||||
autoholds: json,
|
||||
receivedAt: Date.now()
|
||||
})
|
||||
|
||||
const failedAutoholds = error => ({
|
||||
type: AUTOHOLDS_FETCH_FAIL,
|
||||
error
|
||||
})
|
||||
|
||||
export const fetchAutoholds = (tenant) => dispatch => {
|
||||
dispatch(requestAutoholds())
|
||||
return API.fetchAutoholds(tenant.apiPrefix)
|
||||
.then(response => dispatch(receiveAutoholds(tenant.name, response.data)))
|
||||
.catch(error => dispatch(failedAutoholds(error)))
|
||||
}
|
||||
|
||||
const shouldFetchAutoholds = (tenant, state) => {
|
||||
const autoholds = state.autoholds
|
||||
if (!autoholds || autoholds.autoholds.length === 0) {
|
||||
return true
|
||||
}
|
||||
if (autoholds.isFetching) {
|
||||
return false
|
||||
}
|
||||
if (Date.now() - autoholds.receivedAt > 60000) {
|
||||
// Refetch after 1 minutes
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const fetchAutoholdsIfNeeded = (tenant, force) => (
|
||||
dispatch, getState) => {
|
||||
if (force || shouldFetchAutoholds(tenant, getState())) {
|
||||
return dispatch(fetchAutoholds(tenant))
|
||||
}
|
||||
}
|
||||
|
||||
export const requestAutohold = () => ({
|
||||
type: AUTOHOLD_FETCH_REQUEST
|
||||
})
|
||||
|
||||
export const receiveAutohold = (tenant, json) => ({
|
||||
type: AUTOHOLD_FETCH_SUCCESS,
|
||||
autohold: json,
|
||||
receivedAt: Date.now()
|
||||
})
|
||||
|
||||
const failedAutohold = error => ({
|
||||
type: AUTOHOLD_FETCH_FAIL,
|
||||
error
|
||||
})
|
||||
|
||||
export const fetchAutohold = (tenant, requestId) => dispatch => {
|
||||
dispatch(requestAutohold())
|
||||
return API.fetchAutohold(tenant.apiPrefix, requestId)
|
||||
.then(response => dispatch(receiveAutohold(tenant.name, response.data)))
|
||||
.catch(error => dispatch(failedAutohold(error)))
|
||||
}
|
|
@ -47,6 +47,7 @@ function getHomepageUrl(url) {
|
|||
|
||||
// Remove known sub-path
|
||||
const subDir = [
|
||||
'/autohold/',
|
||||
'/build/',
|
||||
'/buildset/',
|
||||
'/job/',
|
||||
|
@ -167,6 +168,12 @@ function fetchLabels(apiPrefix) {
|
|||
function fetchNodes(apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'nodes')
|
||||
}
|
||||
function fetchAutoholds(apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'autohold')
|
||||
}
|
||||
function fetchAutohold(apiPrefix, requestId) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'autohold/' + requestId)
|
||||
}
|
||||
|
||||
// token-protected API
|
||||
function fetchUserAuthorizations(apiPrefix, token) {
|
||||
|
@ -240,8 +247,8 @@ 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) {
|
||||
function autohold(apiPrefix, projectName, job, change, ref,
|
||||
reason, count, node_hold_expiration, token) {
|
||||
const instance = Axios.create({
|
||||
baseURL: apiUrl
|
||||
})
|
||||
|
@ -260,6 +267,17 @@ function autohold (apiPrefix, projectName, job, change, ref,
|
|||
return res
|
||||
}
|
||||
|
||||
function autohold_delete(apiPrefix, requestId, token) {
|
||||
const instance = Axios.create({
|
||||
baseURL: apiUrl
|
||||
})
|
||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token
|
||||
let res = instance.delete(
|
||||
apiPrefix + '/autohold/' + requestId
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
apiUrl,
|
||||
|
@ -284,7 +302,10 @@ export {
|
|||
fetchComponents,
|
||||
fetchTenantInfo,
|
||||
fetchUserAuthorizations,
|
||||
fetchAutoholds,
|
||||
fetchAutohold,
|
||||
autohold,
|
||||
autohold_delete,
|
||||
dequeue,
|
||||
dequeue_ref,
|
||||
enqueue,
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
// Copyright 2020 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 from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
Spinner,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
OutlinedQuestionCircleIcon,
|
||||
HashtagIcon,
|
||||
BuildIcon,
|
||||
CodeBranchIcon,
|
||||
CubeIcon,
|
||||
OutlinedClockIcon,
|
||||
LockIcon,
|
||||
TrashIcon,
|
||||
FingerprintIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
import { Link } from 'react-router-dom'
|
||||
import * as moment from 'moment'
|
||||
|
||||
import { autohold_delete } from '../../api'
|
||||
import { fetchAutoholds } from '../../actions/autoholds'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
|
||||
import { IconProperty } from '../../Misc'
|
||||
|
||||
function AutoholdTable(props) {
|
||||
const { autoholds, fetching, tenant, user, dispatch } = props
|
||||
const columns = [
|
||||
{
|
||||
title: <IconProperty icon={<FingerprintIcon />} value="ID" />,
|
||||
dataLabel: 'Request ID',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CubeIcon />} value="Project" />,
|
||||
dataLabel: 'Project',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<BuildIcon />} value="Job" />,
|
||||
dataLabel: 'Job',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CodeBranchIcon />} value="Ref Filter" />,
|
||||
dataLabel: 'Ref Filter',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<HashtagIcon />} value="Triggers" />,
|
||||
dataLabel: 'Triggers',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<OutlinedQuestionCircleIcon />} value="Reason" />,
|
||||
dataLabel: 'Reason',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<OutlinedClockIcon />} value="Hold for" />,
|
||||
dataLabel: 'Hold for',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataLabel: 'Delete',
|
||||
}
|
||||
]
|
||||
|
||||
function handleAutoholdDelete(requestId) {
|
||||
autohold_delete(tenant.apiPrefix, requestId, user.token)
|
||||
.then(() => {
|
||||
dispatch(addNotification(
|
||||
{
|
||||
text: 'Autohold request deleted successfully.',
|
||||
type: 'success',
|
||||
status: '',
|
||||
url: '',
|
||||
}))
|
||||
dispatch(fetchAutoholds(tenant))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(addApiError(error))
|
||||
})
|
||||
}
|
||||
|
||||
function renderAutoholdDeleteButton(requestId) {
|
||||
return (
|
||||
<TrashIcon
|
||||
title="Delete Autohold request"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
color: 'var(--pf-global--danger-color--100)',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
handleAutoholdDelete(requestId)
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
||||
function createAutoholdRow(autohold) {
|
||||
const count = autohold.current_count + '/' + autohold.max_count
|
||||
const node_expiration = (autohold.node_expiration === 0) ? 'Indefinitely' : moment.duration(autohold.node_expiration, 'seconds').humanize()
|
||||
const delete_button = (user.isAdmin && user.scope.indexOf(tenant.name) !== -1) ? renderAutoholdDeleteButton(autohold.id) : ''
|
||||
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: (
|
||||
<Link to={`${tenant.linkPrefix}/autohold/${autohold.id}`}>{autohold.id}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: autohold.project,
|
||||
},
|
||||
{
|
||||
title: autohold.job,
|
||||
},
|
||||
{
|
||||
title: autohold.ref_filter,
|
||||
},
|
||||
{
|
||||
title: count
|
||||
},
|
||||
{
|
||||
title: autohold.reason,
|
||||
},
|
||||
{
|
||||
title: node_expiration,
|
||||
},
|
||||
{
|
||||
title: delete_button
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
rows = autoholds.map((autohold) => createAutoholdRow(autohold))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Autohold Requests Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any autoholds but are also not
|
||||
fetching */}
|
||||
{!fetching && autoholds.length === 0 && (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={LockIcon} />
|
||||
<Title headingLevel="h1">No autohold requests found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AutoholdTable.propTypes = {
|
||||
autoholds: PropTypes.array.isRequired,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
tenant: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
user: state.user,
|
||||
}))(AutoholdTable)
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2021 Red Hat
|
||||
//
|
||||
// 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 * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
DataList,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
class HeldBuildList extends React.Component {
|
||||
static propTypes = {
|
||||
nodes: PropTypes.array,
|
||||
tenant: PropTypes.object,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
selectedBuildId: null,
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectDataListItem = (buildId) => {
|
||||
this.setState({
|
||||
selectedBuildId: buildId,
|
||||
})
|
||||
}
|
||||
|
||||
/* TODO find a way to add some more useful info than just the build's UUID,
|
||||
like a timestamp and a change number */
|
||||
render() {
|
||||
const { nodes, tenant } = this.props
|
||||
const { selectedBuildId } = this.state
|
||||
return (
|
||||
<DataList
|
||||
className="zuul-build-list"
|
||||
isCompact
|
||||
selectedDataListItemId={selectedBuildId}
|
||||
onSelectDataListItem={this.handleSelectDataListItem}
|
||||
style={{ fontSize: 'var(--pf-global--FontSize--md)' }}
|
||||
>
|
||||
{nodes.map((node) => (
|
||||
<DataListItem key={node.build} id={node.build}>
|
||||
<Link
|
||||
to={`${tenant.linkPrefix}/build/${node.build}`}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: 'var(--pf-global--disabled-color--100)',
|
||||
}}
|
||||
>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={node.build} width={3}>
|
||||
{node.build}
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</Link>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((state) => ({ tenant: state.tenant }))(HeldBuildList)
|
|
@ -0,0 +1,35 @@
|
|||
// 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 * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
|
||||
function AutoholdNodes(props) {
|
||||
const {build, link } = props
|
||||
|
||||
return (
|
||||
<span >
|
||||
<Link to={link}>{build}</Link>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
AutoholdNodes.propTypes = {
|
||||
build: PropTypes.string,
|
||||
link: PropTypes.string,
|
||||
}
|
||||
|
||||
export { AutoholdNodes }
|
|
@ -43,8 +43,8 @@ import {
|
|||
import * as moment from 'moment'
|
||||
import 'moment-duration-format'
|
||||
|
||||
import { BuildResultBadge, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { buildExternalLink, ExternalLink } from '../../Misc'
|
||||
import { BuildResultBadge, BuildResultWithIcon } from './Misc'
|
||||
import { buildExternalLink, ExternalLink, IconProperty } from '../../Misc'
|
||||
|
||||
import { autohold } from '../../api'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
|
@ -117,7 +117,7 @@ function Build({ build, tenant, timezone, user }) {
|
|||
variant="primary"
|
||||
onClick={() => {
|
||||
handleConfirm()
|
||||
setShowAutoholdModal()
|
||||
setShowAutoholdModal(false)
|
||||
}}>Create</Button>,
|
||||
<Button
|
||||
key="autohold_cancel"
|
||||
|
|
|
@ -34,7 +34,8 @@ import {
|
|||
import 'moment-duration-format'
|
||||
import * as moment from 'moment'
|
||||
|
||||
import { BuildResult, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { BuildResult, BuildResultWithIcon } from './Misc'
|
||||
import { IconProperty } from '../../Misc'
|
||||
|
||||
class BuildList extends React.Component {
|
||||
static propTypes = {
|
||||
|
|
|
@ -46,8 +46,8 @@ import {
|
|||
import 'moment-duration-format'
|
||||
import * as moment from 'moment'
|
||||
|
||||
import { BuildResult, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { buildExternalTableLink } from '../../Misc'
|
||||
import { BuildResult, BuildResultWithIcon } from './Misc'
|
||||
import { buildExternalTableLink, IconProperty } from '../../Misc'
|
||||
|
||||
function BuildTable({
|
||||
builds,
|
||||
|
@ -147,13 +147,13 @@ function BuildTable({
|
|||
.format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<BuildResult
|
||||
result={build.result}
|
||||
link={`${tenant.linkPrefix}/build/${build.uuid}`}
|
||||
colored={build.voting}
|
||||
/>
|
||||
),
|
||||
title: (
|
||||
<BuildResult
|
||||
result={build.result}
|
||||
link={`${tenant.linkPrefix}/build/${build.uuid}`}
|
||||
colored={build.voting}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ function BuildTable({
|
|||
cells={columns}
|
||||
rows={rows}
|
||||
actions={actions}
|
||||
className="zuul-build-table"
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
|
|
|
@ -40,8 +40,8 @@ import {
|
|||
import * as moment from 'moment'
|
||||
import 'moment-duration-format'
|
||||
|
||||
import { buildExternalLink } from '../../Misc'
|
||||
import { BuildResultBadge, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { buildExternalLink, IconProperty } from '../../Misc'
|
||||
import { BuildResultBadge, BuildResultWithIcon } from './Misc'
|
||||
import { enqueue, enqueue_ref } from '../../api'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
import { ChartModal } from '../charts/ChartModal'
|
||||
|
|
|
@ -42,8 +42,8 @@ import {
|
|||
cellWidth,
|
||||
} from '@patternfly/react-table'
|
||||
|
||||
import { BuildResult, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
import { buildExternalTableLink } from '../../Misc'
|
||||
import { BuildResult, BuildResultWithIcon } from './Misc'
|
||||
import { buildExternalTableLink, IconProperty } from '../../Misc'
|
||||
|
||||
function BuildsetTable({
|
||||
buildsets,
|
||||
|
@ -109,13 +109,13 @@ function BuildsetTable({
|
|||
title: changeOrRefLink && changeOrRefLink,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<BuildResult
|
||||
result={buildset.result}
|
||||
link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}
|
||||
>
|
||||
</BuildResult>
|
||||
),
|
||||
title: (
|
||||
<BuildResult
|
||||
result={buildset.result}
|
||||
link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}
|
||||
>
|
||||
</BuildResult>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ function BuildsetTable({
|
|||
cells={columns}
|
||||
rows={rows}
|
||||
actions={actions}
|
||||
className="zuul-build-table"
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
|
|
|
@ -177,27 +177,6 @@ BuildResultWithIcon.propTypes = {
|
|||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
function IconProperty(props) {
|
||||
const { icon, value, WrapElement = 'span' } = props
|
||||
return (
|
||||
<WrapElement style={{ marginLeft: '25px' }}>
|
||||
<span
|
||||
style={{
|
||||
marginRight: 'var(--pf-global--spacer--sm)',
|
||||
marginLeft: '-25px',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span>{value}</span>
|
||||
</WrapElement>
|
||||
)
|
||||
}
|
||||
|
||||
IconProperty.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
WrapElement: PropTypes.func,
|
||||
}
|
||||
|
||||
export { BuildResult, BuildResultBadge, BuildResultWithIcon, IconProperty }
|
||||
export { BuildResult, BuildResultBadge, BuildResultWithIcon }
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
HistoryIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
|
||||
import { IconProperty } from '../build/Misc'
|
||||
import { IconProperty } from '../../Misc'
|
||||
|
||||
const STATE_ICON_CONFIGS = {
|
||||
RUNNING: {
|
||||
|
|
|
@ -57,7 +57,7 @@ a.refresh {
|
|||
}
|
||||
|
||||
/* Keep the normal font-size for compact tables */
|
||||
.zuul-build-table td {
|
||||
.zuul-table td {
|
||||
font-size: var(--pf-global--FontSize--md);
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ a.refresh {
|
|||
}
|
||||
|
||||
/* Use the same hover effect on table rows like for the selectable data list */
|
||||
.zuul-build-table tbody tr:hover {
|
||||
.zuul-table tbody tr:hover {
|
||||
box-shadow: var(--pf-global--BoxShadow--sm-top),
|
||||
var(--pf-global--BoxShadow--sm-bottom);
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ a.refresh {
|
|||
show the column names. Thus, we fall back to the border to show the hover
|
||||
effect. The drawback with that is, that we can't show a nice transition.
|
||||
*/
|
||||
.zuul-build-table tbody tr:hover {
|
||||
.zuul-table tbody tr:hover {
|
||||
border-left-color: var(--pf-global--active-color--100);
|
||||
border-left-width: var(--pf-global--BorderWidth--lg);
|
||||
border-left-style: solid;
|
||||
|
@ -96,7 +96,7 @@ a.refresh {
|
|||
/* For the larger screens (normal table layout) we can use the before
|
||||
element on the first table cell to show the same hover effect like for
|
||||
the data list */
|
||||
.zuul-build-table tbody tr td:first-child::before {
|
||||
.zuul-table tbody tr td:first-child::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
@ -107,14 +107,14 @@ a.refresh {
|
|||
transition: var(--pf-global--Transition);
|
||||
}
|
||||
|
||||
.zuul-build-table tbody tr:hover td:first-child::before {
|
||||
.zuul-table tbody tr:hover td:first-child::before {
|
||||
background-color: var(--pf-global--active-color--100);
|
||||
}
|
||||
|
||||
/* Hide the action column with the build link on larger screen. This is only
|
||||
needed for the mobile version as we can't use the "magnifying-glass icon
|
||||
on hover" effect there. */
|
||||
.zuul-build-table .pf-c-table__action {
|
||||
.zuul-table .pf-c-table__action {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
// 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 * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateVariant,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
Flex,
|
||||
FlexItem,
|
||||
List,
|
||||
ListItem,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
LockIcon,
|
||||
BuildIcon,
|
||||
CubeIcon,
|
||||
CodeIcon,
|
||||
HashtagIcon,
|
||||
OutlinedClockIcon,
|
||||
OutlinedCommentDotsIcon,
|
||||
TrashIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import { IconProperty } from '../Misc'
|
||||
|
||||
import { Link } from 'react-router-dom'
|
||||
import * as moment from 'moment'
|
||||
|
||||
import { fetchAutohold } from '../actions/autoholds'
|
||||
import { EmptyPage } from '../containers/Errors'
|
||||
import { Fetching } from '../containers/Fetching'
|
||||
import HeldBuildList from '../containers/autohold/HeldBuildList'
|
||||
|
||||
|
||||
// This is hard-coded in zuul/executor/server.py#3035
|
||||
const EXPIRED_HOLD_REQUEST_TTL = 24 * 60 * 60
|
||||
|
||||
|
||||
class AutoholdPage extends React.Component {
|
||||
static propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
tenant: PropTypes.object.isRequired,
|
||||
autohold: PropTypes.object,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
fetchAutohold: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
updateData = () => {
|
||||
if (!this.props.autohold) {
|
||||
this.props.fetchAutohold(
|
||||
this.props.tenant,
|
||||
this.props.match.params.requestId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = 'Zuul Autohold Request'
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.tenant.name !== prevProps.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { autohold, isFetching, tenant } = this.props
|
||||
|
||||
// Initial page load
|
||||
if (autohold === undefined || isFetching) {
|
||||
return <Fetching />
|
||||
}
|
||||
|
||||
// Fetching finished, but no autohold found
|
||||
if (!autohold) {
|
||||
return (
|
||||
<EmptyPage
|
||||
title="This autohold request does not exist"
|
||||
icon={LockIcon}
|
||||
linkTarget={`${tenant.linkPrefix}/autoholds`}
|
||||
linkText="Show all autohold requests"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Return the build list or an empty state if no builds triggered the autohold.
|
||||
const buildsContent = autohold.nodes.length > 0 ? (
|
||||
<span ><HeldBuildList nodes={autohold.nodes} /></span>
|
||||
) : (
|
||||
<>
|
||||
{/* Using an hr above the empty state ensures that the space between
|
||||
heading (builds) and empty state is filled and the empty state
|
||||
doesn't look like it's lost in space. */}
|
||||
<hr />
|
||||
<EmptyState variant={EmptyStateVariant.small}>
|
||||
<EmptyStateIcon icon={BuildIcon} />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
This autohold request has not triggered yet.
|
||||
</Title>
|
||||
</EmptyState>
|
||||
</>
|
||||
)
|
||||
|
||||
const node_expiration = (autohold.node_expiration === 0) ? 'Indefinitely' : moment.duration(autohold.node_expiration, 'seconds').humanize()
|
||||
console.log(autohold.expired)
|
||||
const elapsed = autohold.expired ? (Date.now() / 1000 - autohold.expired) : false
|
||||
console.log(elapsed)
|
||||
const timeToDeletion = autohold.node_expiration + EXPIRED_HOLD_REQUEST_TTL - elapsed
|
||||
console.log(timeToDeletion)
|
||||
|
||||
|
||||
let deletionInfo, deletionInfoMsg
|
||||
if (autohold.node_expiration !== 0 && elapsed) {
|
||||
deletionInfoMsg = timeToDeletion > 0 ?
|
||||
(<>
|
||||
<strong>Deletion scheduled in </strong> {moment.duration(timeToDeletion, 'seconds').humanize()}
|
||||
</>) :
|
||||
<span>This request is scheduled to be deleted automatically.</span>
|
||||
deletionInfo = <IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<TrashIcon />}
|
||||
value={deletionInfoMsg}
|
||||
/>
|
||||
} else {
|
||||
deletionInfo = <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Title headingLevel="h2">Autohold Request {autohold.id}</Title>
|
||||
|
||||
<Flex className="zuul-autohold-attributes">
|
||||
<Flex flex={{ default: 'flex_1' }}>
|
||||
<FlexItem>
|
||||
<List style={{ listStyle: 'none' }}>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<CubeIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Project </strong> <Link to={`${tenant.linkPrefix}/project/${autohold.project}`}>{autohold.project}</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<CodeIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Filter </strong> {autohold.ref_filter}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<BuildIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Job </strong> <Link to={`${tenant.linkPrefix}/job/${autohold.job}`}>{autohold.job}</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<HashtagIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Trigger Count </strong> {autohold.current_count} out of {autohold.max_count}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<OutlinedClockIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Hold Duration </strong> <span title={autohold.node_expiration + ' seconds'} >{node_expiration}</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<Flex flex={{ default: 'flex_1' }}>
|
||||
<FlexItem>
|
||||
<List style={{ listStyle: 'none' }}>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<OutlinedCommentDotsIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Reason:</strong>
|
||||
<pre>{autohold.reason}</pre>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{deletionInfo}
|
||||
</List>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageSection>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Title headingLevel="h3">
|
||||
<BuildIcon
|
||||
style={{
|
||||
marginRight: 'var(--pf-global--spacer--sm)',
|
||||
verticalAlign: '-0.1em',
|
||||
}}
|
||||
/>{' '}
|
||||
Held Builds
|
||||
</Title>
|
||||
{buildsContent}
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
autohold: state.autoholds.autohold,
|
||||
tenant: state.tenant,
|
||||
isFetching: state.autoholds.isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = { fetchAutohold }
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AutoholdPage)
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2020 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 * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
|
||||
import { fetchAutoholdsIfNeeded } from '../actions/autoholds'
|
||||
import AutoholdTable from '../containers/autohold/AutoholdTable'
|
||||
|
||||
|
||||
|
||||
class AutoholdsPage extends React.Component {
|
||||
static propTypes = {
|
||||
tenant: PropTypes.object,
|
||||
remoteData: PropTypes.object,
|
||||
dispatch: PropTypes.func
|
||||
}
|
||||
|
||||
updateData = (force) => {
|
||||
this.props.dispatch(fetchAutoholdsIfNeeded(this.props.tenant, force))
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.title = 'Zuul Autoholds'
|
||||
if (this.props.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.tenant.name !== prevProps.tenant.name) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { remoteData } = this.props
|
||||
const autoholds = remoteData.autoholds
|
||||
|
||||
return (
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<AutoholdTable
|
||||
autoholds={autoholds}
|
||||
fetching={remoteData.isFetching} />
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
tenant: state.tenant,
|
||||
remoteData: state.autoholds,
|
||||
}))(AutoholdsPage)
|
|
@ -24,7 +24,8 @@ import {
|
|||
FolderIcon,
|
||||
HomeIcon,
|
||||
RepositoryIcon,
|
||||
TrendUpIcon
|
||||
TrendUpIcon,
|
||||
ThumbtackIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import {
|
||||
Table,
|
||||
|
@ -35,7 +36,7 @@ import {
|
|||
import { Fetching } from '../containers/Fetching'
|
||||
import { fetchTenantsIfNeeded } from '../actions/tenants'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
import { IconProperty } from '../containers/build/Misc'
|
||||
import { IconProperty } from '../Misc'
|
||||
|
||||
class TenantsPage extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,15 +48,15 @@ class TenantsPage extends React.Component {
|
|||
this.props.dispatch(fetchTenantsIfNeeded(force))
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
document.title = 'Zuul Tenants'
|
||||
this.updateData()
|
||||
}
|
||||
|
||||
// TODO: fix Refreshable class to work with tenant less page.
|
||||
componentDidUpdate () { }
|
||||
componentDidUpdate() { }
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { remoteData } = this.props
|
||||
if (remoteData.isFetching) {
|
||||
return <Fetching />
|
||||
|
@ -64,46 +65,53 @@ class TenantsPage extends React.Component {
|
|||
const tenants = remoteData.tenants.map((tenant) => {
|
||||
return {
|
||||
cells: [
|
||||
{title: (<b>{tenant.name}</b>)},
|
||||
{title: (<Link to={'/t/' + tenant.name + '/status'}>Status</Link>)},
|
||||
{title: (<Link to={'/t/' + tenant.name + '/projects'}>Projects</Link>)},
|
||||
{title: (<Link to={'/t/' + tenant.name + '/jobs'}>Jobs</Link>)},
|
||||
{title: (<Link to={'/t/' + tenant.name + '/builds'}>Builds</Link>)},
|
||||
{title: (<Link to={'/t/' + tenant.name + '/buildsets'}>Buildsets</Link>)},
|
||||
{ title: (<b>{tenant.name}</b>) },
|
||||
{ title: (<Link to={'/t/' + tenant.name + '/status'}>Status</Link>) },
|
||||
{ title: (<Link to={'/t/' + tenant.name + '/projects'}>Projects</Link>) },
|
||||
{ title: (<Link to={'/t/' + tenant.name + '/jobs'}>Jobs</Link>) },
|
||||
{ title: (<Link to={'/t/' + tenant.name + '/builds'}>Builds</Link>) },
|
||||
{ title: (<Link to={'/t/' + tenant.name + '/buildsets'}>Buildsets</Link>) },
|
||||
{ title: (<Link to={'/t/' + tenant.name + '/autoholds'}>Autoholds</Link>) },
|
||||
tenant.projects,
|
||||
tenant.queue
|
||||
]}})
|
||||
]
|
||||
}
|
||||
})
|
||||
const columns = [
|
||||
{
|
||||
title: <IconProperty icon={<HomeIcon />} value="Name"/>,
|
||||
title: <IconProperty icon={<HomeIcon />} value="Name" />,
|
||||
dataLabel: 'Name',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<DesktopIcon />} value="Status"/>,
|
||||
title: <IconProperty icon={<DesktopIcon />} value="Status" />,
|
||||
dataLabel: 'Status',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CubeIcon />} value="Projects"/>,
|
||||
title: <IconProperty icon={<CubeIcon />} value="Projects" />,
|
||||
dataLabel: 'Projects',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<BuildIcon />} value="Jobs"/>,
|
||||
title: <IconProperty icon={<BuildIcon />} value="Jobs" />,
|
||||
dataLabel: 'Jobs',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<FolderIcon />} value="Builds"/>,
|
||||
title: <IconProperty icon={<FolderIcon />} value="Builds" />,
|
||||
dataLabel: 'Builds',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<RepositoryIcon />} value="Buildsets"/>,
|
||||
title: <IconProperty icon={<RepositoryIcon />} value="Buildsets" />,
|
||||
dataLabel: 'Buildsets',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CubesIcon />} value="Project count"/>,
|
||||
title: <IconProperty icon={<ThumbtackIcon />} value="Autoholds" />,
|
||||
dataLabel: 'Autoholds',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<CubesIcon />} value="Project count" />,
|
||||
dataLabel: 'Project count',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<TrendUpIcon />} value="Queue"/>,
|
||||
title: <IconProperty icon={<TrendUpIcon />} value="Queue" />,
|
||||
dataLabel: 'Queue',
|
||||
}
|
||||
]
|
||||
|
@ -125,4 +133,4 @@ class TenantsPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({remoteData: state.tenants}))(TenantsPage)
|
||||
export default connect(state => ({ remoteData: state.tenants }))(TenantsPage)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2020 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 {
|
||||
AUTOHOLDS_FETCH_FAIL,
|
||||
AUTOHOLDS_FETCH_REQUEST,
|
||||
AUTOHOLDS_FETCH_SUCCESS,
|
||||
AUTOHOLD_FETCH_FAIL,
|
||||
AUTOHOLD_FETCH_REQUEST,
|
||||
AUTOHOLD_FETCH_SUCCESS
|
||||
} from '../actions/autoholds'
|
||||
|
||||
export default (state = {
|
||||
receivedAt: 0,
|
||||
isFetching: false,
|
||||
autoholds: [],
|
||||
autohold: null,
|
||||
}, action) => {
|
||||
switch (action.type) {
|
||||
case AUTOHOLDS_FETCH_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
}
|
||||
case AUTOHOLDS_FETCH_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
autoholds: action.autoholds,
|
||||
receivedAt: action.receivedAt,
|
||||
}
|
||||
case AUTOHOLDS_FETCH_FAIL:
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
}
|
||||
case AUTOHOLD_FETCH_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
}
|
||||
case AUTOHOLD_FETCH_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
autohold: action.autohold,
|
||||
receivedAt: action.receivedAt,
|
||||
}
|
||||
case AUTOHOLD_FETCH_FAIL:
|
||||
return {
|
||||
...state,
|
||||
isFetching: false
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
import { combineReducers } from 'redux'
|
||||
|
||||
import auth from './auth'
|
||||
import autoholds from './autoholds'
|
||||
import configErrors from './configErrors'
|
||||
import change from './change'
|
||||
import component from './component'
|
||||
|
@ -38,6 +39,7 @@ import user from './user'
|
|||
|
||||
const reducers = {
|
||||
auth,
|
||||
autoholds,
|
||||
build,
|
||||
change,
|
||||
component,
|
||||
|
|
|
@ -21,6 +21,8 @@ import JobPage from './pages/Job'
|
|||
import JobsPage from './pages/Jobs'
|
||||
import LabelsPage from './pages/Labels'
|
||||
import NodesPage from './pages/Nodes'
|
||||
import AutoholdsPage from './pages/Autoholds'
|
||||
import AutoholdPage from './pages/Autohold'
|
||||
import BuildPage from './pages/Build'
|
||||
import BuildsPage from './pages/Builds'
|
||||
import BuildsetPage from './pages/Buildset'
|
||||
|
@ -61,6 +63,11 @@ const routes = () => [
|
|||
to: '/nodes',
|
||||
component: NodesPage
|
||||
},
|
||||
{
|
||||
title: 'Autoholds',
|
||||
to: '/autoholds',
|
||||
component: AutoholdsPage
|
||||
},
|
||||
{
|
||||
title: 'Builds',
|
||||
to: '/builds',
|
||||
|
@ -116,6 +123,10 @@ const routes = () => [
|
|||
to: '/buildset/:buildsetId',
|
||||
component: BuildsetPage
|
||||
},
|
||||
{
|
||||
to: '/autohold/:requestId',
|
||||
component: AutoholdPage
|
||||
},
|
||||
{
|
||||
to: '/config-errors',
|
||||
component: ConfigErrorsPage,
|
||||
|
|
Loading…
Reference in New Issue