summaryrefslogtreecommitdiff
path: root/web/src
diff options
context:
space:
mode:
authorFelix Edel <felix.edel@bmw.de>2020-06-24 08:47:54 +0200
committerFelix Edel <felix.edel@bmw.de>2020-07-15 09:06:04 +0200
commit403a828b86b9c7fe1d2f0a9646fad32b480888f3 (patch)
tree154229c6e5ae2f372c024b20deafaf538908a75d /web/src
parent18e55acb61eb25c71e365210dba27aa3cceae7a1 (diff)
downloadzuul-403a828b86b9c7fe1d2f0a9646fad32b480888f3.tar.gz
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
Diffstat (limited to 'web/src')
-rw-r--r--web/src/Misc.jsx46
-rw-r--r--web/src/containers/Errors.jsx53
-rw-r--r--web/src/containers/build/BuildList.jsx115
-rw-r--r--web/src/containers/build/Buildset.jsx221
-rw-r--r--web/src/containers/build/Misc.jsx177
-rw-r--r--web/src/index.css23
-rw-r--r--web/src/pages/Buildset.jsx105
7 files changed, 613 insertions, 127 deletions
diff --git a/web/src/Misc.jsx b/web/src/Misc.jsx
new file mode 100644
index 000000000..a2e9eb0c0
--- /dev/null
+++ b/web/src/Misc.jsx
@@ -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 }
diff --git a/web/src/containers/Errors.jsx b/web/src/containers/Errors.jsx
new file mode 100644
index 000000000..dcd3040e5
--- /dev/null
+++ b/web/src/containers/Errors.jsx
@@ -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 }
diff --git a/web/src/containers/build/BuildList.jsx b/web/src/containers/build/BuildList.jsx
new file mode 100644
index 000000000..b0a3bc3f5
--- /dev/null
+++ b/web/src/containers/build/BuildList.jsx
@@ -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)
diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx
index 42d4f1e48..f6eb0d587 100644
--- a/web/src/containers/build/Buildset.jsx
+++ b/web/src/containers/build/Buildset.jsx
@@ -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)
diff --git a/web/src/containers/build/Misc.jsx b/web/src/containers/build/Misc.jsx
new file mode 100644
index 000000000..f77f7de7a
--- /dev/null
+++ b/web/src/containers/build/Misc.jsx
@@ -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 }
diff --git a/web/src/index.css b/web/src/index.css
index 4c0b67ca8..235e82fcb 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -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;
diff --git a/web/src/pages/Buildset.jsx b/web/src/pages/Buildset.jsx
index 3440cb326..b3a188e25 100644
--- a/web/src/pages/Buildset.jsx
+++ b/web/src/pages/Buildset.jsx
@@ -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>
+ <PageSection variant={PageSectionVariants.light}>
+ <Title headingLevel="h3">
+ <BuildIcon
+ style={{
+ marginRight: 'var(--pf-global--spacer--sm)',
+ verticalAlign: '-0.1em',
+ }}
+ />{' '}
+ Builds
+ </Title>
+ {buildsContent}
</PageSection>
- {buildset && <Buildset buildset={buildset}/>}
- </PageSection>
+ </>
)
}
}
-export default connect(state => ({
+export default connect((state) => ({
tenant: state.tenant,
remoteData: state.build,
}))(BuildsetPage)