PF4: Update buildset result page (new layout and styling)
This updates the buildset result page with Patternfly 4 components and a new design based on inspirations from other CI systems, Patternfly 4 demos and my own ideas. I chose the buildset result page because it doesn't contain too much elements und therefore is a good starting point. If you like the design, I will continue with that also for other pages like the build result pages or the various "list" pages like builds and buildsets. Change-Id: I978ee448ddf6e22e4a6ec8211204f694932eaa4e
This commit is contained in:
parent
18e55acb61
commit
403a828b86
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2020 BMW Group
|
||||
//
|
||||
// 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 { ExternalLinkAltIcon } from '@patternfly/react-icons'
|
||||
|
||||
function ExternalLink(props) {
|
||||
const { target } = props
|
||||
|
||||
return (
|
||||
<a href={target}>
|
||||
<span>
|
||||
{props.children}
|
||||
{/* As we want the icon to be smaller than "sm", we have to specify the
|
||||
font-size directly */}
|
||||
<ExternalLinkAltIcon
|
||||
style={{
|
||||
marginLeft: 'var(--pf-global--spacer--xs)',
|
||||
color: 'var(--pf-global--Color--400)',
|
||||
fontSize: 'var(--pf-global--icon--FontSize--sm)',
|
||||
verticalAlign: 'super',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
ExternalLink.propTypes = {
|
||||
target: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
export { ExternalLink }
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2020 BMW Group
|
||||
//
|
||||
// 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'
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStatePrimary,
|
||||
EmptyStateVariant,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
function EmptyPage(props) {
|
||||
const { title, icon, linkTarget, linkText } = props
|
||||
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.small}>
|
||||
<EmptyStateIcon icon={icon} />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
{title}
|
||||
</Title>
|
||||
<EmptyStatePrimary>
|
||||
<Link to={linkTarget}>
|
||||
<Button variant="link">{linkText}</Button>
|
||||
</Link>
|
||||
</EmptyStatePrimary>
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
||||
EmptyPage.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.func.isRequired,
|
||||
linkTarget: PropTypes.string.isRequired,
|
||||
linkText: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export { EmptyPage }
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright 2020 BMW Group
|
||||
//
|
||||
// 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'
|
||||
import { OutlinedClockIcon } from '@patternfly/react-icons'
|
||||
import 'moment-duration-format'
|
||||
import * as moment from 'moment'
|
||||
|
||||
import { BuildResult, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
|
||||
class BuildList extends React.Component {
|
||||
static propTypes = {
|
||||
builds: PropTypes.array,
|
||||
tenant: PropTypes.object,
|
||||
}
|
||||
|
||||
// TODO (felix): Add a property "isCompact" to be used on the buildresult
|
||||
// page. Without this flag we might then even use this (with more
|
||||
// information) on the /builds page.
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
selectedBuildId: null,
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectDataListItem = (buildId) => {
|
||||
this.setState({
|
||||
selectedBuildId: buildId,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { builds, 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)' }}
|
||||
>
|
||||
{builds.map((build) => (
|
||||
<DataListItem key={build.uuid || build.job_name} id={build.uuid}>
|
||||
<Link
|
||||
to={`${tenant.linkPrefix}/build/${build.uuid}`}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: build.voting
|
||||
? 'inherit'
|
||||
: 'var(--pf-global--disabled-color--100)',
|
||||
}}
|
||||
>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={build.uuid} width={3}>
|
||||
<BuildResultWithIcon
|
||||
result={build.result}
|
||||
colored={build.voting}
|
||||
size="sm"
|
||||
>
|
||||
{build.job_name}
|
||||
{!build.voting && ' (non-voting)'}
|
||||
</BuildResultWithIcon>
|
||||
</DataListCell>,
|
||||
<DataListCell key={`${build.uuid}-time`}>
|
||||
<IconProperty
|
||||
icon={<OutlinedClockIcon />}
|
||||
value={moment
|
||||
.duration(build.duration, 'seconds')
|
||||
.format('h [hr] m [min] s [sec]')}
|
||||
/>
|
||||
</DataListCell>,
|
||||
<DataListCell key={`${build.uuid}-result`}>
|
||||
<BuildResult
|
||||
result={build.result}
|
||||
colored={build.voting}
|
||||
/>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</Link>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((state) => ({ tenant: state.tenant }))(BuildList)
|
|
@ -15,117 +15,122 @@
|
|||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Panel } from 'react-bootstrap'
|
||||
import * as moment from 'moment'
|
||||
import 'moment-duration-format'
|
||||
import { Flex, FlexItem, List, ListItem, Title } from '@patternfly/react-core'
|
||||
import {
|
||||
CodeIcon,
|
||||
CodeBranchIcon,
|
||||
OutlinedCommentDotsIcon,
|
||||
CubeIcon,
|
||||
FingerprintIcon,
|
||||
StreamIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
|
||||
import { ExternalLink } from '../../Misc'
|
||||
import { BuildResultBadge, BuildResultWithIcon, IconProperty } from './Misc'
|
||||
|
||||
class Buildset extends React.Component {
|
||||
static propTypes = {
|
||||
buildset: PropTypes.object,
|
||||
tenant: PropTypes.object,
|
||||
}
|
||||
function Buildset(props) {
|
||||
const { buildset, fetchable } = props
|
||||
|
||||
render () {
|
||||
const { buildset } = this.props
|
||||
const rows = []
|
||||
const myColumns = [
|
||||
'change', 'project', 'branch', 'pipeline', 'result', 'message', 'event_id'
|
||||
]
|
||||
const buildRows = []
|
||||
const buildColumns = [
|
||||
'job', 'result', 'voting', 'duration'
|
||||
]
|
||||
|
||||
myColumns.forEach(column => {
|
||||
let label = column
|
||||
let value = buildset[column]
|
||||
if (column === 'change') {
|
||||
value = (
|
||||
<a href={buildset.ref_url}>
|
||||
{buildset.change},{buildset.patchset}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
if (column === 'event_id') {
|
||||
label = 'event id'
|
||||
}
|
||||
if (value) {
|
||||
rows.push({key: label, value: value})
|
||||
}
|
||||
})
|
||||
|
||||
if (buildset.builds) {
|
||||
buildset.builds.forEach(build => {
|
||||
const row = []
|
||||
buildColumns.forEach(column => {
|
||||
if (column === 'job') {
|
||||
row.push(build.job_name)
|
||||
} else if (column === 'duration') {
|
||||
row.push(moment.duration(build.duration, 'seconds')
|
||||
.format('h [hr] m [min] s [sec]'))
|
||||
} else if (column === 'voting') {
|
||||
row.push(build.voting ? 'true' : 'false')
|
||||
} else if (column === 'result') {
|
||||
row.push(<Link
|
||||
to={this.props.tenant.linkPrefix + '/build/' + build.uuid}>
|
||||
{build.result}
|
||||
</Link>)
|
||||
} else {
|
||||
row.push(build[column])
|
||||
}
|
||||
})
|
||||
buildRows.push(row)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Panel>
|
||||
<Panel.Heading>Buildset result {buildset.uuid}</Panel.Heading>
|
||||
<Panel.Body>
|
||||
<table className="table table-striped table-bordered">
|
||||
<tbody>
|
||||
{rows.map(item => (
|
||||
<tr key={item.key}>
|
||||
<td>{item.key}</td>
|
||||
<td>{item.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
{buildset.builds &&
|
||||
<Panel>
|
||||
<Panel.Heading>Builds</Panel.Heading>
|
||||
<Panel.Body>
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{buildColumns.map(item => (
|
||||
<td key={item}>{item}</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{buildset.builds.map((item, idx) => (
|
||||
<tr key={idx} className={item.result === 'SUCCESS' ? 'success': 'warning'}>
|
||||
{buildRows[idx].map((item, idx) => (
|
||||
<td key={idx}>{item}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Title headingLevel="h2">
|
||||
<BuildResultWithIcon result={buildset.result} size="md">
|
||||
Buildset result
|
||||
</BuildResultWithIcon>
|
||||
<BuildResultBadge result={buildset.result} />
|
||||
{fetchable}
|
||||
</Title>
|
||||
{/* 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
|
||||
affects left/right margin, but not top or bottom (which looks
|
||||
awkward when the items are stacked at certain breakpoints) */}
|
||||
<Flex className="zuul-build-attributes">
|
||||
<Flex flex={{ default: 'flex_1' }}>
|
||||
<FlexItem>
|
||||
<List style={{ listStyle: 'none' }}>
|
||||
{/* TODO (felix): It would be cool if we could differentiate
|
||||
between the SVC system (Github, Gitlab, Gerrit), so we could
|
||||
show the respective icon here (GithubIcon, GitlabIcon,
|
||||
GitIcon - AFAIK the Gerrit icon is not very popular among
|
||||
icon frameworks like fontawesome */}
|
||||
{buildset.change && (
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<CodeIcon />}
|
||||
value={
|
||||
<ExternalLink target={buildset.ref_url}>
|
||||
<strong>Change </strong>
|
||||
{buildset.change},{buildset.patchset}
|
||||
</ExternalLink>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* TODO (felix): Link to project page in Zuul */}
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<CubeIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Project </strong> {buildset.project}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<CodeBranchIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Branch </strong> {buildset.branch}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<StreamIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Pipeline </strong> {buildset.pipeline}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<FingerprintIcon />}
|
||||
value={
|
||||
<span>
|
||||
<strong>UUID </strong> {buildset.uuid} <br />
|
||||
<strong>Event ID </strong> {buildset.event_id} <br />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<Flex flex={{ default: 'flex_1' }}>
|
||||
<FlexItem>
|
||||
<List style={{ listStyle: 'none' }}>
|
||||
<IconProperty
|
||||
WrapElement={ListItem}
|
||||
icon={<OutlinedCommentDotsIcon />}
|
||||
value={
|
||||
<>
|
||||
<strong>Message:</strong>
|
||||
<pre>{buildset.message}</pre>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Buildset.propTypes = {
|
||||
buildset: PropTypes.object,
|
||||
tenant: PropTypes.object,
|
||||
fetchable: PropTypes.node,
|
||||
}
|
||||
|
||||
export default connect(state => ({tenant: state.tenant}))(Buildset)
|
||||
export default connect((state) => ({ tenant: state.tenant }))(Buildset)
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2020 BMW Group
|
||||
//
|
||||
// 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 { Label } from '@patternfly/react-core'
|
||||
import {
|
||||
CheckIcon,
|
||||
ExclamationIcon,
|
||||
QuestionIcon,
|
||||
TimesIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
|
||||
const RESULT_ICON_CONFIGS = {
|
||||
SUCCESS: {
|
||||
icon: CheckIcon,
|
||||
color: 'var(--pf-global--success-color--100)',
|
||||
badgeColor: 'green',
|
||||
},
|
||||
FAILURE: {
|
||||
icon: TimesIcon,
|
||||
color: 'var(--pf-global--danger-color--100)',
|
||||
badgeColor: 'red',
|
||||
},
|
||||
RETRY_LIMIT: {
|
||||
icon: TimesIcon,
|
||||
color: 'var(--pf-global--danger-color--100)',
|
||||
badgeColor: 'red',
|
||||
},
|
||||
SKIPPED: {
|
||||
icon: QuestionIcon,
|
||||
color: 'var(--pf-global--info-color--100)',
|
||||
badgeColor: 'blue',
|
||||
},
|
||||
ABORTED: {
|
||||
icon: QuestionIcon,
|
||||
color: 'var(--pf-global--info-color--100)',
|
||||
badgeColor: 'yellow',
|
||||
},
|
||||
MERGER_FAILURE: {
|
||||
icon: ExclamationIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
badgeColor: 'orange',
|
||||
},
|
||||
NODE_FAILURE: {
|
||||
icon: ExclamationIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
badgeColor: 'orange',
|
||||
},
|
||||
TIMED_OUT: {
|
||||
icon: ExclamationIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
badgeColor: 'orange',
|
||||
},
|
||||
POST_FAILURE: {
|
||||
icon: ExclamationIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
badgeColor: 'orange',
|
||||
},
|
||||
CONFIG_ERROR: {
|
||||
icon: ExclamationIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
badgeColor: 'orange',
|
||||
},
|
||||
}
|
||||
|
||||
const DEFAULT_RESULT_ICON_CONFIG = {
|
||||
icon: ExclamationIcon,
|
||||
color: 'var(--pf-global--warning-color--100)',
|
||||
badgeColor: 'orange',
|
||||
}
|
||||
|
||||
function BuildResult(props) {
|
||||
const { result, colored = true } = props
|
||||
const iconConfig = RESULT_ICON_CONFIGS[result] || DEFAULT_RESULT_ICON_CONFIG
|
||||
const color = colored ? iconConfig.color : 'inherit'
|
||||
|
||||
return <span style={{ color: color }}>{result}</span>
|
||||
}
|
||||
|
||||
BuildResult.propTypes = {
|
||||
result: PropTypes.string,
|
||||
colored: PropTypes.bool,
|
||||
}
|
||||
|
||||
function BuildResultBadge(props) {
|
||||
const { result } = props
|
||||
const iconConfig = RESULT_ICON_CONFIGS[result] || DEFAULT_RESULT_ICON_CONFIG
|
||||
const color = iconConfig.badgeColor
|
||||
|
||||
return (
|
||||
<Label
|
||||
color={color}
|
||||
style={{
|
||||
marginLeft: 'var(--pf-global--spacer--sm)',
|
||||
verticalAlign: '0.15em',
|
||||
}}
|
||||
>
|
||||
{result}
|
||||
</Label>
|
||||
)
|
||||
}
|
||||
|
||||
BuildResultBadge.propTypes = {
|
||||
result: PropTypes.string,
|
||||
}
|
||||
|
||||
function BuildResultWithIcon(props) {
|
||||
const { result, colored = true, size = 'sm' } = props
|
||||
const iconConfig = RESULT_ICON_CONFIGS[result] || DEFAULT_RESULT_ICON_CONFIG
|
||||
|
||||
// Define the verticalAlign based on the size
|
||||
let verticalAlign = '-0.2em'
|
||||
|
||||
if (size === 'md') {
|
||||
verticalAlign = '-0.35em'
|
||||
}
|
||||
|
||||
const Icon = iconConfig.icon
|
||||
const color = colored ? iconConfig.color : 'inherit'
|
||||
|
||||
return (
|
||||
<span style={{ color: color }}>
|
||||
<Icon
|
||||
size={size}
|
||||
style={{
|
||||
marginRight: 'var(--pf-global--spacer--sm)',
|
||||
verticalAlign: verticalAlign,
|
||||
}}
|
||||
/>
|
||||
{props.children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
BuildResultWithIcon.propTypes = {
|
||||
result: PropTypes.string,
|
||||
colored: PropTypes.bool,
|
||||
size: PropTypes.string,
|
||||
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 }
|
|
@ -8,6 +8,10 @@ body {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.pf-c-title {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
a.refresh {
|
||||
cursor: pointer;
|
||||
border-bottom-style: none;
|
||||
|
@ -30,6 +34,25 @@ a.refresh {
|
|||
color: var(--pf-global--Color--dark-100);
|
||||
}
|
||||
|
||||
/* Build Lists */
|
||||
.pf-c-data-list__item.pf-m-selectable:hover:not(.pf-m-selected),
|
||||
.pf-c-data-list__item.pf-m-selectable:focus:not(.pf-m-selected) {
|
||||
/* Improve the hover/focus effect of selected lines */
|
||||
--pf-c-data-list__item--before--BackgroundColor: var(
|
||||
--pf-c-data-list__item--m-selected--before--BackgroundColor
|
||||
);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build/Buildset result page
|
||||
*/
|
||||
.zuul-build-attributes > .pf-l-flex > * {
|
||||
padding-bottom: var(--pf-global--spacer--sm);
|
||||
padding-left: var(--pf-global--spacer--sm);
|
||||
padding-right: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
/* Status page */
|
||||
.zuul-change {
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -15,13 +15,22 @@
|
|||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateVariant,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import { BuildIcon } from '@patternfly/react-icons'
|
||||
|
||||
import { fetchBuildsetIfNeeded } from '../actions/build'
|
||||
import { Fetchable } from '../containers/Fetching'
|
||||
import { EmptyPage } from '../containers/Errors'
|
||||
import { Fetchable, Fetching } from '../containers/Fetching'
|
||||
import BuildList from '../containers/build/BuildList'
|
||||
import Buildset from '../containers/build/Buildset'
|
||||
|
||||
|
||||
class BuildsetPage extends React.Component {
|
||||
static propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
|
@ -31,42 +40,100 @@ class BuildsetPage extends React.Component {
|
|||
}
|
||||
|
||||
updateData = (force) => {
|
||||
this.props.dispatch(fetchBuildsetIfNeeded(
|
||||
this.props.tenant, this.props.match.params.buildsetId, force))
|
||||
this.props.dispatch(
|
||||
fetchBuildsetIfNeeded(
|
||||
this.props.tenant,
|
||||
this.props.match.params.buildsetId,
|
||||
force
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
document.title = 'Zuul Buildset'
|
||||
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 { remoteData, tenant } = this.props
|
||||
const buildset = remoteData.buildsets[this.props.match.params.buildsetId]
|
||||
|
||||
// Initial page load
|
||||
if (!buildset && remoteData.isFetching) {
|
||||
return <Fetching />
|
||||
}
|
||||
|
||||
// Fetching finished, but no buildset found
|
||||
if (!buildset) {
|
||||
// TODO (felix): Provide some generic error (404?) page. Can we somehow
|
||||
// identify the error here?
|
||||
return (
|
||||
<EmptyPage
|
||||
title="This buildset does not exist"
|
||||
icon={BuildIcon}
|
||||
linkTarget={`${tenant.linkPrefix}/buildsets`}
|
||||
linkText="Show all buildsets"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Return the build list or an empty state if no builds are part of the
|
||||
// buildset.
|
||||
const buildsContent = buildset.builds ? (
|
||||
<BuildList builds={buildset.builds} />
|
||||
) : (
|
||||
<>
|
||||
{/* 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 buildset does not contain any builds
|
||||
</Title>
|
||||
</EmptyState>
|
||||
</>
|
||||
)
|
||||
|
||||
const fetchable = (
|
||||
<Fetchable
|
||||
isFetching={remoteData.isFetching}
|
||||
fetchCallback={this.updateData}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<PageSection style={{paddingRight: '5px'}}>
|
||||
<Fetchable
|
||||
isFetching={remoteData.isFetching}
|
||||
fetchCallback={this.updateData}
|
||||
/>
|
||||
<>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Buildset buildset={buildset} fetchable={fetchable} />
|
||||
</PageSection>
|
||||
{buildset && <Buildset buildset={buildset}/>}
|
||||
</PageSection>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Title headingLevel="h3">
|
||||
<BuildIcon
|
||||
style={{
|
||||
marginRight: 'var(--pf-global--spacer--sm)',
|
||||
verticalAlign: '-0.1em',
|
||||
}}
|
||||
/>{' '}
|
||||
Builds
|
||||
</Title>
|
||||
{buildsContent}
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
remoteData: state.build,
|
||||
}))(BuildsetPage)
|
||||
|
|
Loading…
Reference in New Issue