Merge "Add an info banner"

This commit is contained in:
Zuul authored and Gerrit Code Review committed 2026-09-17 22:38:00 +00:00
commit 4cb7887d00
7 files changed
+184 -5

No files matched your search

@@ -0,0 +1,7 @@
---
features:
- |
Tenant admins may now use the tenant management event dialog in
the web UI to add an "info" banner that appears at the top of the
status page. This can be used to make users aware of operational
issues.
+123
View File
@@ -4205,6 +4205,129 @@ class TestTenantScopedWebApi(BaseTestWeb):
dict(name='project-test2', result='SUCCESS', changes='2,1'),
], ordered=False)
def test_state_post_info(self):
# Test setting an info banner and separately pausing events
self.executor_server.hold_jobs_in_build = False
authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'testuser',
'zuul': {
'admin': ['tenant-one', ],
},
'exp': int(time.time()) + 3600,
'iat': int(time.time())}
token = jwt.encode(authz, key='ThisIsABadSecretOnlyUsedForTesting',
algorithm='HS256')
info_msg = 'This is a test message'
args = {
'info': info_msg,
}
req = self.post_url(
'api/tenant/tenant-one/state',
headers={'Authorization': 'Bearer %s' % token},
json=args)
self.assertEqual(200, req.status_code, req.text)
time.sleep(1)
resp = self.get_url("api/tenant/tenant-one/status",
headers={'Authorization': 'Bearer %s' % token})
data = resp.json()
self.assertEqual(info_msg, data['state']['info'])
self.assertEqual(None, data['state']['reason'])
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
self.assertHistory([
dict(name='project-merge', result='SUCCESS', changes='1,1'),
dict(name='project-test1', result='SUCCESS', changes='1,1'),
dict(name='project-test2', result='SUCCESS', changes='1,1'),
], ordered=False)
pause_msg = 'test trigger paused'
args = {
'trigger_queue_paused': True,
'reason': pause_msg,
'info': info_msg,
}
req = self.post_url(
'api/tenant/tenant-one/state',
headers={'Authorization': 'Bearer %s' % token},
json=args)
self.assertEqual(200, req.status_code, req.text)
time.sleep(1)
resp = self.get_url("api/tenant/tenant-one/status",
headers={'Authorization': 'Bearer %s' % token})
data = resp.json()
self.assertEqual(info_msg, data['state']['info'])
self.assertEqual(pause_msg, data['state']['reason'])
B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
time.sleep(5)
self.assertHistory([
dict(name='project-merge', result='SUCCESS', changes='1,1'),
dict(name='project-test1', result='SUCCESS', changes='1,1'),
dict(name='project-test2', result='SUCCESS', changes='1,1'),
], ordered=False)
self.assertEqual(0, len(self.builds))
pause_msg = 'test result paused'
args = {
'trigger_queue_paused': True,
'result_queue_paused': True,
'reason': pause_msg,
}
req = self.post_url(
'api/tenant/tenant-one/state',
headers={'Authorization': 'Bearer %s' % token},
json=args)
self.assertEqual(200, req.status_code, req.text)
time.sleep(5)
resp = self.get_url("api/tenant/tenant-one/status",
headers={'Authorization': 'Bearer %s' % token})
data = resp.json()
self.assertEqual(None, data['state']['info'])
self.assertEqual(pause_msg, data['state']['reason'])
self.assertHistory([
dict(name='project-merge', result='SUCCESS', changes='1,1'),
dict(name='project-test1', result='SUCCESS', changes='1,1'),
dict(name='project-test2', result='SUCCESS', changes='1,1'),
], ordered=False)
self.assertEqual(0, len(self.builds))
args = {
'trigger_queue_paused': False,
'result_queue_paused': False,
'reason': None,
}
req = self.post_url(
'api/tenant/tenant-one/state',
headers={'Authorization': 'Bearer %s' % token},
json=args)
self.assertEqual(200, req.status_code, req.text)
self.waitUntilSettled()
self.assertHistory([
dict(name='project-merge', result='SUCCESS', changes='1,1'),
dict(name='project-test1', result='SUCCESS', changes='1,1'),
dict(name='project-test2', result='SUCCESS', changes='1,1'),
dict(name='project-merge', result='SUCCESS', changes='2,1'),
dict(name='project-test1', result='SUCCESS', changes='2,1'),
dict(name='project-test2', result='SUCCESS', changes='2,1'),
], ordered=False)
time.sleep(5)
resp = self.get_url("api/tenant/tenant-one/status",
headers={'Authorization': 'Bearer %s' % token})
data = resp.json()
self.assertEqual(None, data['state']['info'])
self.assertEqual(None, data['state']['reason'])
def test_state_post_timer(self):
self.executor_server.hold_jobs_in_build = False
+2 -1
View File
@@ -192,7 +192,7 @@ function fetchChangeStatus(apiPrefix, changeId) {
return makeRequest(apiPrefix + 'status/change/' + changeId)
}
function setTenantState(apiPrefix, discardTriggerEvents, pauseTriggerQueue, pauseResultQueue, reason) {
function setTenantState(apiPrefix, discardTriggerEvents, pauseTriggerQueue, pauseResultQueue, reason, info) {
return makeRequest(
apiPrefix + 'state',
'post',
@@ -201,6 +201,7 @@ function setTenantState(apiPrefix, discardTriggerEvents, pauseTriggerQueue, paus
trigger_queue_paused: pauseTriggerQueue,
result_queue_paused: pauseResultQueue,
reason: reason,
info: info,
}
)
}
+16 -2
View File
@@ -31,6 +31,7 @@ import { setTenantState } from '../../api'
function PauseModal({isOpen, setOpen}) {
const tenant = useSelector((state) => state.tenant)
const [reason, setReason] = useState('')
const [info, setInfo] = useState('')
const [queueState, setQueueState] = useState('normal')
const [discardTrigger, setDiscardTrigger] = useState(false)
const [pauseTrigger, setPauseTrigger] = useState(false)
@@ -52,7 +53,8 @@ function PauseModal({isOpen, setOpen}) {
discardTrigger,
pauseTrigger,
pauseResult,
reason)
reason,
info)
.catch(error => {
dispatch(addApiError(error))
})
@@ -116,7 +118,7 @@ function PauseModal({isOpen, setOpen}) {
<FormGroup
label="Reason"
fieldId="pause-form-reason"
helperText="This explanation will appear on the status page.">
helperText="This explanation will appear on the status page while event processing is inactive.">
<TextInput
value={reason}
isRequired
@@ -126,6 +128,18 @@ function PauseModal({isOpen, setOpen}) {
onChange={(value) => { setReason(value) }}
/>
</FormGroup>
<FormGroup
label="Info"
fieldId="pause-form-info"
helperText="This text will appear in a separate info banner on the status page, regardless of event processing status.">
<TextInput
value={info}
type="text"
id="pause-form-info"
name="pauseInfo"
onChange={(value) => { setInfo(value) }}
/>
</FormGroup>
</Modal>
)
}
+30 -2
View File
@@ -37,6 +37,7 @@ import {
FlexItem,
} from '@patternfly/react-core'
import {
InfoCircleIcon,
StreamIcon,
ExclamationTriangleIcon,
} from '@patternfly/react-icons'
@@ -127,11 +128,25 @@ PipelineGallery.propTypes = {
}
function renderBanner(status)
function renderInfoBanner(status)
{
if (!status || !status.state) {
if (!status || !status.state.info) {
return <></>
}
return (
<Banner screenReaderText="Info banner" variant="info">
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
<InfoCircleIcon />
</FlexItem>
<FlexItem>{status.state.info}</FlexItem>
</Flex>
</Banner>
)
}
function renderPauseBanner(status)
{
// This method is able to describe states that the client libraries
// do not support creating (for example, buth discarding and pausing
// the trigger event queue).
@@ -167,6 +182,19 @@ function renderBanner(status)
)
}
function renderBanner(status)
{
if (!status || !status.state) {
return <></>
}
return (
<>
{renderInfoBanner(status)}
{renderPauseBanner(status)}
</>
)
}
function getPipelines(status, location, filterCategories) {
let pipelines = []
let stats = {}
+2
View File
@@ -12238,6 +12238,7 @@ class TenantEventState(zkobject.ZKObject):
trigger_queue_paused=False,
result_queue_paused=False,
reason=None,
info=None,
)
def getPath(self):
@@ -12249,6 +12250,7 @@ class TenantEventState(zkobject.ZKObject):
"trigger_queue_paused": self.trigger_queue_paused,
"result_queue_paused": self.result_queue_paused,
"reason": self.reason,
"info": self.info,
}
return data
+4
View File
@@ -1698,6 +1698,9 @@ class ZuulWebAPI(object):
# We limit the reason to 4096 chars to limit the
# zk node size.
reason = str(reason[:4096])
info = body.get('info')
if info:
info = str(info[:4096])
tqs._set(
trigger_queue_discarding=bool(
body.get('trigger_queue_discarding',
@@ -1709,6 +1712,7 @@ class ZuulWebAPI(object):
body.get('result_queue_paused',
current_state.result_queue_paused)),
reason=reason,
info=info,
)
tqs.internalCreate(ctx)