Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-05 18:06:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-05 18:06:17 +0300
commit4c464055fbcdab02bb8334b148c0e35b981b239e (patch)
tree861562d77b4e8684d0498f25979d8ac85dd8f25a /app
parent791785af5540d18eaa97da24f9ff8638e1960b72 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/merge_request.js9
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/test_report/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue81
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue116
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue129
-rw-r--r--app/assets/javascripts/pipelines/constants.js6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js25
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js30
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js23
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/index.js15
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js19
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js36
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue28
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue2
-rw-r--r--app/assets/stylesheets/framework/vue_transitions.scss24
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss22
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/models/project_services/prometheus_service.rb25
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml10
-rw-r--r--app/views/projects/pipelines/show.html.haml3
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) } }