diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/public/openapi.yaml | 65 | ||||
-rw-r--r-- | web/src/actions/semaphores.js | 61 | ||||
-rw-r--r-- | web/src/api.js | 82 | ||||
-rw-r--r-- | web/src/containers/build/Artifact.jsx | 13 | ||||
-rw-r--r-- | web/src/containers/build/Console.jsx | 14 | ||||
-rw-r--r-- | web/src/containers/semaphore/Semaphore.jsx | 88 | ||||
-rw-r--r-- | web/src/containers/semaphore/SemaphoreTable.jsx | 166 | ||||
-rw-r--r-- | web/src/pages/Semaphore.jsx | 69 | ||||
-rw-r--r-- | web/src/pages/Semaphores.jsx | 61 | ||||
-rw-r--r-- | web/src/reducers/index.js | 2 | ||||
-rw-r--r-- | web/src/reducers/semaphores.js | 48 | ||||
-rw-r--r-- | web/src/routes.js | 11 |
12 files changed, 650 insertions, 30 deletions
diff --git a/web/public/openapi.yaml b/web/public/openapi.yaml index b101c66e0..d69111cf8 100644 --- a/web/public/openapi.yaml +++ b/web/public/openapi.yaml @@ -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: diff --git a/web/src/actions/semaphores.js b/web/src/actions/semaphores.js new file mode 100644 index 000000000..885a488e2 --- /dev/null +++ b/web/src/actions/semaphores.js @@ -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() +} diff --git a/web/src/api.js b/web/src/api.js index 1ba39998c..de2275c7c 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -103,93 +103,130 @@ function getStreamUrl(apiPrefix) { return streamUrl } +function getWithCorsHandling(url) { + // This performs a simple GET and tries to detect if CORS errors are + // due to proxy authentication errors. + const instance = Axios.create({ + baseURL: apiUrl + }) + // First try the request as normal + let res = instance.get(url).catch(err => { + if (err.response === undefined) { + // This is either a Network, DNS, or CORS error, but we can't tell which. + // If we're behind an authz proxy, it's possible our creds have timed out + // and the CORS error is because we're getting a redirect. + // Apache mod_auth_mellon (and possibly other authz proxies) will avoid + // issuing a redirect if X-Requested-With is set to 'XMLHttpRequest' and + // will instead issue a 403. We can use this to detect that case. + instance.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' + let res2 = instance.get(url).catch(err2 => { + if (err2.response && err2.response.status === 403) { + // We might be getting a redirect or something else, + // so reload the page. + console.log('Received 403 after unknown error; reloading') + window.location.reload() + } + // If we're still getting an error, we don't know the cause, + // it could be a transient network error, so we won't reload, we'll just + // wait for it to clear. + throw (err2) + }) + return res2 + } + }) + return res +} + // Direct APIs function fetchInfo() { - return Axios.get(apiUrl + 'info') + return getWithCorsHandling('info') } function fetchComponents() { - return Axios.get(apiUrl + 'components') + return getWithCorsHandling('components') } function fetchTenantInfo(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'info') + return getWithCorsHandling(apiPrefix + 'info') } function fetchOpenApi() { return Axios.get(getHomepageUrl() + 'openapi.yaml') } function fetchTenants() { - return Axios.get(apiUrl + 'tenants') + return getWithCorsHandling(apiUrl + 'tenants') } function fetchConfigErrors(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'config-errors') + return getWithCorsHandling(apiPrefix + 'config-errors') } function fetchStatus(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'status') + return getWithCorsHandling(apiPrefix + 'status') } function fetchChangeStatus(apiPrefix, changeId) { - return Axios.get(apiUrl + apiPrefix + 'status/change/' + changeId) + return getWithCorsHandling(apiPrefix + 'status/change/' + changeId) } function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) { - return Axios.get(apiUrl + apiPrefix + + return getWithCorsHandling(apiPrefix + 'pipeline/' + pipelineName + '/project/' + projectName + '/branch/' + branchName + '/freeze-job/' + jobName) } function fetchBuild(apiPrefix, buildId) { - return Axios.get(apiUrl + apiPrefix + 'build/' + buildId) + return getWithCorsHandling(apiPrefix + 'build/' + buildId) } function fetchBuilds(apiPrefix, queryString) { let path = 'builds' if (queryString) { path += '?' + queryString.slice(1) } - return Axios.get(apiUrl + apiPrefix + path) + return getWithCorsHandling(apiPrefix + path) } function fetchBuildset(apiPrefix, buildsetId) { - return Axios.get(apiUrl + apiPrefix + 'buildset/' + buildsetId) + return getWithCorsHandling(apiPrefix + 'buildset/' + buildsetId) } function fetchBuildsets(apiPrefix, queryString) { let path = 'buildsets' if (queryString) { path += '?' + queryString.slice(1) } - return Axios.get(apiUrl + apiPrefix + path) + return getWithCorsHandling(apiPrefix + path) } function fetchPipelines(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'pipelines') + return getWithCorsHandling(apiPrefix + 'pipelines') } function fetchProject(apiPrefix, projectName) { - return Axios.get(apiUrl + apiPrefix + 'project/' + projectName) + return getWithCorsHandling(apiPrefix + 'project/' + projectName) } function fetchProjects(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'projects') + return getWithCorsHandling(apiPrefix + 'projects') } function fetchJob(apiPrefix, jobName) { - return Axios.get(apiUrl + apiPrefix + 'job/' + jobName) + return getWithCorsHandling(apiPrefix + 'job/' + jobName) } function fetchJobGraph(apiPrefix, projectName, pipelineName, branchName) { - return Axios.get(apiUrl + apiPrefix + + return getWithCorsHandling(apiPrefix + 'pipeline/' + pipelineName + '/project/' + projectName + '/branch/' + branchName + '/freeze-jobs') } function fetchJobs(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'jobs') + return getWithCorsHandling(apiPrefix + 'jobs') } function fetchLabels(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'labels') + return getWithCorsHandling(apiPrefix + 'labels') } function fetchNodes(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'nodes') + return getWithCorsHandling(apiPrefix + 'nodes') +} +function fetchSemaphores(apiPrefix) { + return Axios.get(apiUrl + apiPrefix + 'semaphores') } function fetchAutoholds(apiPrefix) { - return Axios.get(apiUrl + apiPrefix + 'autohold') + return getWithCorsHandling(apiPrefix + 'autohold') } function fetchAutohold(apiPrefix, requestId) { - return Axios.get(apiUrl + apiPrefix + 'autohold/' + requestId) + return getWithCorsHandling(apiPrefix + 'autohold/' + requestId) } // token-protected API @@ -332,6 +369,7 @@ export { fetchLabels, fetchNodes, fetchOpenApi, + fetchSemaphores, fetchTenants, fetchInfo, fetchComponents, diff --git a/web/src/containers/build/Artifact.jsx b/web/src/containers/build/Artifact.jsx index 4d4259440..3793222d9 100644 --- a/web/src/containers/build/Artifact.jsx +++ b/web/src/containers/build/Artifact.jsx @@ -17,6 +17,7 @@ import PropTypes from 'prop-types' import { TreeView, } from 'patternfly-react' +import ReactJson from 'react-json-view' class Artifact extends React.Component { @@ -32,7 +33,17 @@ class Artifact extends React.Component { {Object.keys(artifact.metadata).map(key => ( <tr key={key}> <td>{key}</td> - <td style={{width:'100%'}}>{artifact.metadata[key]}</td> + <td style={{width:'100%'}}> + {typeof(artifact.metadata[key]) === 'object'? + <ReactJson + src={artifact.metadata[key]} + name={null} + collapsed={true} + sortKeys={true} + enableClipboard={false} + displayDataTypes={false}/> + :artifact.metadata[key].toString()} + </td> </tr> ))} </tbody> diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx index 74f1274d7..f24d9cbc8 100644 --- a/web/src/containers/build/Console.jsx +++ b/web/src/containers/build/Console.jsx @@ -64,13 +64,13 @@ class TaskOutput extends React.Component { renderResults(value) { const interesting_results = [] - // This was written to assume "value" is an array of - // key/value mappings to output. This seems to be a - // good assumption for the most part, but "package:" for - // whatever reason outputs a result that is just an array of - // strings with what packages were installed. So, if we - // see an array of strings as the value, we just swizzle - // that into a key/value so it displays usefully. + // This was written to assume "value" is an array of key/value + // mappings to output. This seems to be a good assumption for the + // most part, but "package:" on at least some distros -- + // RedHat/yum/dnf we've found -- outputs a result that is just an + // array of strings with what packages were installed. So, if we + // see an array of strings as the value, we just swizzle that into + // a key/value so it displays usefully. const isAllStrings = value.every(i => typeof i === 'string') if (isAllStrings) { value = [ {output: [...value]} ] diff --git a/web/src/containers/semaphore/Semaphore.jsx b/web/src/containers/semaphore/Semaphore.jsx new file mode 100644 index 000000000..831669c13 --- /dev/null +++ b/web/src/containers/semaphore/Semaphore.jsx @@ -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) diff --git a/web/src/containers/semaphore/SemaphoreTable.jsx b/web/src/containers/semaphore/SemaphoreTable.jsx new file mode 100644 index 000000000..6f400d7b4 --- /dev/null +++ b/web/src/containers/semaphore/SemaphoreTable.jsx @@ -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) diff --git a/web/src/pages/Semaphore.jsx b/web/src/pages/Semaphore.jsx new file mode 100644 index 000000000..a0ae8ddca --- /dev/null +++ b/web/src/pages/Semaphore.jsx @@ -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) diff --git a/web/src/pages/Semaphores.jsx b/web/src/pages/Semaphores.jsx new file mode 100644 index 000000000..0ad0040c0 --- /dev/null +++ b/web/src/pages/Semaphores.jsx @@ -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) diff --git a/web/src/reducers/index.js b/web/src/reducers/index.js index 83c86c105..9a76049dc 100644 --- a/web/src/reducers/index.js +++ b/web/src/reducers/index.js @@ -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, diff --git a/web/src/reducers/semaphores.js b/web/src/reducers/semaphores.js new file mode 100644 index 000000000..5e98bcab7 --- /dev/null +++ b/web/src/reducers/semaphores.js @@ -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 + } +} diff --git a/web/src/routes.js b/web/src/routes.js index e1bfdae16..a60216a97 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -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' @@ -70,6 +72,11 @@ const routes = () => [ component: AutoholdsPage }, { + title: 'Semaphores', + to: '/semaphores', + component: SemaphoresPage + }, + { title: 'Builds', to: '/builds', component: BuildsPage @@ -133,6 +140,10 @@ const routes = () => [ component: AutoholdPage }, { + to: '/semaphore/:semaphoreName', + component: SemaphorePage + }, + { to: '/config-errors', component: ConfigErrorsPage, }, |