diff --git a/releasenotes/notes/web-page-job-77fa7ffb2a1c09de.yaml b/releasenotes/notes/web-page-job-77fa7ffb2a1c09de.yaml
new file mode 100644
index 0000000000..388a993294
--- /dev/null
+++ b/releasenotes/notes/web-page-job-77fa7ffb2a1c09de.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ A new Job page in the web interface enable browsing
+ through job configuration.
diff --git a/web/package.json b/web/package.json
index 46872965a7..1aa81cb0a3 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,6 +13,8 @@
"prop-types": "^15.6.2",
"react": "^16.4.2",
"react-dom": "^16.4.2",
+ "react-height": "^3.0.0",
+ "react-json-view": "^1.19.1",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
diff --git a/web/src/api.js b/web/src/api.js
index 40fc3ee173..d59ee37ee6 100644
--- a/web/src/api.js
+++ b/web/src/api.js
@@ -121,6 +121,9 @@ function fetchBuilds (apiPrefix, queryString) {
}
return Axios.get(apiUrl + apiPrefix + path)
}
+function fetchJob (apiPrefix, jobName) {
+ return Axios.get(apiUrl + apiPrefix + 'job/' + jobName)
+}
function fetchJobs (apiPrefix) {
return Axios.get(apiUrl + apiPrefix + 'jobs')
}
@@ -131,6 +134,7 @@ export {
fetchStatus,
fetchBuild,
fetchBuilds,
+ fetchJob,
fetchJobs,
fetchTenants,
fetchInfo
diff --git a/web/src/containers/SourceContext.jsx b/web/src/containers/SourceContext.jsx
new file mode 100644
index 0000000000..e291544882
--- /dev/null
+++ b/web/src/containers/SourceContext.jsx
@@ -0,0 +1,38 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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'
+
+
+class SourceContext extends React.Component {
+ static propTypes = {
+ context: PropTypes.object.isRequired,
+ showBranch: PropTypes.bool
+ }
+
+ render() {
+ const { context, showBranch } = this.props
+ return (
+
+ {context.project}
+ {showBranch && context.branch !== 'master' &&
+ ' (' + context.branch + ')'}
+ : {context.path}
+
+ )
+ }
+}
+
+export default SourceContext
diff --git a/web/src/containers/job/Job.jsx b/web/src/containers/job/Job.jsx
new file mode 100644
index 0000000000..2e62ae09ec
--- /dev/null
+++ b/web/src/containers/job/Job.jsx
@@ -0,0 +1,100 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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 React from 'react'
+import PropTypes from 'prop-types'
+import {
+ Nav,
+ NavItem,
+ TabContainer,
+ TabPane,
+ TabContent,
+} from 'patternfly-react'
+
+import JobVariant from './JobVariant'
+
+class Job extends React.Component {
+ static propTypes = {
+ job: PropTypes.array.isRequired,
+ }
+
+ state = {
+ variantIdx: 0,
+ descriptionMaxHeight: 0
+ }
+
+ resetMaxHeight = () => {
+ this.setState({descriptionMaxHeight: 0})
+ }
+
+ componentDidUpdate (prevProps, prevState) {
+ if (prevState.descriptionMaxHeight > 0) {
+ this.resetMaxHeight()
+ }
+ }
+
+ renderVariantTitle (variant, selected) {
+ let title = variant.variant_description
+ if (!title) {
+ title = ''
+ variant.branches.forEach((item) => {
+ if (title) {
+ title += ', '
+ }
+ title += item
+ })
+ }
+ if (selected) {
+ title = {title}
+ }
+ return title
+ }
+
+ render () {
+ const { job } = this.props
+ const { variantIdx, descriptionMaxHeight } = this.state
+
+ return (
+
+
{job[0].name}
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Job
diff --git a/web/src/containers/job/JobProject.jsx b/web/src/containers/job/JobProject.jsx
new file mode 100644
index 0000000000..a34271b609
--- /dev/null
+++ b/web/src/containers/job/JobProject.jsx
@@ -0,0 +1,38 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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'
+
+
+class JobProject extends React.Component {
+ static propTypes = {
+ project: PropTypes.object.isRequired
+ }
+
+ render() {
+ const { project } = this.props
+ return (
+
+ {project.project_name}
+ {project.override_branch && (
+ ' ( override-branch: ' + project.override_branch + ')')}
+ {project.override_checkout && (
+ ' ( override-checkout: ' + project.override_checkout+ ')')}
+
+ )
+ }
+}
+
+export default JobProject
diff --git a/web/src/containers/job/JobVariant.jsx b/web/src/containers/job/JobVariant.jsx
new file mode 100644
index 0000000000..80a6f5b01f
--- /dev/null
+++ b/web/src/containers/job/JobVariant.jsx
@@ -0,0 +1,200 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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 React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { Link } from 'react-router-dom'
+import { ReactHeight } from 'react-height'
+import ReactJson from 'react-json-view'
+import {
+ Icon,
+} from 'patternfly-react'
+
+import SourceContext from '../SourceContext'
+import Nodeset from './Nodeset'
+import Role from './Role'
+import JobProject from './JobProject'
+
+
+class JobVariant extends React.Component {
+ static propTypes = {
+ descriptionMaxHeight: PropTypes.number.isRequired,
+ parent: PropTypes.object,
+ tenant: PropTypes.object,
+ variant: PropTypes.object.isRequired
+ }
+
+ renderStatus (variant) {
+ const status = [{
+ icon: variant.voting ? 'connected' : 'disconnected',
+ name: variant.voting ? 'Voting' : 'Non-voting'
+ }]
+ if (variant.abstract) {
+ status.push({
+ icon: 'infrastructure',
+ name: 'Abstract'
+ })
+ }
+ if (variant.final) {
+ status.push({
+ icon: 'infrastructure',
+ name: 'Final'
+ })
+ }
+ if (variant.post_review) {
+ status.push({
+ icon: 'locked',
+ name: 'Post review'
+ })
+ }
+ if (variant.protected) {
+ status.push({
+ icon: 'locked',
+ name: 'Protected'
+ })
+ }
+
+ return (
+
+ )
+ }
+}
+
+export default connect(state => ({tenant: state.tenant}))(JobVariant)
diff --git a/web/src/containers/job/Nodeset.jsx b/web/src/containers/job/Nodeset.jsx
new file mode 100644
index 0000000000..14aba1b7d9
--- /dev/null
+++ b/web/src/containers/job/Nodeset.jsx
@@ -0,0 +1,90 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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 React from 'react'
+import PropTypes from 'prop-types'
+import {
+ AggregateStatusCount,
+ AggregateStatusNotifications,
+ AggregateStatusNotification,
+ Card,
+ CardBody,
+ CardTitle,
+ Icon,
+} from 'patternfly-react'
+
+
+class Nodeset extends React.Component {
+ static propTypes = {
+ nodeset: PropTypes.object.isRequired
+ }
+
+ render () {
+ const { nodeset } = this.props
+ const nodes = (
+
+ )
+ return (
+
+
+ {nodeset.name}
+
+
+
+
+
+
+
+ {nodeset.nodes.length}
+
+
+
+
+
+
+
+ {nodeset.groups.length}
+
+
+
+
+ {nodes}
+
+
+ )
+ }
+}
+
+export default Nodeset
diff --git a/web/src/containers/job/Role.jsx b/web/src/containers/job/Role.jsx
new file mode 100644
index 0000000000..97843f3eee
--- /dev/null
+++ b/web/src/containers/job/Role.jsx
@@ -0,0 +1,34 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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'
+
+
+class Role extends React.Component {
+ static propTypes = {
+ role: PropTypes.object.isRequired
+ }
+
+ render() {
+ const { role } = this.props
+ return (
+
+ {role.target_name} ( {role.project_canonical_name})
+
+ )
+ }
+}
+
+export default Role
diff --git a/web/src/pages/Job.jsx b/web/src/pages/Job.jsx
new file mode 100644
index 0000000000..d4108fe74f
--- /dev/null
+++ b/web/src/pages/Job.jsx
@@ -0,0 +1,65 @@
+// Copyright 2018 Red Hat, Inc
+//
+// 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 React from 'react'
+import { connect } from 'react-redux'
+import PropTypes from 'prop-types'
+
+import Job from '../containers/job/Job'
+import { fetchJob } from '../api'
+
+
+class JobPage extends React.Component {
+ static propTypes = {
+ match: PropTypes.object.isRequired,
+ tenant: PropTypes.object
+ }
+
+ state = {
+ job: null
+ }
+
+ updateData = () => {
+ fetchJob(this.props.tenant.apiPrefix, this.props.match.params.jobName)
+ .then(response => {
+ this.setState({job: response.data})
+ })
+ }
+
+ componentDidMount () {
+ document.title = 'Zuul Job | ' + this.props.match.params.jobName
+ if (this.props.tenant.name) {
+ this.updateData()
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ if (this.props.tenant.name !== prevProps.tenant.name ||
+ this.props.match.params.jobName !== prevProps.match.params.jobName) {
+ this.updateData()
+ }
+ }
+
+ render () {
+ const { job } = this.state
+ if (!job) {
+ return (