summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames E. Blair <jim@acmegating.com>2022-07-27 16:14:22 -0700
committerJames E. Blair <jim@acmegating.com>2022-08-02 08:03:31 -0700
commit2111c7cf96e11b5b2d96f3abda5526022639a4fe (patch)
tree0c4122cd1440a39ba4ebe3e610f2b6d3f378b420
parent97376adc21787494f5e544b271c7b435950d6256 (diff)
downloadzuul-2111c7cf96e11b5b2d96f3abda5526022639a4fe.tar.gz
Add freeze job to web UI
This adds a freeze-job page to the web UI. It can be navigated to by clicking a job in a job graph. Change-Id: Ibd07449314a9454295bc97f8d654347f09dc504c
-rw-r--r--web/src/actions/freezejob.js84
-rw-r--r--web/src/actions/pipelines.js62
-rw-r--r--web/src/api.js12
-rw-r--r--web/src/containers/freezejob/FreezeJobToolbar.jsx200
-rw-r--r--web/src/containers/jobgraph/JobGraphDisplay.jsx25
-rw-r--r--web/src/pages/FreezeJob.jsx149
-rw-r--r--web/src/reducers/freezejob.js55
-rw-r--r--web/src/reducers/index.js4
-rw-r--r--web/src/reducers/pipelines.js45
-rw-r--r--web/src/routes.js5
10 files changed, 634 insertions, 7 deletions
diff --git a/web/src/actions/freezejob.js b/web/src/actions/freezejob.js
new file mode 100644
index 000000000..13b32f124
--- /dev/null
+++ b/web/src/actions/freezejob.js
@@ -0,0 +1,84 @@
+// 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 FREEZE_JOB_FETCH_REQUEST = 'FREEZE_JOB_FETCH_REQUEST'
+export const FREEZE_JOB_FETCH_SUCCESS = 'FREEZE_JOB_FETCH_SUCCESS'
+export const FREEZE_JOB_FETCH_FAIL = 'FREEZE_JOB_FETCH_FAIL'
+
+export const requestFreezeJob = () => ({
+ type: FREEZE_JOB_FETCH_REQUEST
+})
+
+export function makeFreezeJobKey(pipeline, project, branch, job) {
+ return JSON.stringify({
+ pipeline, project, branch, job
+ })
+}
+
+export const receiveFreezeJob = (tenant, freezeJobKey, freezeJob) => {
+ return {
+ type: FREEZE_JOB_FETCH_SUCCESS,
+ tenant: tenant,
+ freezeJobKey: freezeJobKey,
+ freezeJob: freezeJob,
+ receivedAt: Date.now(),
+ }
+}
+
+const failedFreezeJob = error => ({
+ type: FREEZE_JOB_FETCH_FAIL,
+ error
+})
+
+const fetchFreezeJob = (tenant, pipeline, project, branch, job) => dispatch => {
+ dispatch(requestFreezeJob())
+ const freezeJobKey = makeFreezeJobKey(pipeline, project, branch, job)
+ return API.fetchFreezeJob(tenant.apiPrefix,
+ pipeline,
+ project,
+ branch,
+ job)
+ .then(response => dispatch(receiveFreezeJob(
+ tenant.name, freezeJobKey, response.data)))
+ .catch(error => dispatch(failedFreezeJob(error)))
+}
+
+const shouldFetchFreezeJob = (tenant, pipeline, project, branch, job, state) => {
+ const freezeJobKey = makeFreezeJobKey(pipeline, project, branch, job)
+ const tenantFreezeJobs = state.freezejob.freezeJobs[tenant.name]
+ if (tenantFreezeJobs) {
+ const freezeJob = tenantFreezeJobs[freezeJobKey]
+ if (!freezeJob) {
+ return true
+ }
+ if (freezeJob.isFetching) {
+ return false
+ }
+ return false
+ }
+ return true
+}
+
+export const fetchFreezeJobIfNeeded = (tenant, pipeline, project, branch, job,
+ force) => (
+ dispatch, getState) => {
+ if (force || shouldFetchFreezeJob(tenant, pipeline, project, branch, job,
+ getState())) {
+ return dispatch(fetchFreezeJob(tenant, pipeline, project, branch, job))
+ }
+ return Promise.resolve()
+}
diff --git a/web/src/actions/pipelines.js b/web/src/actions/pipelines.js
new file mode 100644
index 000000000..7139524f2
--- /dev/null
+++ b/web/src/actions/pipelines.js
@@ -0,0 +1,62 @@
+// 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 PIPELINES_FETCH_REQUEST = 'PIPELINES_FETCH_REQUEST'
+export const PIPELINES_FETCH_SUCCESS = 'PIPELINES_FETCH_SUCCESS'
+export const PIPELINES_FETCH_FAIL = 'PIPELINES_FETCH_FAIL'
+
+export const requestPipelines = () => ({
+ type: PIPELINES_FETCH_REQUEST
+})
+
+export const receivePipelines = (tenant, json) => ({
+ type: PIPELINES_FETCH_SUCCESS,
+ tenant: tenant,
+ pipelines: json,
+ receivedAt: Date.now()
+})
+
+const failedPipelines = error => ({
+ type: PIPELINES_FETCH_FAIL,
+ error
+})
+
+const fetchPipelines = (tenant) => dispatch => {
+ dispatch(requestPipelines())
+ return API.fetchPipelines(tenant.apiPrefix)
+ .then(response => dispatch(receivePipelines(tenant.name, response.data)))
+ .catch(error => dispatch(failedPipelines(error)))
+}
+
+const shouldFetchPipelines = (tenant, state) => {
+ const pipelines = state.pipelines.pipelines[tenant.name]
+ if (!pipelines || pipelines.length === 0) {
+ return true
+ }
+ if (pipelines.isFetching) {
+ return false
+ }
+ return false
+}
+
+export const fetchPipelinesIfNeeded = (tenant, force) => (
+ dispatch, getState) => {
+ if (force || shouldFetchPipelines(tenant, getState())) {
+ return dispatch(fetchPipelines(tenant))
+ }
+ return Promise.resolve()
+}
diff --git a/web/src/api.js b/web/src/api.js
index 8fcb2ec18..1ba39998c 100644
--- a/web/src/api.js
+++ b/web/src/api.js
@@ -130,6 +130,13 @@ function fetchStatus(apiPrefix) {
function fetchChangeStatus(apiPrefix, changeId) {
return Axios.get(apiUrl + apiPrefix + 'status/change/' + changeId)
}
+function fetchFreezeJob(apiPrefix, pipelineName, projectName, branchName, jobName) {
+ return Axios.get(apiUrl + apiPrefix +
+ 'pipeline/' + pipelineName +
+ '/project/' + projectName +
+ '/branch/' + branchName +
+ '/freeze-job/' + jobName)
+}
function fetchBuild(apiPrefix, buildId) {
return Axios.get(apiUrl + apiPrefix + 'build/' + buildId)
}
@@ -150,6 +157,9 @@ function fetchBuildsets(apiPrefix, queryString) {
}
return Axios.get(apiUrl + apiPrefix + path)
}
+function fetchPipelines(apiPrefix) {
+ return Axios.get(apiUrl + apiPrefix + 'pipelines')
+}
function fetchProject(apiPrefix, projectName) {
return Axios.get(apiUrl + apiPrefix + 'project/' + projectName)
}
@@ -312,6 +322,8 @@ export {
fetchBuilds,
fetchBuildset,
fetchBuildsets,
+ fetchFreezeJob,
+ fetchPipelines,
fetchProject,
fetchProjects,
fetchJob,
diff --git a/web/src/containers/freezejob/FreezeJobToolbar.jsx b/web/src/containers/freezejob/FreezeJobToolbar.jsx
new file mode 100644
index 000000000..c067b7205
--- /dev/null
+++ b/web/src/containers/freezejob/FreezeJobToolbar.jsx
@@ -0,0 +1,200 @@
+// Copyright 2020 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, useState } from 'react'
+import { connect } from 'react-redux'
+import PropTypes from 'prop-types'
+import {
+ Button,
+ TextInput,
+ Dropdown,
+ DropdownItem,
+ DropdownPosition,
+ DropdownToggle,
+ Toolbar,
+ ToolbarContent,
+ ToolbarGroup,
+ ToolbarItem,
+ ToolbarToggleGroup,
+} from '@patternfly/react-core'
+
+import { fetchPipelinesIfNeeded } from '../../actions/pipelines'
+
+function FreezeJobToolbar(props) {
+ const { tenant, fetchPipelinesIfNeeded } = props
+
+ useEffect(() => {
+ fetchPipelinesIfNeeded(tenant)
+ }, [fetchPipelinesIfNeeded, tenant])
+
+ const tenantPipelines = props.pipelines.pipelines[tenant.name]
+ const pipelines = tenantPipelines ? tenantPipelines.map(p => { return p.name }) : []
+
+ const [isPipelineOpen, setIsPipelineOpen] = useState(false)
+ const [currentPipeline, setCurrentPipeline] = useState(props.defaultPipeline || '')
+ const [currentProject, setCurrentProject] = useState(props.defaultProject || '')
+ const [currentBranch, setCurrentBranch] = useState(props.defaultBranch || '')
+ const [currentJob, setCurrentJob] = useState(props.defaultJob || '')
+
+ if (!currentPipeline && pipelines.length) {
+ // We may have gotten a list of pipelines after we loaded the page
+ setCurrentPipeline(pipelines[0])
+ }
+
+ function handlePipelineSelect(event) {
+ setCurrentPipeline(event.target.innerText)
+ setIsPipelineOpen(false)
+ }
+
+ function handlePipelineToggle(isOpen) {
+ setIsPipelineOpen(isOpen)
+ }
+
+ function handleProjectChange(newValue) {
+ setCurrentProject(newValue)
+ }
+
+ function handleBranchChange(newValue) {
+ setCurrentBranch(newValue)
+ }
+
+ function handleJobChange(newValue) {
+ setCurrentJob(newValue)
+ }
+
+ function handleInputSend(event) {
+ // In case the event comes from a key press, only accept "Enter"
+ if (event.key && event.key !== 'Enter') {
+ return
+ }
+
+ // Ignore empty values
+ if (!currentBranch || !currentProject || !currentJob) {
+ return
+ }
+
+ // Notify the parent component about the filter change
+ props.onChange(currentPipeline, currentProject, currentBranch, currentJob)
+ }
+
+ function renderFreezeJobToolbar () {
+ return <>
+ <Toolbar collapseListedFiltersBreakpoint="md">
+ <ToolbarContent>
+ <ToolbarToggleGroup breakpoint="md">
+ <ToolbarGroup variant="filter-group">
+
+ <ToolbarItem key="pipeline">
+ <Dropdown
+ onSelect={handlePipelineSelect}
+ position={DropdownPosition.left}
+ toggle={
+ <DropdownToggle
+ onToggle={handlePipelineToggle}
+ style={{ width: '100%' }}
+ >
+ Pipeline: {currentPipeline}
+ </DropdownToggle>
+ }
+ isOpen={isPipelineOpen}
+ dropdownItems={pipelines.map((pipeline) => (
+ <DropdownItem key={pipeline}>{pipeline}</DropdownItem>
+ ))}
+ style={{ width: '100%' }}
+ menuAppendTo={document.body}
+ />
+ </ToolbarItem>
+
+ <ToolbarItem key="project">
+ <TextInput
+ name="project"
+ id="project-input"
+ type="search"
+ placeholder="Project"
+ defaultValue={props.defaultProject}
+ onChange={handleProjectChange}
+ onKeyDown={(event) => handleInputSend(event)}
+ />
+ </ToolbarItem>
+
+ <ToolbarItem key="branch">
+ <TextInput
+ name="branch"
+ id="branch-input"
+ type="search"
+ placeholder="Branch"
+ defaultValue={props.defaultBranch}
+ onChange={handleBranchChange}
+ onKeyDown={(event) => handleInputSend(event)}
+ />
+ </ToolbarItem>
+
+ <ToolbarItem key="job">
+ <TextInput
+ name="job"
+ id="job-input"
+ type="search"
+ placeholder="Job"
+ defaultValue={props.defaultJob}
+ onChange={handleJobChange}
+ onKeyDown={(event) => handleInputSend(event)}
+ />
+ </ToolbarItem>
+
+ <ToolbarItem key="button">
+ <Button
+ onClick={(event) => handleInputSend(event)}
+ >
+ Freeze Job
+ </Button>
+ </ToolbarItem>
+
+ </ToolbarGroup>
+ </ToolbarToggleGroup>
+ </ToolbarContent>
+ </Toolbar>
+ </>
+ }
+
+ return (
+ <div>
+ {renderFreezeJobToolbar()}
+ </div>
+ )
+}
+
+FreezeJobToolbar.propTypes = {
+ fetchPipelinesIfNeeded: PropTypes.func,
+ tenant: PropTypes.object,
+ pipelines: PropTypes.object,
+ onChange: PropTypes.func.isRequired,
+ defaultPipeline: PropTypes.string,
+ defaultProject: PropTypes.string,
+ defaultBranch: PropTypes.string,
+ defaultJob: PropTypes.string,
+}
+
+function mapStateToProps(state) {
+ return {
+ tenant: state.tenant,
+ pipelines: state.pipelines,
+ }
+}
+
+const mapDispatchToProps = {
+ fetchPipelinesIfNeeded
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FreezeJobToolbar)
diff --git a/web/src/containers/jobgraph/JobGraphDisplay.jsx b/web/src/containers/jobgraph/JobGraphDisplay.jsx
index 1fbcef332..8d55c8f54 100644
--- a/web/src/containers/jobgraph/JobGraphDisplay.jsx
+++ b/web/src/containers/jobgraph/JobGraphDisplay.jsx
@@ -20,11 +20,24 @@ import * as d3 from 'd3'
import { makeJobGraphKey, fetchJobGraphIfNeeded } from '../../actions/jobgraph'
import { graphviz } from 'd3-graphviz'
-function makeDot(jobGraph) {
- let ret = 'digraph job_graph {'
+import { getHomepageUrl } from '../../api'
+
+function makeDot(tenant, pipeline, project, branch, jobGraph) {
+ let ret = 'digraph job_graph {\n'
ret += ' rankdir=LR;\n'
ret += ' node [shape=box];\n'
jobGraph.forEach((job) => {
+ const searchParams = new URLSearchParams('')
+ searchParams.append('pipeline', pipeline)
+ searchParams.append('project', project.name)
+ searchParams.append('job', job.name)
+ searchParams.append('branch', branch)
+ const url = (getHomepageUrl() + tenant.linkPrefix +
+ 'freeze-job?' + searchParams.toString())
+ // Escape ampersands to get it through graphviz and d3; these will
+ // appear unescaped in the DOM.
+ const escaped_url = url.replace(/&/g, '&amp;')
+ ret += ' "' + job.name + '" [URL="' + escaped_url + '"];\n'
if (job.dependencies.length) {
job.dependencies.forEach((dep) => {
let soft = ' [dir=back]'
@@ -33,8 +46,6 @@ function makeDot(jobGraph) {
}
ret += ' "' + dep.name + '" -> "' + job.name + '"' + soft + ';\n'
})
- } else {
- ret += ' "' + job.name + '";\n'
}
})
ret += '}\n'
@@ -80,16 +91,16 @@ function JobGraphDisplay(props) {
fetchJobGraphIfNeeded(tenant, project.name, pipeline, branch)
}, [fetchJobGraphIfNeeded, tenant, project, pipeline, branch])
- const tenantJobGraph = props.jobgraph.jobGraphs[props.tenant.name]
+ const tenantJobGraph = props.jobgraph.jobGraphs[tenant.name]
const jobGraphKey = makeJobGraphKey(props.project.name,
props.pipeline,
props.branch)
const jobGraph = tenantJobGraph ? tenantJobGraph[jobGraphKey] : undefined
useEffect(() => {
if (jobGraph) {
- setDot(makeDot(jobGraph))
+ setDot(makeDot(tenant, pipeline, project, branch, jobGraph))
}
- }, [jobGraph])
+ }, [tenant, pipeline, project, branch, jobGraph])
return (
<>
{dot && <GraphViz dot={dot}/>}
diff --git a/web/src/pages/FreezeJob.jsx b/web/src/pages/FreezeJob.jsx
new file mode 100644
index 000000000..a78cc7b56
--- /dev/null
+++ b/web/src/pages/FreezeJob.jsx
@@ -0,0 +1,149 @@
+// 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, useState } from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { useHistory, useLocation } from 'react-router-dom'
+import {
+ PageSection,
+ PageSectionVariants,
+ Text,
+ TextContent,
+} from '@patternfly/react-core'
+import ReactJson from 'react-json-view'
+
+import FreezeJobToolbar from '../containers/freezejob/FreezeJobToolbar'
+import { makeFreezeJobKey, fetchFreezeJobIfNeeded } from '../actions/freezejob'
+
+function FreezeJobPage(props) {
+ const { tenant, fetchFreezeJobIfNeeded } = props
+
+ const [currentPipeline, setCurrentPipeline] = useState()
+ const [currentProject, setCurrentProject] = useState()
+ const [currentBranch, setCurrentBranch] = useState()
+ const [currentJob, setCurrentJob] = useState()
+ const history = useHistory()
+ const location = useLocation()
+
+ if (!currentBranch) {
+ const urlParams = new URLSearchParams(location.search)
+ const pipeline = urlParams.get('pipeline')
+ const project = urlParams.get('project')
+ const branch = urlParams.get('branch')
+ const job = urlParams.get('job')
+ if (pipeline && branch && project && job) {
+ setCurrentPipeline(pipeline)
+ setCurrentProject(project)
+ setCurrentBranch(branch)
+ setCurrentJob(job)
+ }
+ }
+
+ useEffect(() => {
+ document.title = 'Zuul Frozen Job'
+ if (currentPipeline && currentProject && currentBranch && currentJob) {
+ fetchFreezeJobIfNeeded(tenant, currentPipeline, currentProject,
+ currentBranch, currentJob)
+ }
+ }, [fetchFreezeJobIfNeeded, tenant, currentPipeline, currentProject,
+ currentBranch, currentJob])
+
+ function onChange(pipeline, project, branch, job) {
+ setCurrentPipeline(pipeline)
+ setCurrentProject(project)
+ setCurrentBranch(branch)
+ setCurrentJob(job)
+
+ const searchParams = new URLSearchParams('')
+ searchParams.append('pipeline', pipeline)
+ searchParams.append('project', project)
+ searchParams.append('branch', branch)
+ searchParams.append('job', job)
+ history.push({
+ pathname: location.pathname,
+ search: searchParams.toString(),
+ })
+
+ if (currentPipeline && currentProject && currentBranch && currentJob) {
+ fetchFreezeJobIfNeeded(tenant, currentPipeline, currentProject,
+ currentBranch, currentJob)
+ }
+ }
+
+ const tenantJobs = props.freezejob.freezeJobs[tenant.name]
+ const freezeJobKey = makeFreezeJobKey(currentPipeline,
+ currentProject,
+ currentBranch,
+ currentJob)
+ const job = tenantJobs ? tenantJobs[freezeJobKey] : undefined
+ function renderFrozenJob() {
+ return (
+ <span style={{whiteSpace: 'pre-wrap'}}>
+ <ReactJson
+ src={job}
+ name={null}
+ collapsed={false}
+ sortKeys={true}
+ enableClipboard={false}
+ displayDataTypes={false}/>
+ </span>
+ )
+ }
+
+ return (
+ <>
+ <PageSection variant={PageSectionVariants.light}>
+ <TextContent>
+ <Text component="h1">Freeze Job</Text>
+ <Text component="p">
+ Freezing a job asks Zuul to combine all the
+ <i>project</i> and <i>job</i> configuration
+ stanzas for a job as if a change for a given
+ project and branch were to be enqueued into
+ a specific pipeline. The resulting job
+ configuration is displayed below.
+ </Text>
+ </TextContent>
+ <FreezeJobToolbar
+ onChange={onChange}
+ defaultPipeline={currentPipeline}
+ defaultProject={currentProject}
+ defaultBranch={currentBranch}
+ defaultJob={currentJob}
+ />
+ {job && renderFrozenJob(job)}
+ </PageSection>
+ </>
+ )
+}
+
+FreezeJobPage.propTypes = {
+ fetchFreezeJobIfNeeded: PropTypes.func,
+ tenant: PropTypes.object,
+ freezejob: PropTypes.object,
+}
+
+function mapStateToProps(state) {
+ return {
+ tenant: state.tenant,
+ freezejob: state.freezejob,
+ }
+}
+
+const mapDispatchToProps = {
+ fetchFreezeJobIfNeeded
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(FreezeJobPage)
diff --git a/web/src/reducers/freezejob.js b/web/src/reducers/freezejob.js
new file mode 100644
index 000000000..6e7db4991
--- /dev/null
+++ b/web/src/reducers/freezejob.js
@@ -0,0 +1,55 @@
+// 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 {
+ FREEZE_JOB_FETCH_FAIL,
+ FREEZE_JOB_FETCH_REQUEST,
+ FREEZE_JOB_FETCH_SUCCESS
+} from '../actions/freezejob'
+
+export default (state = {
+ isFetching: false,
+ freezeJobs: {},
+}, action) => {
+ let stateFreezeJobs
+ switch (action.type) {
+ case FREEZE_JOB_FETCH_REQUEST:
+ return {
+ isFetching: true,
+ freezeJobs: state.freezeJobs,
+ }
+ case FREEZE_JOB_FETCH_SUCCESS:
+ stateFreezeJobs = !state.freezeJobs[action.tenant] ?
+ { ...state.freezeJobs, [action.tenant]: {} } :
+ { ...state.freezeJobs }
+ return {
+ isFetching: false,
+ freezeJobs: {
+ ...stateFreezeJobs,
+ [action.tenant]: {
+ ...stateFreezeJobs[action.tenant],
+ [action.freezeJobKey]: action.freezeJob
+ }
+ }
+ }
+ case FREEZE_JOB_FETCH_FAIL:
+ return {
+ isFetching: false,
+ freezeJobs: state.freezeJobs,
+ }
+ default:
+ return state
+ }
+}
diff --git a/web/src/reducers/index.js b/web/src/reducers/index.js
index 5e18dbbea..83c86c105 100644
--- a/web/src/reducers/index.js
+++ b/web/src/reducers/index.js
@@ -19,6 +19,7 @@ import autoholds from './autoholds'
import configErrors from './configErrors'
import change from './change'
import component from './component'
+import freezejob from './freezejob'
import notifications from './notifications'
import build from './build'
import info from './info'
@@ -30,6 +31,7 @@ import logfile from './logfile'
import nodes from './nodes'
import openapi from './openapi'
import project from './project'
+import pipelines from './pipelines'
import projects from './projects'
import preferences from './preferences'
import status from './status'
@@ -45,6 +47,7 @@ const reducers = {
change,
component,
configErrors,
+ freezejob,
notifications,
info,
job,
@@ -54,6 +57,7 @@ const reducers = {
logfile,
nodes,
openapi,
+ pipelines,
project,
projects,
status,
diff --git a/web/src/reducers/pipelines.js b/web/src/reducers/pipelines.js
new file mode 100644
index 000000000..550352572
--- /dev/null
+++ b/web/src/reducers/pipelines.js
@@ -0,0 +1,45 @@
+// 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 {
+ PIPELINES_FETCH_FAIL,
+ PIPELINES_FETCH_REQUEST,
+ PIPELINES_FETCH_SUCCESS
+} from '../actions/pipelines'
+
+export default (state = {
+ isFetching: false,
+ pipelines: {},
+}, action) => {
+ switch (action.type) {
+ case PIPELINES_FETCH_REQUEST:
+ return {
+ isFetching: true,
+ pipelines: state.pipelines,
+ }
+ case PIPELINES_FETCH_SUCCESS:
+ return {
+ isFetching: false,
+ pipelines: { ...state.pipelines, [action.tenant]: action.pipelines },
+ }
+ case PIPELINES_FETCH_FAIL:
+ return {
+ isFetching: false,
+ pipelines: state.pipelines,
+ }
+ default:
+ return state
+ }
+}
diff --git a/web/src/routes.js b/web/src/routes.js
index c2c3a8584..e1bfdae16 100644
--- a/web/src/routes.js
+++ b/web/src/routes.js
@@ -13,6 +13,7 @@
// under the License.
import ComponentsPage from './pages/Components'
+import FreezeJobPage from './pages/FreezeJob'
import StatusPage from './pages/Status'
import ChangeStatusPage from './pages/ChangeStatus'
import ProjectPage from './pages/Project'
@@ -79,6 +80,10 @@ const routes = () => [
component: BuildsetsPage
},
{
+ to: '/freeze-job',
+ component: FreezeJobPage
+ },
+ {
to: '/status/change/:changeId',
component: ChangeStatusPage
},