diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/.eslintrc | 1 | ||||
-rw-r--r-- | web/src/actions/build.js | 136 | ||||
-rw-r--r-- | web/src/actions/logfile.js | 64 | ||||
-rw-r--r-- | web/src/containers/build/Build.jsx | 49 | ||||
-rw-r--r-- | web/src/containers/build/Manifest.jsx | 63 | ||||
-rw-r--r-- | web/src/containers/build/View.jsx | 53 | ||||
-rw-r--r-- | web/src/index.css | 10 | ||||
-rw-r--r-- | web/src/pages/Build.jsx | 2 | ||||
-rw-r--r-- | web/src/pages/View.jsx | 62 | ||||
-rw-r--r-- | web/src/reducers/build.js | 53 | ||||
-rw-r--r-- | web/src/reducers/index.js | 2 | ||||
-rw-r--r-- | web/src/reducers/logfile.js | 44 | ||||
-rw-r--r-- | web/src/routes.js | 5 |
13 files changed, 484 insertions, 60 deletions
diff --git a/web/.eslintrc b/web/.eslintrc index 24a734c02..86ff5afee 100644 --- a/web/.eslintrc +++ b/web/.eslintrc @@ -21,3 +21,4 @@ settings: env: jest/globals: true browser: true + es6: true diff --git a/web/src/actions/build.js b/web/src/actions/build.js index b291c88b8..a14945b22 100644 --- a/web/src/actions/build.js +++ b/web/src/actions/build.js @@ -19,7 +19,14 @@ import * as API from '../api' export const BUILD_FETCH_REQUEST = 'BUILD_FETCH_REQUEST' export const BUILD_FETCH_SUCCESS = 'BUILD_FETCH_SUCCESS' export const BUILD_FETCH_FAIL = 'BUILD_FETCH_FAIL' -export const BUILD_OUTPUT_FETCH_SUCCESS = 'BUILD_OUTPUT_FETCH_SUCCESS' + +export const BUILD_OUTPUT_REQUEST = 'BUILD_OUTPUT_REQUEST' +export const BUILD_OUTPUT_SUCCESS = 'BUILD_OUTPUT_SUCCESS' +export const BUILD_OUTPUT_FAIL = 'BUILD_OUTPUT_FAIL' + +export const BUILD_MANIFEST_REQUEST = 'BUILD_MANIFEST_REQUEST' +export const BUILD_MANIFEST_SUCCESS = 'BUILD_MANIFEST_SUCCESS' +export const BUILD_MANIFEST_FAIL = 'BUILD_MANIFEST_FAIL' export const requestBuild = () => ({ type: BUILD_FETCH_REQUEST @@ -32,6 +39,15 @@ export const receiveBuild = (buildId, build) => ({ receivedAt: Date.now() }) +const failedBuild = error => ({ + type: BUILD_FETCH_FAIL, + error +}) + +export const requestBuildOutput = () => ({ + type: BUILD_OUTPUT_REQUEST +}) + const receiveBuildOutput = (buildId, output) => { const hosts = {} // Compute stats @@ -70,58 +86,108 @@ const receiveBuildOutput = (buildId, output) => { }) }) return { - type: BUILD_OUTPUT_FETCH_SUCCESS, + type: BUILD_OUTPUT_SUCCESS, buildId: buildId, output: hosts, receivedAt: Date.now() } } -const failedBuild = error => ({ - type: BUILD_FETCH_FAIL, +const failedBuildOutput = error => ({ + type: BUILD_OUTPUT_FAIL, error }) -const fetchBuild = (tenant, build) => dispatch => { +export const requestBuildManifest = () => ({ + type: BUILD_MANIFEST_REQUEST +}) + +const receiveBuildManifest = (buildId, manifest) => { + const index = {} + + const renderNode = (root, object) => { + const path = root + '/' + object.name + + if ('children' in object && object.children) { + object.children.map(n => renderNode(path, n)) + } else { + index[path] = object + } + } + + manifest.tree.map(n => renderNode('', n)) + return { + type: BUILD_MANIFEST_SUCCESS, + buildId: buildId, + manifest: {tree: manifest.tree, index: index}, + receivedAt: Date.now() + } +} + +const failedBuildManifest = error => ({ + type: BUILD_MANIFEST_FAIL, + error +}) + +export const fetchBuild = (tenant, buildId, state, force) => dispatch => { + const build = state.build.builds[buildId] + if (!force && build) { + return Promise.resolve() + } dispatch(requestBuild()) - return API.fetchBuild(tenant.apiPrefix, build) + return API.fetchBuild(tenant.apiPrefix, buildId) .then(response => { - dispatch(receiveBuild(build, response.data)) - if (response.data.log_url) { - const url = response.data.log_url.substr( - 0, response.data.log_url.lastIndexOf('/') + 1) - Axios.get(url + 'job-output.json.gz') - .then(response => dispatch(receiveBuildOutput(build, response.data))) - .catch(error => { - if (!error.request) { - throw error - } - // Try without compression - Axios.get(url + 'job-output.json') - .then(response => dispatch(receiveBuildOutput( - build, response.data))) - }) - .catch(error => console.error( - 'Couldn\'t decode job-output...', error)) - } + dispatch(receiveBuild(buildId, response.data)) }) .catch(error => dispatch(failedBuild(error))) } -const shouldFetchBuild = (buildId, state) => { +const fetchBuildOutput = (buildId, state, force) => dispatch => { const build = state.build.builds[buildId] - if (!build) { - return true - } - if (build.isFetching) { - return false + const url = build.log_url.substr(0, build.log_url.lastIndexOf('/') + 1) + if (!force && build.output) { + return Promise.resolve() } - return false + dispatch(requestBuildOutput()) + return Axios.get(url + 'job-output.json.gz') + .then(response => dispatch(receiveBuildOutput(buildId, response.data))) + .catch(error => { + if (!error.request) { + throw error + } + // Try without compression + Axios.get(url + 'job-output.json') + .then(response => dispatch(receiveBuildOutput( + buildId, response.data))) + }) + .catch(error => dispatch(failedBuildOutput(error))) } -export const fetchBuildIfNeeded = (tenant, buildId, force) => ( - dispatch, getState) => { - if (force || shouldFetchBuild(buildId, getState())) { - return dispatch(fetchBuild(tenant, buildId)) +export const fetchBuildManifest = (buildId, state, force) => dispatch => { + const build = state.build.builds[buildId] + if (!force && build.manifest) { + return Promise.resolve() + } + + dispatch(requestBuildManifest()) + for (let artifact of build.artifacts) { + if ('metadata' in artifact && + 'type' in artifact.metadata && + artifact.metadata.type === 'zuul_manifest') { + return Axios.get(artifact.url) + .then(manifest => { + dispatch(receiveBuildManifest(buildId, manifest.data)) + }) + .catch(error => dispatch(failedBuildManifest(error))) } + } + dispatch(failedBuildManifest('no manifest found')) +} + +export const fetchBuildIfNeeded = (tenant, buildId, force) => (dispatch, getState) => { + dispatch(fetchBuild(tenant, buildId, getState(), force)) + .then(() => { + dispatch(fetchBuildOutput(buildId, getState(), force)) + dispatch(fetchBuildManifest(buildId, getState(), force)) + }) } diff --git a/web/src/actions/logfile.js b/web/src/actions/logfile.js new file mode 100644 index 000000000..8e5d99fe3 --- /dev/null +++ b/web/src/actions/logfile.js @@ -0,0 +1,64 @@ +// 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 Axios from 'axios' + +import {fetchBuild, fetchBuildManifest} from './build' + +export const LOGFILE_FETCH_REQUEST = 'LOGFILE_FETCH_REQUEST' +export const LOGFILE_FETCH_SUCCESS = 'LOGFILE_FETCH_SUCCESS' +export const LOGFILE_FETCH_FAIL = 'LOGFILE_FETCH_FAIL' + +export const requestLogfile = (url) => ({ + type: LOGFILE_FETCH_REQUEST, + url: url, +}) + +const receiveLogfile = (data) => ({ + type: LOGFILE_FETCH_SUCCESS, + data: data, + receivedAt: Date.now() +}) + +const failedLogfile = error => ({ + type: LOGFILE_FETCH_FAIL, + error +}) + +const fetchLogfile = (buildId, file, state, force) => dispatch => { + const build = state.build.builds[buildId] + const item = build.manifest.index['/' + file] + const url = build.log_url + item.name + + if (!force && state.logfile.url === url) { + return Promise.resolve() + } + dispatch(requestLogfile()) + if (item.mimetype === 'text/plain') { + return Axios.get(url) + .then(response => dispatch(receiveLogfile(response.data))) + .catch(error => dispatch(failedLogfile(error))) + } + dispatch(failedLogfile(null)) +} + +export const fetchLogfileIfNeeded = (tenant, buildId, file, force) => (dispatch, getState) => { + dispatch(fetchBuild(tenant, buildId, getState(), force)) + .then(() => { + dispatch(fetchBuildManifest(buildId, getState(), force)) + .then(() => { + dispatch(fetchLogfile(buildId, file, getState(), force)) + }) + }) +} diff --git a/web/src/containers/build/Build.jsx b/web/src/containers/build/Build.jsx index a985d377a..1070fc3b5 100644 --- a/web/src/containers/build/Build.jsx +++ b/web/src/containers/build/Build.jsx @@ -17,9 +17,17 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import { Link } from 'react-router-dom' import { Panel } from 'react-bootstrap' +import { + Nav, + NavItem, + TabContainer, + TabPane, + TabContent, +} from 'patternfly-react' import Artifact from './Artifact' import BuildOutput from './BuildOutput' +import Manifest from './Manifest' class Build extends React.Component { @@ -81,16 +89,37 @@ class Build extends React.Component { <Panel> <Panel.Heading>Build result {build.uuid}</Panel.Heading> <Panel.Body> - <table className="table table-striped table-bordered"> - <tbody> - {rows.map(item => ( - <tr key={item.key}> - <td>{item.key}</td> - <td>{item.value}</td> - </tr> - ))} - </tbody> - </table> + <TabContainer id="zuul-project" defaultActiveKey={1}> + <div> + <Nav bsClass="nav nav-tabs nav-tabs-pf"> + <NavItem eventKey={1}> + Summary + </NavItem> + {build.manifest && + <NavItem eventKey={2}> + Logs + </NavItem>} + </Nav> + <TabContent> + <TabPane eventKey={1}> + <table className="table table-striped table-bordered"> + <tbody> + {rows.map(item => ( + <tr key={item.key}> + <td>{item.key}</td> + <td>{item.value}</td> + </tr> + ))} + </tbody> + </table> + </TabPane> + {build.manifest && + <TabPane eventKey={2}> + <Manifest tenant={this.props.tenant} build={build}/> + </TabPane>} + </TabContent> + </div> + </TabContainer> {build.output && <BuildOutput output={build.output}/>} </Panel.Body> </Panel> diff --git a/web/src/containers/build/Manifest.jsx b/web/src/containers/build/Manifest.jsx new file mode 100644 index 000000000..5cade70f3 --- /dev/null +++ b/web/src/containers/build/Manifest.jsx @@ -0,0 +1,63 @@ +// Copyright 2019 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' +import { + TreeView, +} from 'patternfly-react' +import { Link } from 'react-router-dom' + +const renderTree = (tenant, build, obj) => { + const node = {} + let name = obj.name + + if ('children' in obj && obj.children) { + node.nodes = obj.children.map(n => renderTree(tenant, build, n)) + } + if (obj.mimetype === 'application/directory') { + name = obj.name + '/' + } else { + node.icon = 'fa fa-file-o' + } + if (obj.mimetype === 'text/plain') { + node.text = (<Link to={tenant.linkPrefix + '/build/' + build.uuid + '/view/' + name}>{obj.name}</Link>) + } else { + node.text = (<a href={build.log_url + name}>{obj.name}</a>) + } + return node +} + +class Manifest extends React.Component { + static propTypes = { + tenant: PropTypes.object.isRequired, + build: PropTypes.object.isRequired + } + + render() { + const { tenant, build } = this.props + + const nodes = build.manifest.tree.map(n => renderTree(tenant, build, n)) + + return ( + <div className="tree-view-container"> + <TreeView + nodes={nodes} + /> + </div> + ) + } +} + +export default Manifest diff --git a/web/src/containers/build/View.jsx b/web/src/containers/build/View.jsx new file mode 100644 index 000000000..66a6d7338 --- /dev/null +++ b/web/src/containers/build/View.jsx @@ -0,0 +1,53 @@ +// 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 { Panel } from 'react-bootstrap' + +const linkify = (ln, num) => { + return (<a name={num} href={'#'+num}>{ln}</a>) +} + +class View extends React.Component { + static propTypes = { + build: PropTypes.object, + item: PropTypes.object, + tenant: PropTypes.object, + data: PropTypes.object, + } + + render () { + const { build, data } = this.props + + return ( + <Panel> + <Panel.Heading>Build result {build.uuid}</Panel.Heading> + <Panel.Body> + <pre className="zuul-log-output"> + {data.split(/\r?\n/).map((line, idx) => ( + <span key={idx}> + {linkify(line, idx)}{'\n'} + </span> + ))} + </pre> + </Panel.Body> + </Panel> + ) + } +} + + +export default connect(state => ({tenant: state.tenant}))(View) diff --git a/web/src/index.css b/web/src/index.css index 614396d45..1c3d29dce 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -80,6 +80,16 @@ a.refresh { padding: 8px 12px; } +.zuul-log-output { + color: black; +} + +.zuul-log-output a, +.zuul-log-output a:hover { + color: black; + text-decoration: none; +} + .form-inline > .form-group { padding-right: 5px; } diff --git a/web/src/pages/Build.jsx b/web/src/pages/Build.jsx index 30d74de31..6aad08781 100644 --- a/web/src/pages/Build.jsx +++ b/web/src/pages/Build.jsx @@ -30,7 +30,7 @@ class BuildPage extends Refreshable { updateData = (force) => { this.props.dispatch(fetchBuildIfNeeded( - this.props.tenant, this.props.match.params.buildId, force)) + this.props.tenant, this.props.match.params.buildId, null, force)) } componentDidMount () { diff --git a/web/src/pages/View.jsx b/web/src/pages/View.jsx new file mode 100644 index 000000000..fe2aba812 --- /dev/null +++ b/web/src/pages/View.jsx @@ -0,0 +1,62 @@ +// 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 { fetchLogfileIfNeeded } from '../actions/logfile' +import Refreshable from '../containers/Refreshable' +import View from '../containers/build/View' + + +class ViewPage extends Refreshable { + static propTypes = { + match: PropTypes.object.isRequired, + remoteData: PropTypes.object, + tenant: PropTypes.object, + } + + updateData = (force) => { + this.props.dispatch(fetchLogfileIfNeeded( + this.props.tenant, + this.props.match.params.buildId, + this.props.match.params.file, + force)) + } + + componentDidMount () { + document.title = 'Zuul Build Viewer' + super.componentDidMount() + } + + render () { + const { remoteData } = this.props + const build = this.props.build.builds[this.props.match.params.buildId] + return ( + <React.Fragment> + <div style={{float: 'right'}}> + {this.renderSpinner()} + </div> + {remoteData.data && <View build={build} data={remoteData.data}/>} + </React.Fragment> + ) + } +} + +export default connect(state => ({ + tenant: state.tenant, + remoteData: state.logfile, + build: state.build +}))(ViewPage) diff --git a/web/src/reducers/build.js b/web/src/reducers/build.js index 244f5a356..3ef10e67a 100644 --- a/web/src/reducers/build.js +++ b/web/src/reducers/build.js @@ -18,27 +18,52 @@ import { BUILD_FETCH_FAIL, BUILD_FETCH_REQUEST, BUILD_FETCH_SUCCESS, - BUILD_OUTPUT_FETCH_SUCCESS + + BUILD_OUTPUT_FAIL, + BUILD_OUTPUT_REQUEST, + BUILD_OUTPUT_SUCCESS, + + BUILD_MANIFEST_FAIL, + BUILD_MANIFEST_REQUEST, + BUILD_MANIFEST_SUCCESS, } from '../actions/build' export default (state = { isFetching: false, + isFetchingOutput: false, + isFetchingManifest: false, builds: {}, }, action) => { switch (action.type) { - case BUILD_FETCH_REQUEST: - return update(state, {$merge: {isFetching: true}}) - case BUILD_FETCH_SUCCESS: - state.builds = update( - state.builds, {$merge: {[action.buildId]: action.build}}) - return update(state, {$merge: {isFetching: false}}) - case BUILD_FETCH_FAIL: - return update(state, {$merge: {isFetching: false}}) - case BUILD_OUTPUT_FETCH_SUCCESS: - return update( - state, {builds: {[action.buildId]: {$merge: {output: action.output}}}}) - default: - return state + case BUILD_FETCH_REQUEST: + return update(state, {$merge: {isFetching: true}}) + case BUILD_FETCH_SUCCESS: + state.builds = update( + state.builds, {$merge: {[action.buildId]: action.build}}) + return update(state, {$merge: {isFetching: false}}) + case BUILD_FETCH_FAIL: + return update(state, {$merge: {isFetching: false}}) + + case BUILD_OUTPUT_REQUEST: + return update(state, {$merge: {isFetchingOutput: true}}) + case BUILD_OUTPUT_SUCCESS: + state.builds = update( + state.builds, {[action.buildId]: {$merge: {output: action.output}}}) + return update(state, {$merge: {isFetchingOutput: false}}) + case BUILD_OUTPUT_FAIL: + return update(state, {$merge: {isFetchingOutput: false}}) + + case BUILD_MANIFEST_REQUEST: + return update(state, {$merge: {isFetchingManifest: true}}) + case BUILD_MANIFEST_SUCCESS: + state.builds = update( + state.builds, {[action.buildId]: {$merge: {manifest: action.manifest}}}) + return update(state, {$merge: {isFetchingManifest: false}}) + case BUILD_MANIFEST_FAIL: + return update(state, {$merge: {isFetchingManifest: false}}) + + default: + return state } } diff --git a/web/src/reducers/index.js b/web/src/reducers/index.js index a66822837..18abb44e3 100644 --- a/web/src/reducers/index.js +++ b/web/src/reducers/index.js @@ -22,6 +22,7 @@ import info from './info' import job from './job' import jobs from './jobs' import labels from './labels' +import logfile from './logfile' import nodes from './nodes' import project from './project' import projects from './projects' @@ -37,6 +38,7 @@ const reducers = { job, jobs, labels, + logfile, nodes, project, projects, diff --git a/web/src/reducers/logfile.js b/web/src/reducers/logfile.js new file mode 100644 index 000000000..4897cda2f --- /dev/null +++ b/web/src/reducers/logfile.js @@ -0,0 +1,44 @@ +// 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 update from 'immutability-helper' + +import { + LOGFILE_FETCH_FAIL, + LOGFILE_FETCH_REQUEST, + LOGFILE_FETCH_SUCCESS, +} from '../actions/logfile' + + +export default (state = { + isFetching: false, + url: null, + data: null +}, action) => { + switch (action.type) { + case LOGFILE_FETCH_REQUEST: + return update(state, {$merge: {isFetching: true, + url: action.url, + data: null}}) + case LOGFILE_FETCH_SUCCESS: + return update(state, {$merge: {isFetching: false, + data: action.data}}) + case LOGFILE_FETCH_FAIL: + return update(state, {$merge: {isFetching: false, + url: null, + data: null}}) + default: + return state + } +} diff --git a/web/src/routes.js b/web/src/routes.js index fe70535b5..0b3652681 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -21,6 +21,7 @@ import JobsPage from './pages/Jobs' import LabelsPage from './pages/Labels' import NodesPage from './pages/Nodes' import BuildPage from './pages/Build' +import ViewPage from './pages/View' import BuildsPage from './pages/Builds' import BuildsetsPage from './pages/Buildsets' import ConfigErrorsPage from './pages/ConfigErrors' @@ -89,6 +90,10 @@ const routes = () => [ component: BuildPage }, { + to: '/build/:buildId/view/:file*', + component: ViewPage + }, + { to: '/config-errors', component: ConfigErrorsPage, }, |