Implement auto-reload on new status page
Like the old status page, this will reload the status data every 5 seconds when auto-reload is enabled via the preferences (enabled by default). I picked up the idea to use a custom useInterval() react hook from this article [1]. Using this approach avoids a lot of callback chaining and takes care about clearing the interval on every re-render. By using the Page Visibility API in a custom useDocumentVisibility() hook, we can ensure that the data is only fetched when the page is in focus. Thus, when the user switches to another tab in the browser or another window/application, data fetching is skipped. [1]: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ Change-Id: I39fa6a0a5189665f184aeafcf07ce81365a41902
This commit is contained in:
56
web/src/Hooks.jsx
Normal file
56
web/src/Hooks.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2024 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 { useEffect, useRef, useState } from 'react'
|
||||
|
||||
|
||||
function useDocumentVisibility() {
|
||||
const [isDocumentVisible, setIsDocumentVisible] = useState(!document.hidden)
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
setIsDocumentVisible(!document.hidden)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isDocumentVisible
|
||||
}
|
||||
|
||||
|
||||
function useInterval(callback, delay) {
|
||||
const savedCallback = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current()
|
||||
}
|
||||
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay)
|
||||
return () => clearInterval(id)
|
||||
}
|
||||
}, [delay])
|
||||
}
|
||||
|
||||
export { useDocumentVisibility, useInterval }
|
||||
@@ -23,7 +23,29 @@ import {
|
||||
Spinner,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
import { SyncIcon } from '@patternfly/react-icons'
|
||||
import { SyncAltIcon, SyncIcon } from '@patternfly/react-icons'
|
||||
|
||||
function ReloadButton({ isReloading, reloadCallback }) {
|
||||
return (
|
||||
<span
|
||||
className="zuul-reload-button"
|
||||
onClick={() => reloadCallback()}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
isInline
|
||||
icon={<SyncAltIcon />}
|
||||
isLoading={isReloading}
|
||||
/>
|
||||
{isReloading ? 'Reloading' : 'Reload'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
ReloadButton.propTypes = {
|
||||
isReloading: PropTypes.bool.isRequired,
|
||||
reloadCallback: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function Fetchable(props) {
|
||||
const { isFetching, fetchCallback } = props
|
||||
@@ -71,4 +93,4 @@ function Fetching() {
|
||||
)
|
||||
}
|
||||
|
||||
export { Fetchable, Fetching }
|
||||
export { Fetchable, Fetching, ReloadButton }
|
||||
|
||||
@@ -737,6 +737,28 @@ details.foldable[open] summary::before {
|
||||
background: var(--pf-global--palette--red-50);
|
||||
}
|
||||
|
||||
/*
|
||||
* Reload Button
|
||||
*/
|
||||
.zuul-reload-button {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.zuul-reload-button-floating {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.zuul-reload-button button {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.zuul-reload-spinner.pf-c-spinner {
|
||||
--pf-c-spinner--Color: #FFFFFF;
|
||||
}
|
||||
|
||||
/*
|
||||
* Dark Mode overrides
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
@@ -35,7 +35,8 @@ import ChangeQueue from '../containers/status/ChangeQueue'
|
||||
import { PipelineIcon } from '../containers/status/Misc'
|
||||
import { fetchStatusIfNeeded } from '../actions/status'
|
||||
import { EmptyPage } from '../containers/Errors'
|
||||
import { Fetching } from '../containers/Fetching'
|
||||
import { Fetching, ReloadButton } from '../containers/Fetching'
|
||||
import { useDocumentVisibility, useInterval } from '../Hooks'
|
||||
|
||||
function PipelineStats({ pipeline }) {
|
||||
return (
|
||||
@@ -52,12 +53,18 @@ PipelineStats.propTypes = {
|
||||
pipeline: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function PipelineDetails({ pipeline }) {
|
||||
function PipelineDetails({ pipeline, isReloading, reloadCallback }) {
|
||||
|
||||
const pipelineType = pipeline.manager || 'unknown'
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="zuul-reload-button-floating">
|
||||
<ReloadButton
|
||||
isReloading={isReloading}
|
||||
reloadCallback={reloadCallback}
|
||||
/>
|
||||
</span>
|
||||
<Title headingLevel="h1">
|
||||
<PipelineIcon pipelineType={pipelineType} />
|
||||
{pipeline.name}
|
||||
@@ -84,18 +91,43 @@ function PipelineDetails({ pipeline }) {
|
||||
|
||||
PipelineDetails.propTypes = {
|
||||
pipeline: PropTypes.object.isRequired,
|
||||
isReloading: PropTypes.bool.isRequired,
|
||||
reloadCallback: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function PipelineDetailsPage({ pipeline, isFetching, tenant, darkMode, fetchStatusIfNeeded }) {
|
||||
function PipelineDetailsPage({
|
||||
pipeline, isFetching, tenant, darkMode, autoReload, fetchStatusIfNeeded
|
||||
}) {
|
||||
const [isReloading, setIsReloading] = useState(false)
|
||||
|
||||
const isDocumentVisible = useDocumentVisibility()
|
||||
|
||||
const updateData = useCallback((tenant) => {
|
||||
if (tenant.name) {
|
||||
setIsReloading(true)
|
||||
fetchStatusIfNeeded(tenant)
|
||||
.then(() => {
|
||||
setIsReloading(false)
|
||||
})
|
||||
}
|
||||
}, [setIsReloading, fetchStatusIfNeeded])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Pipeline Details'
|
||||
if (tenant.name) {
|
||||
fetchStatusIfNeeded(tenant)
|
||||
}
|
||||
}, [tenant, fetchStatusIfNeeded])
|
||||
// Initial data fetch
|
||||
updateData(tenant)
|
||||
}, [updateData, tenant])
|
||||
|
||||
if (pipeline === undefined || isFetching) {
|
||||
|
||||
// Subsequent data fetches every 5 seconds if auto-reload is enabled
|
||||
useInterval(() => {
|
||||
if (isDocumentVisible && autoReload) {
|
||||
updateData(tenant)
|
||||
}
|
||||
// Reset the interval on a manual refresh
|
||||
}, isReloading ? null : 5000)
|
||||
|
||||
if (pipeline === undefined || (!isReloading && isFetching)) {
|
||||
return <Fetching />
|
||||
}
|
||||
|
||||
@@ -113,7 +145,7 @@ function PipelineDetailsPage({ pipeline, isFetching, tenant, darkMode, fetchStat
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<PipelineDetails pipeline={pipeline} />
|
||||
<PipelineDetails pipeline={pipeline} isReloading={isReloading} reloadCallback={() => updateData(tenant)} />
|
||||
</PageSection>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<Title headingLevel="h3">
|
||||
@@ -136,7 +168,7 @@ function PipelineDetailsPage({ pipeline, isFetching, tenant, darkMode, fetchStat
|
||||
queue => queue.heads.length > 0
|
||||
).map((queue, idx) => (
|
||||
<GalleryItem key={idx}>
|
||||
<ChangeQueue queue={queue} pipeline={pipeline}/>
|
||||
<ChangeQueue queue={queue} pipeline={pipeline} />
|
||||
</GalleryItem>
|
||||
))
|
||||
}
|
||||
@@ -152,6 +184,7 @@ PipelineDetailsPage.propTypes = {
|
||||
isFetching: PropTypes.bool,
|
||||
tenant: PropTypes.object,
|
||||
darkMode: PropTypes.bool,
|
||||
autoReload: PropTypes.bool.isRequired,
|
||||
fetchStatusIfNeeded: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
@@ -171,6 +204,7 @@ function mapStateToProps(state, ownProps) {
|
||||
isFetching: state.status.isFetching,
|
||||
tenant: state.tenant,
|
||||
darkMode: state.preferences.darkMode,
|
||||
autoReload: state.preferences.autoReload,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
@@ -35,10 +36,11 @@ import {
|
||||
import PipelineSummary from '../containers/status/PipelineSummary'
|
||||
|
||||
import { fetchStatusIfNeeded } from '../actions/status'
|
||||
import { Fetching } from '../containers/Fetching'
|
||||
import { Fetching, ReloadButton } from '../containers/Fetching'
|
||||
import { useDocumentVisibility, useInterval } from '../Hooks'
|
||||
|
||||
|
||||
function TenantStats({ stats, timezone }) {
|
||||
function TenantStats({ stats, timezone, isReloading, reloadCallback }) {
|
||||
return (
|
||||
<Level>
|
||||
<LevelItem>
|
||||
@@ -62,6 +64,10 @@ function TenantStats({ stats, timezone }) {
|
||||
{moment_tz.utc(stats.last_reconfigured).tz(timezone).fromNow()}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<ReloadButton
|
||||
isReloading={isReloading}
|
||||
reloadCallback={reloadCallback}
|
||||
/>
|
||||
</LevelItem>
|
||||
</Level>
|
||||
)
|
||||
@@ -70,6 +76,8 @@ function TenantStats({ stats, timezone }) {
|
||||
TenantStats.propTypes = {
|
||||
stats: PropTypes.object,
|
||||
timezone: PropTypes.string,
|
||||
isReloading: PropTypes.bool.isRequired,
|
||||
reloadCallback: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function PipelineGallery({ pipelines, tenant, showAllPipelines }) {
|
||||
@@ -101,29 +109,56 @@ PipelineGallery.propTypes = {
|
||||
}
|
||||
|
||||
function PipelineOverviewPage({
|
||||
pipelines, stats, isFetching, tenant, darkMode, timezone, fetchStatusIfNeeded
|
||||
pipelines, stats, isFetching, tenant, darkMode, autoReload, timezone, fetchStatusIfNeeded
|
||||
}) {
|
||||
const [showAllPipelines, setShowAllPipelines] = useState(false)
|
||||
const [isReloading, setIsReloading] = useState(false)
|
||||
|
||||
const isDocumentVisible = useDocumentVisibility()
|
||||
|
||||
const onShowAllPipelinesToggle = (isChecked) => {
|
||||
setShowAllPipelines(isChecked)
|
||||
}
|
||||
|
||||
const updateData = useCallback((tenant) => {
|
||||
if (tenant.name) {
|
||||
setIsReloading(true)
|
||||
fetchStatusIfNeeded(tenant)
|
||||
.then(() => {
|
||||
setIsReloading(false)
|
||||
})
|
||||
}
|
||||
}, [setIsReloading, fetchStatusIfNeeded])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Status'
|
||||
if (tenant.name) {
|
||||
fetchStatusIfNeeded(tenant)
|
||||
}
|
||||
}, [tenant, fetchStatusIfNeeded])
|
||||
// Initial data fetch
|
||||
updateData(tenant)
|
||||
}, [updateData, tenant])
|
||||
|
||||
if (isFetching) {
|
||||
// Subsequent data fetches every 5 seconds if auto-reload is enabled
|
||||
useInterval(() => {
|
||||
if (isDocumentVisible && autoReload) {
|
||||
updateData(tenant)
|
||||
}
|
||||
// Reset the interval on a manual refresh
|
||||
}, isReloading ? null : 5000)
|
||||
|
||||
// Only show the fetching component on the initial data fetch, but
|
||||
// not on subsequent reloads, as this would overlay the page data.
|
||||
if (!isReloading && isFetching) {
|
||||
return <Fetching />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={darkMode ? PageSectionVariants.dark : PageSectionVariants.light}>
|
||||
<TenantStats stats={stats} timezone={timezone} />
|
||||
<TenantStats
|
||||
stats={stats}
|
||||
timezone={timezone}
|
||||
isReloading={isReloading}
|
||||
reloadCallback={() => updateData(tenant)}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
@@ -158,6 +193,7 @@ PipelineOverviewPage.propTypes = {
|
||||
tenant: PropTypes.object,
|
||||
preferences: PropTypes.object,
|
||||
darkMode: PropTypes.bool,
|
||||
autoReload: PropTypes.bool.isRequired,
|
||||
timezone: PropTypes.string,
|
||||
fetchStatusIfNeeded: PropTypes.func,
|
||||
}
|
||||
@@ -199,6 +235,7 @@ function mapStateToProps(state) {
|
||||
isFetching: state.status.isFetching,
|
||||
tenant: state.tenant,
|
||||
darkMode: state.preferences.darkMode,
|
||||
autoReload: state.preferences.autoReload,
|
||||
timezone: state.timezone,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user