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 ( +
+ {status.map((item, idx) => ( +
+ + {item.name} +
+ ))} +
+ ) + } + + render () { + const { tenant, variant, descriptionMaxHeight } = this.props + const rows = [] + + const jobInfos = [ + 'description', 'context', 'status', + 'parent', 'attempts', 'timeout', 'semaphore', 'implied_branch', + 'nodeset', 'variables', + ] + jobInfos.forEach(key => { + let label = key + let value = variant[key] + + if (label === 'context') { + value = ( + + ) + } + if (label === 'status') { + value = this.renderStatus(variant) + } + + if (!value) { + return + } + + if (label === 'nodeset') { + value = + } + + if (label === 'parent') { + value = ( + + {value} + + ) + } + if (label === 'variables') { + value = ( + + + + ) + } + if (label === 'description') { + const style = { + whiteSpace: 'pre' + } + if (descriptionMaxHeight > 0) { + style.minHeight = descriptionMaxHeight + } + value = ( + { + if (height > descriptionMaxHeight) { + this.props.parent.setState({descriptionMaxHeight: height}) + } + }}> +
+ {value} +
+
+ ) + } + rows.push({label: label, value: value}) + }) + const jobInfosList = [ + 'required_projects', 'dependencies', 'files', 'irrelevant_files', 'roles' + ] + jobInfosList.forEach(key => { + let label = key + let values = variant[key] + + if (values.length === 0) { + return + } + const items = ( + + ) + rows.push({label: label, value: items}) + }) + return ( +
+ + + {rows.map(item => ( + + + + + ))} + +
{item.label}{item.value}
+
+ ) + } +} + +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 (

Loading...

) + } + return ( + + ) + } +} + +export default connect(state => ({tenant: state.tenant}))(JobPage) diff --git a/web/src/pages/Jobs.jsx b/web/src/pages/Jobs.jsx index 8ec0e3d725..d4ef1553f2 100644 --- a/web/src/pages/Jobs.jsx +++ b/web/src/pages/Jobs.jsx @@ -58,6 +58,12 @@ class JobsPage extends React.Component { const headerFormat = value => {value} const cellFormat = (value) => ( {value}) + const cellJobFormat = (value) => ( + + + {value} + + ) const cellBuildFormat = (value) => ( [ to: '/stream/:buildId', component: StreamPage }, + { + to: '/job/:jobName', + component: JobPage + }, { to: '/build/:buildId', component: BuildPage diff --git a/web/yarn.lock b/web/yarn.lock index 0252421681..2cee873f7e 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1209,6 +1209,10 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" + base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -3082,7 +3086,13 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16: +fbemitter@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865" + dependencies: + fbjs "^0.8.4" + +fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" dependencies: @@ -3202,6 +3212,13 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flux@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/flux/-/flux-3.1.3.tgz#d23bed515a79a22d933ab53ab4ada19d05b2f08a" + dependencies: + fbemitter "^2.0.0" + fbjs "^0.8.0" + follow-redirects@^1.0.0, follow-redirects@^1.3.0: version "1.5.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.5.tgz#3c143ca599a2e22e62876687d68b23d55bad788b" @@ -4719,6 +4736,10 @@ lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" +lodash.curry@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -4727,6 +4748,10 @@ lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" +lodash.flow@^3.3.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5986,6 +6011,12 @@ prop-types-extra@^1.0.1: react-is "^16.3.2" warning "^3.0.0" +prop-types@15.5.8: + version "15.5.8" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" + dependencies: + fbjs "^0.8.9" + prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" @@ -6034,6 +6065,10 @@ punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" +pure-color@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -6114,6 +6149,15 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-base16-styling@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c" + dependencies: + base16 "^1.0.0" + lodash.curry "^4.0.1" + lodash.flow "^3.3.0" + pure-color "^1.2.0" + react-bootstrap-switch@^15.5.3: version "15.5.3" resolved "https://registry.yarnpkg.com/react-bootstrap-switch/-/react-bootstrap-switch-15.5.3.tgz#97287791d4ec0d1892d142542e7e5248002b1251" @@ -6210,10 +6254,25 @@ react-fontawesome@^1.6.1: dependencies: prop-types "^15.5.6" +react-height@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-height/-/react-height-3.0.0.tgz#fae322f9da64d3e9e25536f26b77c73954261524" + dependencies: + prop-types "15.5.8" + react-is@^16.3.2: version "16.4.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88" +react-json-view@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.19.1.tgz#95d8e59e024f08a25e5dc8f076ae304eed97cf5c" + dependencies: + flux "^3.1.3" + react-base16-styling "^0.6.0" + react-lifecycles-compat "^3.0.4" + react-textarea-autosize "^6.1.0" + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -6337,6 +6396,12 @@ react-scripts@1.1.4: optionalDependencies: fsevents "^1.1.3" +react-textarea-autosize@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5" + dependencies: + prop-types "^15.6.0" + react-transition-group@^2.0.0, react-transition-group@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.4.0.tgz#1d9391fabfd82e016f26fabd1eec329dbd922b5a"