PF4: Update "fetching info ..." and refresh animation

Currently the "refresh" animations look quite different depending on the
page and the type of event (refresh button, initial page load, ...).

This tries to make a start by updating the "Fetching info ..." animation
(shown on initial page load) with Patternfly's "empty state" pattern [1]
in combination with an animated spinner.

For "Refreshable" pages, a similar animation is used in the upper right
corner (like before) but with an updated spinner and icon. By using a
dedicated React component rather than a base class, the "refresh" button
can be more nicely integrated into the layout of the page/container and
it doesn't look "out of scope" for the refreshable component.

For the API page I've removed the refresh completely since it wasn't
implemented at all.

[1] https://www.patternfly.org/v4/documentation/react/components/emptystate

Change-Id: I2274c212f14aece27ff49bfc7900d9b1a0fd01d0
This commit is contained in:
Felix Edel 2020-06-23 09:57:09 +02:00
parent 141347e082
commit ed9d0446d5
No known key found for this signature in database
GPG Key ID: E95717A102DD3030
18 changed files with 313 additions and 155 deletions

View File

@ -64,6 +64,7 @@ import {
} from '@patternfly/react-icons' } from '@patternfly/react-icons'
import ErrorBoundary from './containers/ErrorBoundary' import ErrorBoundary from './containers/ErrorBoundary'
import { Fetching } from './containers/Fetching'
import SelectTz from './containers/timezone/SelectTz' import SelectTz from './containers/timezone/SelectTz'
import logo from './images/logo.svg' import logo from './images/logo.svg'
import { clearError } from './actions/errors' import { clearError } from './actions/errors'
@ -126,7 +127,7 @@ class App extends React.Component {
const allRoutes = [] const allRoutes = []
if (info.isFetching) { if (info.isFetching) {
return (<h2>Fetching info...</h2>) return <Fetching />
} }
this.menu this.menu
// Do not include '/tenants' route in white-label setup // Do not include '/tenants' route in white-label setup

View File

@ -0,0 +1,74 @@
// 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 {
Button,
Title,
EmptyState,
EmptyStateVariant,
Spinner,
} from '@patternfly/react-core'
import { SyncIcon } from '@patternfly/react-icons'
function Fetchable(props) {
const { isFetching, fetchCallback } = props
let content = null
if (isFetching) {
content = (
<div>
<Spinner size="md" />
</div>
)
} else {
content = (
<Button
variant="link"
isInline icon={<SyncIcon />}
onClick={() => {fetchCallback({force: true})}}
style={{textDecoration: 'none'}}
>
refresh
</Button>
)
}
return (
<div style={{float: 'right'}}>
{content}
</div>
)
}
Fetchable.propTypes = {
isFetching: PropTypes.bool,
fetchCallback: PropTypes.func,
}
function Fetching() {
return (
<EmptyState variant={EmptyStateVariant.small}>
<Spinner />
<Title headingLevel="h4" size="lg">
Fetching info...
</Title>
</EmptyState>
)
}
export { Fetchable, Fetching }

View File

@ -1,57 +0,0 @@
// Copyright 2018 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.
// Boiler plate code to manage refresh button
import * as React from 'react'
import PropTypes from 'prop-types'
import {
Icon,
Spinner
} from 'patternfly-react'
class Refreshable extends React.Component {
static propTypes = {
tenant: PropTypes.object,
remoteData: PropTypes.object,
}
componentDidMount () {
if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
}
renderSpinner () {
const { remoteData } = this.props
return (
<Spinner loading={ remoteData.isFetching }>
{/* Lint warning jsx-a11y/anchor-is-valid */}
{/* eslint-disable-next-line */}
<a className="refresh" onClick={() => {this.updateData(true)}}>
<Icon type="fa" name="refresh" /> refresh&nbsp;&nbsp;
</a>
</Spinner>
)
}
}
export default Refreshable

View File

