diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-05 18:06:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-05 18:06:17 +0300 |
commit | 4c464055fbcdab02bb8334b148c0e35b981b239e (patch) | |
tree | 861562d77b4e8684d0498f25979d8ac85dd8f25a /app | |
parent | 791785af5540d18eaa97da24f9ff8638e1960b72 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
25 files changed, 710 insertions, 25 deletions
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 7223b5c0d43..b967a790fac 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -17,14 +17,7 @@ function MergeRequest(opts) { this.opts = opts != null ? opts : {}; this.submitNoteForm = this.submitNoteForm.bind(this); this.$el = $('.merge-request'); - this.$('.show-all-commits').on( - 'click', - (function(_this) { - return function() { - return _this.showAllCommits(); - }; - })(this), - ); + this.$('.show-all-commits').on('click', () => this.showAllCommits()); this.initTabs(); this.initMRBtnListeners(); diff --git a/app/assets/javascripts/pages/projects/pipelines/test_report/index.js b/app/assets/javascripts/pages/projects/pipelines/test_report/index.js new file mode 100644 index 00000000000..7e69983c2ed --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/test_report/index.js @@ -0,0 +1,2 @@ +// /test_report is an alias for show +import '../show/index'; diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue new file mode 100644 index 00000000000..388b300b39d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue @@ -0,0 +1,81 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import TestSuiteTable from './test_suite_table.vue'; +import TestSummary from './test_summary.vue'; +import TestSummaryTable from './test_summary_table.vue'; +import store from '~/pipelines/stores/test_reports'; + +export default { + name: 'TestReports', + components: { + GlLoadingIcon, + TestSuiteTable, + TestSummary, + TestSummaryTable, + }, + store, + computed: { + ...mapState(['isLoading', 'selectedSuite', 'testReports']), + showSuite() { + return this.selectedSuite.total_count > 0; + }, + showTests() { + return this.testReports.total_count > 0; + }, + }, + methods: { + ...mapActions(['setSelectedSuite', 'removeSelectedSuite']), + summaryBackClick() { + this.removeSelectedSuite(); + }, + summaryTableRowClick(suite) { + this.setSelectedSuite(suite); + }, + beforeEnterTransition() { + document.documentElement.style.overflowX = 'hidden'; + }, + afterLeaveTransition() { + document.documentElement.style.overflowX = ''; + }, + }, +}; +</script> + +<template> + <div v-if="isLoading"> + <gl-loading-icon size="lg" class="prepend-top-default js-loading-spinner" /> + </div> + + <div + v-else-if="!isLoading && showTests" + ref="container" + class="tests-detail position-relative js-tests-detail" + > + <transition + name="slide" + @before-enter="beforeEnterTransition" + @after-leave="afterLeaveTransition" + > + <div v-if="showSuite" key="detail" class="w-100 position-absolute slide-enter-to-element"> + <test-summary :report="selectedSuite" show-back @on-back-click="summaryBackClick" /> + + <test-suite-table /> + </div> + + <div v-else key="summary" class="w-100 position-absolute slide-enter-from-element"> + <test-summary :report="testReports" /> + + <test-summary-table @row-click="summaryTableRowClick" /> + </div> + </transition> + </div> + + <div v-else> + <div class="row prepend-top-default"> + <div class="col-12"> + <p class="js-no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue new file mode 100644 index 00000000000..28b2c706320 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue @@ -0,0 +1,108 @@ +<script> +import { mapGetters } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import store from '~/pipelines/stores/test_reports'; +import { __ } from '~/locale'; + +export default { + name: 'TestsSuiteTable', + components: { + Icon, + }, + store, + props: { + heading: { + type: String, + required: false, + default: __('Tests'), + }, + }, + computed: { + ...mapGetters(['getSuiteTests']), + hasSuites() { + return this.getSuiteTests.length > 0; + }, + }, +}; +</script> + +<template> + <div> + <div class="row prepend-top-default"> + <div class="col-12"> + <h4>{{ heading }}</h4> + </div> + </div> + + <div v-if="hasSuites" class="test-reports-table js-test-cases-table"> + <div role="row" class="gl-responsive-table-row table-row-header font-weight-bold fgray"> + <div role="rowheader" class="table-section section-20"> + {{ __('Class') }} + </div> + <div role="rowheader" class="table-section section-20"> + {{ __('Name') }} + </div> + <div role="rowheader" class="table-section section-10 text-center"> + {{ __('Status') }} + </div> + <div role="rowheader" class="table-section flex-grow-1"> + {{ __('Trace'), }} + </div> + <div role="rowheader" class="table-section section-10 text-right"> + {{ __('Duration') }} + </div> + </div> + + <div + v-for="(testCase, index) in getSuiteTests" + :key="index" + class="gl-responsive-table-row rounded align-items-md-start mt-sm-3 js-case-row" + > + <div class="table-section section-20 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Class') }}</div> + <div class="table-mobile-content pr-md-1">{{ testCase.classname }}</div> + </div> + + <div class="table-section section-20 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div> + <div class="table-mobile-content">{{ testCase.name }}</div> + </div> + + <div class="table-section section-10 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div> + <div class="table-mobile-content text-center"> + <div + class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center" + :class="`ci-status-icon-${testCase.status}`" + > + <icon :size="24" :name="testCase.icon" /> + </div> + </div> + </div> + + <div class="table-section flex-grow-1"> + <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div> + <div class="table-mobile-content"> + <pre + v-if="testCase.system_output" + class="build-trace build-trace-rounded text-left" + ><code class="bash p-0">{{testCase.system_output}}</code></pre> + </div> + </div> + + <div class="table-section section-10 section-wrap"> + <div role="rowheader" class="table-mobile-header"> + {{ __('Duration') }} + </div> + <div class="table-mobile-content text-right"> + {{ testCase.formattedTime }} + </div> + </div> + </div> + </div> + + <div v-else> + <p class="js-no-test-cases">{{ s__('TestReports|There are no test cases to display.') }}</p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue new file mode 100644 index 00000000000..5fc0e220a72 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -0,0 +1,116 @@ +<script> +import { GlButton, GlLink, GlProgressBar } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { formatTime } from '~/lib/utils/datetime_utility'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + name: 'TestSummary', + components: { + GlButton, + GlLink, + GlProgressBar, + Icon, + }, + props: { + report: { + type: Object, + required: true, + }, + showBack: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + heading() { + return this.report.name || __('Summary'); + }, + successPercentage() { + return Math.round((this.report.success_count / this.report.total_count) * 100) || 0; + }, + formattedDuration() { + return formatTime(this.report.total_time * 1000); + }, + progressBarVariant() { + if (this.successPercentage < 33) { + return 'danger'; + } + + if (this.successPercentage >= 33 && this.successPercentage < 66) { + return 'warning'; + } + + if (this.successPercentage >= 66 && this.successPercentage < 90) { + return 'primary'; + } + + return 'success'; + }, + }, + methods: { + onBackClick() { + this.$emit('on-back-click'); + }, + }, +}; +</script> + +<template> + <div> + <div class="row"> + <div class="col-12 d-flex prepend-top-8 align-items-center"> + <gl-button + v-if="showBack" + size="sm" + class="append-right-default js-back-button" + @click="onBackClick" + > + <icon name="angle-left" /> + </gl-button> + + <h4>{{ heading }}</h4> + </div> + </div> + + <div class="row mt-2"> + <div class="col-4 col-md"> + <span class="js-total-tests">{{ + sprintf(s__('TestReports|%{count} jobs'), { count: report.total_count }) + }}</span> + </div> + + <div class="col-4 col-md text-center text-md-center"> + <span class="js-failed-tests">{{ + sprintf(s__('TestReports|%{count} failures'), { count: report.failed_count }) + }}</span> + </div> + + <div class="col-4 col-md text-right text-md-center"> + <span class="js-errored-tests">{{ + sprintf(s__('TestReports|%{count} errors'), { count: report.error_count }) + }}</span> + </div> + + <div class="col-6 mt-3 col-md mt-md-0 text-md-center"> + <span class="js-success-rate">{{ + sprintf(s__('TestReports|%{rate}%{sign} success rate'), { + rate: successPercentage, + sign: '%', + }) + }}</span> + </div> + + <div class="col-6 mt-3 col-md mt-md-0 text-right"> + <span class="js-duration">{{ formattedDuration }}</span> + </div> + </div> + + <div class="row mt-3"> + <div class="col-12"> + <gl-progress-bar :value="successPercentage" :variant="progressBarVariant" height="10px" /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue new file mode 100644 index 00000000000..688baa93b6d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue @@ -0,0 +1,129 @@ +<script> +import { mapGetters } from 'vuex'; +import { s__ } from '~/locale'; +import store from '~/pipelines/stores/test_reports'; + +export default { + name: 'TestsSummaryTable', + store, + props: { + heading: { + type: String, + required: false, + default: s__('TestReports|Test suites'), + }, + }, + computed: { + ...mapGetters(['getTestSuites']), + hasSuites() { + return this.getTestSuites.length > 0; + }, + }, + methods: { + tableRowClick(suite) { + this.$emit('row-click', suite); + }, + }, +}; +</script> + +<template> + <div> + <div class="row prepend-top-default"> + <div class="col-12"> + <h4>{{ heading }}</h4> + </div> + </div> + + <div v-if="hasSuites" class="test-reports-table js-test-suites-table"> + <div role="row" class="gl-responsive-table-row table-row-header font-weight-bold"> + <div role="rowheader" class="table-section section-25 pl-3"> + {{ __('Suite') }} + </div> + <div role="rowheader" class="table-section section-25"> + {{ __('Duration') }} + </div> + <div role="rowheader" class="table-section section-10 text-center"> + {{ __('Failed') }} + </div> + <div role="rowheader" class="table-section section-10 text-center"> + {{ __('Errors'), }} + </div> + <div role="rowheader" class="table-section section-10 text-center"> + {{ __('Skipped'), }} + </div> + <div role="rowheader" class="table-section section-10 text-center"> + {{ __('Passed'), }} + </div> + <div role="rowheader" class="table-section section-10 pr-3 text-right"> + {{ __('Total') }} + </div> + </div> + + <div + v-for="(testSuite, index) in getTestSuites" + :key="index" + role="row" + class="gl-responsive-table-row test-reports-summary-row rounded cursor-pointer js-suite-row" + @click="tableRowClick(testSuite)" + > + <div class="table-section section-25"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Suite') }} + </div> + <div class="table-mobile-content test-reports-summary-suite cgray pl-3"> + {{ testSuite.name }} + </div> + </div> + + <div class="table-section section-25"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Duration') }} + </div> + <div class="table-mobile-content text-md-left"> + {{ testSuite.formattedTime }} + </div> + </div> + + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Failed') }} + </div> + <div class="table-mobile-content">{{ testSuite.failed_count }}</div> + </div> + + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Errors') }} + </div> + <div class="table-mobile-content">{{ testSuite.error_count }}</div> + </div> + + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Skipped') }} + </div> + <div class="table-mobile-content">{{ testSuite.skipped_count }}</div> + </div> + + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Passed') }} + </div> + <div class="table-mobile-content">{{ testSuite.success_count }}</div> + </div> + + <div class="table-section section-10 text-right pr-md-3"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Total') }} + </div> + <div class="table-mobile-content">{{ testSuite.total_count }}</div> + </div> + </div> + </div> + + <div v-else> + <p class="js-no-tests-suites">{{ s__('TestReports|There are no test suites to show.') }}</p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index d27829db50c..c9655d18a04 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,3 +1,9 @@ export const CANCEL_REQUEST = 'CANCEL_REQUEST'; export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const LAYOUT_CHANGE_DELAY = 300; + +export const TestStatus = { + FAILED: 'failed', + SKIPPED: 'skipped', + SUCCESS: 'success', +}; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index b6f8716d37d..d8dbc3c2454 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -7,6 +7,8 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; import PipelinesMediator from './pipeline_details_mediator'; import pipelineHeader from './components/header_component.vue'; import eventHub from './event_hub'; +import TestReports from './components/test_reports/test_reports.vue'; +import testReportsStore from './stores/test_reports'; Vue.use(Translate); @@ -17,7 +19,7 @@ export default () => { mediator.fetchPipeline(); - // eslint-disable-next-line + // eslint-disable-next-line no-new new Vue({ el: '#js-pipeline-graph-vue', components: { @@ -47,7 +49,7 @@ export default () => { }, }); - // eslint-disable-next-line + // eslint-disable-next-line no-new new Vue({ el: '#js-pipeline-header-vue', components: { @@ -81,4 +83,23 @@ export default () => { }); }, }); + + const testReportsEnabled = + window.gon && window.gon.features && window.gon.features.junitPipelineView; + + if (testReportsEnabled) { + testReportsStore.dispatch('setEndpoint', dataset.testReportEndpoint); + testReportsStore.dispatch('fetchReports'); + + // eslint-disable-next-line no-new + new Vue({ + el: '#js-pipeline-tests-detail', + components: { + TestReports, + }, + render(createElement) { + return createElement('test-reports'); + }, + }); + } }; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js new file mode 100644 index 00000000000..71d875c1a83 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js @@ -0,0 +1,30 @@ +import axios from '~/lib/utils/axios_utils'; +import * as types from './mutation_types'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; + +export const setEndpoint = ({ commit }, data) => commit(types.SET_ENDPOINT, data); + +export const fetchReports = ({ state, commit, dispatch }) => { + dispatch('toggleLoading'); + + return axios + .get(state.endpoint) + .then(response => { + const { data } = response; + commit(types.SET_REPORTS, data); + }) + .catch(() => { + createFlash(s__('TestReports|There was an error fetching the test reports.')); + }) + .finally(() => { + dispatch('toggleLoading'); + }); +}; + +export const setSelectedSuite = ({ commit }, data) => commit(types.SET_SELECTED_SUITE, data); +export const removeSelectedSuite = ({ commit }) => commit(types.SET_SELECTED_SUITE, {}); +export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js new file mode 100644 index 00000000000..788c1d32987 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js @@ -0,0 +1,23 @@ +import { addIconStatus, formattedTime, sortTestCases } from './utils'; + +export const getTestSuites = state => { + const { test_suites: testSuites = [] } = state.testReports; + + return testSuites.map(suite => ({ + ...suite, + formattedTime: formattedTime(suite.total_time), + })); +}; + +export const getSuiteTests = state => { + const { selectedSuite } = state; + + if (selectedSuite.test_cases) { + return selectedSuite.test_cases.sort(sortTestCases).map(addIconStatus); + } + + return []; +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js new file mode 100644 index 00000000000..318dff5bcb2 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + actions, + getters, + mutations, + state, +}); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js new file mode 100644 index 00000000000..832e45cf7a1 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js @@ -0,0 +1,4 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; +export const SET_REPORTS = 'SET_REPORTS'; +export const SET_SELECTED_SUITE = 'SET_SELECTED_SUITE'; +export const TOGGLE_LOADING = 'TOGGLE_LOADING'; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js new file mode 100644 index 00000000000..349e6ec0469 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js @@ -0,0 +1,19 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + Object.assign(state, { endpoint }); + }, + + [types.SET_REPORTS](state, testReports) { + Object.assign(state, { testReports }); + }, + + [types.SET_SELECTED_SUITE](state, selectedSuite) { + Object.assign(state, { selectedSuite }); + }, + + [types.TOGGLE_LOADING](state) { + Object.assign(state, { isLoading: !state.isLoading }); + }, +}; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js new file mode 100644 index 00000000000..80a0c2a46a0 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js @@ -0,0 +1,6 @@ +export default () => ({ + endpoint: '', + testReports: {}, + selectedSuite: {}, + isLoading: false, +}); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js new file mode 100644 index 00000000000..c426a5f0bb5 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -0,0 +1,36 @@ +import { TestStatus } from '~/pipelines/constants'; +import { formatTime } from '~/lib/utils/datetime_utility'; + +function iconForTestStatus(status) { + switch (status) { + case 'success': + return 'status_success_borderless'; + case 'failed': + return 'status_failed_borderless'; + default: + return 'status_skipped_borderless'; + } +} + +export const formattedTime = timeInSeconds => formatTime(timeInSeconds * 1000); + +export const addIconStatus = testCase => ({ + ...testCase, + icon: iconForTestStatus(testCase.status), + formattedTime: formattedTime(testCase.execution_time), +}); + +export const sortTestCases = (a, b) => { + if (a.status === b.status) { + return 0; + } + + switch (b.status) { + case TestStatus.SUCCESS: + return -1; + case TestStatus.FAILED: + return 1; + default: + return 0; + } +}; diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index aa270a374ae..d59a2a891ac 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -128,7 +128,7 @@ export default { <div class="ci-status-link"> <gl-link v-if="commit.latestPipeline" - v-gl-tooltip + v-gl-tooltip.left :href="commit.latestPipeline.detailedStatus.detailsPath" :title="statusTitle" class="js-commit-pipeline" diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 610c7e8d99e..98923c79c7a 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -1,5 +1,5 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlSkeletonLoading } from '@gitlab/ui'; import createFlash from '~/flash'; import { sprintf, __ } from '../../../locale'; import getRefMixin from '../../mixins/get_ref'; @@ -13,7 +13,7 @@ const PAGE_SIZE = 100; export default { components: { - GlLoadingIcon, + GlSkeletonLoading, TableHeader, TableRow, ParentRow, @@ -44,6 +44,15 @@ export default { }, computed: { tableCaption() { + if (this.isLoadingFiles) { + return sprintf( + __( + 'Loading files, directories, and submodules in the path %{path} for commit reference %{ref}', + ), + { path: this.path, ref: this.ref }, + ); + } + return sprintf( __('Files, directories, and submodules in the path %{path} for commit reference %{ref}'), { path: this.path, ref: this.ref }, @@ -117,12 +126,7 @@ export default { <template> <div class="tree-content-holder"> <div class="table-holder bordered-box"> - <table class="table tree-table qa-file-tree" aria-live="polite"> - <caption class="sr-only"> - {{ - tableCaption - }} - </caption> + <table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite"> <table-header v-once /> <tbody> <parent-row v-show="showParentRow" :commit-ref="ref" :path="path" /> @@ -141,9 +145,15 @@ export default { :lfs-oid="entry.lfsOid" /> </template> + <template v-if="isLoadingFiles"> + <tr v-for="i in 5" :key="i" aria-hidden="true"> + <td><gl-skeleton-loading :lines="1" class="h-auto" /></td> + <td><gl-skeleton-loading :lines="1" class="h-auto" /></td> + <td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td> + </tr> + </template> </tbody> </table> - <gl-loading-icon v-show="isLoadingFiles" class="my-3" size="md" /> </div> </div> </template> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 81ae5143082..9f5d929b008 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -124,13 +124,18 @@ export default { </template> </td> <td class="d-none d-sm-table-cell tree-commit"> - <gl-link v-if="commit" :href="commit.commitPath" class="str-truncated-100 tree-commit-link"> + <gl-link + v-if="commit" + :href="commit.commitPath" + :title="commit.message" + class="str-truncated-100 tree-commit-link" + > {{ commit.message }} </gl-link> <gl-skeleton-loading v-else :lines="1" class="h-auto" /> </td> <td class="tree-time-ago text-right"> - <timeago-tooltip v-if="commit" :time="commit.committedDate" tooltip-placement="bottom" /> + <timeago-tooltip v-if="commit" :time="commit.committedDate" /> <gl-skeleton-loading v-else :lines="1" class="ml-auto h-auto w-50" /> </td> </tr> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 8bcad7ac765..43935cf31d5 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -32,7 +32,7 @@ export default { </script> <template> <time - v-gl-tooltip="{ placement: tooltipPlacement }" + v-gl-tooltip.viewport="{ placement: tooltipPlacement }" :class="cssClass" :title="tooltipTitle(time)" v-text="timeFormated(time)" diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss index e3bdc0b0199..a082cd25abe 100644 --- a/app/assets/stylesheets/framework/vue_transitions.scss +++ b/app/assets/stylesheets/framework/vue_transitions.scss @@ -11,3 +11,27 @@ .fade-leave-to { opacity: 0; } + +.slide-enter-from-element { + &.slide-enter, + &.slide-leave-to { + transform: translateX(-150%); + } +} + +.slide-enter-to-element { + &.slide-enter, + &.slide-leave-to { + transform: translateX(150%); + } +} + +.slide-enter-active, +.slide-leave-active { + transition: transform 300ms ease-out; +} + +.slide-enter-to, +.slide-leave { + transform: translateX(0); +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 1b2af932733..132476f832c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1082,3 +1082,25 @@ button.mini-pipeline-graph-dropdown-toggle { .legend-success { color: $green-500; } + +.test-reports-table { + color: $gray-700; + + .test-reports-summary-row { + &:hover { + background-color: $gray-light; + + .test-reports-summary-suite { + text-decoration: underline; + } + } + } + + .build-trace { + @include build-trace(); + } +} + +.progress-bar.bg-primary { + background-color: $blue-500 !important; +} diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 6770ff37d91..4d35353d5f5 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -12,6 +12,7 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action do push_frontend_feature_flag(:hide_dismissed_vulnerabilities) + push_frontend_feature_flag(:junit_pipeline_view) end around_action :allow_gitaly_ref_name_caching, only: [:index, :show] diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 6eff2ea2e3a..a0273fe0e5a 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -7,8 +7,15 @@ class PrometheusService < MonitoringService prop_accessor :api_url boolean_accessor :manual_configuration + # We need to allow the self-monitoring project to connect to the internal + # Prometheus instance. + # Since the internal Prometheus instance is usually a localhost URL, we need + # to allow localhost URLs when the following conditions are true: + # 1. project is the self-monitoring project. + # 2. api_url is the internal Prometheus URL. with_options presence: true, if: :manual_configuration? do - validates :api_url, public_url: true + validates :api_url, public_url: true, unless: proc { |object| object.allow_local_api_url? } + validates :api_url, url: true, if: proc { |object| object.allow_local_api_url? } end before_save :synchronize_service_state @@ -82,12 +89,28 @@ class PrometheusService < MonitoringService project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? } end + def allow_local_api_url? + self_monitoring_project? && internal_prometheus_url? + end + private + def self_monitoring_project? + project && project.id == current_settings.instance_administration_project_id + end + + def internal_prometheus_url? + api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri + end + def should_return_client? api_url.present? && manual_configuration? && active? && valid? end + def current_settings + Gitlab::CurrentSettings.current_application_settings + end + def synchronize_service_state self.active = prometheus_available? || manual_configuration? diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 8c3518e3a29..f9408184cb6 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,3 +1,5 @@ +- test_reports_enabled = Feature.enabled?(:junit_pipeline_view) + .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %li.js-pipeline-tab-link @@ -12,6 +14,11 @@ = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do = _('Failed Jobs') %span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count + - if test_reports_enabled + %li.js-tests-tab-link + = link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do + = s_('TestReports|Tests') + %span.badge.badge-pill= pipeline.test_reports.total_count = render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project .tab-content @@ -71,4 +78,7 @@ %pre.build-trace.build-trace-rounded %code.bash.js-build-output = build_summary(build) + + #js-tab-tests.tab-pane + #js-pipeline-tests-detail = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 2b2133b8296..f0b3ab24ea0 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -20,4 +20,5 @@ - else = render "projects/pipelines/with_tabs", pipeline: @pipeline -.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } } +.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), + test_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json) } } |