Add semaphore support to web UI
This updates the OpenAPI docs to include the semaphores endpoint, and adds a Semaphores tab to the web UI to show information about semaphores within a tenant. Change-Id: If78b27131ac76aff93c47a986fce6eae3e068668
This commit is contained in:
parent
06cfe2cacd
commit
fa590a9f50
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Details about the configuration and current usage of semaphores
|
||||
are now available in the web UI under the "Semaphores" tab.
|
|
@ -283,6 +283,31 @@ paths:
|
|||
summary: Get a project public key
|
||||
tags:
|
||||
- tenant
|
||||
/api/tenant/{tenant}/semaphores:
|
||||
get:
|
||||
operationId: list-semaphores
|
||||
parameters:
|
||||
- description: The tenant name
|
||||
in: path
|
||||
name: tenant
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
description: The list of semaphores
|
||||
items:
|
||||
$ref: '#/components/schemas/semaphore'
|
||||
type: array
|
||||
description: Returns the list of semaphores
|
||||
'404':
|
||||
description: Tenant not found
|
||||
summary: List available semaphores
|
||||
tags:
|
||||
- tenant
|
||||
/api/tenant/{tenant}/status:
|
||||
get:
|
||||
operationId: get-status
|
||||
|
@ -520,6 +545,46 @@ components:
|
|||
description: The pipeline name
|
||||
type: string
|
||||
type: object
|
||||
semaphore:
|
||||
description: A semaphore
|
||||
properties:
|
||||
name:
|
||||
description: The semaphore name
|
||||
type: string
|
||||
global:
|
||||
description: Whether the semaphore is global
|
||||
type: boolean
|
||||
max:
|
||||
description: The maximum number of holders
|
||||
type: integer
|
||||
holders:
|
||||
$ref: '#/components/schemas/semaphoreHolders'
|
||||
type: object
|
||||
semaphoreHolders:
|
||||
description: Information about the holders of a semaphore
|
||||
properties:
|
||||
count:
|
||||
description: The number of jobs currently holding this semaphore
|
||||
type: integer
|
||||
this_tenant:
|
||||
description: Holders within this tenant
|
||||
items:
|
||||
$ref: '#/components/schemas/semaphoreHolder'
|
||||
type: array
|
||||
other_tenants:
|
||||
description: The number of jobs in other tenants currently holding this semaphore
|
||||
type: integer
|
||||
type: object
|
||||
semaphoreHolder:
|
||||
description: Information about a holder of a semaphore
|
||||
properties:
|
||||
buildset_uuid:
|
||||
description: The UUID of the job's buildset
|
||||
type: string
|
||||
job_name:
|
||||
description: The name of the job
|
||||
type: string
|
||||
type: object
|
||||
statusJob:
|
||||
description: A job status
|
||||
properties:
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2018 Red Hat, Inc
|
||||
// Copyright 2022 Acme Gating, LLC
|
||||
//
|
||||
// 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 API from '../api'
|
||||
|
||||
export const SEMAPHORES_FETCH_REQUEST = 'SEMAPHORES_FETCH_REQUEST'
|
||||
export const SEMAPHORES_FETCH_SUCCESS = 'SEMAPHORES_FETCH_SUCCESS'
|
||||
export const SEMAPHORES_FETCH_FAIL = 'SEMAPHORES_FETCH_FAIL'
|
||||
|
||||
export const requestSemaphores = () => ({
|
||||
type: SEMAPHORES_FETCH_REQUEST
|
||||
})
|
||||
|
||||
export const receiveSemaphores = (tenant, json) => ({
|
||||
type: SEMAPHORES_FETCH_SUCCESS,
|
||||
tenant: tenant,
|
||||
semaphores: json,
|
||||
receivedAt: Date.now()
|
||||
})
|
||||
|
||||
const failedSemaphores = error => ({
|
||||
type: SEMAPHORES_FETCH_FAIL,
|
||||
error
|
||||
})
|
||||
|
||||
const fetchSemaphores = (tenant) => dispatch => {
|
||||
dispatch(requestSemaphores())
|
||||
return API.fetchSemaphores(tenant.apiPrefix)
|
||||
.then(response => dispatch(receiveSemaphores(tenant.name, response.data)))
|
||||
.catch(error => dispatch(failedSemaphores(error)))
|
||||
}
|
||||
|
||||
const shouldFetchSemaphores = (tenant, state) => {
|
||||
const semaphores = state.semaphores.semaphores[tenant.name]
|
||||
if (!semaphores || semaphores.length === 0) {
|
||||
return true
|
||||
}
|
||||
if (semaphores.isFetching) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const fetchSemaphoresIfNeeded = (tenant, force) => (dispatch, getState) => {
|
||||
if (force || shouldFetchSemaphores(tenant, getState())) {
|
||||
return dispatch(fetchSemaphores(tenant))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
|
@ -185,6 +185,9 @@ function fetchLabels(apiPrefix) {
|
|||
function fetchNodes(apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'nodes')
|
||||
}
|
||||
function fetchSemaphores(apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'semaphores')
|
||||
}
|
||||
function fetchAutoholds(apiPrefix) {
|
||||
return Axios.get(apiUrl + apiPrefix + 'autohold')
|
||||
}
|
||||
|
@ -332,6 +335,7 @@ export {
|
|||
fetchLabels,
|
||||
fetchNodes,
|
||||
fetchOpenApi,
|
||||
fetchSemaphores,
|
||||
fetchTenants,
|
||||
fetchInfo,
|
||||
fetchComponents,
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2018 Red Hat, Inc
|
||||
// Copyright 2022 Acme Gating, LLC
|
||||
//
|
||||
// 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 React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
Spinner,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
function Semaphore({ semaphore, tenant, fetching }) {
|
||||
if (fetching && !semaphore) {
|
||||
return (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
)
|
||||
}
|
||||
if (!semaphore) {
|
||||
return (
|
||||
<div>
|
||||
No semaphore found
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const rows = []
|
||||
rows.push({label: 'Name', value: semaphore.name})
|
||||
rows.push({label: 'Current Holders', value: semaphore.holders.count})
|
||||
rows.push({label: 'Max', value: semaphore.max})
|
||||
rows.push({label: 'Global', value: semaphore.global ? 'Yes' : 'No'})
|
||||
if (semaphore.global) {
|
||||
rows.push({label: 'Holders in Other Tenants',
|
||||
value: semaphore.holders.other_tenants})
|
||||
}
|
||||
semaphore.holders.this_tenant.forEach(holder => {
|
||||
rows.push({label: 'Held By',
|
||||
value: <Link to={`${tenant.linkPrefix}/buildset/${holder.buildset_uuid}`}>
|
||||
{holder.job_name}
|
||||
</Link>})
|
||||
})
|
||||
return (
|
||||
<DescriptionList isHorizontal
|
||||
style={{'--pf-c-description-list--RowGap': '0.5rem'}}
|
||||
className='pf-u-m-xl'>
|
||||
{rows.map((item, idx) => (
|
||||
<DescriptionListGroup key={idx}>
|
||||
<DescriptionListTerm>
|
||||
{item.label}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{item.value}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
))}
|
||||
</DescriptionList>
|
||||
)
|
||||
}
|
||||
|
||||
Semaphore.propTypes = {
|
||||
semaphore: PropTypes.object.isRequired,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
tenant: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tenant: state.tenant,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Semaphore)
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright 2020 Red Hat, Inc
|
||||
// Copyright 2022 Acme Gating, LLC
|
||||
//
|
||||
// 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 React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
Spinner,
|
||||
Title,
|
||||
Label,
|
||||
} from '@patternfly/react-core'
|
||||
import {
|
||||
ResourcesFullIcon,
|
||||
TachometerAltIcon,
|
||||
LockIcon,
|
||||
TenantIcon,
|
||||
FingerprintIcon,
|
||||
} from '@patternfly/react-icons'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableVariant,
|
||||
} from '@patternfly/react-table'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { IconProperty } from '../../Misc'
|
||||
|
||||
function SemaphoreTable(props) {
|
||||
const { semaphores, fetching, tenant } = props
|
||||
const columns = [
|
||||
{
|
||||
title: <IconProperty icon={<FingerprintIcon />} value="Name" />,
|
||||
dataLabel: 'Name',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<TachometerAltIcon />} value="Current" />,
|
||||
dataLabel: 'Current',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<ResourcesFullIcon />} value="Max" />,
|
||||
dataLabel: 'Max',
|
||||
},
|
||||
{
|
||||
title: <IconProperty icon={<TenantIcon />} value="Global" />,
|
||||
dataLabel: 'Global',
|
||||
},
|
||||
]
|
||||
|
||||
function createSemaphoreRow(semaphore) {
|
||||
|
||||
return {
|
||||
cells: [
|
||||
{
|
||||
title: (
|
||||
<Link to={`${tenant.linkPrefix}/semaphore/${semaphore.name}`}>{semaphore.name}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: semaphore.holders.count,
|
||||
},
|
||||
{
|
||||
title: semaphore.max,
|
||||
},
|
||||
{
|
||||
title: semaphore.global ? (
|
||||
<Label
|
||||
style={{
|
||||
marginLeft: 'var(--pf-global--spacer--sm)',
|
||||
verticalAlign: '0.15em',
|
||||
}}
|
||||
>
|
||||
Global
|
||||
</Label>
|
||||
) : ''
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createFetchingRow() {
|
||||
const rows = [
|
||||
{
|
||||
heightAuto: true,
|
||||
cells: [
|
||||
{
|
||||
props: { colSpan: 8 },
|
||||
title: (
|
||||
<center>
|
||||
<Spinner size="xl" />
|
||||
</center>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return rows
|
||||
}
|
||||
|
||||
const haveSemaphores = semaphores && semaphores.length > 0
|
||||
|
||||
let rows = []
|
||||
if (fetching) {
|
||||
rows = createFetchingRow()
|
||||
columns[0].dataLabel = ''
|
||||
} else {
|
||||
if (haveSemaphores) {
|
||||
rows = semaphores.map((semaphore) => createSemaphoreRow(semaphore))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
aria-label="Semaphore Table"
|
||||
variant={TableVariant.compact}
|
||||
cells={columns}
|
||||
rows={rows}
|
||||
className="zuul-table"
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
|
||||
{/* Show an empty state in case we don't have any semaphores but are also not
|
||||
fetching */}
|
||||
{!fetching && !haveSemaphores && (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon icon={LockIcon} />
|
||||
<Title headingLevel="h1">No semaphores found</Title>
|
||||
<EmptyStateBody>
|
||||
Nothing to display.
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SemaphoreTable.propTypes = {
|
||||
semaphores: PropTypes.array,
|
||||
fetching: PropTypes.bool.isRequired,
|
||||
tenant: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
tenant: state.tenant,
|
||||
user: state.user,
|
||||
}))(SemaphoreTable)
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2018 Red Hat, Inc
|
||||
// Copyright 2022 Acme Gating, LLC
|
||||
//
|
||||
// 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 React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import {
|
||||
Title,
|
||||
} from '@patternfly/react-core'
|
||||
import { PageSection, PageSectionVariants } from '@patternfly/react-core'
|
||||
|
||||
import { fetchSemaphoresIfNeeded } from '../actions/semaphores'
|
||||
import Semaphore from '../containers/semaphore/Semaphore'
|
||||
|
||||
function SemaphorePage({ match, semaphores, tenant, fetchSemaphoresIfNeeded, isFetching }) {
|
||||
|
||||
const semaphoreName = match.params.semaphoreName
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `Zuul Semaphore | ${semaphoreName}`
|
||||
fetchSemaphoresIfNeeded(tenant, true)
|
||||
}, [fetchSemaphoresIfNeeded, tenant, semaphoreName])
|
||||
|
||||
const semaphore = semaphores[tenant.name] ? semaphores[tenant.name].find(
|
||||
e => e.name === semaphoreName) : undefined
|
||||
|
||||
return (
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Title headingLevel="h2">
|
||||
Details for Semaphore <span style={{color: 'var(--pf-global--primary-color--100)'}}>{semaphoreName}</span>
|
||||
</Title>
|
||||
|
||||
<Semaphore semaphore={semaphore}
|
||||
fetching={isFetching} />
|
||||
</PageSection>
|
||||
)
|
||||
}
|
||||
|
||||
SemaphorePage.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
semaphores: PropTypes.object.isRequired,
|
||||
tenant: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
fetchSemaphoresIfNeeded: PropTypes.func.isRequired,
|
||||
}
|
||||
const mapDispatchToProps = { fetchSemaphoresIfNeeded }
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tenant: state.tenant,
|
||||
semaphores: state.semaphores.semaphores,
|
||||
isFetching: state.semaphores.isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SemaphorePage)
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2021 BMW Group
|
||||
// Copyright 2022 Acme Gating, LLC
|
||||
//
|
||||
// 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 React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
} from '@patternfly/react-core'
|
||||
|
||||
import { fetchSemaphoresIfNeeded } from '../actions/semaphores'
|
||||
import SemaphoreTable from '../containers/semaphore/SemaphoreTable'
|
||||
|
||||
function SemaphoresPage({ tenant, semaphores, isFetching, fetchSemaphoresIfNeeded }) {
|
||||
useEffect(() => {
|
||||
document.title = 'Zuul Semaphores'
|
||||
fetchSemaphoresIfNeeded(tenant, true)
|
||||
}, [fetchSemaphoresIfNeeded, tenant])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<SemaphoreTable
|
||||
semaphores={semaphores[tenant.name]}
|
||||
fetching={isFetching} />
|
||||
</PageSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SemaphoresPage.propTypes = {
|
||||
tenant: PropTypes.object.isRequired,
|
||||
semaphores: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
fetchSemaphoresIfNeeded: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tenant: state.tenant,
|
||||
semaphores: state.semaphores.semaphores,
|
||||
isFetching: state.semaphores.isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = { fetchSemaphoresIfNeeded }
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SemaphoresPage)
|
|
@ -34,6 +34,7 @@ import project from './project'
|
|||
import pipelines from './pipelines'
|
||||
import projects from './projects'
|
||||
import preferences from './preferences'
|
||||
import semaphores from './semaphores'
|
||||
import status from './status'
|
||||
import tenant from './tenant'
|
||||
import tenants from './tenants'
|
||||
|
@ -60,6 +61,7 @@ const reducers = {
|
|||
pipelines,
|
||||
project,
|
||||
projects,
|
||||
semaphores,
|
||||
status,
|
||||
tenant,
|
||||
tenants,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2018 Red Hat, Inc
|
||||
// Copyright 2022 Acme Gating, LLC
|
||||
//
|
||||
// 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 {
|
||||
SEMAPHORES_FETCH_FAIL,
|
||||
SEMAPHORES_FETCH_REQUEST,
|
||||
SEMAPHORES_FETCH_SUCCESS
|
||||
} from '../actions/semaphores'
|
||||
|
||||
export default (state = {
|
||||
isFetching: false,
|
||||
semaphores: {},
|
||||
}, action) => {
|
||||
switch (action.type) {
|
||||
case SEMAPHORES_FETCH_REQUEST:
|
||||
return {
|
||||
isFetching: true,
|
||||
semaphores: state.semaphores,
|
||||
}
|
||||
case SEMAPHORES_FETCH_SUCCESS:
|
||||
return {
|
||||
isFetching: false,
|
||||
semaphores: {
|
||||
...state.semaphores,
|
||||
[action.tenant]: action.semaphores
|
||||
}
|
||||
}
|
||||
case SEMAPHORES_FETCH_FAIL:
|
||||
return {
|
||||
isFetching: false,
|
||||
semaphores: state.semaphores,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
|
@ -22,6 +22,8 @@ import JobPage from './pages/Job'
|
|||
import JobsPage from './pages/Jobs'
|
||||
import LabelsPage from './pages/Labels'
|
||||
import NodesPage from './pages/Nodes'
|
||||
import SemaphorePage from './pages/Semaphore'
|
||||
import SemaphoresPage from './pages/Semaphores'
|
||||
import AutoholdsPage from './pages/Autoholds'
|
||||
import AutoholdPage from './pages/Autohold'
|
||||
import BuildPage from './pages/Build'
|
||||
|
@ -69,6 +71,11 @@ const routes = () => [
|
|||
to: '/autoholds',
|
||||
component: AutoholdsPage
|
||||
},
|
||||
{
|
||||
title: 'Semaphores',
|
||||
to: '/semaphores',
|
||||
component: SemaphoresPage
|
||||
},
|
||||
{
|
||||
title: 'Builds',
|
||||
to: '/builds',
|
||||
|
@ -132,6 +139,10 @@ const routes = () => [
|
|||
to: '/autohold/:requestId',
|
||||
component: AutoholdPage
|
||||
},
|
||||
{
|
||||
to: '/semaphore/:semaphoreName',
|
||||
component: SemaphorePage
|
||||
},
|
||||
{
|
||||
to: '/config-errors',
|
||||
component: ConfigErrorsPage,
|
||||
|
|
Loading…
Reference in New Issue