@ -18,16 +18,17 @@ import PropTypes from 'prop-types'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchBuildIfNeeded } from '../actions/build' import { fetchBuildIfNeeded } from '../actions/build'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
import Build from '../containers/build/Build' import Build from '../containers/build/Build'
import Summary from '../containers/build/Summary' import Summary from '../containers/build/Summary'
class BuildPage extends Refreshable { class BuildPage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
remoteData: PropTypes.object, remoteData: PropTypes.object,
tenant: PropTypes.object tenant: PropTypes.object,
dispatch: PropTypes.func,
} }
updateData = (force) => { updateData = (force) => {
@ -37,7 +38,15 @@ class BuildPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Build' document.title = 'Zuul Build'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
@ -45,9 +54,12 @@ class BuildPage extends Refreshable {
const build = remoteData.builds[this.props.match.params.buildId] const build = remoteData.builds[this.props.match.params.buildId]
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{build && {build &&
<Build build={build} active='summary'> <Build build={build} active='summary'>
<Summary build={build}/> <Summary build={build}/>

View File

@ -18,16 +18,18 @@ import PropTypes from 'prop-types'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchBuildIfNeeded } from '../actions/build' import { fetchBuildIfNeeded } from '../actions/build'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
import Build from '../containers/build/Build' import Build from '../containers/build/Build'
import Console from '../containers/build/Console' import Console from '../containers/build/Console'
class BuildConsolePage extends Refreshable { class BuildConsolePage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
remoteData: PropTypes.object, remoteData: PropTypes.object,
tenant: PropTypes.object tenant: PropTypes.object,
dispatch: PropTypes.func,
location: PropTypes.object,
} }
updateData = (force) => { updateData = (force) => {
@ -37,7 +39,15 @@ class BuildConsolePage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Build' document.title = 'Zuul Build'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
@ -47,9 +57,12 @@ class BuildConsolePage extends Refreshable {
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{build && build.output && {build && build.output &&
<Build build={build} active='console'> <Build build={build} active='console'>
<Console <Console

View File

@ -18,16 +18,17 @@ import PropTypes from 'prop-types'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchBuildIfNeeded } from '../actions/build' import { fetchBuildIfNeeded } from '../actions/build'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
import Build from '../containers/build/Build' import Build from '../containers/build/Build'
import Manifest from '../containers/build/Manifest' import Manifest from '../containers/build/Manifest'
class BuildLogsPage extends Refreshable { class BuildLogsPage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
remoteData: PropTypes.object, remoteData: PropTypes.object,
tenant: PropTypes.object tenant: PropTypes.object,
dispatch: PropTypes.func,
} }
updateData = (force) => { updateData = (force) => {
@ -37,7 +38,15 @@ class BuildLogsPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Build' document.title = 'Zuul Build'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
@ -45,9 +54,12 @@ class BuildLogsPage extends Refreshable {
const build = remoteData.builds[this.props.match.params.buildId] const build = remoteData.builds[this.props.match.params.buildId]
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{build && build.manifest && {build && build.manifest &&
<Build build={build} active='logs'> <Build build={build} active='logs'>
<Manifest tenant={this.props.tenant} build={build}/> <Manifest tenant={this.props.tenant} build={build}/>

View File

@ -18,15 +18,16 @@ import PropTypes from 'prop-types'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchBuildsetIfNeeded } from '../actions/build' import { fetchBuildsetIfNeeded } from '../actions/build'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
import Buildset from '../containers/build/Buildset' import Buildset from '../containers/build/Buildset'
class BuildsetPage extends Refreshable { class BuildsetPage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
remoteData: PropTypes.object, remoteData: PropTypes.object,
tenant: PropTypes.object tenant: PropTypes.object,
dispatch: PropTypes.func,
} }
updateData = (force) => { updateData = (force) => {
@ -36,17 +37,29 @@ class BuildsetPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Buildset' document.title = 'Zuul Buildset'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
const { remoteData } = this.props const { remoteData } = this.props
const buildset = remoteData.buildsets[this.props.match.params.buildsetId] const buildset = remoteData.buildsets[this.props.match.params.buildsetId]
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{buildset && <Buildset buildset={buildset}/>} {buildset && <Buildset buildset={buildset}/>}
</PageSection> </PageSection>
) )

View File

@ -19,10 +19,10 @@ import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchChangeIfNeeded } from '../actions/change' import { fetchChangeIfNeeded } from '../actions/change'
import ChangePanel from '../containers/status/ChangePanel' import ChangePanel from '../containers/status/ChangePanel'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
class ChangeStatusPage extends Refreshable { class ChangeStatusPage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
tenant: PropTypes.object, tenant: PropTypes.object,
@ -43,7 +43,15 @@ class ChangeStatusPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = this.props.match.params.changeId + ' | Zuul Status' document.title = this.props.match.params.changeId + ' | Zuul Status'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
componentWillUnmount () { componentWillUnmount () {
@ -58,9 +66,12 @@ class ChangeStatusPage extends Refreshable {
const change = remoteData.change const change = remoteData.change
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div><br /> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{change && change.map((item, idx) => ( {change && change.map((item, idx) => (
<div className='row zuul-change-content' key={idx}> <div className='row zuul-change-content' key={idx}>
<ChangePanel <ChangePanel

View File

@ -18,11 +18,11 @@ import PropTypes from 'prop-types'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import Job from '../containers/job/Job' import Job from '../containers/job/Job'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
import { fetchJobIfNeeded } from '../actions/job' import { fetchJobIfNeeded } from '../actions/job'
class JobPage extends Refreshable { class JobPage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
tenant: PropTypes.object, tenant: PropTypes.object,
@ -37,7 +37,9 @@ class JobPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Job | ' + this.props.match.params.jobName document.title = 'Zuul Job | ' + this.props.match.params.jobName
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
@ -53,9 +55,12 @@ class JobPage extends Refreshable {
const jobName = this.props.match.params.jobName const jobName = this.props.match.params.jobName
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{tenantJobs && tenantJobs[jobName] && <Job job={tenantJobs[jobName]} />} {tenantJobs && tenantJobs[jobName] && <Job job={tenantJobs[jobName]} />}
</PageSection> </PageSection>
) )

View File

@ -18,34 +18,46 @@ import { connect } from 'react-redux'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchJobsIfNeeded } from '../actions/jobs' import { fetchJobsIfNeeded } from '../actions/jobs'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
import Jobs from '../containers/jobs/Jobs' import Jobs from '../containers/jobs/Jobs'
class JobsPage extends Refreshable { class JobsPage extends React.Component {
static propTypes = { static propTypes = {
tenant: PropTypes.object, tenant: PropTypes.object,
remoteData: PropTypes.object, remoteData: PropTypes.object,
dispatch: PropTypes.func dispatch: PropTypes.func
} }
updateData (force) { updateData = (force) => {
this.props.dispatch(fetchJobsIfNeeded(this.props.tenant, force)) this.props.dispatch(fetchJobsIfNeeded(this.props.tenant, force))
} }
componentDidMount () { componentDidMount () {
document.title = 'Zuul Jobs' document.title = 'Zuul Jobs'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
const { remoteData } = this.props const { remoteData } = this.props
const jobs = remoteData.jobs[this.props.tenant.name] const jobs = remoteData.jobs[this.props.tenant.name]
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{jobs && jobs.length > 0 && {jobs && jobs.length > 0 &&
<Jobs <Jobs
jobs={jobs} jobs={jobs}

View File

@ -19,10 +19,10 @@ import { Table } from 'patternfly-react'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchLabelsIfNeeded } from '../actions/labels' import { fetchLabelsIfNeeded } from '../actions/labels'
import Refreshable from '../containers/Refreshable' import { Fetchable, Fetching } from '../containers/Fetching'
class LabelsPage extends Refreshable { class LabelsPage extends React.Component {
static propTypes = { static propTypes = {
tenant: PropTypes.object, tenant: PropTypes.object,
remoteData: PropTypes.object, remoteData: PropTypes.object,
@ -35,7 +35,15 @@ class LabelsPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Labels' document.title = 'Zuul Labels'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
@ -43,7 +51,7 @@ class LabelsPage extends Refreshable {
const labels = remoteData.labels[this.props.tenant.name] const labels = remoteData.labels[this.props.tenant.name]
if (!labels) { if (!labels) {
return (<p>Loading...</p>) return <Fetching />
} }
const headerFormat = value => <Table.Heading>{value}</Table.Heading> const headerFormat = value => <Table.Heading>{value}</Table.Heading>
@ -61,9 +69,12 @@ class LabelsPage extends Refreshable {
}) })
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
<Table.PfProvider <Table.PfProvider
striped striped
bordered bordered

View File

@ -19,15 +19,18 @@ import { parse } from 'query-string'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchLogfileIfNeeded } from '../actions/logfile' import { fetchLogfileIfNeeded } from '../actions/logfile'
import Refreshable from '../containers/Refreshable' import { Fetching } from '../containers/Fetching'
import LogFile from '../containers/logfile/LogFile' import LogFile from '../containers/logfile/LogFile'
class LogFilePage extends Refreshable { class LogFilePage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
remoteData: PropTypes.object, remoteData: PropTypes.object,
tenant: PropTypes.object, tenant: PropTypes.object,
dispatch: PropTypes.func,
location: PropTypes.object,
build: PropTypes.object,
} }
state = { state = {
@ -45,7 +48,9 @@ class LogFilePage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Build Logfile' document.title = 'Zuul Build Logfile'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
} }
highlightDidUpdate = (lines) => { highlightDidUpdate = (lines) => {
@ -108,13 +113,14 @@ class LogFilePage extends Refreshable {
render () { render () {
const { remoteData } = this.props const { remoteData } = this.props
if (remoteData.isFetching) {
return <Fetching />
}
const build = this.props.build.builds[this.props.match.params.buildId] const build = this.props.build.builds[this.props.match.params.buildId]
const severity = parse(this.props.location.search).severity const severity = parse(this.props.location.search).severity
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}>
{this.renderSpinner()}
</div>
{remoteData.data && <LogFile build={build} data={remoteData.data} severity={severity}/>} {remoteData.data && <LogFile build={build} data={remoteData.data} severity={severity}/>}
</PageSection> </PageSection>
) )

View File

@ -20,23 +20,31 @@ import * as moment from 'moment'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchNodesIfNeeded } from '../actions/nodes' import { fetchNodesIfNeeded } from '../actions/nodes'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
class NodesPage extends Refreshable { class NodesPage extends React.Component {
static propTypes = { static propTypes = {
tenant: PropTypes.object, tenant: PropTypes.object,
remoteData: PropTypes.object, remoteData: PropTypes.object,
dispatch: PropTypes.func dispatch: PropTypes.func
} }
updateData (force) { updateData = (force) => {
this.props.dispatch(fetchNodesIfNeeded(this.props.tenant, force)) this.props.dispatch(fetchNodesIfNeeded(this.props.tenant, force))
} }
componentDidMount () { componentDidMount () {
document.title = 'Zuul Nodes' document.title = 'Zuul Nodes'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
@ -83,9 +91,12 @@ class NodesPage extends Refreshable {
}) })
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
<Table.PfProvider <Table.PfProvider
striped striped
bordered bordered

View File

@ -20,10 +20,9 @@ import 'swagger-ui/dist/swagger-ui.css'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchOpenApiIfNeeded } from '../actions/openapi' import { fetchOpenApiIfNeeded } from '../actions/openapi'
import Refreshable from '../containers/Refreshable'
class OpenApiPage extends Refreshable { class OpenApiPage extends React.Component {
static propTypes = { static propTypes = {
tenant: PropTypes.object, tenant: PropTypes.object,
remoteData: PropTypes.object, remoteData: PropTypes.object,
@ -53,9 +52,6 @@ class OpenApiPage extends Refreshable {
render() { render() {
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div className="pull-right" style={{display: 'flex'}}>
{this.renderSpinner()}
</div>
<div id="swaggerContainer" /> <div id="swaggerContainer" />
</PageSection> </PageSection>
) )

View File

@ -19,10 +19,10 @@ import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import Project from '../containers/project/Project' import Project from '../containers/project/Project'
import { fetchProjectIfNeeded } from '../actions/project' import { fetchProjectIfNeeded } from '../actions/project'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
class ProjectPage extends Refreshable { class ProjectPage extends React.Component {
static propTypes = { static propTypes = {
match: PropTypes.object.isRequired, match: PropTypes.object.isRequired,
tenant: PropTypes.object, tenant: PropTypes.object,
@ -37,7 +37,15 @@ class ProjectPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Project | ' + this.props.match.params.projectName document.title = 'Zuul Project | ' + this.props.match.params.projectName
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
@ -46,9 +54,12 @@ class ProjectPage extends Refreshable {
const projectName = this.props.match.params.projectName const projectName = this.props.match.params.projectName
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
{tenantProjects && tenantProjects[projectName] && {tenantProjects && tenantProjects[projectName] &&
<Project project={tenantProjects[projectName]} />} <Project project={tenantProjects[projectName]} />}
</PageSection> </PageSection>

View File

@ -20,31 +20,43 @@ import { Table } from 'patternfly-react'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchProjectsIfNeeded } from '../actions/projects' import { fetchProjectsIfNeeded } from '../actions/projects'
import Refreshable from '../containers/Refreshable' import { Fetchable, Fetching } from '../containers/Fetching'
class ProjectsPage extends Refreshable { class ProjectsPage extends React.Component {
static propTypes = { static propTypes = {
tenant: PropTypes.object, tenant: PropTypes.object,
remoteData: PropTypes.object, remoteData: PropTypes.object,
dispatch: PropTypes.func dispatch: PropTypes.func
} }
updateData (force) { updateData = (force) => {
this.props.dispatch(fetchProjectsIfNeeded(this.props.tenant, force)) this.props.dispatch(fetchProjectsIfNeeded(this.props.tenant, force))
} }
componentDidMount () { componentDidMount () {
document.title = 'Zuul Projects' document.title = 'Zuul Projects'
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
}
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
} }
render () { render () {
const { remoteData } = this.props const { remoteData } = this.props
const projects = remoteData.projects[this.props.tenant.name] const projects = remoteData.projects[this.props.tenant.name]
// TODO (felix): Can we somehow differentiate between "no projects yet" (due
// to fetching) and "no projects at all", so we could show an empty state
// in the latter case. The same applies for other pages like labels, nodes,
// buildsets, ... as well.
if (!projects) { if (!projects) {
return (<p>Loading...</p>) return <Fetching />
} }
const headerFormat = value => <Table.Heading>{value}</Table.Heading> const headerFormat = value => <Table.Heading>{value}</Table.Heading>
@ -86,9 +98,12 @@ class ProjectsPage extends Refreshable {
}) })
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}> <PageSection style={{paddingRight: '5px'}}>
{this.renderSpinner()} <Fetchable
</div> isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
</PageSection>
<Table.PfProvider <Table.PfProvider
striped striped
bordered bordered

View File

@ -27,10 +27,10 @@ import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import { fetchStatusIfNeeded } from '../actions/status' import { fetchStatusIfNeeded } from '../actions/status'
import Pipeline from '../containers/status/Pipeline' import Pipeline from '../containers/status/Pipeline'
import Refreshable from '../containers/Refreshable' import { Fetchable } from '../containers/Fetching'
class StatusPage extends Refreshable { class StatusPage extends React.Component {
static propTypes = { static propTypes = {
location: PropTypes.object, location: PropTypes.object,
tenant: PropTypes.object, tenant: PropTypes.object,
@ -96,10 +96,18 @@ class StatusPage extends Refreshable {
componentDidMount () { componentDidMount () {
document.title = 'Zuul Status' document.title = 'Zuul Status'
this.loadState() this.loadState()
super.componentDidMount() if (this.props.tenant.name) {
this.updateData()
}
window.addEventListener('storage', this.loadState) window.addEventListener('storage', this.loadState)
} }
componentDidUpdate (prevProps) {
if (this.props.tenant.name !== prevProps.tenant.name) {
this.updateData()
}
}
componentWillUnmount () { componentWillUnmount () {
if (this.timer) { if (this.timer) {
clearTimeout(this.timer) clearTimeout(this.timer)
@ -214,12 +222,15 @@ class StatusPage extends Refreshable {
) )
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div className="pull-right" style={{display: 'flex'}}> <div style={{display: 'flex', float: 'right'}}>
{this.renderSpinner()} <Fetchable
isFetching={remoteData.isFetching}
fetchCallback={this.updateData}
/>
<Checkbox <Checkbox
defaultChecked={autoReload} defaultChecked={autoReload}
onChange={(e) => {this.setState({autoReload: e.target.checked})}} onChange={(e) => {this.setState({autoReload: e.target.checked})}}
style={{marginTop: '0px'}}> style={{marginTop: '0px', marginLeft: '10px'}}>
auto reload auto reload
</Checkbox> </Checkbox>
</div> </div>

View File

@ -19,11 +19,11 @@ import { Link } from 'react-router-dom'
import { Table } from 'patternfly-react' import { Table } from 'patternfly-react'
import { PageSection, PageSectionVariants } from '@patternfly/react-core' import { PageSection, PageSectionVariants } from '@patternfly/react-core'
import Refreshable from '../containers/Refreshable' import { Fetching } from '../containers/Fetching'
import { fetchTenantsIfNeeded } from '../actions/tenants' import { fetchTenantsIfNeeded } from '../actions/tenants'
class TenantsPage extends Refreshable { class TenantsPage extends React.Component {
static propTypes = { static propTypes = {
remoteData: PropTypes.object, remoteData: PropTypes.object,
dispatch: PropTypes.func dispatch: PropTypes.func
@ -43,6 +43,10 @@ class TenantsPage extends Refreshable {
render () { render () {
const { remoteData } = this.props const { remoteData } = this.props
if (remoteData.isFetching) {
return <Fetching />
}
const tenants = remoteData.tenants const tenants = remoteData.tenants
const headerFormat = value => <Table.Heading>{value}</Table.Heading> const headerFormat = value => <Table.Heading>{value}</Table.Heading>
const cellFormat = (value) => ( const cellFormat = (value) => (
@ -79,9 +83,6 @@ class TenantsPage extends Refreshable {
}) })
return ( return (
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<div style={{float: 'right'}}>
{this.renderSpinner()}
</div>
<Table.PfProvider <Table.PfProvider
striped striped
bordered bordered