summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml13
-rw-r--r--GITALY_SERVER_VERSION2
-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
-rw-r--r--changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml6
-rw-r--r--changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml5
-rw-r--r--changelogs/unreleased/7816-commits-diff-total-api.yml5
-rw-r--r--changelogs/unreleased/dz-fix-clusters-api-doc-2.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.71.0.yml5
-rw-r--r--db/migrate/20180215181245_users_name_lower_index.rb6
-rw-r--r--db/migrate/20180504195842_project_name_lower_index.rb6
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql51
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json171
-rw-r--r--doc/api/project_clusters.md2
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/user/permissions.md6
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md19
-rw-r--r--doc/user/project/protected_branches.md9
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb15
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb23
-rw-r--r--locale/gitlab.pot63
-rw-r--r--spec/frontend/pipelines/test_reports/mock_data.js123
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js109
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js54
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js63
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js64
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js77
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js78
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js54
-rw-r--r--spec/frontend/repository/components/table/index_spec.js6
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb1
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb9
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb31
-rw-r--r--spec/requests/api/commits_spec.rb14
59 files changed, 1712 insertions, 124 deletions
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 00013d5d2ba..07375fca611 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -75,7 +75,7 @@ graphql-reference-verify:
- .default-cache
- .default-only
- .default-before_script
- - .only:changes-graphql
+ - .only:changes-code-backstage-qa
- .use-pg9
stage: test
needs: ["setup-test-env"]
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index f46638e0c4c..d746d8fe030 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -93,6 +93,7 @@
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
.backstage-patterns: &backstage-patterns
- "Dangerfile"
@@ -111,11 +112,6 @@
- "doc/**/*"
- ".markdownlint.json"
-.graphql-patterns: &graphql-patterns
- - "{,ee/}app/graphql/**/*"
- - "{,ee/}lib/gitlab/graphql/**/*"
- - "doc/api/graphql/**/*"
-
.only:changes-code:
only:
changes: *code-patterns
@@ -128,10 +124,6 @@
only:
changes: *docs-patterns
-.only:changes-graphql:
- only:
- changes: *graphql-patterns
-
.only:changes-code-backstage:
only:
changes:
@@ -147,6 +139,7 @@
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
# Backstage changes
- "Dangerfile"
- "danger/**/*"
@@ -170,6 +163,7 @@
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
# QA changes
- ".dockerignore"
- "qa/**/*"
@@ -189,6 +183,7 @@
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
# Backstage changes
- "Dangerfile"
- "danger/**/*"
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 832e9afb6c1..df484cbb1d9 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.70.0
+1.71.0
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) } }
diff --git a/changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml b/changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml
new file mode 100644
index 00000000000..9deb1f23d2a
--- /dev/null
+++ b/changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml
@@ -0,0 +1,6 @@
+---
+title: Added Tests tab to pipeline detail that contains a UI for browsing test reports
+ produced by JUnit
+merge_request: 18255
+author:
+type: added
diff --git a/changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml b/changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml
new file mode 100644
index 00000000000..9cb44979f3e
--- /dev/null
+++ b/changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove IIFEs from merge_request.js
+merge_request: 19294
+author: minghuan lei
+type: other
diff --git a/changelogs/unreleased/7816-commits-diff-total-api.yml b/changelogs/unreleased/7816-commits-diff-total-api.yml
new file mode 100644
index 00000000000..95b8afda682
--- /dev/null
+++ b/changelogs/unreleased/7816-commits-diff-total-api.yml
@@ -0,0 +1,5 @@
+---
+title: Show correct total number of commit diff's changes
+merge_request: 19424
+author:
+type: fixed
diff --git a/changelogs/unreleased/dz-fix-clusters-api-doc-2.yml b/changelogs/unreleased/dz-fix-clusters-api-doc-2.yml
new file mode 100644
index 00000000000..ca8842190cf
--- /dev/null
+++ b/changelogs/unreleased/dz-fix-clusters-api-doc-2.yml
@@ -0,0 +1,5 @@
+---
+title: Fix api docs for deleting project cluster
+merge_request: 19558
+author:
+type: other
diff --git a/changelogs/unreleased/gitaly-version-v1.71.0.yml b/changelogs/unreleased/gitaly-version-v1.71.0.yml
new file mode 100644
index 00000000000..306153ff4d7
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.71.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.71.0
+merge_request: 19611
+author:
+type: changed
diff --git a/db/migrate/20180215181245_users_name_lower_index.rb b/db/migrate/20180215181245_users_name_lower_index.rb
index 3b80601a727..fa1a115a78a 100644
--- a/db/migrate/20180215181245_users_name_lower_index.rb
+++ b/db/migrate/20180215181245_users_name_lower_index.rb
@@ -20,10 +20,6 @@ class UsersNameLowerIndex < ActiveRecord::Migration[4.2]
def down
return unless Gitlab::Database.postgresql?
- if supports_drop_index_concurrently?
- execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
- else
- execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
- end
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
end
end
diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb
index 3fe90c3fbb1..fa74330d5d9 100644
--- a/db/migrate/20180504195842_project_name_lower_index.rb
+++ b/db/migrate/20180504195842_project_name_lower_index.rb
@@ -22,11 +22,7 @@ class ProjectNameLowerIndex < ActiveRecord::Migration[4.2]
return unless Gitlab::Database.postgresql?
disable_statement_timeout do
- if supports_drop_index_concurrently?
- execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
- else
- execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
- end
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
end
end
end
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index cbc974a69cf..a74ce3491d3 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -3231,6 +3231,51 @@ type MergeRequestPermissions {
}
"""
+Autogenerated input type of MergeRequestSetMilestone
+"""
+input MergeRequestSetMilestoneInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ The milestone to assign to the merge request.
+ """
+ milestoneId: ID
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of MergeRequestSetMilestone
+"""
+type MergeRequestSetMilestonePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
Autogenerated input type of MergeRequestSetWip
"""
input MergeRequestSetWipInput {
@@ -3314,6 +3359,11 @@ type Milestone {
dueDate: Time
"""
+ ID of the milestone
+ """
+ id: ID!
+
+ """
Timestamp of the milestone start date
"""
startDate: Time
@@ -3360,6 +3410,7 @@ type Mutation {
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
+ mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index fd0ce344eab..5f48eb01f72 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -7741,6 +7741,24 @@
"deprecationReason": null
},
{
+ "name": "id",
+ "description": "ID of the milestone",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "startDate",
"description": "Timestamp of the milestone start date",
"args": [
@@ -14459,6 +14477,33 @@
"deprecationReason": null
},
{
+ "name": "mergeRequestSetMilestone",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetMilestoneInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetMilestonePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "mergeRequestSetWip",
"description": null,
"args": [
@@ -15090,6 +15135,132 @@
},
{
"kind": "OBJECT",
+ "name": "MergeRequestSetMilestonePayload",
+ "description": "Autogenerated return type of MergeRequestSetMilestone",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetMilestoneInput",
+ "description": "Autogenerated input type of MergeRequestSetMilestone",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "milestoneId",
+ "description": "The milestone to assign to the merge request.\n",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "MergeRequestSetWipPayload",
"description": "Autogenerated return type of MergeRequestSetWip",
"fields": [
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index 49e0e01e6cf..1aa225d30ab 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -382,5 +382,5 @@ Parameters:
Example request:
```bash
-curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/23'
+curl --request DELETE --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/23
```
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index ccedb96d27d..b579f812d99 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -59,10 +59,10 @@ graph TB
Unicorn --> Gitaly
Sidekiq --> Redis
Sidekiq --> PgBouncer
+ Sidekiq --> Gitaly
GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis
GitLabWorkhorse --> Gitaly
- Gitaly --> Redis
NGINX --> GitLabWorkhorse
NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
NGINX --> Grafana[Grafana]
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 90874eca2eb..2a6b241751b 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -55,7 +55,7 @@ The following table depicts the various user permission levels in a project.
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View GitLab Pages protected by [access control](project/pages/introduction.md#gitlab-pages-access-control-core) | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View wiki pages | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| See a list of jobs | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
@@ -73,7 +73,7 @@ The following table depicts the various user permission levels in a project.
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
-| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
+| See a list of merge requests | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View project statistics | | ✓ | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
| Pull from [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
@@ -83,7 +83,7 @@ The following table depicts the various user permission levels in a project.
| Push to non-protected branches | | | ✓ | ✓ | ✓ |
| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
| Remove non-protected branches | | | ✓ | ✓ | ✓ |
-| Create new merge request | | | ✓ | ✓ | ✓ |
+| Create new merge request | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Assign merge requests | | | ✓ | ✓ | ✓ |
| Label merge requests | | | ✓ | ✓ | ✓ |
| Lock merge request threads | | | ✓ | ✓ | ✓ |
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
index c9b504dc6ee..1d64e843e46 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
@@ -5,7 +5,8 @@ description: "Automatic Let's Encrypt SSL certificates for GitLab Pages."
# GitLab Pages integration with Let's Encrypt
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996) in GitLab 12.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996) in GitLab 12.1. For versions earlier than GitLab 12.1, see the [manual Let's Encrypt instructions](../lets_encrypt_for_gitlab_pages.md).
+This feature is in **beta** and may still have bugs. See all the related issues linked from this [issue's description](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996) for more information.
The GitLab Pages integration with Let's Encrypt (LE) allows you
to use LE certificates for your Pages website with custom domains
@@ -16,19 +17,11 @@ GitLab does it for you, out-of-the-box.
open source Certificate Authority.
CAUTION: **Caution:**
-This feature is in **beta** and might present bugs and UX issues
-such as [#64870](https://gitlab.com/gitlab-org/gitlab-foss/issues/64870).
-See all the related issues linked from this [issue's description](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996)
-for more information.
-
-CAUTION: **Caution:**
-This feature covers only certificates for **custom domains**,
-not the wildcard certificate required to run [Pages daemon](../../../../administration/pages/index.md) **(CORE ONLY)**.
-Wildcard certificate generation is tracked in [this issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3342).
+This feature covers only certificates for **custom domains**, not the wildcard certificate required to run [Pages daemon](../../../../administration/pages/index.md) **(CORE ONLY)**. Wildcard certificate generation is tracked in [this issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3342).
## Requirements
-Before you can enable automatic provisioning of a SSL certificate for your domain, make sure you have:
+Before you can enable automatic provisioning of an SSL certificate for your domain, make sure you have:
- Created a [project](../getting_started_part_two.md) in GitLab
containing your website's source code.
@@ -36,7 +29,7 @@ Before you can enable automatic provisioning of a SSL certificate for your domai
pointing it to your Pages website.
- [Added your domain to your Pages project](index.md#1-add-a-custom-domain-to-pages)
and verified your ownership.
-- Have your website up and running, accessible through your custom domain.
+- Verified your website is up and running, accessible through your custom domain.
NOTE: **Note:**
GitLab's Let's Encrypt integration is enabled and available on GitLab.com.
@@ -45,7 +38,7 @@ For **self-managed** GitLab instances, make sure your administrator has
## Enabling Let's Encrypt integration for your custom domain
-Once you've met the requirements, to enable Let's Encrypt integration:
+Once you've met the requirements, enable Let's Encrypt integration:
1. Navigate to your project's **Settings > Pages**.
1. Find your domain and click **Details**.
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 0aea7f76b30..8ce575222b9 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -118,10 +118,11 @@ all matching branches:
When a protected branch or wildcard protected branches are set to
[**No one** is **Allowed to push**](#using-the-allowed-to-merge-and-allowed-to-push-settings),
-Developers (and users with higher [permission levels](../permissions.md)) are allowed
-to create a new protected branch, but only via the UI or through the API (to avoid
-creating protected branches accidentally from the command line or from a Git
-client application).
+Developers (and users with higher [permission levels](../permissions.md)) are
+allowed to create a new protected branch as long as they are
+[**Allowed to merge**](#using-the-allowed-to-merge-and-allowed-to-push-settings).
+This can only be done via the UI or through the API (to avoid creating protected
+branches accidentally from the command line or from a Git client application).
To create a new branch through the user interface:
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index ffff40141de..3b9ac602c56 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -169,7 +169,7 @@ module API
not_found! 'Commit' unless commit
- raw_diffs = ::Kaminari.paginate_array(commit.raw_diffs.to_a)
+ raw_diffs = ::Kaminari.paginate_array(commit.diffs(expanded: true).diffs.to_a)
present paginate(raw_diffs), with: Entities::Diff
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index ae29546cdac..6bada921ad4 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -108,9 +108,7 @@ module Gitlab
'in the body of your migration class'
end
- if supports_drop_index_concurrently?
- options = options.merge({ algorithm: :concurrently })
- end
+ options = options.merge({ algorithm: :concurrently })
unless index_exists?(table_name, column_name, options)
Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger
@@ -136,9 +134,7 @@ module Gitlab
'in the body of your migration class'
end
- if supports_drop_index_concurrently?
- options = options.merge({ algorithm: :concurrently })
- end
+ options = options.merge({ algorithm: :concurrently })
unless index_exists_by_name?(table_name, index_name)
Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}" # rubocop:disable Gitlab/RailsLogger
@@ -150,13 +146,6 @@ module Gitlab
end
end
- # Only available on Postgresql >= 9.2
- def supports_drop_index_concurrently?
- version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
-
- version >= 90200
- end
-
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index 4677e984305..8cd9694b741 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -21,7 +21,6 @@ module Gitlab
:create_project,
:save_project_id,
:add_group_members,
- :add_to_whitelist,
:add_prometheus_manual_configuration
def initialize
@@ -126,28 +125,6 @@ module Gitlab
end
end
- def add_to_whitelist(result)
- return success(result) unless prometheus_enabled?
- return success(result) unless prometheus_listen_address.present?
-
- uri = parse_url(internal_prometheus_listen_address_uri)
- return error(_('Prometheus listen_address in config/gitlab.yml is not a valid URI')) unless uri
-
- application_settings.add_to_outbound_local_requests_whitelist([uri.normalized_host])
- response = application_settings.save
-
- if response
- # Expire the Gitlab::CurrentSettings cache after updating the whitelist.
- # This happens automatically in an after_commit hook, but in migrations,
- # the after_commit hook only runs at the end of the migration.
- Gitlab::CurrentSettings.expire_current_application_settings
- success(result)
- else
- log_error("Could not add prometheus URL to whitelist, errors: %{errors}" % { errors: application_settings.errors.full_messages })
- error(_('Could not add prometheus URL to whitelist'))
- end
- end
-
def add_prometheus_manual_configuration(result)
return success(result) unless prometheus_enabled?
return success(result) unless prometheus_listen_address.present?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 05eb287274a..c08a72813d6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3266,6 +3266,9 @@ msgstr ""
msgid "CiVariable|Validation failed"
msgstr ""
+msgid "Class"
+msgstr ""
+
msgid "Classification Label (optional)"
msgstr ""
@@ -4632,9 +4635,6 @@ msgstr ""
msgid "Could not add admins as members"
msgstr ""
-msgid "Could not add prometheus URL to whitelist"
-msgstr ""
-
msgid "Could not authorize chat nickname. Try again!"
msgstr ""
@@ -5876,6 +5876,9 @@ msgstr ""
msgid "Due date"
msgstr ""
+msgid "Duration"
+msgstr ""
+
msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
msgstr ""
@@ -10013,6 +10016,9 @@ msgstr ""
msgid "Loading contribution stats for group members"
msgstr ""
+msgid "Loading files, directories, and submodules in the path %{path} for commit reference %{ref}"
+msgstr ""
+
msgid "Loading functions timed out. Please reload the page to try again."
msgstr ""
@@ -11738,6 +11744,9 @@ msgstr ""
msgid "Part of merge request changes"
msgstr ""
+msgid "Passed"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -13289,9 +13298,6 @@ msgstr ""
msgid "ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}"
msgstr ""
-msgid "Prometheus listen_address in config/gitlab.yml is not a valid URI"
-msgstr ""
-
msgid "PrometheusAlerts|%{count} alerts applied"
msgstr ""
@@ -15504,6 +15510,9 @@ msgstr ""
msgid "Skip this for now"
msgstr ""
+msgid "Skipped"
+msgstr ""
+
msgid "Slack application"
msgstr ""
@@ -16305,6 +16314,12 @@ msgstr ""
msgid "Suggestions:"
msgstr ""
+msgid "Suite"
+msgstr ""
+
+msgid "Summary"
+msgstr ""
+
msgid "Sunday"
msgstr ""
@@ -16533,6 +16548,39 @@ msgstr ""
msgid "TestHooks|Ensure the wiki is enabled and has pages."
msgstr ""
+msgid "TestReports|%{count} errors"
+msgstr ""
+
+msgid "TestReports|%{count} failures"
+msgstr ""
+
+msgid "TestReports|%{count} jobs"
+msgstr ""
+
+msgid "TestReports|%{rate}%{sign} success rate"
+msgstr ""
+
+msgid "TestReports|Test suites"
+msgstr ""
+
+msgid "TestReports|Tests"
+msgstr ""
+
+msgid "TestReports|There are no test cases to display."
+msgstr ""
+
+msgid "TestReports|There are no test suites to show."
+msgstr ""
+
+msgid "TestReports|There are no tests to show."
+msgstr ""
+
+msgid "TestReports|There was an error fetching the test reports."
+msgstr ""
+
+msgid "Tests"
+msgstr ""
+
msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly."
msgstr ""
@@ -17718,6 +17766,9 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
+msgid "Trace"
+msgstr ""
+
msgid "Tracing"
msgstr ""
diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/pipelines/test_reports/mock_data.js
new file mode 100644
index 00000000000..b0f22bc63fb
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/mock_data.js
@@ -0,0 +1,123 @@
+import { formatTime } from '~/lib/utils/datetime_utility';
+import { TestStatus } from '~/pipelines/constants';
+
+export const testCases = [
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.000748,
+ name: 'Test#subtract when a is 1 and b is 2 raises an error',
+ stack_trace: null,
+ status: TestStatus.SUCCESS,
+ system_output: null,
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.000064,
+ name: 'Test#subtract when a is 2 and b is 1 returns correct result',
+ stack_trace: null,
+ status: TestStatus.SUCCESS,
+ system_output: null,
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.009292,
+ name: 'Test#sum when a is 1 and b is 2 returns summary',
+ stack_trace: null,
+ status: TestStatus.FAILED,
+ system_output:
+ "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'",
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.00018,
+ name: 'Test#sum when a is 100 and b is 200 returns summary',
+ stack_trace: null,
+ status: TestStatus.FAILED,
+ system_output:
+ "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'",
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0,
+ name: 'Test#skipped text',
+ stack_trace: null,
+ status: TestStatus.SKIPPED,
+ system_output: null,
+ },
+];
+
+export const testCasesFormatted = [
+ {
+ ...testCases[2],
+ icon: 'status_failed_borderless',
+ formattedTime: formatTime(testCases[0].execution_time * 1000),
+ },
+ {
+ ...testCases[3],
+ icon: 'status_failed_borderless',
+ formattedTime: formatTime(testCases[1].execution_time * 1000),
+ },
+ {
+ ...testCases[4],
+ icon: 'status_skipped_borderless',
+ formattedTime: formatTime(testCases[2].execution_time * 1000),
+ },
+ {
+ ...testCases[0],
+ icon: 'status_success_borderless',
+ formattedTime: formatTime(testCases[3].execution_time * 1000),
+ },
+ {
+ ...testCases[1],
+ icon: 'status_success_borderless',
+ formattedTime: formatTime(testCases[4].execution_time * 1000),
+ },
+];
+
+export const testSuites = [
+ {
+ error_count: 0,
+ failed_count: 2,
+ name: 'rspec:osx',
+ skipped_count: 0,
+ success_count: 2,
+ test_cases: testCases,
+ total_count: 4,
+ total_time: 60,
+ },
+ {
+ error_count: 0,
+ failed_count: 10,
+ name: 'rspec:osx',
+ skipped_count: 0,
+ success_count: 50,
+ test_cases: [],
+ total_count: 60,
+ total_time: 0.010284,
+ },
+];
+
+export const testSuitesFormatted = testSuites.map(x => ({
+ ...x,
+ formattedTime: formatTime(x.total_time * 1000),
+}));
+
+export const testReports = {
+ error_count: 0,
+ failed_count: 2,
+ skipped_count: 0,
+ success_count: 2,
+ test_suites: testSuites,
+ total_count: 4,
+ total_time: 0.010284,
+};
+
+export const testReportsWithNoSuites = {
+ error_count: 0,
+ failed_count: 2,
+ skipped_count: 0,
+ success_count: 2,
+ test_suites: [],
+ total_count: 4,
+ total_time: 0.010284,
+};
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
new file mode 100644
index 00000000000..c1721e12234
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -0,0 +1,109 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/pipelines/stores/test_reports/actions';
+import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import { TEST_HOST } from '../../../helpers/test_constants';
+import testAction from '../../../helpers/vuex_action_helper';
+import createFlash from '~/flash';
+import { testReports } from '../mock_data';
+
+jest.mock('~/flash.js');
+
+describe('Actions TestReports Store', () => {
+ let mock;
+ let state;
+
+ const endpoint = `${TEST_HOST}/test_reports.json`;
+ const defaultState = {
+ endpoint,
+ testReports: {},
+ selectedSuite: {},
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = defaultState;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('fetch reports', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/test_reports.json`).replyOnce(200, testReports, {});
+ });
+
+ it('sets testReports and shows tests', done => {
+ testAction(
+ actions.fetchReports,
+ null,
+ state,
+ [{ type: types.SET_REPORTS, payload: testReports }],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ done,
+ );
+ });
+
+ it('should create flash on API error', done => {
+ testAction(
+ actions.fetchReports,
+ null,
+ {
+ endpoint: null,
+ },
+ [],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('set selected suite', () => {
+ const selectedSuite = testReports.test_suites[0];
+
+ it('sets selectedSuite', done => {
+ testAction(
+ actions.setSelectedSuite,
+ selectedSuite,
+ state,
+ [{ type: types.SET_SELECTED_SUITE, payload: selectedSuite }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('remove selected suite', () => {
+ it('sets selectedSuite to {}', done => {
+ testAction(
+ actions.removeSelectedSuite,
+ {},
+ state,
+ [{ type: types.SET_SELECTED_SUITE, payload: {} }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggles loading', () => {
+ it('sets isLoading to true', done => {
+ testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], [], done);
+ });
+
+ it('toggles isLoading to false', done => {
+ testAction(
+ actions.toggleLoading,
+ {},
+ { ...state, isLoading: true },
+ [{ type: types.TOGGLE_LOADING }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
new file mode 100644
index 00000000000..e630a005409
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -0,0 +1,54 @@
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { testReports, testSuitesFormatted, testCasesFormatted } from '../mock_data';
+
+describe('Getters TestReports Store', () => {
+ let state;
+
+ const defaultState = {
+ testReports,
+ selectedSuite: testReports.test_suites[0],
+ };
+
+ const emptyState = {
+ testReports: {},
+ selectedSuite: {},
+ };
+
+ beforeEach(() => {
+ state = {
+ testReports,
+ };
+ });
+
+ const setupState = (testState = defaultState) => {
+ state = testState;
+ };
+
+ describe('getTestSuites', () => {
+ it('should return the test suites', () => {
+ setupState();
+
+ expect(getters.getTestSuites(state)).toEqual(testSuitesFormatted);
+ });
+
+ it('should return an empty array when testReports is empty', () => {
+ setupState(emptyState);
+
+ expect(getters.getTestSuites(state)).toEqual([]);
+ });
+ });
+
+ describe('getSuiteTests', () => {
+ it('should return the test cases inside the suite', () => {
+ setupState();
+
+ expect(getters.getSuiteTests(state)).toEqual(testCasesFormatted);
+ });
+
+ it('should return an empty array when testReports is empty', () => {
+ setupState(emptyState);
+
+ expect(getters.getSuiteTests(state)).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
new file mode 100644
index 00000000000..ad5b7f91163
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -0,0 +1,63 @@
+import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import mutations from '~/pipelines/stores/test_reports/mutations';
+import { testReports, testSuites } from '../mock_data';
+
+describe('Mutations TestReports Store', () => {
+ let mockState;
+
+ const defaultState = {
+ endpoint: '',
+ testReports: {},
+ selectedSuite: {},
+ isLoading: false,
+ };
+
+ beforeEach(() => {
+ mockState = defaultState;
+ });
+
+ describe('set endpoint', () => {
+ it('should set endpoint', () => {
+ const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
+ mutations[types.SET_ENDPOINT](mockState, 'foo');
+
+ expect(mockState.endpoint).toEqual(expectedState.endpoint);
+ });
+ });
+
+ describe('set reports', () => {
+ it('should set testReports', () => {
+ const expectedState = Object.assign({}, mockState, { testReports });
+ mutations[types.SET_REPORTS](mockState, testReports);
+
+ expect(mockState.testReports).toEqual(expectedState.testReports);
+ });
+ });
+
+ describe('set selected suite', () => {
+ it('should set selectedSuite', () => {
+ const expectedState = Object.assign({}, mockState, { selectedSuite: testSuites[0] });
+ mutations[types.SET_SELECTED_SUITE](mockState, testSuites[0]);
+
+ expect(mockState.selectedSuite).toEqual(expectedState.selectedSuite);
+ });
+ });
+
+ describe('toggle loading', () => {
+ it('should set to true', () => {
+ const expectedState = Object.assign({}, mockState, { isLoading: true });
+ mutations[types.TOGGLE_LOADING](mockState);
+
+ expect(mockState.isLoading).toEqual(expectedState.isLoading);
+ });
+
+ it('should toggle back to false', () => {
+ const expectedState = Object.assign({}, mockState, { isLoading: false });
+ mockState.isLoading = true;
+
+ mutations[types.TOGGLE_LOADING](mockState);
+
+ expect(mockState.isLoading).toEqual(expectedState.isLoading);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
new file mode 100644
index 00000000000..4d6422745a9
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -0,0 +1,64 @@
+import Vuex from 'vuex';
+import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
+import { shallowMount } from '@vue/test-utils';
+import { testReports } from './mock_data';
+import * as actions from '~/pipelines/stores/test_reports/actions';
+
+describe('Test reports app', () => {
+ let wrapper;
+ let store;
+
+ const loadingSpinner = () => wrapper.find('.js-loading-spinner');
+ const testsDetail = () => wrapper.find('.js-tests-detail');
+ const noTestsToShow = () => wrapper.find('.js-no-tests-to-show');
+
+ const createComponent = (state = {}) => {
+ store = new Vuex.Store({
+ state: {
+ isLoading: false,
+ selectedSuite: {},
+ testReports,
+ ...state,
+ },
+ actions,
+ });
+
+ wrapper = shallowMount(TestReports, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => createComponent({ isLoading: true }));
+
+ it('shows the loading spinner', () => {
+ expect(noTestsToShow().exists()).toBe(false);
+ expect(testsDetail().exists()).toBe(false);
+ expect(loadingSpinner().exists()).toBe(true);
+ });
+ });
+
+ describe('when the api returns no data', () => {
+ beforeEach(() => createComponent({ testReports: {} }));
+
+ it('displays that there are no tests to show', () => {
+ const noTests = noTestsToShow();
+
+ expect(noTests.exists()).toBe(true);
+ expect(noTests.text()).toBe('There are no tests to show.');
+ });
+ });
+
+ describe('when the api returns data', () => {
+ beforeEach(() => createComponent());
+
+ it('sets testReports and shows tests', () => {
+ expect(wrapper.vm.testReports).toBeTruthy();
+ expect(wrapper.vm.showTests).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
new file mode 100644
index 00000000000..b4305719ea8
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -0,0 +1,77 @@
+import Vuex from 'vuex';
+import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { TestStatus } from '~/pipelines/constants';
+import { shallowMount } from '@vue/test-utils';
+import { testSuites, testCases } from './mock_data';
+
+describe('Test reports suite table', () => {
+ let wrapper;
+ let store;
+
+ const noCasesMessage = () => wrapper.find('.js-no-test-cases');
+ const allCaseRows = () => wrapper.findAll('.js-case-row');
+ const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index);
+ const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
+
+ const createComponent = (suite = testSuites[0]) => {
+ store = new Vuex.Store({
+ state: {
+ selectedSuite: suite,
+ },
+ getters,
+ });
+
+ wrapper = shallowMount(SuiteTable, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('should not render', () => {
+ beforeEach(() => createComponent([]));
+
+ it('a table when there are no test cases', () => {
+ expect(noCasesMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('when a test suite is supplied', () => {
+ beforeEach(() => createComponent());
+
+ it('renders the correct number of rows', () => {
+ expect(allCaseRows().length).toBe(testCases.length);
+ });
+
+ it('renders the failed tests first', () => {
+ const failedCaseNames = testCases
+ .filter(x => x.status === TestStatus.FAILED)
+ .map(x => x.name);
+
+ const skippedCaseNames = testCases
+ .filter(x => x.status === TestStatus.SKIPPED)
+ .map(x => x.name);
+
+ expect(findCaseRowAtIndex(0).text()).toContain(failedCaseNames[0]);
+ expect(findCaseRowAtIndex(1).text()).toContain(failedCaseNames[1]);
+ expect(findCaseRowAtIndex(2).text()).toContain(skippedCaseNames[0]);
+ });
+
+ it('renders the correct icon for each status', () => {
+ const failedTest = testCases.findIndex(x => x.status === TestStatus.FAILED);
+ const skippedTest = testCases.findIndex(x => x.status === TestStatus.SKIPPED);
+ const successTest = testCases.findIndex(x => x.status === TestStatus.SUCCESS);
+
+ const failedRow = findCaseRowAtIndex(failedTest);
+ const skippedRow = findCaseRowAtIndex(skippedTest);
+ const successRow = findCaseRowAtIndex(successTest);
+
+ expect(findIconForRow(failedRow, TestStatus.FAILED).exists()).toBe(true);
+ expect(findIconForRow(skippedRow, TestStatus.SKIPPED).exists()).toBe(true);
+ expect(findIconForRow(successRow, TestStatus.SUCCESS).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
new file mode 100644
index 00000000000..19a7755dbdc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -0,0 +1,78 @@
+import Summary from '~/pipelines/components/test_reports/test_summary.vue';
+import { mount } from '@vue/test-utils';
+import { testSuites } from './mock_data';
+
+describe('Test reports summary', () => {
+ let wrapper;
+
+ const backButton = () => wrapper.find('.js-back-button');
+ const totalTests = () => wrapper.find('.js-total-tests');
+ const failedTests = () => wrapper.find('.js-failed-tests');
+ const erroredTests = () => wrapper.find('.js-errored-tests');
+ const successRate = () => wrapper.find('.js-success-rate');
+ const duration = () => wrapper.find('.js-duration');
+
+ const defaultProps = {
+ report: testSuites[0],
+ showBack: false,
+ };
+
+ const createComponent = props => {
+ wrapper = mount(Summary, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('should not render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('a back button by default', () => {
+ expect(backButton().exists()).toBe(false);
+ });
+ });
+
+ describe('should render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('a back button and emit on-back-click event', () => {
+ createComponent({
+ showBack: true,
+ });
+
+ expect(backButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when a report is supplied', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the correct total', () => {
+ expect(totalTests().text()).toBe('4 jobs');
+ });
+
+ it('displays the correct failure count', () => {
+ expect(failedTests().text()).toBe('2 failures');
+ });
+
+ it('displays the correct error count', () => {
+ expect(erroredTests().text()).toBe('0 errors');
+ });
+
+ it('calculates and displays percentages correctly', () => {
+ expect(successRate().text()).toBe('50% success rate');
+ });
+
+ it('displays the correctly formatted duration', () => {
+ expect(duration().text()).toBe('00:01:00');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
new file mode 100644
index 00000000000..e7599d5cdbc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -0,0 +1,54 @@
+import Vuex from 'vuex';
+import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { testReports, testReportsWithNoSuites } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Test reports summary table', () => {
+ let wrapper;
+ let store;
+
+ const allSuitesRows = () => wrapper.findAll('.js-suite-row');
+ const noSuitesToShow = () => wrapper.find('.js-no-tests-suites');
+
+ const defaultProps = {
+ testReports,
+ };
+
+ const createComponent = (reports = null) => {
+ store = new Vuex.Store({
+ state: {
+ testReports: reports || testReports,
+ },
+ getters,
+ });
+
+ wrapper = mount(SummaryTable, {
+ propsData: defaultProps,
+ store,
+ localVue,
+ });
+ };
+
+ describe('when test reports are supplied', () => {
+ beforeEach(() => createComponent());
+
+ it('renders the correct number of rows', () => {
+ expect(noSuitesToShow().exists()).toBe(false);
+ expect(allSuitesRows().length).toBe(testReports.test_suites.length);
+ });
+ });
+
+ describe('when there are no test suites', () => {
+ beforeEach(() => {
+ createComponent({ testReportsWithNoSuites });
+ });
+
+ it('displays the no suites to show message', () => {
+ expect(noSuitesToShow().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 827927e6d9a..0c5a7370fca 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
import Table from '~/repository/components/table/index.vue';
let vm;
@@ -35,7 +35,7 @@ describe('Repository table component', () => {
vm.setData({ ref });
- expect(vm.find('caption').text()).toEqual(
+ expect(vm.find('.table').attributes('aria-label')).toEqual(
`Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
);
});
@@ -45,7 +45,7 @@ describe('Repository table component', () => {
vm.setData({ isLoadingFiles: true });
- expect(vm.find(GlLoadingIcon).isVisible()).toBe(true);
+ expect(vm.find(GlSkeletonLoading).exists()).toBe(true);
});
describe('normalizeData', () => {
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 49f92f14559..449eee7a371 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -142,7 +142,6 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:transaction_open?).and_return(false)
allow(model).to receive(:index_exists?).and_return(true)
allow(model).to receive(:disable_statement_timeout).and_call_original
- allow(model).to receive(:supports_drop_index_concurrently?).and_return(true)
end
describe 'by column name' do
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
index aab6fbcbbd1..5b1a17e734d 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
@@ -164,15 +164,6 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do
end
it_behaves_like 'has prometheus service', 'http://localhost:9090'
-
- it 'does not overwrite the existing whitelist' do
- application_setting.outbound_local_requests_whitelist = ['example.com']
-
- expect(result[:status]).to eq(:success)
- expect(application_setting.outbound_local_requests_whitelist).to contain_exactly(
- 'example.com', 'localhost'
- )
- end
end
context 'with non default prometheus address' do
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index e5ac6ca65d6..bc22818ede7 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -65,6 +65,37 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end
end
end
+
+ context 'with self-monitoring project and internal Prometheus' do
+ before do
+ service.api_url = 'http://localhost:9090'
+
+ stub_application_setting(instance_administration_project_id: project.id)
+ stub_config(prometheus: { enable: true, listen_address: 'localhost:9090' })
+ end
+
+ it 'allows self-monitoring project to connect to internal Prometheus' do
+ aggregate_failures do
+ ['127.0.0.1', '192.168.2.3'].each do |url|
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
+
+ expect(service.can_query?).to be true
+ end
+ end
+ end
+
+ it 'does not allow self-monitoring project to connect to other local URLs' do
+ service.api_url = 'http://localhost:8000'
+
+ aggregate_failures do
+ ['127.0.0.1', '192.168.2.3'].each do |url|
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
+
+ expect(service.can_query?).to be false
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index fa24958a79f..9cea4866c4c 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1089,6 +1089,20 @@ describe API::Commits do
expect(json_response.first.keys).to include 'diff'
end
+ context 'when hard limits are lower than the number of files' do
+ before do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 1)
+ end
+
+ it 'respects the limit' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to be <= 1
+ end
+ end
+
context 'when ref does not exist' do
let(:commit_id) { 'unknown' }