Commit 4c464055 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 791785af
...@@ -75,7 +75,7 @@ graphql-reference-verify: ...@@ -75,7 +75,7 @@ graphql-reference-verify:
- .default-cache - .default-cache
- .default-only - .default-only
- .default-before_script - .default-before_script
- .only:changes-graphql - .only:changes-code-backstage-qa
- .use-pg9 - .use-pg9
stage: test stage: test
needs: ["setup-test-env"] needs: ["setup-test-env"]
......
...@@ -93,6 +93,7 @@ ...@@ -93,6 +93,7 @@
- "config.ru" - "config.ru"
- "{package.json,yarn.lock}" - "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
.backstage-patterns: &backstage-patterns .backstage-patterns: &backstage-patterns
- "Dangerfile" - "Dangerfile"
...@@ -111,11 +112,6 @@ ...@@ -111,11 +112,6 @@
- "doc/**/*" - "doc/**/*"
- ".markdownlint.json" - ".markdownlint.json"
.graphql-patterns: &graphql-patterns
- "{,ee/}app/graphql/**/*"
- "{,ee/}lib/gitlab/graphql/**/*"
- "doc/api/graphql/**/*"
.only:changes-code: .only:changes-code:
only: only:
changes: *code-patterns changes: *code-patterns
...@@ -128,10 +124,6 @@ ...@@ -128,10 +124,6 @@
only: only:
changes: *docs-patterns changes: *docs-patterns
.only:changes-graphql:
only:
changes: *graphql-patterns
.only:changes-code-backstage: .only:changes-code-backstage:
only: only:
changes: changes:
...@@ -147,6 +139,7 @@ ...@@ -147,6 +139,7 @@
- "config.ru" - "config.ru"
- "{package.json,yarn.lock}" - "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
# Backstage changes # Backstage changes
- "Dangerfile" - "Dangerfile"
- "danger/**/*" - "danger/**/*"
...@@ -170,6 +163,7 @@ ...@@ -170,6 +163,7 @@
- "config.ru" - "config.ru"
- "{package.json,yarn.lock}" - "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
# QA changes # QA changes
- ".dockerignore" - ".dockerignore"
- "qa/**/*" - "qa/**/*"
...@@ -189,6 +183,7 @@ ...@@ -189,6 +183,7 @@
- "config.ru" - "config.ru"
- "{package.json,yarn.lock}" - "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/**/*"
# Backstage changes # Backstage changes
- "Dangerfile" - "Dangerfile"
- "danger/**/*" - "danger/**/*"
......
...@@ -17,14 +17,7 @@ function MergeRequest(opts) { ...@@ -17,14 +17,7 @@ function MergeRequest(opts) {
this.opts = opts != null ? opts : {}; this.opts = opts != null ? opts : {};
this.submitNoteForm = this.submitNoteForm.bind(this); this.submitNoteForm = this.submitNoteForm.bind(this);
this.$el = $('.merge-request'); this.$el = $('.merge-request');
this.$('.show-all-commits').on( this.$('.show-all-commits').on('click', () => this.showAllCommits());
'click',
(function(_this) {
return function() {
return _this.showAllCommits();
};
})(this),
);
this.initTabs(); this.initTabs();
this.initMRBtnListeners(); this.initMRBtnListeners();
......
// /test_report is an alias for show
import '../show/index';
<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>
<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>
<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>
<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>
export const CANCEL_REQUEST = 'CANCEL_REQUEST'; export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300; export const LAYOUT_CHANGE_DELAY = 300;
export const TestStatus = {
FAILED: 'failed',
SKIPPED: 'skipped',
SUCCESS: 'success',
};
...@@ -7,6 +7,8 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; ...@@ -7,6 +7,8 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator'; import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue'; import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
import TestReports from './components/test_reports/test_reports.vue';
import testReportsStore from './stores/test_reports';
Vue.use(Translate); Vue.use(Translate);
...@@ -17,7 +19,7 @@ export default () => { ...@@ -17,7 +19,7 @@ export default () => {
mediator.fetchPipeline(); mediator.fetchPipeline();
// eslint-disable-next-line // eslint-disable-next-line no-new
new Vue({ new Vue({
el: '#js-pipeline-graph-vue', el: '#js-pipeline-graph-vue',
components: { components: {
...@@ -47,7 +49,7 @@ export default () => { ...@@ -47,7 +49,7 @@ export default () => {
}, },
}); });
// eslint-disable-next-line // eslint-disable-next-line no-new
new Vue({ new Vue({
el: '#js-pipeline-header-vue', el: '#js-pipeline-header-vue',
components: { components: {
...@@ -81,4 +83,23 @@ export default () => { ...@@ -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');
},
});
}
}; };
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 () => {};
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 () => {};
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,
});
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';
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 });
},
};
export default () => ({
endpoint: '',
testReports: {},
selectedSuite: {},
isLoading: false,
});
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;
}
};
...@@ -128,7 +128,7 @@ export default { ...@@ -128,7 +128,7 @@ export default {
<div class="ci-status-link"> <div class="ci-status-link">
<gl-link <gl-link
v-if="commit.latestPipeline" v-if="commit.latestPipeline"
v-gl-tooltip v-gl-tooltip.left
:href="commit.latestPipeline.detailedStatus.detailsPath" :href="commit.latestPipeline.detailedStatus.detailsPath"
:title="statusTitle" :title="statusTitle"
class="js-commit-pipeline" class="js-commit-pipeline"
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlSkeletonLoading } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { sprintf, __ } from '../../../locale'; import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref'; import getRefMixin from '../../mixins/get_ref';
...@@ -13,7 +13,7 @@ const PAGE_SIZE = 100; ...@@ -13,7 +13,7 @@ const PAGE_SIZE = 100;
export default { export default {
components: { components: {
GlLoadingIcon, GlSkeletonLoading,
TableHeader, TableHeader,
TableRow, TableRow,
ParentRow, ParentRow,
...@@ -44,6 +44,15 @@ export default { ...@@ -44,6 +44,15 @@ export default {
}, },
computed: { computed: {
tableCaption() { 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( return sprintf(
__('Files, directories, and submodules in the path %{path} for commit reference %{ref}'), __('Files, directories, and submodules in the path %{path} for commit reference %{ref}'),
{ path: this.path, ref: this.ref }, { path: this.path, ref: this.ref },
...@@ -117,12 +126,7 @@ export default { ...@@ -117,12 +126,7 @@ export default {
<template> <template>
<div class="tree-content-holder"> <div class="tree-content-holder">
<div class="table-holder bordered-box"> <div class="table-holder bordered-box">
<table class="table tree-table qa-file-tree" aria-live="polite"> <table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite">
<caption class="sr-only">
{{
tableCaption
}}
</caption>
<table-header v-once /> <table-header v-once />
<tbody> <tbody>
<parent-row v-show="showParentRow" :commit-ref="ref" :path="path" /> <parent-row v-show="showParentRow" :commit-ref="ref" :path="path" />
...@@ -141,9 +145,15 @@ export default { ...@@ -141,9 +145,15 @@ export default {
:lfs-oid="entry.lfsOid" :lfs-oid="entry.lfsOid"
/> />
</template> </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> </tbody>
</table> </table>
<gl-loading-icon v-show="isLoadingFiles" class="my-3" size="md" />
</div> </div>
</div> </div>
</template> </template>
...@@ -124,13 +124,18 @@ export default { ...@@ -124,13 +124,18 @@ export default {
</template> </template>
</td> </td>
<td class="d-none d-sm-table-cell tree-commit"> <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 }} {{ commit.message }}
</gl-link> </gl-link>
<gl-skeleton-loading v-else :lines="1" class="h-auto" /> <gl-skeleton-loading v-else :lines="1" class="h-auto" />
</td> </td>
<td class="tree-time-ago text-right"> <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" /> <gl-skeleton-loading v-else :lines="1" class="ml-auto h-auto w-50" />
</td> </td>
</tr> </tr>
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
</script> </script>
<template> <template>
<time <time
v-gl-tooltip="{ placement: tooltipPlacement }" v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass" :class="cssClass"
:title="tooltipTitle(time)" :title="tooltipTitle(time)"
v-text="timeFormated(time)" v-text="timeFormated(time)"
......
...@@ -11,3 +11,27 @@ ...@@ -11,3 +11,27 @@
.fade-leave-to { .fade-leave-to {
opacity: 0; 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);
}
...@@ -1082,3 +1082,25 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -1082,3 +1082,25 @@ button.mini-pipeline-graph-dropdown-toggle {
.legend-success { .legend-success {
color: $green-500; 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;
}
...@@ -12,6 +12,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -12,6 +12,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do before_action do
push_frontend_feature_flag(:hide_dismissed_vulnerabilities) push_frontend_feature_flag(:hide_dismissed_vulnerabilities)
push_frontend_feature_flag(:junit_pipeline_view)
end end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show] around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
......
...@@ -7,8 +7,15 @@ class PrometheusService < MonitoringService ...@@ -7,8 +7,15 @@ class PrometheusService < MonitoringService
prop_accessor :api_url prop_accessor :api_url
boolean_accessor :manual_configuration 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 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 end
before_save :synchronize_service_state before_save :synchronize_service_state
...@@ -82,12 +89,28 @@ class PrometheusService < MonitoringService ...@@ -82,12 +89,28 @@ class PrometheusService < MonitoringService
project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? } project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
end end
def allow_local_api_url?
self_monitoring_project? && internal_prometheus_url?
end
private 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? def should_return_client?
api_url.present? && manual_configuration? && active? && valid? api_url.present? && manual_configuration? && active? && valid?
end end
def current_settings
Gitlab::CurrentSettings.current_application_settings
end
def synchronize_service_state def synchronize_service_state
self.active = prometheus_available? || manual_configuration? self.active = prometheus_available? || manual_configuration?
......
- test_reports_enabled = Feature.enabled?(:junit_pipeline_view)
.tabs-holder .tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link %li.js-pipeline-tab-link
...@@ -12,6 +14,11 @@ ...@@ -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 = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _('Failed Jobs') = _('Failed Jobs')
%span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count %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 = render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content .tab-content
...@@ -71,4 +78,7 @@ ...@@ -71,4 +78,7 @@
%pre.build-trace.build-trace-rounded %pre.build-trace.build-trace-rounded
%code.bash.js-build-output %code.bash.js-build-output
= build_summary(build) = build_summary(build)
#js-tab-tests.tab-pane
#js-pipeline-tests-detail
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
...@@ -20,4 +20,5 @@ ...@@ -20,4 +20,5 @@
- else - else
= render "projects/pipelines/with_tabs", pipeline: @pipeline = 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) } }
---
title: Added Tests tab to pipeline detail that contains a UI for browsing test reports
produced by JUnit
merge_request: 18255
author:
type: added
---
title: Remove IIFEs from merge_request.js
merge_request: 19294
author: minghuan lei
type: other
---
title: Show correct total number of commit diff's changes
merge_request: 19424
author:
type: fixed
---
title: Fix api docs for deleting project cluster
merge_request: 19558
author:
type: other
---
title: Upgrade to Gitaly v1.71.0
merge_request: 19611
author:
type: changed
...@@ -20,10 +20,6 @@ class UsersNameLowerIndex < ActiveRecord::Migration[4.2] ...@@ -20,10 +20,6 @@ class UsersNameLowerIndex < ActiveRecord::Migration[4.2]
def down def down
return unless Gitlab::Database.postgresql? return unless Gitlab::Database.postgresql?
if supports_drop_index_concurrently? execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
else
execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
end
end end
end end
...@@ -22,11 +22,7 @@ class ProjectNameLowerIndex < ActiveRecord::Migration[4.2] ...@@ -22,11 +22,7 @@ class ProjectNameLowerIndex < ActiveRecord::Migration[4.2]
return unless Gitlab::Database.postgresql? return unless Gitlab::Database.postgresql?
disable_statement_timeout do disable_statement_timeout do
if supports_drop_index_concurrently? execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
else
execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
end
end end
end end
end end
...@@ -3230,6 +3230,51 @@ type MergeRequestPermissions { ...@@ -3230,6 +3230,51 @@ type MergeRequestPermissions {
updateMergeRequest: Boolean! updateMergeRequest: Boolean!
} }
"""
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 Autogenerated input type of MergeRequestSetWip
""" """
...@@ -3313,6 +3358,11 @@ type Milestone { ...@@ -3313,6 +3358,11 @@ type Milestone {
""" """
dueDate: Time dueDate: Time
"""
ID of the milestone
"""
id: ID!
""" """
Timestamp of the milestone start date Timestamp of the milestone start date
""" """
...@@ -3360,6 +3410,7 @@ type Mutation { ...@@ -3360,6 +3410,7 @@ type Mutation {
destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
......
...@@ -7740,6 +7740,24 @@ ...@@ -7740,6 +7740,24 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "startDate",
"description": "Timestamp of the milestone start date", "description": "Timestamp of the milestone start date",
...@@ -14458,6 +14476,33 @@ ...@@ -14458,6 +14476,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "mergeRequestSetWip",
"description": null, "description": null,
...@@ -15088,6 +15133,132 @@ ...@@ -15088,6 +15133,132 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"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", "kind": "OBJECT",
"name": "MergeRequestSetWipPayload", "name": "MergeRequestSetWipPayload",
......
...@@ -382,5 +382,5 @@ Parameters: ...@@ -382,5 +382,5 @@ Parameters:
Example request: Example request:
```bash ```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
``` ```
...@@ -59,10 +59,10 @@ graph TB ...@@ -59,10 +59,10 @@ graph TB
Unicorn --> Gitaly Unicorn --> Gitaly
Sidekiq --> Redis Sidekiq --> Redis
Sidekiq --> PgBouncer Sidekiq --> PgBouncer
Sidekiq --> Gitaly
GitLabWorkhorse[GitLab Workhorse] --> Unicorn GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis GitLabWorkhorse --> Redis
GitLabWorkhorse --> Gitaly GitLabWorkhorse --> Gitaly
Gitaly --> Redis
NGINX --> GitLabWorkhorse NGINX --> GitLabWorkhorse
NGINX -- TCP 8090 --> GitLabPages[GitLab Pages] NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
NGINX --> Grafana[Grafana] NGINX --> Grafana[Grafana]
......
...@@ -55,7 +55,7 @@ The following table depicts the various user permission levels in a project. ...@@ -55,7 +55,7 @@ The following table depicts the various user permission levels in a project.
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View GitLab Pages protected by [access control](project/pages/introduction.md#gitlab-pages-access-control-core) | ✓ | ✓ | ✓ | ✓ | ✓ | | 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 list of jobs | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ | | See a job log | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ | | Download and browse job artifacts | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
...@@ -73,7 +73,7 @@ The following table depicts the various user permission levels in a project. ...@@ -73,7 +73,7 @@ The following table depicts the various user permission levels in a project.
| See a commit status | | ✓ | ✓ | ✓ | ✓ | | See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ | | See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View project statistics | | ✓ | ✓ | ✓ | ✓ | | View project statistics | | ✓ | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ | | 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)** | | ✓ | ✓ | ✓ | ✓ | | 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. ...@@ -83,7 +83,7 @@ The following table depicts the various user permission levels in a project.
| Push to non-protected branches | | | ✓ | ✓ | ✓ | | Push to non-protected branches | | | ✓ | ✓ | ✓ |
| Force push to non-protected branches | | | ✓ | ✓ | ✓ | | Force push to non-protected branches | | | ✓ | ✓ | ✓ |
| Remove non-protected branches | | | ✓ | ✓ | ✓ | | Remove non-protected branches | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ | | Create new merge request | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Assign merge requests | | | ✓ | ✓ | ✓ | | Assign merge requests | | | ✓ | ✓ | ✓ |
| Label merge requests | | | ✓ | ✓ | ✓ | | Label merge requests | | | ✓ | ✓ | ✓ |
| Lock merge request threads | | | ✓ | ✓ | ✓ | | Lock merge request threads | | | ✓ | ✓ | ✓ |
......
...@@ -5,7 +5,8 @@ description: "Automatic Let's Encrypt SSL certificates for GitLab Pages." ...@@ -5,7 +5,8 @@ description: "Automatic Let's Encrypt SSL certificates for GitLab Pages."
# GitLab Pages integration with Let's Encrypt # 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 The GitLab Pages integration with Let's Encrypt (LE) allows you
to use LE certificates for your Pages website with custom domains to use LE certificates for your Pages website with custom domains
...@@ -16,19 +17,11 @@ GitLab does it for you, out-of-the-box. ...@@ -16,19 +17,11 @@ GitLab does it for you, out-of-the-box.
open source Certificate Authority. open source Certificate Authority.
CAUTION: **Caution:** CAUTION: **Caution:**
This feature is in **beta** and might present bugs and UX issues 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).
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).
## Requirements ## 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 - Created a [project](../getting_started_part_two.md) in GitLab
containing your website's source code. containing your website's source code.
...@@ -36,7 +29,7 @@ Before you can enable automatic provisioning of a SSL certificate for your domai ...@@ -36,7 +29,7 @@ Before you can enable automatic provisioning of a SSL certificate for your domai
pointing it to your Pages website. pointing it to your Pages website.
- [Added your domain to your Pages project](index.md#1-add-a-custom-domain-to-pages) - [Added your domain to your Pages project](index.md#1-add-a-custom-domain-to-pages)
and verified your ownership. 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:** NOTE: **Note:**
GitLab's Let's Encrypt integration is enabled and available on GitLab.com. 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 ...@@ -45,7 +38,7 @@ For **self-managed** GitLab instances, make sure your administrator has
## Enabling Let's Encrypt integration for your custom domain ## 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. Navigate to your project's **Settings > Pages**.
1. Find your domain and click **Details**. 1. Find your domain and click **Details**.
......
...@@ -118,10 +118,11 @@ all matching branches: ...@@ -118,10 +118,11 @@ all matching branches:
When a protected branch or wildcard protected branches are set to 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), [**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 Developers (and users with higher [permission levels](../permissions.md)) are
to create a new protected branch, but only via the UI or through the API (to avoid allowed to create a new protected branch as long as they are
creating protected branches accidentally from the command line or from a Git [**Allowed to merge**](#using-the-allowed-to-merge-and-allowed-to-push-settings).
client application). 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: To create a new branch through the user interface:
......
...@@ -169,7 +169,7 @@ module API ...@@ -169,7 +169,7 @@ module API
not_found! 'Commit' unless commit 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 present paginate(raw_diffs), with: Entities::Diff
end end
......
...@@ -108,9 +108,7 @@ module Gitlab ...@@ -108,9 +108,7 @@ module Gitlab
'in the body of your migration class' 'in the body of your migration class'
end end
if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently })
options = options.merge({ algorithm: :concurrently })
end
unless index_exists?(table_name, column_name, options) 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 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 ...@@ -136,9 +134,7 @@ module Gitlab
'in the body of your migration class' 'in the body of your migration class'
end end
if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently })
options = options.merge({ algorithm: :concurrently })
end
unless index_exists_by_name?(table_name, index_name) 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 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 ...@@ -150,13 +146,6 @@ module Gitlab
end end
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. # Adds a foreign key with only minimal locking on the tables involved.
# #
# This method only requires minimal locking # This method only requires minimal locking
......
...@@ -21,7 +21,6 @@ module Gitlab ...@@ -21,7 +21,6 @@ module Gitlab
:create_project, :create_project,
:save_project_id, :save_project_id,
:add_group_members, :add_group_members,
:add_to_whitelist,
:add_prometheus_manual_configuration :add_prometheus_manual_configuration
def initialize def initialize
...@@ -126,28 +125,6 @@ module Gitlab ...@@ -126,28 +125,6 @@ module Gitlab
end end
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) def add_prometheus_manual_configuration(result)
return success(result) unless prometheus_enabled? return success(result) unless prometheus_enabled?
return success(result) unless prometheus_listen_address.present? return success(result) unless prometheus_listen_address.present?
......
...@@ -3266,6 +3266,9 @@ msgstr "" ...@@ -3266,6 +3266,9 @@ msgstr ""
msgid "CiVariable|Validation failed" msgid "CiVariable|Validation failed"
msgstr "" msgstr ""
msgid "Class"
msgstr ""
msgid "Classification Label (optional)" msgid "Classification Label (optional)"
msgstr "" msgstr ""
...@@ -4632,9 +4635,6 @@ msgstr "" ...@@ -4632,9 +4635,6 @@ msgstr ""
msgid "Could not add admins as members" msgid "Could not add admins as members"
msgstr "" msgstr ""
msgid "Could not add prometheus URL to whitelist"
msgstr ""
msgid "Could not authorize chat nickname. Try again!" msgid "Could not authorize chat nickname. Try again!"
msgstr "" msgstr ""
...@@ -5876,6 +5876,9 @@ msgstr "" ...@@ -5876,6 +5876,9 @@ msgstr ""
msgid "Due date" msgid "Due date"
msgstr "" msgstr ""
msgid "Duration"
msgstr ""
msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below." msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
msgstr "" msgstr ""
...@@ -10013,6 +10016,9 @@ msgstr "" ...@@ -10013,6 +10016,9 @@ msgstr ""
msgid "Loading contribution stats for group members" msgid "Loading contribution stats for group members"
msgstr "" 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." msgid "Loading functions timed out. Please reload the page to try again."
msgstr "" msgstr ""
...@@ -11738,6 +11744,9 @@ msgstr "" ...@@ -11738,6 +11744,9 @@ msgstr ""
msgid "Part of merge request changes" msgid "Part of merge request changes"
msgstr "" msgstr ""
msgid "Passed"
msgstr ""
msgid "Password" msgid "Password"
msgstr "" msgstr ""
...@@ -13289,9 +13298,6 @@ 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}" msgid "ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}"
msgstr "" msgstr ""
msgid "Prometheus listen_address in config/gitlab.yml is not a valid URI"
msgstr ""
msgid "PrometheusAlerts|%{count} alerts applied" msgid "PrometheusAlerts|%{count} alerts applied"
msgstr "" msgstr ""
...@@ -15504,6 +15510,9 @@ msgstr "" ...@@ -15504,6 +15510,9 @@ msgstr ""
msgid "Skip this for now" msgid "Skip this for now"
msgstr "" msgstr ""
msgid "Skipped"
msgstr ""
msgid "Slack application" msgid "Slack application"
msgstr "" msgstr ""
...@@ -16305,6 +16314,12 @@ msgstr "" ...@@ -16305,6 +16314,12 @@ msgstr ""
msgid "Suggestions:" msgid "Suggestions:"
msgstr "" msgstr ""
msgid "Suite"
msgstr ""
msgid "Summary"
msgstr ""
msgid "Sunday" msgid "Sunday"
msgstr "" msgstr ""
...@@ -16533,6 +16548,39 @@ msgstr "" ...@@ -16533,6 +16548,39 @@ msgstr ""
msgid "TestHooks|Ensure the wiki is enabled and has pages." msgid "TestHooks|Ensure the wiki is enabled and has pages."
msgstr "" 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." msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly."
msgstr "" msgstr ""
...@@ -17718,6 +17766,9 @@ msgstr "" ...@@ -17718,6 +17766,9 @@ msgstr ""
msgid "Total: %{total}" msgid "Total: %{total}"
msgstr "" msgstr ""
msgid "Trace"
msgstr ""
msgid "Tracing" msgid "Tracing"
msgstr "" msgstr ""
......
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,
};
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,
);
});
});
});
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([]);
});
});
});
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);
});
});
});
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();
});
});
});
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);
});
});
});
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');
});
});
});
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);
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlSkeletonLoading } from '@gitlab/ui';
import Table from '~/repository/components/table/index.vue'; import Table from '~/repository/components/table/index.vue';
let vm; let vm;
...@@ -35,7 +35,7 @@ describe('Repository table component', () => { ...@@ -35,7 +35,7 @@ describe('Repository table component', () => {
vm.setData({ ref }); 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}`, `Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
); );
}); });
...@@ -45,7 +45,7 @@ describe('Repository table component', () => { ...@@ -45,7 +45,7 @@ describe('Repository table component', () => {
vm.setData({ isLoadingFiles: true }); vm.setData({ isLoadingFiles: true });
expect(vm.find(GlLoadingIcon).isVisible()).toBe(true); expect(vm.find(GlSkeletonLoading).exists()).toBe(true);
}); });
describe('normalizeData', () => { describe('normalizeData', () => {
......
...@@ -142,7 +142,6 @@ describe Gitlab::Database::MigrationHelpers do ...@@ -142,7 +142,6 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:transaction_open?).and_return(false) allow(model).to receive(:transaction_open?).and_return(false)
allow(model).to receive(:index_exists?).and_return(true) allow(model).to receive(:index_exists?).and_return(true)
allow(model).to receive(:disable_statement_timeout).and_call_original allow(model).to receive(:disable_statement_timeout).and_call_original
allow(model).to receive(:supports_drop_index_concurrently?).and_return(true)
end end
describe 'by column name' do describe 'by column name' do
......
...@@ -164,15 +164,6 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do ...@@ -164,15 +164,6 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do
end end
it_behaves_like 'has prometheus service', 'http://localhost:9090' 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 end
context 'with non default prometheus address' do context 'with non default prometheus address' do
......
...@@ -65,6 +65,37 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do ...@@ -65,6 +65,37 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end end
end 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
end end
......
...@@ -1089,6 +1089,20 @@ describe API::Commits do ...@@ -1089,6 +1089,20 @@ describe API::Commits do
expect(json_response.first.keys).to include 'diff' expect(json_response.first.keys).to include 'diff'
end 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 context 'when ref does not exist' do
let(:commit_id) { 'unknown' } let(:commit_id) { 'unknown' }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment