diff options
Diffstat (limited to 'app')
15 files changed, 358 insertions, 26 deletions
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue new file mode 100644 index 00000000000..4d234a36fe5 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -0,0 +1,136 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import _ from 'underscore'; +import { __ } from '../../../locale'; +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import ScrollButton from './detail/scroll_button.vue'; +import JobDescription from './detail/description.vue'; + +const scrollPositions = { + top: 0, + bottom: 1, +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + ScrollButton, + JobDescription, + }, + data() { + return { + scrollPos: scrollPositions.top, + }; + }, + computed: { + ...mapState('pipelines', ['detailJob']), + isScrolledToBottom() { + return this.scrollPos === scrollPositions.bottom; + }, + isScrolledToTop() { + return this.scrollPos === scrollPositions.top; + }, + jobOutput() { + return this.detailJob.output || __('No messages were logged'); + }, + }, + mounted() { + this.getTrace(); + }, + methods: { + ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']), + scrollDown() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight); + } + }, + scrollUp() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, 0); + } + }, + scrollBuildLog: _.throttle(function buildLogScrollDebounce() { + const { scrollTop } = this.$refs.buildTrace; + const { offsetHeight, scrollHeight } = this.$refs.buildTrace; + + if (scrollTop + offsetHeight === scrollHeight) { + this.scrollPos = scrollPositions.bottom; + } else if (scrollTop === 0) { + this.scrollPos = scrollPositions.top; + } else { + this.scrollPos = ''; + } + }), + getTrace() { + return this.fetchJobTrace().then(() => this.scrollDown()); + }, + }, +}; +</script> + +<template> + <div class="ide-pipeline build-page d-flex flex-column flex-fill"> + <header class="ide-job-header d-flex align-items-center"> + <button + class="btn btn-default btn-sm d-flex" + @click="setDetailJob(null)" + > + <icon + name="chevron-left" + /> + {{ __('View jobs') }} + </button> + </header> + <div class="top-bar d-flex border-left-0"> + <job-description + :job="detailJob" + /> + <div class="controllers ml-auto"> + <a + v-tooltip + :title="__('Show complete raw log')" + data-placement="top" + data-container="body" + class="controllers-buttons" + :href="detailJob.rawPath" + target="_blank" + > + <i + aria-hidden="true" + class="fa fa-file-text-o" + ></i> + </a> + <scroll-button + direction="up" + :disabled="isScrolledToTop" + @click="scrollUp" + /> + <scroll-button + direction="down" + :disabled="isScrolledToBottom" + @click="scrollDown" + /> + </div> + </div> + <pre + class="build-trace mb-0 h-100" + ref="buildTrace" + @scroll="scrollBuildLog" + > + <code + class="bash" + v-html="jobOutput" + > + </code> + <div + v-show="detailJob.isLoading" + class="build-loader-animation" + > + </div> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue new file mode 100644 index 00000000000..def6bac3157 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue @@ -0,0 +1,47 @@ +<script> +import Icon from '../../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../../vue_shared/components/ci_icon.vue'; + +export default { + components: { + Icon, + CiIcon, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + jobId() { + return `#${this.job.id}`; + }, + }, +}; +</script> + +<template> + <div class="d-flex align-items-center"> + <ci-icon + class="d-flex" + :status="job.status" + :borderless="true" + :size="24" + /> + <span class="prepend-left-8"> + {{ job.name }} + <a + :href="job.path" + target="_blank" + class="ide-external-link" + > + {{ jobId }} + <icon + name="external-link" + :size="12" + /> + </a> + </span> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue new file mode 100644 index 00000000000..4e19e6e9c84 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue @@ -0,0 +1,66 @@ +<script> +import { __ } from '../../../../locale'; +import Icon from '../../../../vue_shared/components/icon.vue'; +import tooltip from '../../../../vue_shared/directives/tooltip'; + +const directions = { + up: 'up', + down: 'down', +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + }, + props: { + direction: { + type: String, + required: true, + validator(value) { + return Object.keys(directions).includes(value); + }, + }, + disabled: { + type: Boolean, + required: true, + }, + }, + computed: { + tooltipTitle() { + return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom'); + }, + iconName() { + return `scroll_${this.direction}`; + }, + }, + methods: { + clickedScroll() { + this.$emit('click'); + }, + }, +}; +</script> + +<template> + <div + v-tooltip + class="controllers-buttons" + data-container="body" + data-placement="top" + :title="tooltipTitle" + > + <button + class="btn-scroll btn-transparent btn-blank" + type="button" + :disabled="disabled" + @click="clickedScroll" + > + <icon + :name="iconName" + /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue index c33936021d4..c8e621504f0 100644 --- a/app/assets/javascripts/ide/components/jobs/item.vue +++ b/app/assets/javascripts/ide/components/jobs/item.vue @@ -1,11 +1,9 @@ <script> -import Icon from '../../../vue_shared/components/icon.vue'; -import CiIcon from '../../../vue_shared/components/ci_icon.vue'; +import JobDescription from './detail/description.vue'; export default { components: { - Icon, - CiIcon, + JobDescription, }, props: { job: { @@ -18,29 +16,29 @@ export default { return `#${this.job.id}`; }, }, + methods: { + clickViewLog() { + this.$emit('clickViewLog', this.job); + }, + }, }; </script> <template> <div class="ide-job-item"> - <ci-icon - :status="job.status" - :borderless="true" - :size="24" + <job-description + class="append-right-default" + :job="job" /> - <span class="prepend-left-8"> - {{ job.name }} - <a - :href="job.path" - target="_blank" - class="ide-external-link" + <div class="ml-auto align-self-center"> + <button + v-if="job.started" + type="button" + class="btn btn-default btn-sm" + @click="clickViewLog" > - {{ jobId }} - <icon - name="external-link" - :size="12" - /> - </a> - </span> + {{ __('View log') }} + </button> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue index bdd0364c9b9..3b16b860ecd 100644 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -19,7 +19,7 @@ export default { }, }, methods: { - ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']), + ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']), }, }; </script> @@ -38,6 +38,7 @@ export default { :stage="stage" @fetch="fetchJobs" @toggleCollapsed="toggleStageCollapsed" + @clickViewLog="setDetailJob" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 5b24bb1f5a7..b1428f885fb 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -48,6 +48,9 @@ export default { toggleCollapsed() { this.$emit('toggleCollapsed', this.stage.id); }, + clickViewLog(job) { + this.$emit('clickViewLog', job); + }, }, }; </script> @@ -101,6 +104,7 @@ export default { v-for="job in stage.jobs" :key="job.id" :job="job" + @clickViewLog="clickViewLog" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 703c4a70cfa..aafd6a15a78 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -4,6 +4,7 @@ import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; import { rightSidebarViews } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; +import JobsDetail from '../jobs/detail.vue'; export default { directives: { @@ -12,9 +13,16 @@ export default { components: { Icon, PipelinesList, + JobsDetail, }, computed: { ...mapState(['rightPane']), + pipelinesActive() { + return ( + this.rightPane === rightSidebarViews.pipelines || + this.rightPane === rightSidebarViews.jobsDetail + ); + }, }, methods: { ...mapActions(['setRightPane']), @@ -48,7 +56,7 @@ export default { :title="__('Pipelines')" class="ide-sidebar-link is-right" :class="{ - active: rightPane === $options.rightSidebarViews.pipelines + active: pipelinesActive }" type="button" @click="clickTab($event, $options.rightSidebarViews.pipelines)" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 33cd20caf52..65886c02b92 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -23,4 +23,5 @@ export const viewerTypes = { export const rightSidebarViews = { pipelines: 'pipelines-list', + jobsDetail: 'jobs-detail', }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 1ebe487263b..3de3e6d3376 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -4,6 +4,7 @@ import { __ } from '../../../../locale'; import flash from '../../../../flash'; import Poll from '../../../../lib/utils/poll'; import service from '../../../services'; +import { rightSidebarViews } from '../../../constants'; import * as types from './mutation_types'; let eTagPoll; @@ -77,4 +78,28 @@ export const fetchJobs = ({ dispatch }, stage) => { export const toggleStageCollapsed = ({ commit }, stageId) => commit(types.TOGGLE_STAGE_COLLAPSE, stageId); +export const setDetailJob = ({ commit, dispatch }, job) => { + commit(types.SET_DETAIL_JOB, job); + dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { + root: true, + }); +}; + +export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE); +export const receiveJobTraceError = ({ commit }) => { + flash(__('Error fetching job trace')); + commit(types.RECEIVE_JOB_TRACE_ERROR); +}; +export const receiveJobTraceSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOB_TRACE_SUCCESS, data); + +export const fetchJobTrace = ({ dispatch, state }) => { + dispatch('requestJobTrace'); + + return axios + .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } }) + .then(({ data }) => dispatch('receiveJobTraceSuccess', data)) + .catch(() => dispatch('receiveJobTraceError')); +}; + export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js index 3ddc8409c5b..f4c36b9d96f 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; + +export const SET_DETAIL_JOB = 'SET_DETAIL_JOB'; + +export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE'; +export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR'; +export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 745797e1ee5..5a2213bbe89 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -63,4 +63,17 @@ export default { isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, })); }, + [types.SET_DETAIL_JOB](state, job) { + state.detailJob = { ...job }; + }, + [types.REQUEST_JOB_TRACE](state) { + state.detailJob.isLoading = true; + }, + [types.RECEIVE_JOB_TRACE_ERROR](state) { + state.detailJob.isLoading = false; + }, + [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) { + state.detailJob.isLoading = false; + state.detailJob.output = data.html; + }, }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js index 0f83b315fff..8651e267b53 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -3,4 +3,5 @@ export default () => ({ isLoadingJobs: false, latestPipeline: null, stages: [], + detailJob: null, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js index 9f4b0d7d726..a6caca2d2dc 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -4,4 +4,8 @@ export const normalizeJob = job => ({ name: job.name, status: job.status, path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + output: '', + isLoading: false, }); diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 9ee02ca1d83..9213ccd4cdf 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -75,6 +75,7 @@ .top-bar { height: 35px; + min-height: 35px; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 2b3cc33c8ae..ffa8d13b09c 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1146,8 +1146,13 @@ } .ide-external-link { + position: relative; + svg { display: none; + position: absolute; + top: 2px; + right: -$gl-padding; } &:hover, @@ -1178,6 +1183,8 @@ display: flex; flex-direction: column; height: 100%; + margin-top: -$grid-size; + margin-bottom: -$grid-size; .empty-state { margin-top: auto; @@ -1194,6 +1201,17 @@ margin: 0; } } + + .build-trace, + .top-bar { + margin-left: -$gl-padding; + } + + &.build-page .top-bar { + top: 0; + font-size: 12px; + border-top-right-radius: $border-radius-default; + } } .ide-pipeline-list { @@ -1202,7 +1220,7 @@ } .ide-pipeline-header { - min-height: 50px; + min-height: 55px; padding-left: $gl-padding; padding-right: $gl-padding; @@ -1222,8 +1240,7 @@ .ci-status-icon { display: flex; justify-content: center; - height: 20px; - margin-top: -2px; + min-width: 24px; overflow: hidden; } } @@ -1253,3 +1270,7 @@ overflow: hidden; text-overflow: ellipsis; } + +.ide-job-header { + min-height: 60px; +} |