diff options
author | James E. Blair <jeblair@redhat.com> | 2019-07-20 11:08:05 -0700 |
---|---|---|
committer | James E. Blair <jeblair@redhat.com> | 2019-07-24 09:25:13 -0700 |
commit | 8fdc387c83140305d2374b954916868f8fbf0e40 (patch) | |
tree | 3aeaad1019f779a8b6587e3a504c84417ce51cf8 /web/src/actions | |
parent | d48c2b82fc7e69dc9f2f493859236af342ec888e (diff) | |
download | zuul-8fdc387c83140305d2374b954916868f8fbf0e40.tar.gz |
Add log browsing to build page
This looks for a zuul_manifest artifact, and if it is present,
it fetches it and shows a tree view of logs. Text logs are
displayed in-app with some basic line anchoring.
Part of the implementation of
https://zuul-ci.org/docs/zuul/developer/specs/logs.html
es6 added to .eslint to allow use of Promise.
Change-Id: Ib04d013b4118005ba66a91d2bec0b0c429d12863
Diffstat (limited to 'web/src/actions')
-rw-r--r-- | web/src/actions/build.js | 136 | ||||
-rw-r--r-- | web/src/actions/logfile.js | 64 |
2 files changed, 165 insertions, 35 deletions
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)) + }) + }) +} |