diff options
author | John Jarvis <jarv@gitlab.com> | 2019-04-08 11:50:19 +0300 |
---|---|---|
committer | John Jarvis <jarv@gitlab.com> | 2019-04-08 11:50:19 +0300 |
commit | b8ed65619075d3b5892a3ec16e562518d5285a75 (patch) | |
tree | f6a31b4819339aeb170f9df5886afab23638f172 | |
parent | 9ed8b61b8692dbafe189fce68237efcc9345ee9f (diff) | |
parent | 435c492bac54f588b92e733b632edd4149a6db37 (diff) |
Merge branch 'master' into 11-10-stable
645 files changed, 8976 insertions, 3584 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e2ed66cfd..c99e487d04b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.9.6 (2019-04-04) + +### Fixed (3 changes) + +- Force to recreate all MR diffs on import. !26480 +- Fix API /project/:id/branches not returning correct merge status. !26785 +- Avoid excessive recursive calls with Rugged TreeEntries. !26813 + +### Performance (1 change) + +- Force a full GC after importing a project. !26803 + + ## 11.9.5 (2019-04-03) ### Fixed (3 changes) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97dbe2f512b..82e16b4fbf4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ## Contributing Documentation has been moved As of July 2018, all the documentation for contributing to the GitLab project has been moved to a new location. -[view the new documentation](doc/development/contributing/index.md) to find the latest information. +[View the new documentation](doc/development/contributing/index.md) to find the latest information. ## Contribute to GitLab diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 34aae156b19..7aa332e4163 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.31.0 +1.33.0 @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'rails', '5.0.7.1' +gem 'rails', '5.0.7.2' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Improves copy-on-write performance for MRI @@ -139,10 +139,7 @@ gem 'icalendar' gem 'diffy', '~> 3.1.0' # Application server -# The 2.0.6 version of rack requires monkeypatch to be present in -# `config.ru`. This can be removed once a new update for Rack -# is available that contains https://github.com/rack/rack/pull/1201. -gem 'rack', '2.0.6' +gem 'rack', '~> 2.0.7' group :unicorn do gem 'unicorn', '~> 5.4.1' diff --git a/Gemfile.lock b/Gemfile.lock index e8053ada8b2..b522aa85b39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM RedCloth (4.3.2) abstract_type (0.0.7) ace-rails-ap (4.1.2) - actioncable (5.0.7.1) - actionpack (= 5.0.7.1) + actioncable (5.0.7.2) + actionpack (= 5.0.7.2) nio4r (>= 1.2, < 3.0) websocket-driver (~> 0.6.1) - actionmailer (5.0.7.1) - actionpack (= 5.0.7.1) - actionview (= 5.0.7.1) - activejob (= 5.0.7.1) + actionmailer (5.0.7.2) + actionpack (= 5.0.7.2) + actionview (= 5.0.7.2) + activejob (= 5.0.7.2) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.7.1) - actionview (= 5.0.7.1) - activesupport (= 5.0.7.1) + actionpack (5.0.7.2) + actionview (= 5.0.7.2) + activesupport (= 5.0.7.2) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.7.1) - activesupport (= 5.0.7.1) + actionview (5.0.7.2) + activesupport (= 5.0.7.2) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.0.7.1) - activesupport (= 5.0.7.1) + activejob (5.0.7.2) + activesupport (= 5.0.7.2) globalid (>= 0.3.6) - activemodel (5.0.7.1) - activesupport (= 5.0.7.1) - activerecord (5.0.7.1) - activemodel (= 5.0.7.1) - activesupport (= 5.0.7.1) + activemodel (5.0.7.2) + activesupport (= 5.0.7.2) + activerecord (5.0.7.2) + activemodel (= 5.0.7.2) + activesupport (= 5.0.7.2) arel (~> 7.0) activerecord_sane_schema_dumper (1.0) rails (>= 5, < 6) - activesupport (5.0.7.1) + activesupport (5.0.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -298,7 +298,7 @@ GEM omniauth (~> 1.3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) rubyntlm (~> 0.5) - globalid (0.4.1) + globalid (0.4.2) activesupport (>= 4.2.0) gon (6.2.0) actionpack (>= 3.0) @@ -385,7 +385,7 @@ GEM mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.2.0) + i18n (1.6.0) concurrent-ruby (~> 1.0) icalendar (2.4.1) ice_nine (0.11.2) @@ -617,7 +617,7 @@ GEM puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) raabro (1.1.6) - rack (2.0.6) + rack (2.0.7) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) @@ -635,17 +635,17 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (5.0.7.1) - actioncable (= 5.0.7.1) - actionmailer (= 5.0.7.1) - actionpack (= 5.0.7.1) - actionview (= 5.0.7.1) - activejob (= 5.0.7.1) - activemodel (= 5.0.7.1) - activerecord (= 5.0.7.1) - activesupport (= 5.0.7.1) + rails (5.0.7.2) + actioncable (= 5.0.7.2) + actionmailer (= 5.0.7.2) + actionpack (= 5.0.7.2) + actionview (= 5.0.7.2) + activejob (= 5.0.7.2) + activemodel (= 5.0.7.2) + activerecord (= 5.0.7.2) + activesupport (= 5.0.7.2) bundler (>= 1.3.0) - railties (= 5.0.7.1) + railties (= 5.0.7.2) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.2) actionpack (~> 5.x, >= 5.0.1) @@ -661,9 +661,9 @@ GEM rails-i18n (5.1.1) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) - railties (5.0.7.1) - actionpack (= 5.0.7.1) - activesupport (= 5.0.7.1) + railties (5.0.7.2) + actionpack (= 5.0.7.2) + activesupport (= 5.0.7.2) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -1100,12 +1100,12 @@ DEPENDENCIES pry-rails (~> 0.3.4) puma (~> 3.12) puma_worker_killer - rack (= 2.0.6) + rack (~> 2.0.7) rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.9.3) rack-proxy (~> 0.6.0) - rails (= 5.0.7.1) + rails (= 5.0.7.2) rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/avatar_picker.js index dcda625f587..d38e0b4abaa 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/avatar_picker.js @@ -1,11 +1,12 @@ import $ from 'jquery'; -export default function groupAvatar() { - $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() { +export default function initAvatarPicker() { + $('.js-choose-avatar-button').on('click', function onClickAvatar() { const form = $(this).closest('form'); - return form.find('.js-group-avatar-input').click(); + return form.find('.js-avatar-input').click(); }); - $('.js-group-avatar-input').on('change', function onChangeAvatarInput() { + + $('.js-avatar-input').on('change', function onChangeAvatarInput() { const form = $(this).closest('form'); const filename = $(this) .val() diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 90ab3a76342..206573dd444 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import { GlTooltipDirective } from '@gitlab/ui'; import { sprintf, __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -92,6 +93,9 @@ export default { const { referencePath, groupId } = this.issue; return !groupId ? referencePath.split('#')[0] : null; }, + orderedLabels() { + return _.sortBy(this.issue.labels, 'title'); + }, }, methods: { isIndexLessThanlimit(index) { @@ -176,7 +180,7 @@ export default { </div> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <button - v-for="label in issue.labels" + v-for="label in orderedLabels" v-if="showLabel(label)" :key="label.id" v-gl-tooltip diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 10577da9305..a5ed695af35 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -8,7 +8,11 @@ import boardsStore from '../stores/boards_store'; $(document) .off('created.label') - .on('created.label', (e, label) => { + .on('created.label', (e, label, addNewList) => { + if (!addNewList) { + return; + } + boardsStore.new({ title: label.title, position: boardsStore.state.lists.length - 2, diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index dd92d3c8552..2edb6723ada 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -119,7 +119,17 @@ class ListIssue { } const projectPath = this.project ? this.project.path : ''; - return Vue.http.patch(`${this.path}.json`, data); + return Vue.http.patch(`${this.path}.json`, data).then(({ body = {} } = {}) => { + /** + * Since post implementation of Scoped labels, server can reject + * same key-ed labels. To keep the UI and server Model consistent, + * we're just assigning labels that server echo's back to us when we + * PATCH the said object. + */ + if (body) { + this.labels = body.labels; + } + }); } } diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index 7951348d8b2..4d5d6bb864b 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -14,6 +14,9 @@ const BreakpointInstance = { return breakpoint; }, + isDesktop() { + return ['lg', 'md'].includes(this.getBreakpointSize); + }, }; export default BreakpointInstance; diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index c95d7608e37..df855261b3c 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -262,7 +262,7 @@ export default class Clusters { this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'statusReason', null); - this.service.installApplication(appId, data.params).catch(() => { + return this.service.installApplication(appId, data.params).catch(() => { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); this.store.updateAppProperty( appId, @@ -288,10 +288,11 @@ export default class Clusters { } toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { - const helpTextHidden = ingressNewState.status !== APPLICATION_STATUS.INSTALLED; - const domainSnippetText = `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`; + const { externalIp, status } = ingressNewState; + const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp; + const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`; - if (ingressPreviousState.status !== ingressNewState.status) { + if (ingressPreviousState.status !== status) { this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden); this.ingressDomainSnippet.textContent = domainSnippetText; } diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index d4ecfa4aa93..bc666aef54b 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -71,29 +71,39 @@ export default class ImageFile { // eslint-disable-next-line class-methods-use-this initDraggable($el, padding, callback) { var dragging = false; - var $body = $('body'); - var $offsetEl = $el.parent(); - - $el.off('mousedown').on('mousedown', function() { + const $body = $('body'); + const $offsetEl = $el.parent(); + const dragStart = function() { dragging = true; $body.css('user-select', 'none'); - }); + }; + const dragStop = function() { + dragging = false; + $body.css('user-select', ''); + }; + const dragMove = function(e) { + const moveX = e.pageX || e.touches[0].pageX; + const left = moveX - ($offsetEl.offset().left + padding); + if (!dragging) return; + + callback(e, left); + }; + + $el + .off('mousedown') + .off('touchstart') + .on('mousedown', dragStart) + .on('touchstart', dragStart); $body .off('mouseup') .off('mousemove') - .on('mouseup', function() { - dragging = false; - $body.css('user-select', ''); - }) - .on('mousemove', function(e) { - var left; - if (!dragging) return; - - left = e.pageX - ($offsetEl.offset().left + padding); - - callback(e, left); - }); + .off('touchend') + .off('touchmove') + .on('mouseup', dragStop) + .on('touchend', dragStop) + .on('mousemove', dragMove) + .on('touchmove', dragMove); } prepareFrames(view) { diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 67fcdd082a2..03dea1ec0a5 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -41,6 +41,9 @@ export default class ContextualSidebar { this.toggleCollapsedSidebar(value, true); } }); + this.$page.on('transitionstart transitionend', () => { + $(document).trigger('content.resize'); + }); $(window).on('resize', () => _.debounce(this.render(), 100)); } diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 28ca7d97314..eac0e37bcaa 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -14,6 +14,7 @@ export default class CreateLabelDropdown { this.$newLabelField = $('#new_label_name', this.$el); this.$newColorField = $('#new_label_color', this.$el); this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el); + this.$addList = $('.js-add-list', this.$el); this.$newLabelError = $('.js-label-error', this.$el); this.$newLabelCreateButton = $('.js-new-label-btn', this.$el); this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el); @@ -21,6 +22,8 @@ export default class CreateLabelDropdown { this.$newLabelError.hide(); this.$newLabelCreateButton.disable(); + this.addListDefault = this.$addList.is(':checked'); + this.cleanBinding(); this.addBinding(); } @@ -83,6 +86,8 @@ export default class CreateLabelDropdown { this.$newColorField.val('').trigger('change'); + this.$addList.prop('checked', this.addListDefault); + this.$colorPreview .css('background-color', '') .parent() @@ -116,9 +121,9 @@ export default class CreateLabelDropdown { this.$newLabelError.html(errors).show(); } else { + const addNewList = this.$addList.is(':checked'); this.$dropdownBack.trigger('click'); - - $(document).trigger('created.label', label); + $(document).trigger('created.label', [label, addNewList]); } }, ); diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index e8f8c09152a..5e74998579b 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -20,6 +20,7 @@ import { MAX_TREE_WIDTH, TREE_HIDE_STATS_WIDTH, MR_TREE_SHOW_KEY, + CENTERED_LIMITED_CONTAINER_CLASSES, } from '../constants'; export default { @@ -114,6 +115,9 @@ export default { hideFileStats() { return this.treeWidth <= TREE_HIDE_STATS_WIDTH; }, + isLimitedContainer() { + return !this.showTreeList && !this.isParallelView; + }, }, watch: { diffViewType() { @@ -148,6 +152,7 @@ export default { this.adjustView(); eventHub.$once('fetchedNotesData', this.setDiscussions); eventHub.$once('fetchDiffData', this.fetchData); + this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; }, beforeDestroy() { eventHub.$off('fetchDiffData', this.fetchData); @@ -202,8 +207,6 @@ export default { adjustView() { if (this.shouldShow) { this.$nextTick(() => { - window.mrTabs.resetViewContainer(); - window.mrTabs.expandViewContainer(this.showTreeList); this.setEventListeners(); }); } else { @@ -256,6 +259,7 @@ export default { :merge-request-diffs="mergeRequestDiffs" :merge-request-diff="mergeRequestDiff" :target-branch="targetBranch" + :is-limited-container="isLimitedContainer" /> <hidden-files-warning @@ -285,7 +289,12 @@ export default { /> <tree-list :hide-file-stats="hideFileStats" /> </div> - <div class="diff-files-holder"> + <div + class="diff-files-holder" + :class="{ + [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, + }" + > <commit-widget v-if="commit" :commit="commit" /> <template v-if="renderDiffFiles"> <diff-file diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index fe49dfff10b..363ebad1594 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -7,6 +7,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue'; import DiffStats from './diff_stats.vue'; +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; export default { components: { @@ -35,6 +36,11 @@ export default { required: false, default: null, }, + isLimitedContainer: { + type: Boolean, + required: false, + default: false, + }, }, computed: { ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']), @@ -62,6 +68,9 @@ export default { return this.mergeRequestDiff.base_version_path; }, }, + created() { + this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; + }, mounted() { polyfillSticky(this.$el); }, @@ -77,8 +86,13 @@ export default { </script> <template> - <div class="mr-version-controls" :class="{ 'is-fileTreeOpen': showTreeList }"> - <div class="mr-version-menus-container content-block"> + <div class="mr-version-controls border-top border-bottom"> + <div + class="mr-version-menus-container content-block" + :class="{ + [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, + }" + > <button v-gl-tooltip.hover type="button" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index fda7b7c5fd9..32e5fa5bf8b 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -1,7 +1,7 @@ <script> import _ from 'underscore'; import { mapActions, mapGetters } from 'vuex'; -import { polyfillSticky } from '~/lib/utils/sticky'; +import { polyfillSticky, stickyMonitor } from '~/lib/utils/sticky'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; @@ -11,7 +11,7 @@ import { __, s__, sprintf } from '~/locale'; import { diffViewerModes } from '~/ide/constants'; import EditButton from './edit_button.vue'; import DiffStats from './diff_stats.vue'; -import { scrollToElement } from '~/lib/utils/common_utils'; +import { scrollToElement, contentTop } from '~/lib/utils/common_utils'; export default { components: { @@ -137,9 +137,20 @@ export default { isModeChanged() { return this.diffFile.viewer.name === diffViewerModes.mode_changed; }, + showExpandDiffToFullFileEnabled() { + return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded; + }, + expandDiffToFullFileTitle() { + if (this.diffFile.isShowingFullFile) { + return s__('MRDiff|Show changes only'); + } + return s__('MRDiff|Show full file'); + }, }, mounted() { polyfillSticky(this.$refs.header); + const fileHeaderHeight = this.$refs.header.clientHeight; + stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false); }, methods: { ...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']), @@ -243,70 +254,70 @@ export default { class="file-actions d-none d-sm-block" > <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> - <template v-if="diffFile.blob && diffFile.blob.readable_text"> - <button - :disabled="!diffHasDiscussions(diffFile)" - :class="{ active: hasExpandedDiscussions }" - :title="s__('MergeRequests|Toggle comments for this file')" - class="js-btn-vue-toggle-comments btn" - type="button" - @click="handleToggleDiscussions" - > - <icon name="comment" /> - </button> - - <edit-button - v-if="!diffFile.deleted_file" - :can-current-user-fork="canCurrentUserFork" - :edit-path="diffFile.edit_path" - :can-modify-blob="diffFile.can_modify_blob" - @showForkMessage="showForkMessage" - /> - </template> + <div class="btn-group" role="group"> + <template v-if="diffFile.blob && diffFile.blob.readable_text"> + <button + :disabled="!diffHasDiscussions(diffFile)" + :class="{ active: hasExpandedDiscussions }" + :title="s__('MergeRequests|Toggle comments for this file')" + class="js-btn-vue-toggle-comments btn" + type="button" + @click="handleToggleDiscussions" + > + <icon name="comment" /> + </button> - <a - v-if="diffFile.replaced_view_path" - :href="diffFile.replaced_view_path" - class="btn view-file js-view-replaced-file" - v-html="viewReplacedFileButtonText" - > - </a> - <gl-tooltip :target="() => $refs.viewButton" placement="bottom"> - <span v-html="viewFileButtonText"></span> - </gl-tooltip> - <gl-button - ref="viewButton" - :href="diffFile.view_path" - target="blank" - class="view-file js-view-file-button" - > - <icon name="external-link" /> - </gl-button> - <gl-button - v-if="!diffFile.is_fully_expanded" - class="expand-file js-expand-file" - @click="toggleFullDiff(diffFile.file_path)" - > - <template v-if="diffFile.isShowingFullFile"> - {{ s__('MRDiff|Show changes only') }} - </template> - <template v-else> - {{ s__('MRDiff|Show full file') }} + <edit-button + v-if="!diffFile.deleted_file" + :can-current-user-fork="canCurrentUserFork" + :edit-path="diffFile.edit_path" + :can-modify-blob="diffFile.can_modify_blob" + @showForkMessage="showForkMessage" + /> </template> - <gl-loading-icon v-if="diffFile.isLoadingFullFile" inline /> - </gl-button> - <a - v-if="diffFile.external_url" - v-gl-tooltip.hover - :href="diffFile.external_url" - :title="`View on ${diffFile.formatted_external_url}`" - target="_blank" - rel="noopener noreferrer" - class="btn btn-file-option js-external-url" - > - <icon name="external-link" /> - </a> + <a + v-if="diffFile.replaced_view_path" + :href="diffFile.replaced_view_path" + class="btn view-file js-view-replaced-file" + v-html="viewReplacedFileButtonText" + > + </a> + <gl-button + v-if="!diffFile.is_fully_expanded" + ref="expandDiffToFullFileButton" + v-gl-tooltip.hover + :title="expandDiffToFullFileTitle" + class="expand-file js-expand-file" + @click="toggleFullDiff(diffFile.file_path)" + > + <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline /> + <icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" /> + <icon v-else name="doc-expand" /> + </gl-button> + <gl-button + ref="viewButton" + v-gl-tooltip.hover + :href="diffFile.view_path" + target="blank" + class="view-file js-view-file-button" + :title="viewFileButtonText" + > + <icon name="external-link" /> + </gl-button> + + <a + v-if="diffFile.external_url" + v-gl-tooltip.hover + :href="diffFile.external_url" + :title="`View on ${diffFile.formatted_external_url}`" + target="_blank" + rel="noopener noreferrer" + class="btn btn-file-option js-external-url" + > + <icon name="external-link" /> + </a> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 6f380fe6ece..5dabe224baa 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -47,3 +47,6 @@ export const OLD_LINE_KEY = 'old_line'; export const NEW_LINE_KEY = 'new_line'; export const TYPE_KEY = 'type'; export const LEFT_LINE_KEY = 'left'; + +export const CENTERED_LIMITED_CONTAINER_CLASSES = + 'container-limited limit-container-width mx-auto px-3'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 0c2e87521d9..efd03ec952f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; +import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; @@ -36,10 +37,11 @@ export default class FilteredSearchManager { this.tokensContainer = this.container.querySelector('.tokens-container'); this.filteredSearchTokenKeys = filteredSearchTokenKeys; this.stateFiltersSelector = stateFiltersSelector; - this.recentsStorageKeyNames = { - issues: 'issue-recent-searches', - merge_requests: 'merge-request-recent-searches', - }; + + const { multipleAssignees } = this.filteredSearchInput.dataset; + if (multipleAssignees && this.filteredSearchTokenKeys.enableMultipleAssignees) { + this.filteredSearchTokenKeys.enableMultipleAssignees(); + } this.recentSearchesStore = new RecentSearchesStore({ isLocalStorageAvailable: RecentSearchesService.isAvailable(), @@ -51,7 +53,7 @@ export default class FilteredSearchManager { const fullPath = this.searchHistoryDropdownElement ? this.searchHistoryDropdownElement.dataset.fullPath : 'project'; - const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`; + const recentSearchesKey = `${fullPath}-${recentSearchesStorageKeys[this.page]}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); } diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js new file mode 100644 index 00000000000..7e9b809e9b2 --- /dev/null +++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js @@ -0,0 +1,4 @@ +export default { + issues: 'issue-recent-searches', + merge_requests: 'merge-request-recent-searches', +}; diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue index 42d14b65b3a..92c3bcb5012 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue @@ -1,6 +1,9 @@ <script> /* eslint-disable vue/require-default-prop */ -import Identicon from '../../vue_shared/components/identicon.vue'; +import _ from 'underscore'; +import Identicon from '~/vue_shared/components/identicon.vue'; +import highlight from '~/lib/utils/highlight'; +import { truncateNamespace } from '~/lib/utils/text_utility'; export default { components: { @@ -36,43 +39,13 @@ export default { }, computed: { hasAvatar() { - return this.avatarUrl !== null; + return _.isString(this.avatarUrl) && !_.isEmpty(this.avatarUrl); }, - highlightedItemName() { - if (this.matcher) { - const matcherRegEx = new RegExp(this.matcher, 'gi'); - const matches = this.itemName.match(matcherRegEx); - - if (matches && matches.length > 0) { - return this.itemName.replace(matches[0], `<b>${matches[0]}</b>`); - } - } - return this.itemName; - }, - /** - * Smartly truncates item namespace by doing two things; - * 1. Only include Group names in path by removing item name - * 2. Only include first and last group names in the path - * when namespace has more than 2 groups present - * - * First part (removal of item name from namespace) can be - * done from backend but doing so involves migration of - * existing item namespaces which is not wise thing to do. - */ truncatedNamespace() { - if (!this.namespace) { - return null; - } - const namespaceArr = this.namespace.split(' / '); - - namespaceArr.splice(-1, 1); - let namespace = namespaceArr.join(' / '); - - if (namespaceArr.length > 2) { - namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; - } - - return namespace; + return truncateNamespace(this.namespace); + }, + highlightedItemName() { + return highlight(this.itemName, this.matcher); }, }, }; @@ -92,8 +65,16 @@ export default { /> </div> <div class="frequent-items-item-metadata-container"> - <div :title="itemName" class="frequent-items-item-title" v-html="highlightedItemName"></div> - <div v-if="truncatedNamespace" :title="namespace" class="frequent-items-item-namespace"> + <div + :title="itemName" + class="frequent-items-item-title js-frequent-items-item-title" + v-html="highlightedItemName" + ></div> + <div + v-if="namespace" + :title="namespace" + class="frequent-items-item-namespace js-frequent-items-item-namespace" + > {{ truncatedNamespace }} </div> </div> diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 27d8669b256..1c6b18c0e03 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -561,10 +561,9 @@ GitLabDropdown = (function() { !$target.data('isLink') ) { e.stopPropagation(); - return false; - } else { - return true; } + + return true; } }; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index d5d5954ce6a..c4fd719c8d0 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -15,7 +15,7 @@ export default class GlFieldErrors { initValidators() { // register selectors here as needed - const validateSelectors = [':text', ':password', '[type=email]'] + const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]'] .map(selector => `input${selector}`) .join(','); diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js new file mode 100644 index 00000000000..2c2a04d5b5e --- /dev/null +++ b/app/assets/javascripts/helpers/monitor_helper.js @@ -0,0 +1,17 @@ +/* eslint-disable import/prefer-default-export */ + +export const makeDataSeries = (queryResults, defaultConfig) => + queryResults.reduce((acc, result) => { + const data = result.values.filter(([, value]) => !Number.isNaN(value)); + if (!data.length) { + return acc; + } + const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_'); + const name = result.metric[relevantMetric]; + const series = { data }; + if (name) { + series.name = `${defaultConfig.name}: ${name}`; + } + + return acc.concat({ ...defaultConfig, ...series }); + }, []); diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index d360dc42cd3..1824a0f6147 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,17 +1,23 @@ <script> import _ from 'underscore'; -import { mapActions, mapState, mapGetters } from 'vuex'; +import { mapActions, mapState, mapGetters, createNamespacedHelpers } from 'vuex'; import { sprintf, __ } from '~/locale'; -import * as consts from '../../stores/modules/commit/constants'; +import consts from '../../stores/modules/commit/constants'; import RadioGroup from './radio_group.vue'; +const { mapState: mapCommitState, mapGetters: mapCommitGetters } = createNamespacedHelpers( + 'commit', +); + export default { components: { RadioGroup, }, computed: { ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), - ...mapGetters(['currentProject', 'currentBranch']), + ...mapCommitState(['commitAction', 'shouldCreateMR', 'shouldDisableNewMrOption']), + ...mapGetters(['currentProject', 'currentBranch', 'currentMergeRequest']), + ...mapCommitGetters(['shouldDisableNewMrOption']), commitToCurrentBranchText() { return sprintf( __('Commit to %{branchName} branch'), @@ -32,7 +38,7 @@ export default { this.updateSelectedCommitAction(); }, methods: { - ...mapActions('commit', ['updateCommitAction']), + ...mapActions('commit', ['updateCommitAction', 'toggleShouldCreateMR']), updateSelectedCommitAction() { if (this.currentBranch && !this.currentBranch.can_push) { this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH); @@ -43,7 +49,6 @@ export default { }, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, - commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, currentBranchPermissionsTooltip: __( "This option is disabled as you don't have write permissions for the current branch", ), @@ -64,13 +69,17 @@ export default { :label="__('Create a new branch')" :show-input="true" /> - <radio-group - v-if="currentProject.merge_requests_enabled" - :value="$options.commitToNewBranchMR" - :label="__('Create a new branch and merge request')" - :title="__('This option is disabled while you still have unstaged changes')" - :show-input="true" - :disabled="disableMergeRequestRadio" - /> + <hr class="my-2" /> + <label class="mb-0"> + <input + :checked="shouldCreateMR" + :disabled="shouldDisableNewMrOption" + type="checkbox" + @change="toggleShouldCreateMR" + /> + <span class="prepend-left-10" :class="{ 'text-secondary': shouldDisableNewMrOption }"> + {{ __('Start a new merge request') }} + </span> + </label> </div> </template> diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index d6673cf0421..80a6ab9598a 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -23,7 +23,7 @@ export default { type: Object, required: true, }, - mouseOver: { + dropdownOpen: { type: Boolean, required: true, }, @@ -92,8 +92,9 @@ export default { <new-dropdown :type="file.type" :path="file.path" - :mouse-over="mouseOver" + :is-open="dropdownOpen" class="prepend-left-8" + v-on="$listeners" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 593a9162a06..27d24fa5e1d 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -21,38 +21,29 @@ export default { required: false, default: '', }, - mouseOver: { + isOpen: { type: Boolean, - required: true, + required: false, + default: false, }, }, - data() { - return { - dropdownOpen: false, - }; - }, watch: { - dropdownOpen() { + isOpen() { this.$nextTick(() => { this.$refs.dropdownMenu.scrollIntoView({ block: 'nearest', }); }); }, - mouseOver() { - if (!this.mouseOver) { - this.dropdownOpen = false; - } - }, }, methods: { ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']), createNewItem(type) { this.openNewEntryModal({ type, path: this.path }); - this.dropdownOpen = false; + this.$emit('toggle', false); }, openDropdown() { - this.dropdownOpen = !this.dropdownOpen; + this.$emit('toggle', !this.isOpen); }, }, modalTypes, @@ -63,7 +54,7 @@ export default { <div class="ide-new-btn"> <div :class="{ - show: dropdownOpen, + show: isOpen, }" class="dropdown d-flex" > diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 8dd88f187d4..99f1d4a573d 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -5,7 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import CommitFilesList from './commit_sidebar/list.vue'; import EmptyState from './commit_sidebar/empty_state.vue'; -import * as consts from '../stores/modules/commit/constants'; +import consts from '../stores/modules/commit/constants'; import { activityBarViews, stageKeys } from '../constants'; export default { diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 362ced248a1..1273e375859 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -4,10 +4,11 @@ import service from '../../services'; import * as types from '../mutation_types'; import { activityBarViews } from '../../constants'; -export const getMergeRequestsForBranch = ({ commit }, { projectId, branchId } = {}) => +export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) => service .getProjectMergeRequests(`${projectId}`, { source_branch: branchId, + source_project_id: state.projects[projectId].id, order_by: 'created_at', per_page: 1, }) diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 06ed5c0b572..4b10d148ebf 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -147,6 +147,11 @@ export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath if (treeEntry) { dispatch('handleTreeEntryAction', treeEntry); + } else { + dispatch('createTempEntry', { + name: path, + type: 'blob', + }); } } }) diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 8ad85074d6b..490658a4543 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -25,7 +25,10 @@ export const projectsWithTrees = state => }); export const currentMergeRequest = state => { - if (state.projects[state.currentProjectId]) { + if ( + state.projects[state.currentProjectId] && + state.projects[state.currentProjectId].mergeRequests + ) { return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId]; } return null; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 24c2f71ae2b..c2760eb1554 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -6,7 +6,7 @@ import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; import router from '../../../ide_router'; import service from '../../../services'; import * as types from './mutation_types'; -import * as consts from './constants'; +import consts from './constants'; import { activityBarViews } from '../../../constants'; import eventHub from '../../../eventhub'; @@ -18,16 +18,23 @@ export const discardDraft = ({ commit }) => { commit(types.UPDATE_COMMIT_MESSAGE, ''); }; -export const updateCommitAction = ({ commit }, commitAction) => { - commit(types.UPDATE_COMMIT_ACTION, commitAction); +export const updateCommitAction = ({ commit, rootGetters }, commitAction) => { + commit(types.UPDATE_COMMIT_ACTION, { + commitAction, + currentMergeRequest: rootGetters.currentMergeRequest, + }); +}; + +export const toggleShouldCreateMR = ({ commit }) => { + commit(types.TOGGLE_SHOULD_CREATE_MR); }; export const updateBranchName = ({ commit }, branchName) => { commit(types.UPDATE_NEW_BRANCH_NAME, branchName); }; -export const setLastCommitMessage = ({ rootState, commit }, data) => { - const currentProject = rootState.projects[rootState.currentProjectId]; +export const setLastCommitMessage = ({ commit, rootGetters }, data) => { + const { currentProject } = rootGetters; const commitStats = data.stats ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { additions: data.stats.additions, @@ -48,8 +55,8 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); }; -export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => { - const selectedProject = rootState.projects[rootState.currentProjectId]; +export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetters }, { data }) => { + const selectedProject = rootGetters.currentProject; const lastCommit = { commit_path: `${selectedProject.web_url}/commit/${data.id}`, commit: { @@ -135,14 +142,15 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo branch: getters.branchName, }) .then(() => { - if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { + if (state.shouldCreateMR) { + const { currentProject } = rootGetters; + const targetBranch = getters.isCreatingNewBranch + ? rootState.currentBranchId + : currentProject.default_branch; + dispatch( 'redirectToUrl', - createNewMergeRequestUrl( - rootState.projects[rootState.currentProjectId].web_url, - getters.branchName, - rootState.currentBranchId, - ), + createNewMergeRequestUrl(currentProject.web_url, getters.branchName, targetBranch), { root: true }, ); } diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js index 230b0a3d9b5..c6c3701effe 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/constants.js +++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js @@ -1,3 +1,7 @@ -export const COMMIT_TO_CURRENT_BRANCH = '1'; -export const COMMIT_TO_NEW_BRANCH = '2'; -export const COMMIT_TO_NEW_BRANCH_MR = '3'; +const COMMIT_TO_CURRENT_BRANCH = '1'; +const COMMIT_TO_NEW_BRANCH = '2'; + +export default { + COMMIT_TO_CURRENT_BRANCH, + COMMIT_TO_NEW_BRANCH, +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index bbe40b2ec2f..6aa5d22a4ea 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -1,5 +1,5 @@ import { sprintf, n__, __ } from '../../../../locale'; -import * as consts from './constants'; +import consts from './constants'; const BRANCH_SUFFIX_COUNT = 5; const createTranslatedTextForFiles = (files, text) => { @@ -20,10 +20,7 @@ export const placeholderBranchName = (state, _, rootState) => )}`; export const branchName = (state, getters, rootState) => { - if ( - state.commitAction === consts.COMMIT_TO_NEW_BRANCH || - state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR - ) { + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { if (state.newBranchName === '') { return getters.placeholderBranchName; } @@ -49,5 +46,10 @@ export const preBuiltCommitMessage = (state, _, rootState) => { .join('\n'); }; +export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH; + +export const shouldDisableNewMrOption = (state, _getters, _rootState, rootGetters) => + rootGetters.currentMergeRequest && state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js index 9221f054e9f..7ad8f3570b7 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js @@ -2,3 +2,4 @@ export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE'; export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; export const UPDATE_LOADING = 'UPDATE_LOADING'; +export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js index 797357e3df9..be0f894c059 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -1,4 +1,5 @@ import * as types from './mutation_types'; +import consts from './constants'; export default { [types.UPDATE_COMMIT_MESSAGE](state, commitMessage) { @@ -6,9 +7,13 @@ export default { commitMessage, }); }, - [types.UPDATE_COMMIT_ACTION](state, commitAction) { + [types.UPDATE_COMMIT_ACTION](state, { commitAction, currentMergeRequest }) { Object.assign(state, { commitAction, + shouldCreateMR: + commitAction === consts.COMMIT_TO_CURRENT_BRANCH && currentMergeRequest + ? false + : state.shouldCreateMR, }); }, [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { @@ -21,4 +26,9 @@ export default { submitCommitLoading, }); }, + [types.TOGGLE_SHOULD_CREATE_MR](state) { + Object.assign(state, { + shouldCreateMR: !state.shouldCreateMR, + }); + }, }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js index 8dae50961b0..5c0e6a41ca1 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/state.js +++ b/app/assets/javascripts/ide/stores/modules/commit/state.js @@ -3,4 +3,5 @@ export default () => ({ commitAction: '1', newBranchName: '', submitCommitLoading: false, + shouldCreateMR: false, }); diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index eac7441ee54..359943b4ab7 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -1,5 +1,5 @@ import * as types from '../mutation_types'; -import { sortTree } from '../utils'; +import { sortTree, mergeTrees } from '../utils'; export default { [types.TOGGLE_TREE_OPEN](state, path) { @@ -23,9 +23,15 @@ export default { }); }, [types.SET_DIRECTORY_DATA](state, { data, treePath }) { - Object.assign(state.trees[treePath], { - tree: data, - }); + const selectedTree = state.trees[treePath]; + + // If we opened files while loading the tree, we need to merge them + // Otherwise, simply overwrite the tree + const tree = !selectedTree.tree.length + ? data + : selectedTree.loading && mergeTrees(selectedTree.tree, data); + + Object.assign(selectedTree, { tree }); }, [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) { Object.assign(tree, { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 0b2a18e9c8a..3ab8f3f11be 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -170,3 +170,31 @@ export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) export const getChangesCountForFiles = (files, path) => files.filter(f => filePathMatches(f.path, path)).length; + +export const mergeTrees = (fromTree, toTree) => { + if (!fromTree || !fromTree.length) { + return toTree; + } + + const recurseTree = (n, t) => { + if (!n) { + return t; + } + const existingTreeNode = t.find(el => el.path === n.path); + + if (existingTreeNode && n.tree.length > 0) { + existingTreeNode.opened = true; + recurseTree(n.tree[0], existingTreeNode.tree); + } else if (!existingTreeNode) { + const sorted = sortTree(t.concat(n)); + t.splice(0, t.length + 1, ...sorted); + } + return t; + }; + + for (let i = 0, l = fromTree.length; i < l; i += 1) { + recurseTree(fromTree[i], toTree); + } + + return toTree; +}; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index b3508f36cf9..cd1afb6ba83 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -15,7 +15,6 @@ export default class Issue { Issue.$btnNewBranch = $('#new-branch'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); - Issue.initMergeRequests(); if (document.querySelector('#related-branches')) { Issue.initRelatedBranches(); } @@ -143,19 +142,6 @@ export default class Issue { } } - static initMergeRequests() { - var $container; - $container = $('#merge-requests'); - return axios - .get($container.data('url')) - .then(({ data }) => { - if ('html' in data) { - $container.html(data.html); - } - }) - .catch(() => flash('Failed to load referenced merge requests')); - } - static initRelatedBranches() { var $container; $container = $('#related-branches'); diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index d08e8ba0c4b..529b6386221 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,12 +1,9 @@ import Vue from 'vue'; -import sanitize from 'sanitize-html'; import issuableApp from './components/app.vue'; +import { parseIssuableData } from './utils/parse_data'; import '../vue_shared/vue_resource_interceptor'; export default function initIssueableApp() { - const initialDataEl = document.getElementById('js-issuable-app-initial-data'); - const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"')); - return new Vue({ el: document.getElementById('js-issuable-app'), components: { @@ -14,7 +11,7 @@ export default function initIssueableApp() { }, render(createElement) { return createElement('issuable-app', { - props, + props: parseIssuableData(), }); }, }); diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js new file mode 100644 index 00000000000..05e384adad3 --- /dev/null +++ b/app/assets/javascripts/issue_show/utils/parse_data.js @@ -0,0 +1,15 @@ +import sanitize from 'sanitize-html'; + +export const parseIssuableData = () => { + try { + const initialDataEl = document.getElementById('js-issuable-app-initial-data'); + + return JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"')); + } catch (e) { + console.error(e); // eslint-disable-line no-console + + return {}; + } +}; + +export default {}; diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index 668fcf3d673..04f910b6b80 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -49,7 +49,7 @@ export default { <div class="text-content"> <h4 class="js-job-empty-state-title text-center">{{ title }}</h4> - <p v-if="content" class="js-job-empty-state-content">{{ content }}</p> + <p v-if="content" class="js-job-empty-state-content text-center">{{ content }}</p> <div v-if="action" class="text-center"> <gl-link diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index dbadd224251..0670e2b06b9 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -15,6 +15,7 @@ import ErasedBlock from './erased_block.vue'; import Log from './job_log.vue'; import LogTopBar from './job_log_controllers.vue'; import StuckBlock from './stuck_block.vue'; +import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue'; import Sidebar from './sidebar.vue'; import { sprintf } from '~/locale'; import delayedJobMixin from '../mixins/delayed_job_mixin'; @@ -32,6 +33,7 @@ export default { Log, LogTopBar, StuckBlock, + UnmetPrerequisitesBlock, Sidebar, GlLoadingIcon, SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'), @@ -48,6 +50,11 @@ export default { required: false, default: null, }, + deploymentHelpUrl: { + type: String, + required: false, + default: null, + }, endpoint: { type: String, required: true, @@ -82,6 +89,7 @@ export default { ]), ...mapGetters([ 'headerTime', + 'hasUnmetPrerequisitesFailure', 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', 'hasEnvironment', @@ -210,7 +218,10 @@ export default { /> </div> - <callout v-if="shouldRenderCalloutMessage" :message="job.callout_message" /> + <callout + v-if="shouldRenderCalloutMessage && !hasUnmetPrerequisitesFailure" + :message="job.callout_message" + /> </header> <!-- EO Header Section --> @@ -223,6 +234,12 @@ export default { :runners-path="runnerSettingsUrl" /> + <unmet-prerequisites-block + v-if="hasUnmetPrerequisitesFailure" + class="js-job-failed" + :help-path="deploymentHelpUrl" + /> + <shared-runner v-if="shouldRenderSharedRunnerLimitWarning" class="js-shared-runner-limit" diff --git a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue new file mode 100644 index 00000000000..25a8da84873 --- /dev/null +++ b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue @@ -0,0 +1,30 @@ +<script> +import { GlLink } from '@gitlab/ui'; +/** + * Renders Unmet Prerequisites block for job's view. + */ +export default { + components: { + GlLink, + }, + props: { + helpPath: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="bs-callout bs-callout-danger"> + <p class="js-failed-unmet-prerequisites append-bottom-0"> + {{ + s__(`Job|This job failed because the necessary resources were not successfully created.`) + }} + + <gl-link :href="helpPath" class="js-help-path"> + <strong> {{ __('More information') }} </strong> + </gl-link> + </p> + </div> +</template> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index a32e945627c..25132449458 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -12,6 +12,7 @@ export default () => { render(createElement) { return createElement('job-app', { props: { + deploymentHelpUrl: element.dataset.deploymentHelpUrl, runnerHelpUrl: element.dataset.runnerHelpUrl, runnerSettingsUrl: element.dataset.runnerSettingsUrl, endpoint: element.dataset.endpoint, diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 73c1cbc3a99..406b1a2e375 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -3,6 +3,9 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); +export const hasUnmetPrerequisitesFailure = state => + state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites'; + export const shouldRenderCalloutMessage = state => !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index cca4927c115..7d21a216443 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -11,6 +11,7 @@ import CreateLabelDropdown from './create_label'; import flash from './flash'; import ModalStore from './boards/stores/modal_store'; import boardsStore from './boards/stores/boards_store'; +import { isEE } from '~/lib/utils/common_utils'; export default class LabelsSelect { constructor(els, options = {}) { @@ -86,8 +87,9 @@ export default class LabelsSelect { return this.value; }) .get(); + const scopedLabels = $dropdown.data('scopedLabels'); + const scopedLabelsDocumentationLink = $dropdown.data('scopedLabelsDocumentationLink'); const { handleClick } = options; - $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { @@ -132,8 +134,49 @@ export default class LabelsSelect { template = LabelsSelect.getLabelTemplate({ labels: data.labels, issueUpdateURL, + enableScopedLabels: scopedLabels, + scopedLabelsDocumentationLink, }); labelCount = data.labels.length; + + // EE Specific + if (isEE) { + /** + * For Scoped labels, the last label selected with the + * same key will be applied to the current issueable. + * + * If these are the labels - priority::1, priority::2; and if + * we apply them in the same order, only priority::2 will stick + * with the issuable. + * + * In the current dropdown implementation, we keep track of all + * the labels selected via a hidden DOM element. Since a User + * can select priority::1 and priority::2 at the same time, the + * DOM will have 2 hidden input and the dropdown will show both + * the items selected but in reality server only applied + * priority::2. + * + * We find all the labels then find all the labels server accepted + * and then remove the excess ones. + */ + const toRemoveIds = Array.from( + $form.find(`input[type="hidden"][name="${fieldName}"]`), + ) + .map(el => el.value) + .map(Number); + + data.labels.forEach(label => { + const index = toRemoveIds.indexOf(label.id); + toRemoveIds.splice(index, 1); + }); + + toRemoveIds.forEach(id => { + $form + .find(`input[type="hidden"][name="${fieldName}"][value="${id}"]`) + .last() + .remove(); + }); + } } else { template = '<span class="no-value">None</span>'; } @@ -358,6 +401,7 @@ export default class LabelsSelect { } else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); + $dropdown.data('glDropdown').clearMenu(); } } } @@ -471,19 +515,62 @@ export default class LabelsSelect { // so best approach is to use traditional way of // concatenation // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays - const tpl = _.template( + + const labelTemplate = _.template( [ - '<% _.each(labels, function(label){ %>', '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', - '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<span class="badge label has-tooltip color-label" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels, escapeStr }) %>" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;">', '<%- label.title %>', '</span>', '</a>', + ].join(''), + ); + + const infoIconTemplate = _.template( + [ + '<a href="<%= scopedLabelsDocumentationLink %>" class="label scoped-label" target="_blank" rel="noopener">', + '<i class="fa fa-question-circle" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;"></i>', + '</a>', + ].join(''), + ); + + const tooltipTitleTemplate = _.template( + [ + '<% if (isScopedLabel(label) && enableScopedLabels) { %>', + "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>", + '<br />', + '<%= escapeStr(label.description) %>', + '<% } else { %>', + '<%= escapeStr(label.description) %>', + '<% } %>', + ].join(''), + ); + + const isScopedLabel = label => label.title.indexOf('::') !== -1; + + const tpl = _.template( + [ + '<% _.each(labels, function(label){ %>', + '<% if (isScopedLabel(label) && enableScopedLabels) { %>', + '<span class="d-inline-block position-relative scoped-label-wrapper">', + '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>', + '<%= infoIconTemplate({ label, scopedLabelsDocumentationLink, escapeStr }) %>', + '</span>', + '<% } else { %>', + '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>', + '<% } %>', '<% }); %>', ].join(''), ); - return tpl(tplData); + return tpl({ + ...tplData, + labelTemplate, + infoIconTemplate, + tooltipTitleTemplate, + isScopedLabel, + escapeStr: _.escape, + }); } bindEvents() { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 59930f8d4a3..2906604da57 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -7,7 +7,7 @@ import axios from './axios_utils'; import { getLocationHash } from './url_utility'; import { convertToCamelCase } from './text_utility'; import { isObject } from './type_utility'; -import BreakpointInstance from '../../breakpoints'; +import breakpointInstance from '../../breakpoints'; export const getPagePath = (index = 0) => { const page = $('body').attr('data-page') || ''; @@ -198,11 +198,10 @@ export const contentTop = () => { const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0; const headerHeight = $('.navbar-gitlab').outerHeight() || 0; const diffFilesChanged = $('.js-diff-files-changed').outerHeight() || 0; - const mdScreenOrBigger = ['lg', 'md'].includes(BreakpointInstance.getBreakpointSize()); + const isDesktop = breakpointInstance.isDesktop(); const diffFileTitleBar = - (mdScreenOrBigger && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0; - const compareVersionsHeaderHeight = - (mdScreenOrBigger && $('.mr-version-controls').outerHeight()) || 0; + (isDesktop && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0; + const compareVersionsHeaderHeight = (isDesktop && $('.mr-version-controls').outerHeight()) || 0; return ( perfBar + diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js new file mode 100644 index 00000000000..4f7eff2cca1 --- /dev/null +++ b/app/assets/javascripts/lib/utils/highlight.js @@ -0,0 +1,44 @@ +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import _ from 'underscore'; +import sanitize from 'sanitize-html'; + +/** + * Wraps substring matches with HTML `<span>` elements. + * Inputs are sanitized before highlighting, so this + * filter is safe to use with `v-html` (as long as `matchPrefix` + * and `matchSuffix` are not being dynamically generated). + * + * Note that this function can't be used inside `v-html` as a filter + * (Vue filters cannot be used inside `v-html`). + * + * @param {String} string The string to highlight + * @param {String} match The substring match to highlight in the string + * @param {String} matchPrefix The string to insert at the beginning of a match + * @param {String} matchSuffix The string to insert at the end of a match + */ +export default function highlight(string, match = '', matchPrefix = '<b>', matchSuffix = '</b>') { + if (_.isUndefined(string) || _.isNull(string)) { + return ''; + } + + if (_.isUndefined(match) || _.isNull(match) || match === '') { + return string; + } + + const sanitizedValue = sanitize(string.toString(), { allowedTags: [] }); + + // occurences is an array of character indices that should be + // highlighted in the original string, i.e. [3, 4, 5, 7] + const occurences = fuzzaldrinPlus.match(sanitizedValue, match.toString()); + + return sanitizedValue + .split('') + .map((character, i) => { + if (_.contains(occurences, i)) { + return `${matchPrefix}${character}${matchSuffix}`; + } + + return character; + }) + .join(''); +} diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index c49b1bb5a2f..1b7f8732c65 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + /** * Adds a , to a string composed by numbers, at every 3 chars. * @@ -160,3 +162,33 @@ export const splitCamelCase = string => .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') .replace(/([a-z\d])([A-Z])/g, '$1 $2') .trim(); + +/** + * Intelligently truncates an item's namespace by doing two things: + * 1. Only include group names in path by removing the item name + * 2. Only include the first and last group names in the path + * when the namespace includes more than 2 groups + * + * @param {String} string A string namespace, + * i.e. "My Group / My Subgroup / My Project" + */ +export const truncateNamespace = (string = '') => { + if (_.isNull(string) || !_.isString(string)) { + return ''; + } + + const namespaceArray = string.split(' / '); + + if (namespaceArray.length === 1) { + return string; + } + + namespaceArray.splice(-1, 1); + let namespace = namespaceArray.join(' / '); + + if (namespaceArray.length > 2) { + namespace = `${namespaceArray[0]} / ... / ${namespaceArray.pop()}`; + } + + return namespace; +}; diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index d453dc1fdb7..afe8d87a8d6 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -1,16 +1,18 @@ <script> -import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { debounceByAnimationFrame } from '~/lib/utils/common_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import Icon from '~/vue_shared/components/icon.vue'; import { chartHeight, graphTypes, lineTypes } from '../../constants'; +import { makeDataSeries } from '~/helpers/monitor_helper'; let debouncedResize; export default { components: { GlAreaChart, + GlChartSeriesLabel, Icon, }, inheritAttrs: false, @@ -41,10 +43,10 @@ export default { required: false, default: () => [], }, - alertData: { - type: Object, + thresholds: { + type: Array, required: false, - default: () => ({}), + default: () => [], }, }, data() { @@ -63,7 +65,10 @@ export default { }, computed: { chartData() { - return this.graphData.queries.map(query => { + // Transforms & supplements query data to render appropriate labels & styles + // Input: [{ queryAttributes1 }, { queryAttributes2 }] + // Output: [{ seriesAttributes1 }, { seriesAttributes2 }] + return this.graphData.queries.reduce((acc, query) => { const { appearance } = query; const lineType = appearance && appearance.line && appearance.line.type @@ -74,9 +79,8 @@ export default { ? appearance.line.width : undefined; - return { + const series = makeDataSeries(query.result, { name: this.formatLegendLabel(query), - data: this.concatenateResults(query.result), lineStyle: { type: lineType, width: lineWidth, @@ -87,8 +91,10 @@ export default { ? appearance.area.opacity : undefined, }, - }; - }); + }); + + return acc.concat(series); + }, []); }, chartOptions() { return { @@ -119,6 +125,9 @@ export default { }, earliestDatapoint() { return this.chartData.reduce((acc, series) => { + if (!series.data.length) { + return acc; + } const [[timestamp]] = series.data.sort(([a], [b]) => { if (a < b) { return -1; @@ -129,6 +138,9 @@ export default { return timestamp < acc || acc === null ? timestamp : acc; }, null); }, + isMultiSeries() { + return this.tooltip.content.length > 1; + }, recentDeployments() { return this.deploymentData.reduce((acc, deployment) => { if (deployment.created_at >= this.earliestDatapoint) { @@ -175,9 +187,6 @@ export default { this.setSvg('scroll-handle'); }, methods: { - concatenateResults(results) { - return results.reduce((acc, result) => acc.concat(result.values), []); - }, formatLegendLabel(query) { return `${query.label}`; }, @@ -192,7 +201,7 @@ export default { ); this.tooltip.sha = deploy.sha.substring(0, 8); } else { - const { seriesName } = seriesData; + const { seriesName, color } = seriesData; // seriesData.value contains the chart's [x, y] value pair // seriesData.value[1] is threfore the chart y value const value = seriesData.value[1].toFixed(3); @@ -200,6 +209,7 @@ export default { this.tooltip.content.push({ name: seriesName, value, + color, }); } }); @@ -236,29 +246,38 @@ export default { :data="chartData" :option="chartOptions" :format-tooltip-text="formatTooltipText" - :thresholds="alertData" + :thresholds="thresholds" :width="width" :height="height" @updated="onChartUpdated" > - <template slot="tooltipTitle"> - <div v-if="tooltip.isDeployment"> + <template v-if="tooltip.isDeployment"> + <template slot="tooltipTitle"> {{ __('Deployed') }} - </div> - {{ tooltip.title }} - </template> - <template slot="tooltipContent"> - <div v-if="tooltip.isDeployment" class="d-flex align-items-center"> + </template> + <div slot="tooltipContent" class="d-flex align-items-center"> <icon name="commit" class="mr-2" /> {{ tooltip.sha }} </div> - <template v-else> + </template> + <template v-else> + <template slot="tooltipTitle"> + <div class="text-nowrap"> + {{ tooltip.title }} + </div> + </template> + <template slot="tooltipContent"> <div v-for="(content, key) in tooltip.content" :key="key" class="d-flex justify-content-between" > - {{ content.name }} {{ content.value }} + <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> + {{ content.name }} + </gl-chart-series-label> + <div class="prepend-left-32"> + {{ content.value }} + </div> </div> </template> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index ba6a17827f7..f2bd4150b6d 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,5 +1,6 @@ <script> import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import _ from 'underscore'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import '~/vue_shared/mixins/is_ee'; @@ -9,6 +10,8 @@ import MonitorAreaChart from './charts/area.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; +import { timeWindows } from '../constants'; +import { getTimeDiff } from '../utils'; const sidebarAnimationDuration = 150; let sidebarMutationObserver; @@ -87,6 +90,10 @@ export default { type: String, required: true, }, + showTimeWindowDropdown: { + type: Boolean, + required: true, + }, }, data() { return { @@ -94,6 +101,7 @@ export default { state: 'gettingStarted', showEmptyState: true, elWidth: 0, + selectedTimeWindow: '', }; }, created() { @@ -102,6 +110,9 @@ export default { deploymentEndpoint: this.deploymentEndpoint, environmentsEndpoint: this.environmentsEndpoint, }); + + this.timeWindows = timeWindows; + this.selectedTimeWindow = this.timeWindows.eightHours; }, beforeDestroy() { if (sidebarMutationObserver) { @@ -142,8 +153,13 @@ export default { } }, methods: { - getGraphAlerts(graphId) { - return this.alertData ? this.alertData[graphId] || {} : {}; + getGraphAlerts(queries) { + if (!this.allAlerts) return {}; + const metricIdsForChart = queries.map(q => q.metricId); + return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId)); + }, + getGraphAlertValues(queries) { + return Object.values(this.getGraphAlerts(queries)); }, getGraphsData() { this.state = 'loading'; @@ -160,33 +176,73 @@ export default { this.state = 'unableToConnect'; }); }, + getGraphsDataWithTime(timeFrame) { + this.state = 'loading'; + this.showEmptyState = true; + this.service + .getGraphsData(getTimeDiff(this.timeWindows[timeFrame])) + .then(data => { + this.store.storeMetrics(data); + this.selectedTimeWindow = this.timeWindows[timeFrame]; + }) + .catch(() => { + Flash(s__('Metrics|Not enough data to display')); + }) + .finally(() => { + this.showEmptyState = false; + }); + }, onSidebarMutation() { setTimeout(() => { this.elWidth = this.$el.clientWidth; }, sidebarAnimationDuration); }, + activeTimeWindow(key) { + return this.timeWindows[key] === this.selectedTimeWindow; + }, }, }; </script> <template> <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> - <div v-if="environmentsEndpoint" class="environments d-flex align-items-center"> - <strong>{{ s__('Metrics|Environment') }}</strong> - <gl-dropdown - class="prepend-left-10 js-environments-dropdown" - toggle-class="dropdown-menu-toggle" - :text="currentEnvironmentName" - :disabled="store.environmentsData.length === 0" - > - <gl-dropdown-item - v-for="environment in store.environmentsData" - :key="environment.id" - :active="environment.name === currentEnvironmentName" - active-class="is-active" - >{{ environment.name }}</gl-dropdown-item + <div + v-if="environmentsEndpoint" + class="dropdowns d-flex align-items-center justify-content-between" + > + <div class="d-flex align-items-center"> + <strong>{{ s__('Metrics|Environment') }}</strong> + <gl-dropdown + class="prepend-left-10 js-environments-dropdown" + toggle-class="dropdown-menu-toggle" + :text="currentEnvironmentName" + :disabled="store.environmentsData.length === 0" + > + <gl-dropdown-item + v-for="environment in store.environmentsData" + :key="environment.id" + :active="environment.name === currentEnvironmentName" + active-class="is-active" + >{{ environment.name }}</gl-dropdown-item + > + </gl-dropdown> + </div> + <div v-if="showTimeWindowDropdown" class="d-flex align-items-center"> + <strong>{{ s__('Metrics|Show last') }}</strong> + <gl-dropdown + class="prepend-left-10 js-time-window-dropdown" + toggle-class="dropdown-menu-toggle" + :text="selectedTimeWindow" > - </gl-dropdown> + <gl-dropdown-item + v-for="(value, key) in timeWindows" + :key="key" + :active="activeTimeWindow(key)" + @click="getGraphsDataWithTime(key)" + >{{ value }}</gl-dropdown-item + > + </gl-dropdown> + </div> </div> <graph-group v-for="(groupData, index) in store.groups" @@ -199,17 +255,15 @@ export default { :key="graphIndex" :graph-data="graphData" :deployment-data="store.deploymentData" - :alert-data="getGraphAlerts(graphData.id)" + :thresholds="getGraphAlertValues(graphData.queries)" :container-width="elWidth" group-id="monitor-area-chart" > <alert-widget - v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData.id" + v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData" :alerts-endpoint="alertsEndpoint" - :label="getGraphLabel(graphData)" - :current-alerts="getQueryAlerts(graphData)" - :custom-metric-id="graphData.id" - :alert-data="alertData[graphData.id]" + :relevant-queries="graphData.queries" + :alerts-to-manage="getGraphAlerts(graphData.queries)" @setAlerts="setAlerts" /> </monitor-area-chart> diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 55ecf3b5334..9e5d0d0fd28 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const chartHeight = 300; export const graphTypes = { @@ -7,3 +9,14 @@ export const graphTypes = { export const lineTypes = { default: 'solid', }; + +export const timeWindows = { + thirtyMinutes: __('30 minutes'), + threeHours: __('3 hours'), + eightHours: __('8 hours'), + oneDay: __('1 day'), + threeDays: __('3 days'), + oneWeek: __('1 week'), +}; + +export const msPerMinute = 60000; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 9d78b5ea110..2b4ddd7afbc 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -14,6 +14,7 @@ export default () => { props: { ...el.dataset, hasMetrics: parseBoolean(el.dataset.hasMetrics), + showTimeWindowDropdown: gon.features.metricsTimeWindow, }, }); }, diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 24b4acaf6da..5fcc2c8cfac 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -32,11 +32,11 @@ export default class MonitoringService { this.environmentsEndpoint = environmentsEndpoint; } - getGraphsData() { - return backOffRequest(() => axios.get(this.metricsEndpoint)) + getGraphsData(params = {}) { + return backOffRequest(() => axios.get(this.metricsEndpoint, { params })) .then(resp => resp.data) .then(response => { - if (!response || !response.data) { + if (!response || !response.data || !response.success) { throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); } return response.data; diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 70635059bd9..9761fe168be 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -27,10 +27,47 @@ function removeTimeSeriesNoData(queries) { return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []); } +// Metrics and queries are currently stored 1:1, so `queries` is an array of length one. +// We want to group queries onto a single chart by title & y-axis label. +// This function will no longer be required when metrics:queries are 1:many, +// though there is no consequence if the function stays in use. +// @param metrics [Array<Object>] +// Ex) [ +// { id: 1, title: 'title', y_label: 'MB', queries: [{ ...query1Attrs }] }, +// { id: 2, title: 'title', y_label: 'MB', queries: [{ ...query2Attrs }] }, +// { id: 3, title: 'new title', y_label: 'MB', queries: [{ ...query3Attrs }] } +// ] +// @return [Array<Object>] +// Ex) [ +// { title: 'title', y_label: 'MB', queries: [{ metricId: 1, ...query1Attrs }, +// { metricId: 2, ...query2Attrs }] }, +// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]} +// ] +function groupQueriesByChartInfo(metrics) { + const metricsByChart = metrics.reduce((accumulator, metric) => { + const { id, queries, ...chart } = metric; + + const chartKey = `${chart.title}|${chart.y_label}`; + accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] }; + + queries.forEach(queryAttrs => + accumulator[chartKey].queries.push({ metricId: id.toString(), ...queryAttrs }), + ); + + return accumulator; + }, {}); + + return Object.values(metricsByChart); +} + function normalizeMetrics(metrics) { - return metrics.map(metric => { + const groupedMetrics = groupQueriesByChartInfo(metrics); + + return groupedMetrics.map(metric => { const queries = metric.queries.map(query => ({ ...query, + // custom metrics do not require a label, so we should ensure this attribute is defined + label: query.label || metric.y_label, result: query.result.map(result => ({ ...result, values: result.values.map(([timestamp, value]) => [ diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js new file mode 100644 index 00000000000..e379827b769 --- /dev/null +++ b/app/assets/javascripts/monitoring/utils.js @@ -0,0 +1,34 @@ +import { timeWindows, msPerMinute } from './constants'; + +/** + * method that converts a predetermined time window to minutes + * defaults to 8 hours as the default option + * @param {String} timeWindow - The time window to convert to minutes + * @returns {number} The time window in minutes + */ +const getTimeDifferenceMinutes = timeWindow => { + switch (timeWindow) { + case timeWindows.thirtyMinutes: + return 30; + case timeWindows.threeHours: + return 60 * 3; + case timeWindows.oneDay: + return 60 * 24 * 1; + case timeWindows.threeDays: + return 60 * 24 * 3; + case timeWindows.oneWeek: + return 60 * 24 * 7 * 1; + default: + return 60 * 8; + } +}; + +export const getTimeDiff = selectedTimeWindow => { + const end = Date.now(); + const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow); + const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime(); + + return { start, end }; +}; + +export default {}; diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js index d3d125a1859..ad7276132b9 100644 --- a/app/assets/javascripts/pages/admin/groups/edit/index.js +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -1,3 +1,3 @@ -import groupAvatar from '~/group_avatar'; +import initAvatarPicker from '~/avatar_picker'; -document.addEventListener('DOMContentLoaded', groupAvatar); +document.addEventListener('DOMContentLoaded', initAvatarPicker); diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js index 21f1ce222ac..6de740ee9ce 100644 --- a/app/assets/javascripts/pages/admin/groups/new/index.js +++ b/app/assets/javascripts/pages/admin/groups/new/index.js @@ -1,9 +1,9 @@ import BindInOut from '../../../../behaviors/bind_in_out'; import Group from '../../../../group'; -import groupAvatar from '../../../../group_avatar'; +import initAvatarPicker from '~/avatar_picker'; document.addEventListener('DOMContentLoaded', () => { BindInOut.initAll(); new Group(); // eslint-disable-line no-new - groupAvatar(); + initAvatarPicker(); }); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 01ef445c901..d036ff07d89 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -1,4 +1,4 @@ -import groupAvatar from '~/group_avatar'; +import initAvatarPicker from '~/avatar_picker'; import TransferDropdown from '~/groups/transfer_dropdown'; import initConfirmDangerModal from '~/confirm_danger_modal'; import initSettingsPanels from '~/settings_panels'; @@ -9,7 +9,7 @@ import groupsSelect from '~/groups_select'; import projectSelect from '~/project_select'; document.addEventListener('DOMContentLoaded', () => { - groupAvatar(); + initAvatarPicker(); new TransferDropdown(); // eslint-disable-line no-new initConfirmDangerModal(); initSettingsPanels(); diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/groups/labels/edit/index.js +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/groups/labels/new/index.js +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index b2f275dc5ea..57b53eb9e5d 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -1,9 +1,9 @@ import BindInOut from '~/behaviors/bind_in_out'; import Group from '~/group'; -import groupAvatar from '~/group_avatar'; +import initAvatarPicker from '~/avatar_picker'; document.addEventListener('DOMContentLoaded', () => { BindInOut.initAll(); new Group(); // eslint-disable-line no-new - groupAvatar(); + initAvatarPicker(); }); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 899d5925956..278c35d3846 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -3,7 +3,7 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; -import fileUpload from '~/lib/utils/file_upload'; +import initAvatarPicker from '~/avatar_picker'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectPermissionsSettings from '../shared/permissions'; @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { setupProjectEdit(); // Initialize expandable settings panels initSettingsPanels(); - fileUpload('.js-choose-project-avatar-button', '.js-project-avatar-input'); + initAvatarPicker(); initProjectPermissionsSettings(); initConfirmDangerModal(); mountBadgeSettings(PROJECT_BADGE); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 8987c8e3f47..0447d1f79fb 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -4,9 +4,11 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; import initIssueableApp from '~/issue_show'; +import initRelatedMergeRequestsApp from '~/related_merge_requests'; export default function() { initIssueableApp(); + initRelatedMergeRequestsApp(); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/projects/labels/edit/index.js +++ b/app/assets/javascripts/pages/projects/labels/edit/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js index fa81ad914ba..83d6ac9fd14 100644 --- a/app/assets/javascripts/pages/projects/labels/new/index.js +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -1,3 +1,3 @@ -import Labels from '~/labels'; +import Labels from 'ee_else_ce/labels'; document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 2b32a6e4a98..0d5afe04e8e 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -57,6 +57,9 @@ export default { }, }, computed: { + boundary() { + return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; + }, status() { return this.job && this.job.status ? this.job.status : {}; }, @@ -104,7 +107,7 @@ export default { <div class="ci-job-component"> <gl-link v-if="status.has_details" - v-gl-tooltip + v-gl-tooltip="{ boundary, placement: 'bottom' }" :href="status.details_path" :title="tooltipText" :class="cssClassJobName" @@ -115,7 +118,7 @@ export default { <div v-else - v-gl-tooltip + v-gl-tooltip="{ boundary, placement: 'bottom' }" :title="tooltipText" :class="cssClassJobName" class="js-job-component-tooltip non-details-job-component" diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index 1bfab2a7fc0..02451839330 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -27,7 +27,8 @@ export default { <template> <span class="ci-job-name-component"> <ci-icon :status="status" /> - - <span class="ci-status-text"> {{ name }} </span> + <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom"> + {{ name }} + </span> </span> </template> diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue new file mode 100644 index 00000000000..52d4b75a3a1 --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue @@ -0,0 +1,121 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { sprintf, n__, s__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; +import { parseIssuableData } from '../../issue_show/utils/parse_data'; + +export default { + name: 'RelatedMergeRequests', + components: { + Icon, + GlLoadingIcon, + RelatedIssuableItem, + }, + props: { + endpoint: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState(['isFetchingMergeRequests', 'mergeRequests', 'totalCount']), + closingMergeRequestsText() { + if (!this.hasClosingMergeRequest) { + return ''; + } + + const mrText = n__( + 'When this merge request is accepted', + 'When these merge requests are accepted', + this.totalCount, + ); + + return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText }); + }, + }, + mounted() { + this.setInitialState({ apiEndpoint: this.endpoint }); + this.fetchMergeRequests(); + }, + created() { + this.hasClosingMergeRequest = parseIssuableData().hasClosingMergeRequest; + }, + methods: { + ...mapActions(['setInitialState', 'fetchMergeRequests']), + getAssignees(mr) { + if (mr.assignees) { + return mr.assignees; + } + + return mr.assignee ? [mr.assignee] : []; + }, + }, +}; +</script> + +<template> + <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)"> + <div id="merge-requests" class="card-slim mt-3"> + <div class="card-header"> + <div class="card-title mt-0 mb-0 h5 merge-requests-title"> + <span class="mr-1"> + {{ __('Related merge requests') }} + </span> + <div v-if="totalCount" class="d-inline-flex lh-100 align-middle"> + <div class="mr-count-badge"> + <div class="mr-count-badge-count"> + <svg class="s16 mr-1 text-secondary"> + <icon name="merge-request" class="mr-1 text-secondary" /> + </svg> + <span class="js-items-count">{{ totalCount }}</span> + </div> + </div> + </div> + </div> + </div> + <div> + <div + v-if="isFetchingMergeRequests" + class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon" + > + <gl-loading-icon label="Fetching related merge requests" class="py-2" /> + </div> + <ul v-else class="content-list related-items-list"> + <li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0"> + <related-issuable-item + :id-key="mr.id" + :display-reference="mr.reference" + :title="mr.title" + :milestone="mr.milestone" + :assignees="getAssignees(mr)" + :created-at="mr.created_at" + :closed-at="mr.closed_at" + :merged-at="mr.merged_at" + :path="mr.web_url" + :state="mr.state" + :is-merge-request="true" + :pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status" + path-id-separator="!" + /> + </li> + </ul> + </div> + </div> + <div + v-if="hasClosingMergeRequest && !isFetchingMergeRequests" + class="issue-closed-by-widget second-block" + > + {{ closingMergeRequestsText }} + </div> + </div> +</template> diff --git a/app/assets/javascripts/related_merge_requests/index.js b/app/assets/javascripts/related_merge_requests/index.js new file mode 100644 index 00000000000..092ff1df00f --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import RelatedMergeRequests from './components/related_merge_requests.vue'; +import createStore from './store'; + +export default function initRelatedMergeRequests() { + const relatedMergeRequestsElement = document.querySelector('#js-related-merge-requests'); + + if (relatedMergeRequestsElement) { + const { endpoint, projectPath, projectNamespace } = relatedMergeRequestsElement.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el: relatedMergeRequestsElement, + components: { + RelatedMergeRequests, + }, + store: createStore(), + render: createElement => + createElement('related-merge-requests', { + props: { endpoint, projectNamespace, projectPath }, + }), + }); + } +} diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js new file mode 100644 index 00000000000..69abeaaf7db --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/store/actions.js @@ -0,0 +1,37 @@ +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import { normalizeHeaders } from '~/lib/utils/common_utils'; +import * as types from './mutation_types'; + +const REQUEST_PAGE_COUNT = 100; + +export const setInitialState = ({ commit }, props) => { + commit(types.SET_INITIAL_STATE, props); +}; + +export const requestData = ({ commit }) => commit(types.REQUEST_DATA); + +export const receiveDataSuccess = ({ commit }, data) => commit(types.RECEIVE_DATA_SUCCESS, data); + +export const receiveDataError = ({ commit }) => commit(types.RECEIVE_DATA_ERROR); + +export const fetchMergeRequests = ({ state, dispatch }) => { + dispatch('requestData'); + + return axios + .get(`${state.apiEndpoint}?per_page=${REQUEST_PAGE_COUNT}`) + .then(res => { + const { headers, data } = res; + const total = Number(normalizeHeaders(headers)['X-TOTAL']) || 0; + + dispatch('receiveDataSuccess', { data, total }); + }) + .catch(() => { + dispatch('receiveDataError'); + createFlash(s__('Something went wrong while fetching related merge requests.')); + }); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/related_merge_requests/store/index.js b/app/assets/javascripts/related_merge_requests/store/index.js new file mode 100644 index 00000000000..dcb70c22bcb --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/store/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import createState from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default () => + new Vuex.Store({ + state: createState(), + actions, + mutations, + }); diff --git a/app/assets/javascripts/related_merge_requests/store/mutation_types.js b/app/assets/javascripts/related_merge_requests/store/mutation_types.js new file mode 100644 index 00000000000..31d4fe032e1 --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/store/mutation_types.js @@ -0,0 +1,4 @@ +export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; +export const REQUEST_DATA = 'REQUEST_DATA'; +export const RECEIVE_DATA_SUCCESS = 'RECEIVE_DATA_SUCCESS'; +export const RECEIVE_DATA_ERROR = 'RECEIVE_DATA_ERROR'; diff --git a/app/assets/javascripts/related_merge_requests/store/mutations.js b/app/assets/javascripts/related_merge_requests/store/mutations.js new file mode 100644 index 00000000000..11ca28a5fb9 --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/store/mutations.js @@ -0,0 +1,19 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_STATE](state, { apiEndpoint }) { + state.apiEndpoint = apiEndpoint; + }, + [types.REQUEST_DATA](state) { + state.isFetchingMergeRequests = true; + }, + [types.RECEIVE_DATA_SUCCESS](state, { data, total }) { + state.isFetchingMergeRequests = false; + state.mergeRequests = data; + state.totalCount = total; + }, + [types.RECEIVE_DATA_ERROR](state) { + state.isFetchingMergeRequests = false; + state.hasErrorFetchingMergeRequests = true; + }, +}; diff --git a/app/assets/javascripts/related_merge_requests/store/state.js b/app/assets/javascripts/related_merge_requests/store/state.js new file mode 100644 index 00000000000..bc3468a025b --- /dev/null +++ b/app/assets/javascripts/related_merge_requests/store/state.js @@ -0,0 +1,7 @@ +export default () => ({ + apiEndpoint: '', + isFetchingMergeRequests: false, + hasErrorFetchingMergeRequests: false, + mergeRequests: [], + totalCount: 0, +}); diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 2946fbc6a1f..04fba43b2f3 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -13,6 +13,11 @@ export default { type: String, required: true, }, + statusIconSize: { + type: Number, + required: false, + default: 32, + }, }, computed: { iconName() { @@ -45,6 +50,6 @@ export default { }" class="report-block-list-icon" > - <icon :name="iconName" :size="32" /> + <icon :name="iconName" :size="statusIconSize" /> </div> </template> diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue index 839e86bdf17..d2106f9ad2e 100644 --- a/app/assets/javascripts/reports/components/report_item.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -24,6 +24,11 @@ export default { type: String, required: true, }, + statusIconSize: { + type: Number, + required: false, + default: 32, + }, isNew: { type: Boolean, required: false, @@ -34,7 +39,7 @@ export default { </script> <template> <li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue"> - <issue-status-icon :status="status" class="append-right-5" /> + <issue-status-icon :status="status" :status-icon-size="statusIconSize" class="append-right-5" /> <component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" /> </li> diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue new file mode 100644 index 00000000000..32c9d6eccb8 --- /dev/null +++ b/app/assets/javascripts/serverless/components/area.vue @@ -0,0 +1,146 @@ +<script> +import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { debounceByAnimationFrame } from '~/lib/utils/common_utils'; +import dateFormat from 'dateformat'; +import { X_INTERVAL } from '../constants'; +import { validateGraphData } from '../utils'; + +let debouncedResize; + +export default { + components: { + GlAreaChart, + }, + inheritAttrs: false, + props: { + graphData: { + type: Object, + required: true, + validator: validateGraphData, + }, + containerWidth: { + type: Number, + required: true, + }, + }, + data() { + return { + tooltipPopoverTitle: '', + tooltipPopoverContent: '', + width: this.containerWidth, + }; + }, + computed: { + chartData() { + return this.graphData.queries.reduce((accumulator, query) => { + accumulator[query.unit] = query.result.reduce((acc, res) => acc.concat(res.values), []); + return accumulator; + }, {}); + }, + extractTimeData() { + return this.chartData.requests.map(data => data.time); + }, + generateSeries() { + return { + name: 'Invocations', + type: 'line', + data: this.chartData.requests.map(data => [data.time, data.value]), + symbolSize: 0, + }; + }, + getInterval() { + const { result } = this.graphData.queries[0]; + + if (result.length === 0) { + return 1; + } + + const split = result[0].values.reduce( + (acc, pair) => (pair.value > acc ? pair.value : acc), + 1, + ); + + return split < X_INTERVAL ? split : X_INTERVAL; + }, + chartOptions() { + return { + xAxis: { + name: 'time', + type: 'time', + axisLabel: { + formatter: date => dateFormat(date, 'h:MM TT'), + }, + data: this.extractTimeData, + nameTextStyle: { + padding: [18, 0, 0, 0], + }, + }, + yAxis: { + name: this.yAxisLabel, + nameTextStyle: { + padding: [0, 0, 36, 0], + }, + splitNumber: this.getInterval, + }, + legend: { + formatter: this.xAxisLabel, + }, + series: this.generateSeries, + }; + }, + xAxisLabel() { + return this.graphData.queries.map(query => query.label).join(', '); + }, + yAxisLabel() { + const [query] = this.graphData.queries; + return `${this.graphData.y_label} (${query.unit})`; + }, + }, + watch: { + containerWidth: 'onResize', + }, + beforeDestroy() { + window.removeEventListener('resize', debouncedResize); + }, + created() { + debouncedResize = debounceByAnimationFrame(this.onResize); + window.addEventListener('resize', debouncedResize); + }, + methods: { + formatTooltipText(params) { + const [seriesData] = params.seriesData; + this.tooltipPopoverTitle = dateFormat(params.value, 'dd mmm yyyy, h:MMTT'); + this.tooltipPopoverContent = `${this.yAxisLabel}: ${seriesData.value[1]}`; + }, + onResize() { + const { width } = this.$refs.areaChart.$el.getBoundingClientRect(); + this.width = width; + }, + }, +}; +</script> + +<template> + <div class="prometheus-graph"> + <div class="prometheus-graph-header"> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> + </div> + <gl-area-chart + ref="areaChart" + v-bind="$attrs" + :data="[]" + :option="chartOptions" + :format-tooltip-text="formatTooltipText" + :width="width" + :include-legend-avg-max="false" + > + <template slot="tooltipTitle"> + {{ tooltipPopoverTitle }} + </template> + <template slot="tooltipContent"> + {{ tooltipPopoverContent }} + </template> + </gl-area-chart> + </div> +</template> diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue index 4f89ad69129..b8906cfca4e 100644 --- a/app/assets/javascripts/serverless/components/function_details.vue +++ b/app/assets/javascripts/serverless/components/function_details.vue @@ -1,39 +1,77 @@ <script> +import _ from 'underscore'; +import { mapState, mapActions, mapGetters } from 'vuex'; import PodBox from './pod_box.vue'; import Url from './url.vue'; +import AreaChart from './area.vue'; +import MissingPrometheus from './missing_prometheus.vue'; export default { components: { PodBox, Url, + AreaChart, + MissingPrometheus, }, props: { func: { type: Object, required: true, }, + hasPrometheus: { + type: Boolean, + required: false, + default: false, + }, + clustersPath: { + type: String, + required: true, + }, + helpPath: { + type: String, + required: true, + }, + }, + data() { + return { + elWidth: 0, + }; }, computed: { name() { return this.func.name; }, description() { - return this.func.description; + return _.isString(this.func.description) ? this.func.description : ''; }, funcUrl() { return this.func.url; }, podCount() { - return this.func.podcount || 0; + return Number(this.func.podcount) || 0; }, + ...mapState(['graphData', 'hasPrometheusData']), + ...mapGetters(['hasPrometheusMissingData']), + }, + created() { + this.fetchMetrics({ + metricsPath: this.func.metricsUrl, + hasPrometheus: this.hasPrometheus, + }); + }, + mounted() { + this.elWidth = this.$el.clientWidth; + }, + methods: { + ...mapActions(['fetchMetrics']), }, }; </script> <template> <section id="serverless-function-details"> - <h3>{{ name }}</h3> - <div class="append-bottom-default"> + <h3 class="serverless-function-name">{{ name }}</h3> + <div class="append-bottom-default serverless-function-description"> <div v-for="(line, index) in description.split('\n')" :key="index">{{ line }}</div> </div> <url :uri="funcUrl" /> @@ -52,5 +90,13 @@ export default { </p> </div> <div v-else><p>No pods loaded at this time.</p></div> + + <area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" /> + <missing-prometheus + v-if="!hasPrometheus || hasPrometheusMissingData" + :help-path="helpPath" + :clusters-path="clustersPath" + :missing-data="hasPrometheusMissingData" + /> </section> </template> diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue index 773d18781fd..4b3bb078eae 100644 --- a/app/assets/javascripts/serverless/components/function_row.vue +++ b/app/assets/javascripts/serverless/components/function_row.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; import Url from './url.vue'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -19,6 +20,10 @@ export default { return this.func.name; }, description() { + if (!_.isString(this.func.description)) { + return ''; + } + const desc = this.func.description.split('\n'); if (desc.length > 1) { return desc[1]; diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index 4bde409f906..f9b4e789563 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -1,5 +1,6 @@ <script> -import { GlSkeletonLoading } from '@gitlab/ui'; +import { mapState, mapActions, mapGetters } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; import FunctionRow from './function_row.vue'; import EnvironmentRow from './environment_row.vue'; import EmptyState from './empty_state.vue'; @@ -9,14 +10,9 @@ export default { EnvironmentRow, FunctionRow, EmptyState, - GlSkeletonLoading, + GlLoadingIcon, }, props: { - functions: { - type: Object, - required: true, - default: () => ({}), - }, installed: { type: Boolean, required: true, @@ -29,17 +25,23 @@ export default { type: String, required: true, }, - loadingData: { - type: Boolean, - required: false, - default: true, - }, - hasFunctionData: { - type: Boolean, - required: false, - default: true, + statusPath: { + type: String, + required: true, }, }, + computed: { + ...mapState(['isLoading', 'hasFunctionData']), + ...mapGetters(['getFunctions']), + }, + created() { + this.fetchFunctions({ + functionsPath: this.statusPath, + }); + }, + methods: { + ...mapActions(['fetchFunctions']), + }, }; </script> @@ -47,14 +49,16 @@ export default { <section id="serverless-functions"> <div v-if="installed"> <div v-if="hasFunctionData"> - <template v-if="loadingData"> - <div v-for="j in 3" :key="j" class="gl-responsive-table-row"><gl-skeleton-loading /></div> - </template> + <gl-loading-icon + v-if="isLoading" + :size="2" + class="prepend-top-default append-bottom-default" + /> <template v-else> <div class="groups-list-tree-container"> <ul class="content-list group-list-tree"> <environment-row - v-for="(env, index) in functions" + v-for="(env, index) in getFunctions" :key="index" :env="env" :env-name="index" diff --git a/app/assets/javascripts/serverless/components/missing_prometheus.vue b/app/assets/javascripts/serverless/components/missing_prometheus.vue new file mode 100644 index 00000000000..6c19434f202 --- /dev/null +++ b/app/assets/javascripts/serverless/components/missing_prometheus.vue @@ -0,0 +1,63 @@ +<script> +import { GlButton, GlLink } from '@gitlab/ui'; +import { s__ } from '../../locale'; + +export default { + components: { + GlButton, + GlLink, + }, + props: { + clustersPath: { + type: String, + required: true, + }, + helpPath: { + type: String, + required: true, + }, + missingData: { + type: Boolean, + required: true, + }, + }, + computed: { + missingStateClass() { + return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state'; + }, + prometheusHelpPath() { + return `${this.helpPath}#prometheus-support`; + }, + description() { + return this.missingData + ? s__(`ServerlessDetails|Invocation metrics loading or not available at this time.`) + : s__( + `ServerlessDetails|Function invocation metrics require Prometheus to be installed first.`, + ); + }, + }, +}; +</script> + +<template> + <div class="row" :class="missingStateClass"> + <div class="col-12"> + <div class="text-content"> + <h4 class="state-title text-left">{{ s__(`ServerlessDetails|Invocations`) }}</h4> + <p class="state-description"> + {{ description }} + <gl-link :href="prometheusHelpPath">{{ + s__(`ServerlessDetails|More information`) + }}</gl-link + >. + </p> + + <div v-if="!missingData" class="text-left"> + <gl-button :href="clustersPath" variant="success"> + {{ s__('ServerlessDetails|Install Prometheus') }} + </gl-button> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js new file mode 100644 index 00000000000..35f77205f2c --- /dev/null +++ b/app/assets/javascripts/serverless/constants.js @@ -0,0 +1,3 @@ +export const MAX_REQUESTS = 3; // max number of times to retry + +export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js index 47a510d5fb5..2d3f086ffee 100644 --- a/app/assets/javascripts/serverless/serverless_bundle.js +++ b/app/assets/javascripts/serverless/serverless_bundle.js @@ -1,13 +1,7 @@ -import Visibility from 'visibilityjs'; import Vue from 'vue'; -import { s__ } from '../locale'; -import Flash from '../flash'; -import Poll from '../lib/utils/poll'; -import ServerlessStore from './stores/serverless_store'; -import ServerlessDetailsStore from './stores/serverless_details_store'; -import GetFunctionsService from './services/get_functions_service'; import Functions from './components/functions.vue'; import FunctionDetails from './components/function_details.vue'; +import { createStore } from './store'; export default class Serverless { constructor() { @@ -19,10 +13,12 @@ export default class Serverless { serviceUrl, serviceNamespace, servicePodcount, + serviceMetricsUrl, + prometheus, + clustersPath, + helpPath, } = document.querySelector('.js-serverless-function-details-page').dataset; const el = document.querySelector('#js-serverless-function-details'); - this.store = new ServerlessDetailsStore(); - const { store } = this; const service = { name: serviceName, @@ -31,20 +27,19 @@ export default class Serverless { url: serviceUrl, namespace: serviceNamespace, podcount: servicePodcount, + metricsUrl: serviceMetricsUrl, }; - this.store.updateDetailedFunction(service); this.functionDetails = new Vue({ el, - data() { - return { - state: store.state, - }; - }, + store: createStore(), render(createElement) { return createElement(FunctionDetails, { props: { - func: this.state.functionDetail, + func: service, + hasPrometheus: prometheus !== undefined, + clustersPath, + helpPath, }, }); }, @@ -54,95 +49,27 @@ export default class Serverless { '.js-serverless-functions-page', ).dataset; - this.service = new GetFunctionsService(statusPath); - this.knativeInstalled = installed !== undefined; - this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath); - this.initServerless(); - this.functionLoadCount = 0; - - if (statusPath && this.knativeInstalled) { - this.initPolling(); - } - } - } - - initServerless() { - const { store } = this; - const el = document.querySelector('#js-serverless-functions'); - - this.functions = new Vue({ - el, - data() { - return { - state: store.state, - }; - }, - render(createElement) { - return createElement(Functions, { - props: { - functions: this.state.functions, - installed: this.state.installed, - clustersPath: this.state.clustersPath, - helpPath: this.state.helpPath, - loadingData: this.state.loadingData, - hasFunctionData: this.state.hasFunctionData, - }, - }); - }, - }); - } - - initPolling() { - this.poll = new Poll({ - resource: this.service, - method: 'fetchData', - successCallback: data => this.handleSuccess(data), - errorCallback: () => Serverless.handleError(), - }); - - if (!Visibility.hidden()) { - this.poll.makeRequest(); - } else { - this.service - .fetchData() - .then(data => this.handleSuccess(data)) - .catch(() => Serverless.handleError()); - } - - Visibility.change(() => { - if (!Visibility.hidden() && !this.destroyed) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - } - - handleSuccess(data) { - if (data.status === 200) { - this.store.updateFunctionsFromServer(data.data); - this.store.updateLoadingState(false); - } else if (data.status === 204) { - /* Time out after 3 attempts to retrieve data */ - this.functionLoadCount += 1; - if (this.functionLoadCount === 3) { - this.poll.stop(); - this.store.toggleNoFunctionData(); - } + const el = document.querySelector('#js-serverless-functions'); + this.functions = new Vue({ + el, + store: createStore(), + render(createElement) { + return createElement(Functions, { + props: { + installed: installed !== undefined, + clustersPath, + helpPath, + statusPath, + }, + }); + }, + }); } } - static handleError() { - Flash(s__('Serverless|An error occurred while retrieving serverless components')); - } - destroy() { this.destroyed = true; - if (this.poll) { - this.poll.stop(); - } - this.functions.$destroy(); this.functionDetails.$destroy(); } diff --git a/app/assets/javascripts/serverless/services/get_functions_service.js b/app/assets/javascripts/serverless/services/get_functions_service.js deleted file mode 100644 index 303b42dc66c..00000000000 --- a/app/assets/javascripts/serverless/services/get_functions_service.js +++ /dev/null @@ -1,11 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; - -export default class GetFunctionsService { - constructor(endpoint) { - this.endpoint = endpoint; - } - - fetchData() { - return axios.get(this.endpoint); - } -} diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js new file mode 100644 index 00000000000..826501c9022 --- /dev/null +++ b/app/assets/javascripts/serverless/store/actions.js @@ -0,0 +1,113 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import statusCodes from '~/lib/utils/http_status'; +import { backOff } from '~/lib/utils/common_utils'; +import createFlash from '~/flash'; +import { MAX_REQUESTS } from '../constants'; + +export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING); +export const receiveFunctionsSuccess = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_SUCCESS, data); +export const receiveFunctionsNoDataSuccess = ({ commit }) => + commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS); +export const receiveFunctionsError = ({ commit }, error) => + commit(types.RECEIVE_FUNCTIONS_ERROR, error); + +export const receiveMetricsSuccess = ({ commit }, data) => + commit(types.RECEIVE_METRICS_SUCCESS, data); +export const receiveMetricsNoPrometheus = ({ commit }) => + commit(types.RECEIVE_METRICS_NO_PROMETHEUS); +export const receiveMetricsNoDataSuccess = ({ commit }, data) => + commit(types.RECEIVE_METRICS_NODATA_SUCCESS, data); +export const receiveMetricsError = ({ commit }, error) => + commit(types.RECEIVE_METRICS_ERROR, error); + +export const fetchFunctions = ({ dispatch }, { functionsPath }) => { + let retryCount = 0; + + dispatch('requestFunctionsLoading'); + + backOff((next, stop) => { + axios + .get(functionsPath) + .then(response => { + if (response.status === statusCodes.NO_CONTENT) { + retryCount += 1; + if (retryCount < MAX_REQUESTS) { + next(); + } else { + stop(null); + } + } else { + stop(response.data); + } + }) + .catch(stop); + }) + .then(data => { + if (data !== null) { + dispatch('receiveFunctionsSuccess', data); + } else { + dispatch('receiveFunctionsNoDataSuccess'); + } + }) + .catch(error => { + dispatch('receiveFunctionsError', error); + createFlash(error); + }); +}; + +export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => { + let retryCount = 0; + + if (!hasPrometheus) { + dispatch('receiveMetricsNoPrometheus'); + return; + } + + backOff((next, stop) => { + axios + .get(metricsPath) + .then(response => { + if (response.status === statusCodes.NO_CONTENT) { + retryCount += 1; + if (retryCount < MAX_REQUESTS) { + next(); + } else { + dispatch('receiveMetricsNoDataSuccess'); + stop(null); + } + } else { + stop(response.data); + } + }) + .catch(stop); + }) + .then(data => { + if (data === null) { + return; + } + + const updatedMetric = data.metrics; + const queries = data.metrics.queries.map(query => ({ + ...query, + result: query.result.map(result => ({ + ...result, + values: result.values.map(([timestamp, value]) => ({ + time: new Date(timestamp * 1000).toISOString(), + value: Number(value), + })), + })), + })); + + updatedMetric.queries = queries; + dispatch('receiveMetricsSuccess', updatedMetric); + }) + .catch(error => { + dispatch('receiveMetricsError', error); + createFlash(error); + }); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/serverless/store/getters.js b/app/assets/javascripts/serverless/store/getters.js new file mode 100644 index 00000000000..071f663d9d2 --- /dev/null +++ b/app/assets/javascripts/serverless/store/getters.js @@ -0,0 +1,10 @@ +import { translate } from '../utils'; + +export const hasPrometheusMissingData = state => state.hasPrometheus && !state.hasPrometheusData; + +// Convert the function list into a k/v grouping based on the environment scope + +export const getFunctions = state => translate(state.functions); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/serverless/store/index.js b/app/assets/javascripts/serverless/store/index.js new file mode 100644 index 00000000000..5f72060633e --- /dev/null +++ b/app/assets/javascripts/serverless/store/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import createState from './state'; + +Vue.use(Vuex); + +export const createStore = () => + new Vuex.Store({ + actions, + getters, + mutations, + state: createState(), + }); + +export default createStore(); diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js new file mode 100644 index 00000000000..25b2f7ac38a --- /dev/null +++ b/app/assets/javascripts/serverless/store/mutation_types.js @@ -0,0 +1,9 @@ +export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING'; +export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS'; +export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS'; +export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR'; + +export const RECEIVE_METRICS_NO_PROMETHEUS = 'RECEIVE_METRICS_NO_PROMETHEUS'; +export const RECEIVE_METRICS_SUCCESS = 'RECEIVE_METRICS_SUCCESS'; +export const RECEIVE_METRICS_NODATA_SUCCESS = 'RECEIVE_METRICS_NODATA_SUCCESS'; +export const RECEIVE_METRICS_ERROR = 'RECEIVE_METRICS_ERROR'; diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js new file mode 100644 index 00000000000..991f32a275d --- /dev/null +++ b/app/assets/javascripts/serverless/store/mutations.js @@ -0,0 +1,38 @@ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_FUNCTIONS_LOADING](state) { + state.isLoading = true; + }, + [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) { + state.functions = data; + state.isLoading = false; + state.hasFunctionData = true; + }, + [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) { + state.isLoading = false; + state.hasFunctionData = false; + }, + [types.RECEIVE_FUNCTIONS_ERROR](state, error) { + state.error = error; + state.hasFunctionData = false; + state.isLoading = false; + }, + [types.RECEIVE_METRICS_SUCCESS](state, data) { + state.isLoading = false; + state.hasPrometheusData = true; + state.graphData = data; + }, + [types.RECEIVE_METRICS_NODATA_SUCCESS](state) { + state.isLoading = false; + state.hasPrometheusData = false; + }, + [types.RECEIVE_METRICS_ERROR](state, error) { + state.hasPrometheusData = false; + state.error = error; + }, + [types.RECEIVE_METRICS_NO_PROMETHEUS](state) { + state.hasPrometheusData = false; + state.hasPrometheus = false; + }, +}; diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js new file mode 100644 index 00000000000..afc3f37d7ba --- /dev/null +++ b/app/assets/javascripts/serverless/store/state.js @@ -0,0 +1,13 @@ +export default () => ({ + error: null, + isLoading: true, + + // functions + functions: [], + hasFunctionData: true, + + // function_details + hasPrometheus: true, + hasPrometheusData: false, + graphData: {}, +}); diff --git a/app/assets/javascripts/serverless/stores/serverless_details_store.js b/app/assets/javascripts/serverless/stores/serverless_details_store.js deleted file mode 100644 index 5394d2cded1..00000000000 --- a/app/assets/javascripts/serverless/stores/serverless_details_store.js +++ /dev/null @@ -1,11 +0,0 @@ -export default class ServerlessDetailsStore { - constructor() { - this.state = { - functionDetail: {}, - }; - } - - updateDetailedFunction(func) { - this.state.functionDetail = func; - } -} diff --git a/app/assets/javascripts/serverless/stores/serverless_store.js b/app/assets/javascripts/serverless/stores/serverless_store.js deleted file mode 100644 index 816d55a03f9..00000000000 --- a/app/assets/javascripts/serverless/stores/serverless_store.js +++ /dev/null @@ -1,29 +0,0 @@ -export default class ServerlessStore { - constructor(knativeInstalled = false, clustersPath, helpPath) { - this.state = { - functions: {}, - hasFunctionData: true, - loadingData: true, - installed: knativeInstalled, - clustersPath, - helpPath, - }; - } - - updateFunctionsFromServer(upstreamFunctions = []) { - this.state.functions = upstreamFunctions.reduce((rv, func) => { - const envs = rv; - envs[func.environment_scope] = (rv[func.environment_scope] || []).concat([func]); - - return envs; - }, {}); - } - - updateLoadingState(loadingData) { - this.state.loadingData = loadingData; - } - - toggleNoFunctionData() { - this.state.hasFunctionData = false; - } -} diff --git a/app/assets/javascripts/serverless/utils.js b/app/assets/javascripts/serverless/utils.js new file mode 100644 index 00000000000..8b9e96ce9aa --- /dev/null +++ b/app/assets/javascripts/serverless/utils.js @@ -0,0 +1,23 @@ +// Validate that the object coming in has valid query details and results +export const validateGraphData = data => + data.queries && + Array.isArray(data.queries) && + data.queries.filter(query => { + if (Array.isArray(query.result)) { + return query.result.filter(res => Array.isArray(res.values)).length === query.result.length; + } + + return false; + }).length === data.queries.length; + +export const translate = functions => + functions.reduce( + (acc, func) => + Object.assign(acc, { + [func.environment_scope]: (acc[func.environment_scope] || []).concat([func]), + }), + {}, + ); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index 780ced4d382..392eb6fb425 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -33,7 +33,7 @@ export default { </script> <template> <div class="space-children d-flex append-right-10 widget-status-icon"> - <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon /></div> + <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="md" /></div> <ci-icon v-else :status="statusObj" :size="24" /> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue index b25aebd7c98..2b5b2269ec8 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue @@ -40,12 +40,15 @@ export default { }, beforeDestroy() { document.body.removeEventListener('mouseup', this.stopDrag); - this.$refs.dragger.removeEventListener('mousedown', this.startDrag); + document.body.removeEventListener('touchend', this.stopDrag); + document.body.removeEventListener('mousemove', this.dragMove); + document.body.removeEventListener('touchmove', this.dragMove); }, methods: { dragMove(e) { if (!this.dragging) return; - const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left; + const moveX = e.pageX || e.touches[0].pageX; + const left = moveX - this.$refs.dragTrack.getBoundingClientRect().left; const dragTrackWidth = this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; @@ -60,11 +63,13 @@ export default { this.dragging = true; document.body.style.userSelect = 'none'; document.body.addEventListener('mousemove', this.dragMove); + document.body.addEventListener('touchmove', this.dragMove); }, stopDrag() { this.dragging = false; document.body.style.userSelect = ''; document.body.removeEventListener('mousemove', this.dragMove); + document.body.removeEventListener('touchmove', this.dragMove); }, prepareOnionSkin() { if (this.onionOldImgInfo && this.onionNewImgInfo) { @@ -82,6 +87,7 @@ export default { this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; document.body.addEventListener('mouseup', this.stopDrag); + document.body.addEventListener('touchend', this.stopDrag); } }, onionNewImgLoaded(imgInfo) { @@ -140,7 +146,14 @@ export default { </div> <div class="controls"> <div class="transparent"></div> - <div ref="dragTrack" class="drag-track" @mousedown="startDrag" @mouseup="stopDrag"> + <div + ref="dragTrack" + class="drag-track" + @mousedown="startDrag" + @mouseup="stopDrag" + @touchstart="startDrag" + @touchend="stopDrag" + > <div ref="dragger" :style="{ left: onionDraggerPixelPos }" class="dragger"></div> </div> <div class="opaque"></div> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue index eddafc759a2..ad3b3b81ac5 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue @@ -46,6 +46,8 @@ export default { window.removeEventListener('resize', this.resizeThrottled, false); document.body.removeEventListener('mouseup', this.stopDrag); document.body.removeEventListener('mousemove', this.dragMove); + document.body.removeEventListener('touchend', this.stopDrag); + document.body.removeEventListener('touchmove', this.dragMove); }, mounted() { window.addEventListener('resize', this.resize, false); @@ -54,7 +56,8 @@ export default { dragMove(e) { if (!this.dragging) return; - let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left; + const moveX = e.pageX || e.touches[0].pageX; + let leftValue = moveX - this.$refs.swipeFrame.getBoundingClientRect().left; const spaceLeft = 20; const { clientWidth } = this.$refs.swipeFrame; if (leftValue <= 0) { @@ -69,10 +72,12 @@ export default { startDrag() { this.dragging = true; document.body.addEventListener('mousemove', this.dragMove); + document.body.addEventListener('touchmove', this.dragMove); }, stopDrag() { this.dragging = false; document.body.removeEventListener('mousemove', this.dragMove); + document.body.removeEventListener('touchmove', this.dragMove); }, prepareSwipe() { if (this.swipeOldImgInfo && this.swipeNewImgInfo) { @@ -83,6 +88,7 @@ export default { Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2; document.body.addEventListener('mouseup', this.stopDrag); + document.body.addEventListener('touchend', this.stopDrag); } }, swipeNewImgLoaded(imgInfo) { @@ -143,6 +149,8 @@ export default { class="swipe-bar" @mousedown="startDrag" @mouseup="stopDrag" + @touchstart="startDrag" + @touchend="stopDrag" > <span class="top-handle"></span> <span class="bottom-handle"></span> </span> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 0cbcdbf2eb4..1bfa91500cb 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -39,7 +39,7 @@ export default { }, data() { return { - mouseOver: false, + dropdownOpen: false, }; }, computed: { @@ -123,8 +123,8 @@ export default { return this.$router.currentRoute.path === `/project${this.file.url}`; }, - toggleHover(over) { - this.mouseOver = over; + toggleDropdown(val) { + this.dropdownOpen = val; }, }, }; @@ -140,8 +140,7 @@ export default { class="file-row" role="button" @click="clickFile" - @mouseover="toggleHover(true)" - @mouseout="toggleHover(false)" + @mouseleave="toggleDropdown(false)" > <div class="file-row-name-container"> <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> @@ -160,7 +159,8 @@ export default { :is="extraComponent" v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')" :file="file" - :mouse-over="mouseOver" + :dropdown-open="dropdownOpen" + @toggle="toggleDropdown($event)" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index 27cfa8abb24..d4d18614f93 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -1,15 +1,17 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; -import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; -import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin'; +import { sprintf } from '~/locale'; +import IssueMilestone from '../../components/issue/issue_milestone.vue'; +import IssueAssignees from '../../components/issue/issue_assignees.vue'; +import relatedIssuableMixin from '../../mixins/related_issuable_mixin'; +import CiIcon from '../ci_icon.vue'; export default { name: 'IssueItem', components: { IssueMilestone, IssueAssignees, + CiIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -27,9 +29,9 @@ export default { return sprintf( '<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>', { - state: this.isOpen ? __('Opened') : __('Closed'), - timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords, - timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp, + state: this.stateText, + timeInWords: this.stateTimeInWords, + timestamp: this.stateTimestamp, }, ); }, @@ -84,6 +86,11 @@ export default { {{ pathIdSeparator }}{{ itemId }} </div> <div class="item-meta-child d-flex align-items-center"> + <span v-if="hasPipeline" class="mr-ci-status pr-2"> + <a :href="pipelineStatus.details_path"> + <ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" /> + </a> + </span> <issue-milestone v-if="hasMilestone" :milestone="milestone" diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue new file mode 100644 index 00000000000..071bae7f665 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue @@ -0,0 +1,74 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; +import highlight from '~/lib/utils/highlight'; +import { truncateNamespace } from '~/lib/utils/text_utility'; +import _ from 'underscore'; + +export default { + name: 'ProjectListItem', + components: { + Icon, + ProjectAvatar, + GlButton, + }, + props: { + project: { + type: Object, + required: true, + validator: p => _.isFinite(p.id) && _.isString(p.name) && _.isString(p.name_with_namespace), + }, + selected: { + type: Boolean, + required: true, + }, + matcher: { + type: String, + required: false, + default: '', + }, + }, + computed: { + truncatedNamespace() { + return truncateNamespace(this.project.name_with_namespace); + }, + highlightedProjectName() { + return highlight(this.project.name, this.matcher); + }, + }, + methods: { + onClick() { + this.$emit('click'); + }, + }, +}; +</script> +<template> + <gl-button + class="d-flex align-items-center btn pt-1 pb-1 border-0 project-list-item" + @click="onClick" + > + <icon + class="prepend-left-10 append-right-10 flex-shrink-0 position-top-0 js-selected-icon" + :class="{ 'js-selected visible': selected, 'js-unselected invisible': !selected }" + name="mobile-issue-close" + /> + <project-avatar class="flex-shrink-0 js-project-avatar" :project="project" :size="32" /> + <div class="d-flex flex-wrap project-namespace-name-container"> + <div + v-if="truncatedNamespace" + :title="project.name_with_namespace" + class="text-secondary text-truncate js-project-namespace" + > + {{ truncatedNamespace }} + <span v-if="truncatedNamespace" class="text-secondary">/ </span> + </div> + <div + :title="project.name" + class="js-project-name text-truncate" + v-html="highlightedProjectName" + ></div> + </div> + </gl-button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue new file mode 100644 index 00000000000..596fd48f96a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -0,0 +1,103 @@ +<script> +import _ from 'underscore'; +import { GlLoadingIcon } from '@gitlab/ui'; +import ProjectListItem from './project_list_item.vue'; + +const SEARCH_INPUT_TIMEOUT_MS = 500; + +export default { + name: 'ProjectSelector', + components: { + GlLoadingIcon, + ProjectListItem, + }, + props: { + projectSearchResults: { + type: Array, + required: true, + }, + selectedProjects: { + type: Array, + required: true, + }, + showNoResultsMessage: { + type: Boolean, + required: false, + default: false, + }, + showMinimumSearchQueryMessage: { + type: Boolean, + required: false, + default: false, + }, + showLoadingIndicator: { + type: Boolean, + required: false, + default: false, + }, + showSearchErrorMessage: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + searchQuery: '', + }; + }, + methods: { + projectClicked(project) { + this.$emit('projectClicked', project); + }, + isSelected(project) { + return Boolean(_.findWhere(this.selectedProjects, { id: project.id })); + }, + focusSearchInput() { + this.$refs.searchInput.focus(); + }, + onInput: _.debounce(function debouncedOnInput() { + this.$emit('searched', this.searchQuery); + }, SEARCH_INPUT_TIMEOUT_MS), + }, +}; +</script> +<template> + <div> + <input + ref="searchInput" + v-model="searchQuery" + :placeholder="__('Search your projects')" + type="search" + class="form-control mb-3 js-project-selector-input" + autofocus + @input="onInput" + /> + <div class="d-flex flex-column"> + <gl-loading-icon v-if="showLoadingIndicator" :size="2" class="py-2 px-4" /> + <div v-if="!showLoadingIndicator" class="d-flex flex-column"> + <project-list-item + v-for="project in projectSearchResults" + :key="project.id" + :selected="isSelected(project)" + :project="project" + :matcher="searchQuery" + class="js-project-list-item" + @click="projectClicked(project)" + /> + </div> + <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message"> + {{ __('Sorry, no projects matched your search') }} + </div> + <div + v-if="showMinimumSearchQueryMessage" + class="text-muted ml-2 js-minimum-search-query-message" + > + {{ __('Enter at least three characters to search') }} + </div> + <div v-if="showSearchErrorMessage" class="text-danger ml-2 js-search-error-message"> + {{ __('Something went wrong, unable to search projects') }} + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue new file mode 100644 index 00000000000..1f3d248e991 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue @@ -0,0 +1,40 @@ +<script> +import { debounceByAnimationFrame } from '~/lib/utils/common_utils'; +import $ from 'jquery'; + +export default { + data() { + return { + width: 0, + height: 0, + }; + }, + beforeDestroy() { + this.contentResizeHandler.off('content.resize', this.debouncedResize); + window.removeEventListener('resize', this.debouncedResize); + }, + created() { + this.debouncedResize = debounceByAnimationFrame(this.onResize); + + // Handle when we explicictly trigger a custom resize event + this.contentResizeHandler = $(document).on('content.resize', this.debouncedResize); + + // Handle window resize + window.addEventListener('resize', this.debouncedResize); + }, + methods: { + onResize() { + // Slot dimensions + const { clientWidth, clientHeight } = this.$refs.chartWrapper; + this.width = clientWidth; + this.height = clientHeight; + }, + }, +}; +</script> + +<template> + <div ref="chartWrapper"> + <slot :width="width" :height="height"> </slot> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index f66e81b1e08..9c258c4651f 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -75,6 +75,16 @@ export default { required: false, default: false, }, + enableScopedLabels: { + type: Boolean, + require: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + require: false, + default: '#', + }, }, computed: { hiddenInputName() { @@ -123,7 +133,12 @@ export default { @onValueClick="handleCollapsedValueClick" /> <dropdown-title :can-edit="canEdit" /> - <dropdown-value :labels="context.labels" :label-filter-base-path="labelFilterBasePath"> + <dropdown-value + :labels="context.labels" + :label-filter-base-path="labelFilterBasePath" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + :enable-scoped-labels="enableScopedLabels" + > <slot></slot> </dropdown-value> <div v-if="canEdit" class="selectbox js-selectbox" style="display: none;"> @@ -142,6 +157,8 @@ export default { :namespace="namespace" :labels="context.labels" :show-extra-options="!showCreate" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + :enable-scoped-labels="enableScopedLabels" /> <div class="dropdown-menu dropdown-select dropdown-menu-paging diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue index 498b507d11d..1eed8907bb7 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -31,6 +31,16 @@ export default { type: Boolean, required: true, }, + enableScopedLabels: { + type: Boolean, + require: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + require: false, + default: '#', + }, }, computed: { dropdownToggleText() { @@ -61,6 +71,8 @@ export default { :data-labels="labelsPath" :data-namespace-path="namespace" :data-show-any="showExtraOptions" + :data-scoped-labels="enableScopedLabels" + :data-scoped-labels-documentation-link="scopedLabelsDocumentationLink" type="button" class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" data-toggle="dropdown" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue index 6faf3fafad1..ddc488adbcb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -1,9 +1,11 @@ <script> -import tooltip from '~/vue_shared/directives/tooltip'; +import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue'; +import DropdownValueRegularLabel from './dropdown_value_regular_label.vue'; export default { - directives: { - tooltip, + components: { + DropdownValueScopedLabel, + DropdownValueRegularLabel, }, props: { labels: { @@ -14,6 +16,16 @@ export default { type: String, required: true, }, + enableScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, }, computed: { isEmpty() { @@ -30,6 +42,12 @@ export default { backgroundColor: label.color, }; }, + scopedLabelsDescription({ description = '' }) { + return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`; + }, + showScopedLabels({ title = '' }) { + return this.enableScopedLabels && title.indexOf('::') !== -1; + }, }, }; </script> @@ -44,17 +62,24 @@ export default { <span v-if="isEmpty" class="text-secondary"> <slot>{{ __('None') }}</slot> </span> - <a v-for="label in labels" v-else :key="label.id" :href="labelFilterUrl(label)"> - <span - v-tooltip - :style="labelStyle(label)" - :title="label.description" - class="badge color-label" - data-placement="bottom" - data-container="body" - > - {{ label.title }} - </span> - </a> + + <template v-for="label in labels" v-else> + <dropdown-value-scoped-label + v-if="showScopedLabels(label)" + :key="label.id" + :label="label" + :label-filter-url="labelFilterUrl(label)" + :label-style="labelStyle(label)" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + /> + + <dropdown-value-regular-label + v-else + :key="label.id" + :label="label" + :label-filter-url="labelFilterUrl(label)" + :label-style="labelStyle(label)" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue new file mode 100644 index 00000000000..282b181f11e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue @@ -0,0 +1,35 @@ +<script> +import { GlLink, GlTooltip } from '@gitlab/ui'; + +export default { + components: { + GlTooltip, + GlLink, + }, + props: { + label: { + type: Object, + required: true, + }, + labelStyle: { + type: Object, + required: true, + }, + labelFilterUrl: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <a ref="regularLabelRef" :href="labelFilterUrl"> + <span :style="labelStyle" class="badge color-label"> + {{ label.title }} + </span> + <gl-tooltip :target="() => $refs.regularLabelRef" placement="top" boundary="viewport"> + {{ label.description }} + </gl-tooltip> + </a> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue new file mode 100644 index 00000000000..ad5a86de166 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue @@ -0,0 +1,47 @@ +<script> +import { GlLink, GlTooltip } from '@gitlab/ui'; + +export default { + components: { + GlTooltip, + GlLink, + }, + props: { + label: { + type: Object, + required: true, + }, + labelStyle: { + type: Object, + required: true, + }, + scopedLabelsDocumentationLink: { + type: String, + required: true, + }, + labelFilterUrl: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <span class="d-inline-block position-relative scoped-label-wrapper"> + <a :href="labelFilterUrl"> + <span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label"> + {{ label.title }} + </span> + <gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport"> + <span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span + ><br /> + {{ label.description }} + </gl-tooltip> + </a> + + <gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label" + ><i class="fa fa-question-circle" :style="labelStyle"></i + ></gl-link> + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js index 455ae832234..8e0e4baa75a 100644 --- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { sprintf, __ } from '~/locale'; import { formatDate } from '~/lib/utils/datetime_utility'; import tooltip from '~/vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue'; @@ -58,6 +59,11 @@ const mixins = { required: false, default: '', }, + mergedAt: { + type: String, + required: false, + default: '', + }, milestone: { type: Object, required: false, @@ -83,6 +89,16 @@ const mixins = { required: false, default: false, }, + isMergeRequest: { + type: Boolean, + required: false, + default: false, + }, + pipelineStatus: { + type: Object, + required: false, + default: () => ({}), + }, }, components: { icon, @@ -95,12 +111,18 @@ const mixins = { hasState() { return this.state && this.state.length > 0; }, + hasPipeline() { + return this.isMergeRequest && this.pipelineStatus && Object.keys(this.pipelineStatus).length; + }, isOpen() { return this.state === 'opened'; }, isClosed() { return this.state === 'closed'; }, + isMerged() { + return this.state === 'merged'; + }, hasTitle() { return this.title.length > 0; }, @@ -108,9 +130,17 @@ const mixins = { return !_.isEmpty(this.milestone); }, iconName() { + if (this.isMergeRequest && this.isMerged) { + return 'merge'; + } + return this.isOpen ? 'issue-open-m' : 'issue-close'; }, iconClass() { + if (this.isMergeRequest && this.isClosed) { + return 'merge-request-status closed issue-token-state-icon-closed'; + } + return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed'; }, computedLinkElementType() { @@ -131,12 +161,44 @@ const mixins = { createdAtTimestamp() { return this.createdAt ? formatDate(new Date(this.createdAt)) : ''; }, + mergedAtTimestamp() { + return this.mergedAt ? formatDate(new Date(this.mergedAt)) : ''; + }, + mergedAtInWords() { + return this.mergedAt ? this.timeFormated(this.mergedAt) : ''; + }, closedAtInWords() { return this.closedAt ? this.timeFormated(this.closedAt) : ''; }, closedAtTimestamp() { return this.closedAt ? formatDate(new Date(this.closedAt)) : ''; }, + stateText() { + if (this.isMerged) { + return __('Merged'); + } + + return this.isOpen ? __('Opened') : __('Closed'); + }, + stateTimeInWords() { + if (this.isMerged) { + return this.mergedAtInWords; + } + + return this.isOpen ? this.createdAtInWords : this.closedAtInWords; + }, + stateTimestamp() { + if (this.isMerged) { + return this.mergedAtTimestamp; + } + + return this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp; + }, + pipelineStatusTooltip() { + return this.hasPipeline + ? sprintf(__('Pipeline: %{status}'), { status: this.pipelineStatus.label }) + : ''; + }, }, methods: { onRemoveRequest() { diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 86189143525..8aaa9772715 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,40 +6,29 @@ *= require cropper.css */ -/* - * Welcome to GitLab css! - * If you need to add or modify UI component that is common for many pages - * like a table or typography then make changes in the framework/ directory. - * If you need to add unique style that should affect only one page - use pages/ - * directory. - */ - +// Welcome to GitLab css! +// If you need to add or modify UI component that is common for many pages +// like a table or typography then make changes in the framework/ directory. +// If you need to add unique style that should affect only one page - use pages/ +// directory. @import "../../../node_modules/at.js/dist/css/jquery.atwho"; @import "../../../node_modules/pikaday/scss/pikaday"; @import "../../../node_modules/dropzone/dist/basic"; @import "../../../node_modules/select2/select2"; -/* - * GitLab UI framework - */ +// GitLab UI framework @import "framework"; -/* - * Font icons - */ +// Font icons @import "font-awesome"; -/* - * Page specific styles (issues, projects etc): - */ +// Page specific styles (issues, projects etc): @import "pages/**/*"; -/* - * Component specific styles, will be moved to gitlab-ui - */ +// Component specific styles, will be moved to gitlab-ui @import "components/**/*"; -/* - * Styles for JS behaviors. - */ +// Styles for JS behaviors. @import "behaviors"; + +@import "utilities"; diff --git a/app/assets/stylesheets/components/project_list_item.scss b/app/assets/stylesheets/components/project_list_item.scss new file mode 100644 index 00000000000..8e7c2c4398c --- /dev/null +++ b/app/assets/stylesheets/components/project_list_item.scss @@ -0,0 +1,24 @@ +.project-list-item { + &:not(:disabled):not(.disabled) { + &:focus, + &:active, + &:focus:active { + outline: none; + box-shadow: none; + } + } +} + +// When housed inside a modal, the edge of each item +// should extend to the edge of the modal. +.modal-body { + .project-list-item { + border-radius: 0; + margin-left: -$gl-padding; + margin-right: -$gl-padding; + + .project-namespace-name-container { + overflow: hidden; + } + } +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 695ce014659..ab8f397f3a0 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -139,6 +139,7 @@ @include btn-white; color: $gl-text-color; + white-space: nowrap; &:focus:active { outline: 0; diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 0d8e4afa76f..643b20c56bc 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -28,6 +28,10 @@ background-color: $red-100; border-color: $red-200; color: $red-700; + + a { + color: $red-700; + } } .bs-callout-warning { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index d72597a6147..db6c107210e 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -428,6 +428,7 @@ img.emoji { .mw-460 { max-width: 460px; } .mw-6em { max-width: 6em; } +.mw-70p { max-width: 70%; } .min-height-0 { min-height: 0; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 8d38310e8e6..53d3645cd63 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -331,6 +331,10 @@ span.idiff { padding: 5px $gl-padding; margin: 0; border-radius: $border-radius-default $border-radius-default 0 0; + + &.is-stuck { + border-radius: 0; + } } .file-header-content { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index c36c15a85be..1c23c14c2de 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -204,8 +204,10 @@ label { margin-top: #{$grid-size / 2}; } -.gl-field-error { +.gl-field-error, +.invalid-feedback { color: $red-500; + font-size: $gl-font-size; } .gl-show-field-errors { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7d9781ffb87..da1f196afdb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -110,6 +110,84 @@ $gray-800: #4f4f4f; $gray-900: #2e2e2e; $gray-950: #1f1f1f; +$greens: ( + '50': $green-50, + '100': $green-100, + '200': $green-200, + '300': $green-300, + '400': $green-400, + '500': $green-500, + '600': $green-600, + '700': $green-700, + '800': $green-800, + '900': $green-900, + '950': $green-950 +); + +$blues: ( + '50': $blue-50, + '100': $blue-100, + '200': $blue-200, + '300': $blue-300, + '400': $blue-400, + '500': $blue-500, + '600': $blue-600, + '700': $blue-700, + '800': $blue-800, + '900': $blue-900, + '950': $blue-950 +); + +$oranges: ( + '50': $orange-50, + '100': $orange-100, + '200': $orange-200, + '300': $orange-300, + '400': $orange-400, + '500': $orange-500, + '600': $orange-600, + '700': $orange-700, + '800': $orange-800, + '900': $orange-900, + '950': $orange-950 +); + +$reds: ( + '50': $red-50, + '100': $red-100, + '200': $red-200, + '300': $red-300, + '400': $red-400, + '500': $red-500, + '600': $red-600, + '700': $red-700, + '800': $red-800, + '900': $red-900, + '950': $red-950 +); + +$grays: ( + '50': $gray-50, + '100': $gray-100, + '200': $gray-200, + '300': $gray-300, + '400': $gray-400, + '500': $gray-500, + '600': $gray-600, + '700': $gray-700, + '800': $gray-800, + '900': $gray-900, + '950': $gray-950 +); + +$color-ranges: ( + 'primary': $blues, + 'secondary': $grays, + 'success': $greens, + 'warning': $oranges, + 'danger': $reds +); + // GitLab themes $indigo-50: #f7f7ff; @@ -219,6 +297,15 @@ $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; +$type-scale: ( + 1: 12px, + 2: 14px, + 3: 16px, + 4: 20px, + 5: 28px, + 6: 42px +); + /* * Lists */ @@ -575,6 +662,11 @@ $feature-toggle-text-color: #fff; $feature-toggle-color-enabled: #4a8bee; /* + * Monitor Charts + */ +$chart-tooltip-max-width: 512px; + +/* Stat Graph */ $stat-graph-common-bg: #f3f3f3; @@ -718,3 +810,8 @@ $compare-branches-sticky-header-height: 68px; - Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242 */ $enable-validation-icons: false; + +/* +Licenses +*/ +$license-header-cell-width: 150px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 54d985df9b5..5ea96392afa 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -7,12 +7,15 @@ cursor: pointer; @media (min-width: map-get($grid-breakpoints, md)) { - $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height; + // The `-1` below is to prevent two borders from clashing up against eachother - + // the bottom of the compare-versions header and the top of the file header + $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height - 1; position: -webkit-sticky; position: sticky; top: $mr-file-header-top; z-index: 102; + height: $mr-version-controls-height; &::before { content: ''; @@ -54,7 +57,8 @@ background-color: $gray-normal; } - a { + a, + button { color: $gray-700; } @@ -167,6 +171,23 @@ } } + .frame { + top: 0; + right: 0; + + &.old-diff { + /* only for commit / compare view */ + position: absolute; + } + + &.deleted { + margin: 0; + display: block; + top: 13px; + right: 7px; + } + } + .swipe-bar { display: block; height: 100%; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 0eb854ecf98..8e1ee51628d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -204,7 +204,7 @@ } .prometheus-graphs { - .environments { + .dropdowns { .dropdown-menu-toggle { svg { position: absolute; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6415d902ca6..9be3f8138a0 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -110,6 +110,16 @@ font-size: 0; margin-bottom: -5px; } + + .scoped-label-wrapper { + .color-label { + padding-right: $gl-padding-24; + } + + .scoped-label { + right: 12px; + } + } } .right-sidebar { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 6f98b4f7f13..e7fd7fab32b 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -402,3 +402,39 @@ .priority-labels-empty-state .svg-content img { max-width: $priority-label-empty-state-width; } + +.scoped-label-tooltip-title { + color: $indigo-300; +} + +.scoped-label-wrapper { + &.label-link .color-label a { + color: inherit; + } + + .color-label { + padding-right: $gl-padding-24; + } + + .scoped-label { + position: absolute; + top: 4px; + right: 8px; + padding: 0; + margin: 0; + line-height: $gl-line-height; + } +} + +// Label inside title of Delete Label Modal +.modal-header .page-title { + .scoped-label-wrapper { + .scoped-label { + line-height: 20px; + } + + span.color-label { + padding-right: $gl-padding-24; + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7f8b8ea8100..86b58c1b1b2 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -595,7 +595,6 @@ color: $gl-text-color; } - .git-merge-container { justify-content: space-between; flex: 1; @@ -737,7 +736,6 @@ background: $gray-light; color: $gl-text-color; margin-top: -1px; - border-top: 1px solid $border-color; .mr-version-menus-container { display: flex; @@ -760,6 +758,7 @@ .content-block { padding: $gl-padding-top $gl-padding; + border-bottom: 0; } .comments-disabled-notif { @@ -784,16 +783,18 @@ padding-right: 5px; } + // Shortening button height by 1px to make compare-versions + // header 56px and fit into our 8px design grid + button { + height: 34px; + } + @include media-breakpoint-up(md) { position: -webkit-sticky; position: sticky; top: $header-height + $mr-tabs-height; - width: 100%; - - &.is-fileTreeOpen { - margin-left: -16px; - width: calc(100% + 32px); - } + margin-left: -16px; + width: calc(100% + 32px); .mr-version-menus-container { flex-wrap: nowrap; @@ -805,7 +806,8 @@ } } -.merge-request-tabs-holder { +.merge-request-tabs-holder, +.epic-tabs-holder { top: $header-height; z-index: 250; background-color: $white-light; @@ -823,11 +825,6 @@ @include media-breakpoint-down(xs) { right: 0; } - - .merge-request-tabs-container { - padding-left: $gl-padding; - padding-right: $gl-padding; - } } .nav-links { @@ -835,11 +832,21 @@ } } -.with-performance-bar .merge-request-tabs-holder { - top: $header-height + $performance-bar-height; +.merge-request-tabs-holder.affix .merge-request-tabs-container, +.epic-tabs-holder.affix .epic-tabs-container { + padding-left: $gl-padding; + padding-right: $gl-padding; } -.merge-request-tabs { +.with-performance-bar { + .merge-request-tabs-holder, + .epic-tabs-holder { + top: $header-height + $performance-bar-height; + } +} + +.merge-request-tabs, +.epic-tabs { display: flex; flex-wrap: nowrap; margin-bottom: 0; @@ -847,7 +854,8 @@ } .limit-container-width { - .merge-request-tabs-container { + .merge-request-tabs-container, + .epic-tabs-container { max-width: $limited-layout-width; margin-left: auto; margin-right: auto; @@ -860,7 +868,8 @@ } } -.merge-request-tabs-container { +.merge-request-tabs-container, +.epic-tabs-container { display: flex; justify-content: space-between; @@ -878,10 +887,9 @@ } .limit-container-width:not(.container-limited) { - .merge-request-tabs-holder:not(.affix) { - .merge-request-tabs-container { - max-width: $limited-layout-width - ($gl-padding * 2); - } + .merge-request-tabs-holder:not(.affix) .merge-request-tabs-container, + .epic-tabs-holder:not(.affix) .epic-tabs-container { + max-width: $limited-layout-width - ($gl-padding * 2); } } diff --git a/app/assets/stylesheets/pages/monitor.scss b/app/assets/stylesheets/pages/monitor.scss new file mode 100644 index 00000000000..25ff5abd774 --- /dev/null +++ b/app/assets/stylesheets/pages/monitor.scss @@ -0,0 +1,5 @@ +.chart-tooltip > .popover { + min-width: 0; + width: max-content; + max-width: $chart-tooltip-max-width; +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index bb08440fda8..3e6aa43175e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -712,15 +712,9 @@ top: 8px; } +.ci-build-text, .ci-status-text { - max-width: 110px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: bottom; - display: inline-block; - position: relative; - font-weight: $gl-font-weight-normal; + font-weight: 200; } @mixin mini-pipeline-graph-color( @@ -912,26 +906,6 @@ button.mini-pipeline-graph-dropdown-toggle { flex: 1; } - // build name - .ci-build-text, - .ci-status-text { - font-weight: 200; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 70%; - margin-left: 2px; - display: inline-block; - - &::after { - content: ''; - display: block; - } - - @include media-breakpoint-down(xs) { - max-width: 60%; - } - } .ci-status-icon { @extend .append-right-8; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index e4ed685bd1b..7b0538dca20 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -232,7 +232,7 @@ } } -.settings-flex-row { +.content-list > .settings-flex-row { display: flex; align-items: center; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss new file mode 100644 index 00000000000..3648ec5e239 --- /dev/null +++ b/app/assets/stylesheets/utilities.scss @@ -0,0 +1,17 @@ +@each $variant, $range in $color-ranges { + @each $suffix, $color in $range { + #{'.bg-#{$variant}-#{$suffix}'} { + background-color: $color; + } + + #{'.text-#{$variant}-#{$suffix}'} { + color: $color; + } + } +} + +@each $index, $size in $type-scale { + #{'.text-#{$index}'} { + font-size: $size; + } +} diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index e0ecdb0c0e9..15f7ef881c8 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -89,7 +89,8 @@ class Admin::GroupsController < Admin::ApplicationController :request_access_enabled, :visibility_level, :require_two_factor_authentication, - :two_factor_grace_period + :two_factor_grace_period, + :project_creation_level ] end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 8ef3b6502df..85aeecbf90b 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,6 +7,9 @@ module IssuableActions included do before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_admin_issuable!, only: :bulk_update + before_action only: :show do + push_frontend_feature_flag(:scoped_labels, default_enabled: true) + end end def permitted_keys diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index c529aabf797..6d6e0cc6c7f 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -100,6 +100,7 @@ module IssuableCollections if @project options[:project_id] = @project.id + options[:attempt_project_search_optimizations] = true elsif @group options[:group_id] = @group.id options[:include_subgroups] = true diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 87b8ef03313..e936d771502 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -187,7 +187,8 @@ class GroupsController < Groups::ApplicationController :create_chat_team, :chat_team_name, :require_two_factor_authentication, - :two_factor_grace_period + :two_factor_grace_period, + :project_creation_level ] end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 6504fd6c08a..781eac7f080 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -88,4 +88,10 @@ class Projects::ApplicationController < ApplicationController def check_issues_available! return render_404 unless @project.feature_available?(:issues, current_user) end + + def allow_gitaly_ref_name_caching + ::Gitlab::GitalyClient.allow_ref_name_caching do + yield + end + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7e072788fc9..b04ffe80db4 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -9,6 +9,8 @@ class Projects::BlobController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper prepend_before_action :authenticate_user!, only: [:edit] + around_action :allow_gitaly_ref_name_caching, only: [:show] + before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 2510a31c9b3..f540ccee386 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -7,6 +7,7 @@ class Projects::CommitsController < Projects::ApplicationController include RendersCommits prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } + around_action :allow_gitaly_ref_name_caching before_action :whitelist_query_limiting, except: :commits_root before_action :require_non_empty_project before_action :assign_ref_vars, except: :commits_root diff --git a/app/controllers/projects/environments/prometheus_api_controller.rb b/app/controllers/projects/environments/prometheus_api_controller.rb new file mode 100644 index 00000000000..fd3320637b0 --- /dev/null +++ b/app/controllers/projects/environments/prometheus_api_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Projects::Environments::PrometheusApiController < Projects::ApplicationController + before_action :authorize_read_prometheus! + before_action :environment + + def proxy + result = Prometheus::ProxyService.new( + environment, + request.method, + params[:proxy_path], + params.permit! + ).execute + + if result.nil? + return render status: :accepted, json: { + status: _('processing'), + message: _('Not ready yet. Try again later.') + } + end + + if result[:status] == :success + render status: result[:http_status], json: result[:body] + else + render( + status: result[:http_status] || :bad_request, + json: { status: result[:status], message: result[:message] } + ) + end + end + + private + + def environment + @environment ||= project.environments.find(params[:id]) + end +end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 55d5fce9214..85628dd32d8 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -98,10 +98,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController def repo_type parse_repo_path unless defined?(@repo_type) - # When there a project did not exist, the parsed repo_type would be empty. - # In that case, we want to continue with a regular project repository. As we - # could create the project if the user pushing is allowed to do so. - @repo_type || Gitlab::GlRepository::PROJECT + + @repo_type end def handle_basic_authentication(login, password) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 34cb0416965..39ba2a651d4 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -16,6 +16,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] + around_action :allow_gitaly_ref_name_caching, only: [:index, :show] + def index @merge_requests = @issuables @@ -315,9 +317,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def serializer - ::Gitlab::GitalyClient.allow_ref_name_caching do - MergeRequestSerializer.new(current_user: current_user, project: merge_request.project) - end + MergeRequestSerializer.new(current_user: current_user, project: merge_request.project) end def define_edit_vars diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index c306ba3ffcf..22c4b8eef1f 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -8,6 +8,8 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] + around_action :allow_gitaly_ref_name_caching, only: [:index, :show] + wrap_parameters Ci::Pipeline POLLING_INTERVAL = 10_000 @@ -148,12 +150,10 @@ class Projects::PipelinesController < Projects::ApplicationController private def serialize_pipelines - ::Gitlab::GitalyClient.allow_ref_name_caching do - PipelineSerializer - .new(project: @project, current_user: @current_user) - .with_pagination(request, response) - .represent(@pipelines, disable_coverage: true, preload: true) - end + PipelineSerializer + .new(project: @project, current_user: @current_user) + .with_pagination(request, response) + .represent(@pipelines, disable_coverage: true, preload: true) end def render_show diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index b97fbe19bbf..b3447812ef2 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -4,6 +4,8 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath include TreeHelper + around_action :allow_gitaly_ref_name_caching, only: [:logs_tree] + before_action :require_non_empty_project before_action :validate_ref_id before_action :assign_ref_vars diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb index 39eca10134f..8c3d141c888 100644 --- a/app/controllers/projects/serverless/functions_controller.rb +++ b/app/controllers/projects/serverless/functions_controller.rb @@ -7,19 +7,14 @@ module Projects before_action :authorize_read_cluster! - INDEX_PRIMING_INTERVAL = 15_000 - INDEX_POLLING_INTERVAL = 60_000 - def index respond_to do |format| format.json do functions = finder.execute if functions.any? - Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL) render json: serialize_function(functions) else - Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL) head :no_content end end @@ -33,6 +28,8 @@ module Projects def show @service = serialize_function(finder.service(params[:environment_id], params[:id])) + @prometheus = finder.has_prometheus?(params[:environment_id]) + return not_found if @service.nil? respond_to do |format| @@ -44,10 +41,24 @@ module Projects end end + def metrics + respond_to do |format| + format.json do + metrics = finder.invocation_metrics(params[:environment_id], params[:id]) + + if metrics.nil? + head :no_content + else + render json: metrics + end + end + end + end + private def finder - Projects::Serverless::FunctionsFinder.new(project.clusters) + Projects::Serverless::FunctionsFinder.new(project) end def serialize_function(function) diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 90d53aa08ea..7509cc29a76 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -6,6 +6,8 @@ class Projects::TreeController < Projects::ApplicationController include CreatesCommit include ActionView::Helpers::SanitizeHelper + around_action :allow_gitaly_ref_name_caching, only: [:show] + before_action :require_non_empty_project, except: [:new, :create] before_action :assign_ref_vars before_action :assign_dir_vars, only: [:create_dir] diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index f76e6663995..89dc43a48a1 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,6 +10,8 @@ class ProjectsController < Projects::ApplicationController prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } + around_action :allow_gitaly_ref_name_caching, only: [:index, :show] + before_action :whitelist_query_limiting, only: [:create] before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve] before_action :redirect_git_extension, only: [:show] diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b6be2895d85..64c88505a16 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -83,7 +83,7 @@ class IssuableFinder # https://www.postgresql.org/docs/current/static/queries-with.html items = by_search(items) - items = sort(items) unless use_cte_for_count? + items = sort(items) items end @@ -91,7 +91,6 @@ class IssuableFinder def filter_items(items) items = by_project(items) items = by_group(items) - items = by_subquery(items) items = by_scope(items) items = by_created_at(items) items = by_updated_at(items) @@ -131,10 +130,12 @@ class IssuableFinder # This does not apply when we are using a CTE for the search, as the labels # GROUP BY is inside the subquery in that case, so we set labels_count to 1. # - # We always use CTE when searching in Groups if the feature flag is enabled, - # but never when searching in Projects. + # Groups and projects have separate feature flags to suggest the use + # of a CTE. The CTE will not be used if the sort doesn't support it, + # but will always be used for the counts here as we ignore sorting + # anyway. labels_count = label_names.any? ? label_names.count : 1 - labels_count = 1 if use_cte_for_count? + labels_count = 1 if use_cte_for_search? finder.execute.reorder(nil).group(:state).count.each do |key, value| counts[count_key(key)] += value / labels_count @@ -308,15 +309,14 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord - def use_subquery_for_search? - strong_memoize(:use_subquery_for_search) do - !force_cte? && attempt_group_search_optimizations? - end - end + def use_cte_for_search? + strong_memoize(:use_cte_for_search) do + next false unless search + next false unless Gitlab::Database.postgresql? + # Only simple unsorted & simple sorts can use CTE + next false if params[:sort].present? && !params[:sort].in?(klass.simple_sorts.keys) - def use_cte_for_count? - strong_memoize(:use_cte_for_count) do - force_cte? && attempt_group_search_optimizations? + attempt_group_search_optimizations? || attempt_project_search_optimizations? end end @@ -331,12 +331,15 @@ class IssuableFinder end def attempt_group_search_optimizations? - search && - Gitlab::Database.postgresql? && - params[:attempt_group_search_optimizations] && + params[:attempt_group_search_optimizations] && Feature.enabled?(:attempt_group_search_optimizations, default_enabled: true) end + def attempt_project_search_optimizations? + params[:attempt_project_search_optimizations] && + Feature.enabled?(:attempt_project_search_optimizations) + end + def count_key(value) Array(value).last.to_sym end @@ -407,20 +410,11 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord - # Wrap projects and groups in a subquery if the conditions are met. - def by_subquery(items) - if use_subquery_for_search? - klass.where(id: items.select(:id)) # rubocop: disable CodeReuse/ActiveRecord - else - items - end - end - # rubocop: disable CodeReuse/ActiveRecord def by_search(items) return items unless search - if use_cte_for_count? + if use_cte_for_search? cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name) cte << items diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 84689ff5dc7..29947bc94d5 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -40,7 +40,8 @@ class MergeRequestsFinder < IssuableFinder items = by_commit(super) items = by_source_branch(items) items = by_wip(items) - by_target_branch(items) + items = by_target_branch(items) + by_source_project_id(items) end private @@ -74,6 +75,16 @@ class MergeRequestsFinder < IssuableFinder items.where(target_branch: target_branch) end + def source_project_id + @source_project_id ||= params[:source_project_id].presence + end + + def by_source_project_id(items) + return items unless source_project_id + + items.where(source_project_id: source_project_id) + end + def by_wip(items) if params[:wip] == 'yes' items.where(wip_match(items.arel_table)) diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb index 2f2816a4a08..d9802598c64 100644 --- a/app/finders/projects/serverless/functions_finder.rb +++ b/app/finders/projects/serverless/functions_finder.rb @@ -3,8 +3,9 @@ module Projects module Serverless class FunctionsFinder - def initialize(clusters) - @clusters = clusters + def initialize(project) + @clusters = project.clusters + @project = project end def execute @@ -19,6 +20,23 @@ module Projects knative_service(environment_scope, name)&.first end + def invocation_metrics(environment_scope, name) + return unless prometheus_adapter&.can_query? + + cluster = clusters_with_knative_installed.preload_knative.find do |c| + environment_scope == c.environment_scope + end + + func = ::Serverless::Function.new(@project, name, cluster.platform_kubernetes&.actual_namespace) + prometheus_adapter.query(:knative_invocation, func) + end + + def has_prometheus?(environment_scope) + clusters_with_knative_installed.preload_knative.to_a.any? do |cluster| + environment_scope == cluster.environment_scope && cluster.application_prometheus_available? + end + end + private def knative_service(environment_scope, name) @@ -55,6 +73,12 @@ module Projects def clusters_with_knative_installed @clusters.with_knative_installed end + + # rubocop: disable CodeReuse/ServiceClass + def prometheus_adapter + @prometheus_adapter ||= ::Prometheus::AdapterService.new(@project).prometheus_adapter + end + # rubocop: enable CodeReuse/ServiceClass end end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e635f608237..e275e4278a4 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -137,6 +137,7 @@ module ApplicationSettingsHelper :default_artifacts_expire_in, :default_branch_protection, :default_group_visibility, + :default_project_creation, :default_project_visibility, :default_projects_limit, :default_snippet_visibility, diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 0622cdfc196..52c49498e9b 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -277,6 +277,8 @@ module IssuablesHelper initialTaskStatus: issuable.task_status } + data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue) + if parent.is_a?(Group) data[:groupPath] = parent.path else diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 8e10fd15f6a..e91e8f85515 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -46,7 +46,7 @@ module LabelsHelper if block_given? link_to link, class: css_class, &block else - link_to render_colored_label(label, tooltip: tooltip), link, class: css_class + render_label(label, tooltip: tooltip, link: link, css: css_class) end end @@ -78,19 +78,33 @@ module LabelsHelper end end - def render_colored_label(label, label_suffix = '', tooltip: true) + def render_label(label, tooltip: true, link: nil, css: nil) + # if scoped label is used then EE wraps label tag with scoped label + # doc link + html = render_colored_label(label, tooltip: tooltip) + html = link_to(html, link, class: css) if link + + html + end + + def render_colored_label(label, label_suffix: '', tooltip: true, title: nil) text_color = text_color_for_bg(label.color) + title ||= tooltip ? label_tooltip_title(label) : '' # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) + - %(style="background-color: #{label.color}; color: #{text_color}" ) + - %(title="#{escape_once(label.description)}" data-container="body">) + + %(data-html="true" style="background-color: #{label.color}; color: #{text_color}" ) + + %(title="#{escape_once(title)}" data-container="body">) + %(#{escape_once(label.name)}#{label_suffix}</span>) span.html_safe end + def label_tooltip_title(label) + label.description + end + def suggested_colors [ '#0033CC', @@ -231,6 +245,37 @@ module LabelsHelper labels.sort_by(&:title) end + def label_dropdown_data(project, opts = {}) + { + toggle: "dropdown", + field_name: opts[:field_name] || "label_name[]", + show_no: "true", + show_any: "true", + project_id: project&.try(:id), + namespace_path: project&.try(:namespace)&.try(:full_path), + project_path: project&.try(:path) + }.merge(opts) + end + + def sidebar_label_dropdown_data(issuable_type, issuable_sidebar) + label_dropdown_data(nil, { + default_label: "Labels", + field_name: "#{issuable_type}[label_names][]", + ability_name: issuable_type, + namespace_path: issuable_sidebar[:namespace_path], + project_path: issuable_sidebar[:project_path], + issue_update: issuable_sidebar[:issuable_json_path], + labels: issuable_sidebar[:project_labels_path], + display: 'static' + }) + end + + def label_from_hash(hash) + klass = hash[:group_id] ? GroupLabel : ProjectLabel + + klass.new(hash.slice(:color, :description, :title, :group_id, :project_id)) + end + # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :text_color_for_bg, :escape_once + module_function :render_colored_label, :text_color_for_bg, :escape_once, :label_tooltip_title end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index ea3bcfc791a..572d68cb4a3 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -49,6 +49,13 @@ module NamespacesHelper end end + def namespaces_options_with_developer_maintainer_access(options = {}) + selected = options.delete(:selected) || :current_user + options[:groups] = current_user.manageable_groups_with_routes(include_groups_with_developer_maintainer_access: true) + + namespaces_options(selected, options) + end + private # Many importers create a temporary Group, so use the real diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9e91e4ab4b9..7ec8505b33a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -223,4 +223,11 @@ class ApplicationSetting < ApplicationRecord reset_memoized_terms end after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') } + + def self.create_from_defaults + super + rescue ActiveRecord::RecordNotUnique + # We already have an ApplicationSetting record, so just return it. + current_without_cache + end end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 265aa1d4965..b413ffddb9d 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -26,6 +26,7 @@ module ApplicationSettingImplementation default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_project_creation: Settings.gitlab['default_project_creation'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1bd517641ac..b8a76e662b0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -83,8 +83,13 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } scope :with_artifacts_archive, ->() do - where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', - '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) + if Feature.enabled?(:ci_enable_legacy_artifacts) + where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', + '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) + else + where('EXISTS (?)', + Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) + end end scope :with_existing_job_artifacts, ->(query) do @@ -135,6 +140,8 @@ module Ci where("EXISTS (?)", matcher) end + ## + # TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata @@ -775,7 +782,7 @@ module Ci private def erase_old_artifacts! - # TODO: To be removed once we get rid of + # TODO: To be removed once we get rid of ci_enable_legacy_artifacts feature flag remove_artifacts_file! remove_artifacts_metadata! save diff --git a/app/models/concerns/artifact_migratable.rb b/app/models/concerns/artifact_migratable.rb index cbd63ba8876..7c9f579b480 100644 --- a/app/models/concerns/artifact_migratable.rb +++ b/app/models/concerns/artifact_migratable.rb @@ -13,7 +13,7 @@ module ArtifactMigratable end def artifacts? - !artifacts_expired? && artifacts_file.exists? + !artifacts_expired? && artifacts_file&.exists? end def artifacts_metadata? @@ -43,4 +43,16 @@ module ArtifactMigratable def artifacts_size read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i end + + def legacy_artifacts_file + return unless Feature.enabled?(:ci_enable_legacy_artifacts) + + super + end + + def legacy_artifacts_metadata + return unless Feature.enabled?(:ci_enable_legacy_artifacts) + + super + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 51a8395c013..17f94b4bd9b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -172,6 +172,10 @@ module Issuable fuzzy_search(query, matched_columns) end + def simple_sorts + super.except('name_asc', 'name_desc') + end + def sort_by_attribute(method, excluded_labels: []) sorted = case method.to_s diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb index decbbbd87f2..258c819f243 100644 --- a/app/models/concerns/prometheus_adapter.rb +++ b/app/models/concerns/prometheus_adapter.rb @@ -36,7 +36,7 @@ module PrometheusAdapter def calculate_reactive_cache(query_class_name, *args) return unless prometheus_client - data = Kernel.const_get(query_class_name).new(prometheus_client_wrapper).query(*args) + data = Object.const_get(query_class_name, false).new(prometheus_client_wrapper).query(*args) { success: true, data: data, diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 29e48f0c5f7..df1a9e3fe6e 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -21,19 +21,21 @@ module Sortable class_methods do def order_by(method) - case method.to_s - when 'created_asc' then order_created_asc - when 'created_date' then order_created_desc - when 'created_desc' then order_created_desc - when 'id_asc' then order_id_asc - when 'id_desc' then order_id_desc - when 'name_asc' then order_name_asc - when 'name_desc' then order_name_desc - when 'updated_asc' then order_updated_asc - when 'updated_desc' then order_updated_desc - else - all - end + simple_sorts.fetch(method.to_s, -> { all }).call + end + + def simple_sorts + { + 'created_asc' => -> { order_created_asc }, + 'created_date' => -> { order_created_desc }, + 'created_desc' => -> { order_created_desc }, + 'id_asc' => -> { order_id_asc }, + 'id_desc' => -> { order_id_desc }, + 'name_asc' => -> { order_name_asc }, + 'name_desc' => -> { order_name_desc }, + 'updated_asc' => -> { order_updated_asc }, + 'updated_desc' => -> { order_updated_desc } + } end private diff --git a/app/models/global_label.rb b/app/models/global_label.rb index c5b2492bbf6..572cb12b26a 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -4,7 +4,7 @@ class GlobalLabel attr_accessor :title, :labels alias_attribute :name, :title - delegate :color, :text_color, :description, to: :@first_label + delegate :color, :text_color, :description, :scoped_label?, to: :@first_label def for_display @first_label diff --git a/app/models/group.rb b/app/models/group.rb index ac66815705c..8bc9b75f0a9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -404,6 +404,10 @@ class Group < Namespace Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true) end + def project_creation_level + super || ::Gitlab::CurrentSettings.default_project_creation + end + private def update_two_factor_requirement diff --git a/app/models/serverless/function.rb b/app/models/serverless/function.rb new file mode 100644 index 00000000000..5d4f8e0c9e2 --- /dev/null +++ b/app/models/serverless/function.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Serverless + class Function + attr_accessor :name, :namespace + + def initialize(project, name, namespace) + @project = project + @name = name + @namespace = namespace + end + + def id + @project.id.to_s + "/" + @name + "/" + @namespace + end + + def self.find_by_id(id) + array = id.split("/") + project = Project.find_by_id(array[0]) + name = array[1] + namespace = array[2] + + self.new(project, name, namespace) + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index b426d100537..d3524bfd6ae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -105,6 +105,7 @@ class User < ApplicationRecord has_many :groups, through: :group_members has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group + has_many :developer_groups, -> { where(members: { access_level: ::Gitlab::Access::DEVELOPER }) }, through: :group_members, source: :group has_many :owned_or_maintainers_groups, -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) }, through: :group_members, @@ -159,7 +160,7 @@ class User < ApplicationRecord # Validations # # Note: devise :validatable above adds validations for :email and :password - validates :name, presence: true + validates :name, presence: true, length: { maximum: 128 } validates :email, confirmation: true validates :notification_email, presence: true validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email } @@ -883,7 +884,12 @@ class User < ApplicationRecord # rubocop: enable CodeReuse/ServiceClass def several_namespaces? - owned_groups.any? || maintainers_groups.any? + union_sql = ::Gitlab::SQL::Union.new( + [owned_groups, + maintainers_groups, + groups_with_developer_maintainer_project_access]).to_sql + + ::Group.from("(#{union_sql}) #{::Group.table_name}").any? end def namespace_id @@ -1169,12 +1175,24 @@ class User < ApplicationRecord @manageable_namespaces ||= [namespace] + manageable_groups end - def manageable_groups - Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants + def manageable_groups(include_groups_with_developer_maintainer_access: false) + owned_and_maintainer_group_hierarchy = Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants + + if include_groups_with_developer_maintainer_access + union_sql = ::Gitlab::SQL::Union.new( + [owned_and_maintainer_group_hierarchy, + groups_with_developer_maintainer_project_access]).to_sql + + ::Group.from("(#{union_sql}) #{::Group.table_name}") + else + owned_and_maintainer_group_hierarchy + end end - def manageable_groups_with_routes - manageable_groups.eager_load(:route).order('routes.path') + def manageable_groups_with_routes(include_groups_with_developer_maintainer_access: false) + manageable_groups(include_groups_with_developer_maintainer_access: include_groups_with_developer_maintainer_access) + .eager_load(:route) + .order('routes.path') end def namespaces @@ -1573,4 +1591,16 @@ class User < ApplicationRecord ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) end + + def groups_with_developer_maintainer_project_access + project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS] + + if ::Gitlab::CurrentSettings.default_project_creation == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS + project_creation_levels << nil + end + + developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants + ::Group.where(id: developer_groups_hierarchy.select(:id), + project_creation_level: project_creation_levels) + end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index db49d3bed9c..eb2e536e8e9 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -35,6 +35,14 @@ class GroupPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:request_access_enabled) { @subject.request_access_enabled } + condition(:create_projects_disabled) do + @subject.project_creation_level == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS + end + + condition(:developer_maintainer_access) do + @subject.project_creation_level == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS + end + rule { public_group }.policy do enable :read_group enable :read_list @@ -115,6 +123,9 @@ class GroupPolicy < BasePolicy rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + rule { developer & developer_maintainer_access }.enable :create_projects + rule { create_projects_disabled }.prevent :create_projects + def access_level return GroupMember::NO_ACCESS if @user.nil? diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 75825c8fac0..26d7d6e84c4 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -204,6 +204,7 @@ class ProjectPolicy < BasePolicy enable :read_merge_request enable :read_sentry_issue enable :read_release + enable :read_prometheus end # We define `:public_user_access` separately because there are cases in gitlab-ee diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 9ddce0d2c80..62c26809eeb 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -45,6 +45,8 @@ class BuildDetailsEntity < JobEntity erase_project_job_path(project, build) end + expose :failure_reason, if: -> (*) { build.failed? } + expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build| terminal_project_job_path(project, build) end diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb index c98dc1a1c4a..a46f8af1466 100644 --- a/app/serializers/projects/serverless/service_entity.rb +++ b/app/serializers/projects/serverless/service_entity.rb @@ -32,6 +32,13 @@ module Projects service.dig('podcount') end + expose :metrics_url do |service| + project_serverless_metrics_path( + request.project, + service.dig('environment_scope'), + service.dig('metadata', 'name')) + ".json" + end + expose :created_at do |service| service.dig('metadata', 'creationTimestamp') end diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb index 4e92353a13c..8d08f0cda94 100644 --- a/app/services/error_tracking/list_projects_service.rb +++ b/app/services/error_tracking/list_projects_service.rb @@ -15,8 +15,8 @@ module ErrorTracking result = setting.list_sentry_projects rescue Sentry::Client::Error => e return error(e.message, :bad_request) - rescue Sentry::Client::SentryError => e - return error(e.message, :unprocessable_entity) + rescue Sentry::Client::MissingKeysError => e + return error(e.message, :internal_server_error) end success(projects: result[:projects]) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 04dfcfbc22d..7a4ccf0d178 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -107,12 +107,13 @@ class IssuableBaseService < BaseService @labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params) end - def process_label_ids(attributes, existing_label_ids: nil) + def process_label_ids(attributes, existing_label_ids: nil, extra_label_ids: []) label_ids = attributes.delete(:label_ids) add_label_ids = attributes.delete(:add_label_ids) remove_label_ids = attributes.delete(:remove_label_ids) new_label_ids = existing_label_ids || label_ids || [] + new_label_ids |= extra_label_ids if add_label_ids.blank? && remove_label_ids.blank? new_label_ids = label_ids if label_ids @@ -147,7 +148,7 @@ class IssuableBaseService < BaseService params.delete(:state_event) params[:author] ||= current_user - params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params) + params[:label_ids] = process_label_ids(params, extra_label_ids: issuable.label_ids.to_a) issuable.assign_attributes(params) diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 79c43b8e7d5..d3ef892875b 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -7,7 +7,7 @@ module MergeRequests def execute(commit_status) return if commit_status.allow_failure? || commit_status.retried? - commit_status_merge_requests(commit_status) do |merge_request| + pipeline_merge_requests(commit_status.pipeline) do |merge_request| todo_service.merge_request_build_failed(merge_request) end end @@ -16,7 +16,7 @@ module MergeRequests # build is retried # def close(commit_status) - commit_status_merge_requests(commit_status) do |merge_request| + pipeline_merge_requests(commit_status.pipeline) do |merge_request| todo_service.merge_request_build_retried(merge_request) end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index f968e3693da..8a9e5ebb014 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -99,22 +99,11 @@ module MergeRequests # rubocop: enable CodeReuse/ActiveRecord def pipeline_merge_requests(pipeline) - merge_requests_for(pipeline.ref).each do |merge_request| + pipeline.all_merge_requests.opened.each do |merge_request| next unless pipeline == merge_request.head_pipeline yield merge_request end end - - def commit_status_merge_requests(commit_status) - merge_requests_for(commit_status.ref).each do |merge_request| - pipeline = merge_request.head_pipeline - - next unless pipeline - next unless pipeline.sha == commit_status.sha - - yield merge_request - end - end end end diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb new file mode 100644 index 00000000000..c5d2b84878b --- /dev/null +++ b/app/services/prometheus/proxy_service.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Prometheus + class ProxyService < BaseService + include ReactiveCaching + include Gitlab::Utils::StrongMemoize + + self.reactive_cache_key = ->(service) { service.cache_key } + self.reactive_cache_lease_timeout = 30.seconds + self.reactive_cache_refresh_interval = 30.seconds + self.reactive_cache_lifetime = 1.minute + self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) } + + attr_accessor :proxyable, :method, :path, :params + + PROXY_SUPPORT = { + 'query' => { + method: ['GET'], + params: %w(query time timeout) + }, + 'query_range' => { + method: ['GET'], + params: %w(query start end step timeout) + } + }.freeze + + def self.from_cache(proxyable_class_name, proxyable_id, method, path, params) + proxyable_class = begin + proxyable_class_name.constantize + rescue NameError + nil + end + return unless proxyable_class + + proxyable = proxyable_class.find(proxyable_id) + + new(proxyable, method, path, params) + end + + # proxyable can be any model which responds to .prometheus_adapter + # like Environment. + def initialize(proxyable, method, path, params) + @proxyable = proxyable + @path = path + + # Convert ActionController::Parameters to hash because reactive_cache_worker + # does not play nice with ActionController::Parameters. + @params = filter_params(params, path).to_hash + + @method = method + end + + def id + nil + end + + def execute + return cannot_proxy_response unless can_proxy? + return no_prometheus_response unless can_query? + + with_reactive_cache(*cache_key) do |result| + result + end + end + + def calculate_reactive_cache(proxyable_class_name, proxyable_id, method, path, params) + return no_prometheus_response unless can_query? + + response = prometheus_client_wrapper.proxy(path, params) + + success(http_status: response.code, body: response.body) + rescue Gitlab::PrometheusClient::Error => err + service_unavailable_response(err) + end + + def cache_key + [@proxyable.class.name, @proxyable.id, @method, @path, @params] + end + + private + + def service_unavailable_response(exception) + error(exception.message, :service_unavailable) + end + + def no_prometheus_response + error('No prometheus server found', :service_unavailable) + end + + def cannot_proxy_response + error('Proxy support for this API is not available currently') + end + + def prometheus_adapter + strong_memoize(:prometheus_adapter) do + @proxyable.prometheus_adapter + end + end + + def prometheus_client_wrapper + prometheus_adapter&.prometheus_client_wrapper + end + + def can_query? + prometheus_adapter&.can_query? + end + + def filter_params(params, path) + params.slice(*PROXY_SUPPORT.dig(path, :params)) + end + + def can_proxy? + PROXY_SUPPORT.dig(@path, :method)&.include?(@method) + end + end +end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 3f503f3da28..30f7743c56e 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -26,7 +26,7 @@ module Users end end - identity_attrs = params.slice(:extern_uid, :provider) + identity_attrs = params.slice(*identity_params) unless identity_attrs.empty? user.identities.build(identity_attrs) @@ -37,6 +37,10 @@ module Users private + def identity_params + [:extern_uid, :provider] + end + def can_create_user? (current_user.nil? && Gitlab::CurrentSettings.allow_signup?) || current_user&.admin? end diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb index a9afc104ed1..fac3c3dcb8f 100644 --- a/app/uploaders/legacy_artifact_uploader.rb +++ b/app/uploaders/legacy_artifact_uploader.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +## +# TODO: Remove this uploader when we remove :ci_enable_legacy_artifacts feature flag +# See https://gitlab.com/gitlab-org/gitlab-ce/issues/58595 class LegacyArtifactUploader < GitlabUploader extend Workhorse::UploadPath include ObjectStorage::Concern diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 8122d81f578..03ef2924617 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -5,7 +5,9 @@ .form-group = f.label :default_branch_protection, class: 'label-bold' = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' - = render_if_exists 'admin/application_settings/project_creation_level', form: f, application_setting: @application_setting + .form-group + = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold' + = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control' .form-group.visibility-level-setting = f.label :default_project_visibility, class: 'label-bold' = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 5e05568e384..8fb38f6a690 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -8,7 +8,7 @@ .form-group.row.group-description-holder = f.label :avatar, _("Group avatar"), class: 'col-form-label col-sm-2' .col-sm-10 - = render 'shared/choose_group_avatar_button', f: f + = render 'shared/choose_avatar_button', f: f = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index c787d7420b7..3b6fc85e70e 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -8,12 +8,12 @@ - if status.has_details? = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) - %span.ci-build-text= subject.name + %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name - else .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } %span{ class: klass }= sprite_icon(status.icon) - %span.ci-build-text= subject.name + %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name - if status.has_action? = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 00582e19662..3e0f8955081 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -7,25 +7,27 @@ - help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon } %p - - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} + - link_to_help_page = link_to(s_('ClusterIntegration|help page'), + help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page } %p= link_to('Select a different Google account', @authorize_url) -= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| - = form_errors(@gcp_cluster) - .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') += bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20', + data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field| + = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'), + label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold' - if has_multiple_clusters? - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') + = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'), + class: 'label-bold' } do + = field.text_field :environment_scope, required: true, class: 'form-control', + title: 'Environment scope is required.', wrapper: false .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group - = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-bold' + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), + class: 'label-bold' .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } = provider_gcp_field.hidden_field :gcp_project_id .dropdown @@ -47,9 +49,9 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end } - .form-group - = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-bold' - = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' + = provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3', + title: s_('ClusterIntegration|Number of nodes must be a numerical value.'), + label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold' .form-group = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold' @@ -64,13 +66,14 @@ = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } .form-group - .form-check - = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true - = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' + = provider_gcp_field.check_box :legacy_abac, { label: s_('ClusterIntegration|RBAC-enabled cluster'), + label_class: 'label-bold' }, false, true + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', + anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' .form-group - = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true + = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), + class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 136f98d0126..27b11e8469f 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -1,40 +1,39 @@ -= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field| - = form_errors(@user_cluster) - .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') += bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' }, + url: clusterable.create_user_clusters_path, as: :cluster do |field| + = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'), + label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold' - if has_multiple_clusters? - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') - .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") + = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'), + class: 'label-bold' } do + = field.text_field :environment_scope, required: true, + title: 'Environment scope is required.', wrapper: false + .form-text.text-muted + = s_("ClusterIntegration|Choose which of your environments will use this cluster.") = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| - .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold' - = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') - - .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold' - = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') - - .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold' - = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off' + = platform_kubernetes_field.url_field :api_url, required: true, + title: s_('ClusterIntegration|API URL should be a valid http/https url.'), + label: s_('ClusterIntegration|API URL'), label_class: 'label-bold' + = platform_kubernetes_field.text_area :ca_cert, + placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), + label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold' + = platform_kubernetes_field.text_field :token, required: true, + title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'), + autocomplete: 'off', label_class: 'label-bold' - if @user_cluster.allow_user_defined_namespace? - .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + = platform_kubernetes_field.text_field :namespace, + label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold' - .form-group - .form-check - = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' + = platform_kubernetes_field.form_group :authorization_type do + = platform_kubernetes_field.check_box :authorization_type, + { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'), + label_class: 'label-bold', inline: true }, 'rbac', 'abac' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', + anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml index b5ddca7ccb9..f9f8097cb38 100644 --- a/app/views/clusters/platforms/kubernetes/_form.html.haml +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -1,58 +1,51 @@ -= form_for cluster, url: update_cluster_url_path, as: :cluster do |field| - = form_errors(cluster) - - .form-group - - if cluster.read_only_kubernetes_platform_fields? - %label.append-bottom-10{ for: 'cluster-name' } - = s_('ClusterIntegration|Kubernetes cluster name') - .input-group - %input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true } - %span.input-group-append - = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') - - else - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - .input-group - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') += bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' }, + as: :cluster do |field| + - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + = field.text_field :name, class: 'js-select-on-focus cluster-name', required: true, + title: s_('ClusterIntegration|Cluster name is required.'), + readonly: cluster.read_only_kubernetes_platform_fields?, + label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: copy_name_btn = field.fields_for :platform_kubernetes, platform do |platform_field| - .form-group - = platform_field.label :api_url, s_('ClusterIntegration|API URL') - .input-group - = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.read_only_kubernetes_platform_fields? - - if cluster.read_only_kubernetes_platform_fields? - %span.input-group-append - = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default') + - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + = platform_field.text_field :api_url, class: 'js-select-on-focus', required: true, + title: s_('ClusterIntegration|API URL should be a valid http/https url.'), + readonly: cluster.read_only_kubernetes_platform_fields?, + label: s_('ClusterIntegration|API URL'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: copy_api_url + + - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? + = platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5', + readonly: cluster.read_only_kubernetes_platform_fields?, + placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), + label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn - .form-group - = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') - .input-group - = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.read_only_kubernetes_platform_fields? - - if cluster.read_only_kubernetes_platform_fields? - %span.input-group-append.clipboard-addon - = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank') + - show_token_btn = (platform_field.button s_('ClusterIntegration|Show'), + type: 'button', class: 'js-show-cluster-token btn btn-default') + - copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'), + class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields? - .form-group - = platform_field.label :token, s_('ClusterIntegration|Token') - .input-group - = platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.read_only_kubernetes_platform_fields? - %span.input-group-append - %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' } - = s_('ClusterIntegration|Show') - - if cluster.read_only_kubernetes_platform_fields? - = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') + = platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token', + required: true, title: s_('ClusterIntegration|Service token is required.'), + readonly: cluster.read_only_kubernetes_platform_fields?, + label: s_('ClusterIntegration|Service Token'), label_class: 'label-bold', + input_group_class: 'gl-field-error-anchor', append: show_token_btn + copy_token_btn - if cluster.allow_user_defined_namespace? - .form-group - = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + = platform_field.text_field :namespace, label: s_('ClusterIntegration|Project namespace (optional, unique)'), + label_class: 'label-bold' - .form-group - .form-check - = platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = platform_field.form_group :authorization_type do + = platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'), + label_class: 'label-bold', inline: true }, 'rbac', 'abac' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index ff59013ed67..7390c42aba2 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -9,6 +9,10 @@ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/ %span.descr This setting can be overridden in each project. +.form-group.row + = f.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'col-form-label col-sm-2' + .col-sm-10 + = f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, @group.project_creation_level), {}, class: 'form-control' .form-group.row = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2 pt-0' diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 51dcc9d0cda..6269678079a 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -27,7 +27,7 @@ .form-group.group-description-holder.col-sm-12 = f.label :avatar, _("Group avatar"), class: 'label-bold' %div - = render 'shared/choose_group_avatar_button', f: f + = render 'shared/choose_avatar_button', f: f .form-group.col-sm-12 %label.label-bold diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index 9ed71d19d32..c382a1ed168 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -23,10 +23,10 @@ .avatar-container.rect-avatar.s90 = group_icon(@group, alt: '', class: 'avatar group-avatar s90') = f.label :avatar, _('Group avatar'), class: 'label-bold d-block' - = render 'shared/choose_group_avatar_button', f: f + = render 'shared/choose_avatar_button', f: f - if @group.avatar? %hr - = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted' + = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 6b0a6e7ed99..0a14830c666 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -18,6 +18,7 @@ %span.descr.text-muted= share_with_group_lock_help_text(@group) = render 'groups/settings/lfs', f: f + = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f = render_if_exists 'groups/member_lock_setting', f: f, group: @group diff --git a/app/views/groups/settings/_project_creation_level.html.haml b/app/views/groups/settings/_project_creation_level.html.haml new file mode 100644 index 00000000000..9f711e6aade --- /dev/null +++ b/app/views/groups/settings/_project_creation_level.html.haml @@ -0,0 +1,3 @@ +.form-group + = f.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'label-bold' + = f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, group.project_creation_level), {}, class: 'form-control' diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index 935581643cd..9082bfc409d 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -2,34 +2,29 @@ .form-group = label_tag :merge_method_merge, class: 'label-bold' do - Merge method + = _('Merge method') .form-check = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input" = label_tag :project_merge_method_merge, class: 'form-check-label' do - %strong Merge commit - %br - %span.descr - A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. + .mb-3 + = _('Merge commit') + .text-secondary + = _('A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.') - .form-check - = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input" - = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do - %strong Merge commit with semi-linear history - %br - %span.descr - A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. - This way you could make sure that if this merge request would build, after merging to target branch it would also build. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. +.form-check + = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input" + = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do + .mb-3 + = _('Merge commit with semi-linear history') + .text-secondary + = _('A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. This way you could make sure that if this merge request would build, after merging to target branch it would also build.') + .text-secondary + = _('When fast-forward merge is not possible, the user is given the option to rebase.') - .form-check - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input" - = label_tag :project_merge_method_ff, class: 'form-check-label' do - %strong Fast-forward merge - %br - %span.descr - No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. +.form-check + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input" + = label_tag :project_merge_method_ff, class: 'form-check-label' do + .mb-3 + = _('Fast-forward merge') + .text-secondary + = _('No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. When fast-forward merge is not possible, the user is given the option to rebase.') diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 6ac2e06afa5..3a9f7ca42db 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -4,21 +4,21 @@ .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input' = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do - %strong Only allow merge requests to be merged if the pipeline succeeds - %br - %span.descr - Pipelines need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank' + .mb-3 + = _('Only allow merge requests to be merged if the pipeline succeeds') + .text-secondary + = _('Pipelines need to be configured to enable this feature.') + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank' = render_if_exists 'projects/merge_pipelines_settings', form: form .form-check = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input' = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do - %strong Only allow merge requests to be merged if all discussions are resolved + %p= _('Only allow merge requests to be merged if all discussions are resolved') .form-check = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input' = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do - %strong Automatically resolve merge request diff discussions when they become outdated + %p= _('Automatically resolve merge request diff discussions when they become outdated') .form-check = form.check_box :printing_merge_request_link_enabled, class: 'form-check-input' = form.label :printing_merge_request_link_enabled, class: 'form-check-label' do - %strong Show link to create/view merge request when pushing from the command line + %p= _('Show link to create/view merge request when pushing from the command line') diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 5129f11875c..1c1c7d832bd 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -19,9 +19,9 @@ = root_url - namespace_id = namespace_id_from(params) = f.select(:namespace_id, - namespaces_options(namespace_id || :current_user, - display_path: true, - extra_group: namespace_id), + namespaces_options_with_developer_maintainer_access(selected: namespace_id, + display_path: true, + extra_group: namespace_id), {}, { class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }}) diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml index 6d6bd79bc3c..07b9378ba97 100644 --- a/app/views/projects/blob/viewers/_route_map.html.haml +++ b/app/views/projects/blob/viewers/_route_map.html.haml @@ -6,4 +6,4 @@ This Route Map is invalid: = viewer.validation_message -= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') += link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages') diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml index a5f73fb0197..f11c047e85a 100644 --- a/app/views/projects/blob/viewers/_route_map_loading.html.haml +++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml @@ -1,4 +1,4 @@ = icon('spinner spin fw') Validating Route Map… -= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') += link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages') diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml index 70521ed892e..566dfe798c6 100644 --- a/app/views/projects/diffs/_replaced_image_diff.html.haml +++ b/app/views/projects/diffs/_replaced_image_diff.html.haml @@ -35,10 +35,10 @@ .swipe.view.hide .swipe-frame - .frame.deleted + .frame.deleted.old-diff = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false) .swipe-wrap.left-oriented - = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path } + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added old-diff js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path } %span.swipe-bar %span.top-handle %span.bottom-handle diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml index 454f814795a..daac543b939 100644 --- a/app/views/projects/diffs/_single_image_diff.html.haml +++ b/app/views/projects/diffs/_single_image_diff.html.haml @@ -10,5 +10,5 @@ .image.js-single-image{ data: diff_view_data } .wrap - single_class_name = diff_file.deleted_file? ? 'deleted' : 'added' - = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.file_path } + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} old-diff js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.file_path } %p.image-info= number_to_human_size(blob.size) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a6a8ca489a9..98017bea0c9 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -40,23 +40,16 @@ = f.label :tag_list, "Topics", class: 'label-bold' = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" %p.form-text.text-muted Separate topics with commas. - %fieldset.features - %h5.prepend-top-0= _("Project avatar") - .form-group - - if @project.avatar? - .avatar-container.rect-avatar.s160.append-bottom-15 - = project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160) - - if @project.avatar_in_git - %p.light - = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git } - .prepend-top-5.append-bottom-10 - %button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...") - %span.file_name.prepend-left-default.js-filename= _("No file chosen") - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .form-text.text-muted= _("The maximum file size allowed is 200KB.") - - if @project.avatar? - %hr - = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" + + .form-group.prepend-top-default.append-bottom-20 + .avatar-container.s90 + = project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90') + = f.label :avatar, _('Project avatar'), class: 'label-bold d-block' + = render 'shared/choose_avatar_button', f: f + - if @project.avatar? + %hr + = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link' + = f.submit 'Save changes', class: "btn btn-success js-btn-success-general-project-settings" %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) } diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml deleted file mode 100644 index 38469ed4774..00000000000 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.issue-closed-by-widget.second-block - - pluralized_mr_this = merge_request_count > 1 ? "these" : "this" - - pluralized_mr_is = merge_request_count > 1 ? "are" : "is" - When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically. diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml deleted file mode 100644 index 6a66c2e57cc..00000000000 --- a/app/views/projects/issues/_merge_requests.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- if @merge_requests.any? - .card-slim.mt-3 - .card-header - %h2.card-title.mt-0.mb-0.h5.merge-requests-title - %span.mr-1.bold - = _('Related merge requests') - .d-inline-flex.lh-100.align-middle - .mr-count-badge - .mr-count-badge-count - = sprite_icon('merge-request', size: 16, css_class: 'mr-1 text-secondary') - = @merge_requests.count - %ul.content-list.related-items-list - - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id) - - @merge_requests.each do |merge_request| - - merge_request = merge_request.present(current_user: current_user) - %li.list-item.py-0.px-0 - .item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3 - .item-contents - .item-title.d-flex.align-items-center.mr-title - = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-none d-xl-block append-right-8' } - = link_to merge_request.title, merge_request_path(merge_request), { class: 'mr-title-link'} - .item-meta - = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-xl-none d-lg-block append-right-5' } - %span.d-flex.align-items-center.append-right-8.mr-item-path.item-path-id.mt-0 - %span.path-id-text.bold.text-truncate{ data: { toggle: 'tooltip'}, title: merge_request.target_project.full_path } - = merge_request.target_project.full_path - = merge_request.to_reference - %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 - - if merge_request.can_read_pipeline? - = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), tooltip_placement: 'bottom' - - elsif has_any_head_pipeline - = icon('blank fw') - - - if @closed_by_merge_requests.present? - %p - = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f8969fbb5a2..2c95ac6dbb3 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -77,12 +77,11 @@ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') - #merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } } - // This element is filled in using JavaScript. + #js-related-merge-requests{ data: { endpoint: api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid), project_namespace: @project.namespace.path, project_path: @project.path } } - if can?(current_user, :download_code, @project) #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } } - // This element is filled in using JavaScript. + -# This element is filled in using JavaScript. .content-block.emoji-block.emoji-block-sticky .row diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 475bae887ec..81a53f22f67 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -8,6 +8,7 @@ %div{ class: container_class } #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json), + deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'), build_options: javascript_build_options } } diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index 9da42fe99ac..4d1d078661d 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -2,10 +2,6 @@ - page_title _('CI / CD Charts') %div{ class: container_class } - .sub-header-block - .oneline - = _("A collection of graphs regarding Continuous Integration") - #charts.ci-charts .row .col-md-6 diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 41fe704601e..bfcaa09ae8c 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -10,7 +10,7 @@ = form_errors(@pipeline) .form-group.row .col-sm-12 - = f.label :ref, s_('Pipeline|Create for'), class: 'col-form-label' + = f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label' = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch = dropdown_tag(params[:ref] || @project.default_branch, options: { toggle_class: 'js-branch-select wide monospace', @@ -28,7 +28,7 @@ = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe .form-actions - = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3 + = f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3 = link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right' -# haml-lint:disable InlineJavaScript diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml index 635580eac5c..9c69aedfbfc 100644 --- a/app/views/projects/serverless/functions/index.html.haml +++ b/app/views/projects/serverless/functions/index.html.haml @@ -5,7 +5,10 @@ - status_path = project_serverless_functions_path(@project, format: :json) - clusters_path = project_clusters_path(@project) -.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } } +.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, + installed: @installed, + clusters_path: clusters_path, + help_path: help_page_path('user/project/clusters/serverless/index') } } %div{ class: [container_class, ('limit-container-width' unless fluid_layout)] } .js-serverless-functions-notice diff --git a/app/views/projects/serverless/functions/show.html.haml b/app/views/projects/serverless/functions/show.html.haml index 29737b7014a..d1fe208ce60 100644 --- a/app/views/projects/serverless/functions/show.html.haml +++ b/app/views/projects/serverless/functions/show.html.haml @@ -1,14 +1,19 @@ - @no_container = true - @content_class = "limit-container-width" unless fluid_layout +- clusters_path = project_clusters_path(@project) +- help_path = help_page_path('user/project/clusters/serverless/index') - add_to_breadcrumbs('Serverless', project_serverless_functions_path(@project)) - page_title @service[:name] -.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json } } +.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json, + prometheus: @prometheus, + clusters_path: clusters_path, + help_path: help_path } } + %div{ class: [container_class, ('limit-container-width' unless fluid_layout)] } - .top-area.adjust - .serverless-function-details#js-serverless-function-details + .serverless-function-details#js-serverless-function-details .js-serverless-function-notice .flash-container diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index fac68a36e79..fe74dc122c3 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -17,10 +17,11 @@ = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' .card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' } - %p.settings-message.text-center - - kubernetes_cluster_link = help_page_path('user/project/clusters/index') - - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link } - = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe } + - if @project.all_clusters.empty? + %p.settings-message.text-center + - kubernetes_cluster_link = help_page_path('user/project/clusters/index') + - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link } + = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe } %label.prepend-top-10 %strong= s_('CICD|Deployment strategy') .form-check diff --git a/app/views/shared/_choose_avatar_button.html.haml b/app/views/shared/_choose_avatar_button.html.haml new file mode 100644 index 00000000000..0d46d047134 --- /dev/null +++ b/app/views/shared/_choose_avatar_button.html.haml @@ -0,0 +1,4 @@ +%button.btn.js-choose-avatar-button{ type: 'button' }= _("Choose file…") +%span.file_name.js-avatar-filename= _("No file chosen") += f.file_field :avatar, class: "js-avatar-input hidden" +.form-text.text-muted= _("The maximum file size allowed is 200KB.") diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml deleted file mode 100644 index 0552fe62090..00000000000 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...") -%span.file_name.js-avatar-filename= _("No file chosen") -= f.file_field :avatar, class: "js-group-avatar-input hidden" -.form-text.text-muted= _("The maximum file size allowed is 200KB.") diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml index b96380923ac..dbd3bbb43af 100644 --- a/app/views/shared/_delete_label_modal.html.haml +++ b/app/views/shared/_delete_label_modal.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %h3.page-title Delete #{render_colored_label(label, tooltip: false)} ? + %h3.page-title Delete #{render_label(label, tooltip: false)} ? %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %span{ "aria-hidden": true } × diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index c5ea15a7f63..6651f12f6de 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -7,7 +7,7 @@ - if defined?(@project) = link_to_label(label, subject: @project, tooltip: false) - else - = render_colored_label(label, tooltip: false) + = render_label(label, tooltip: false) .label-description .append-right-default.prepend-left-default - if label.description.present? diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 19159684420..47cc912a9a1 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -21,17 +21,11 @@ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", ":data-selected" => "selectedLabels", ":data-labels" => "issue.assignableLabelsEndpoint", - data: { toggle: "dropdown", - field_name: "issue[label_names][]", - show_no: "true", - show_any: "true", - project_id: @project&.try(:id), - namespace_path: @namespace_path, - project_path: @project.try(:path) } } + data: label_dropdown_data(@project, namespace_path: @namespace_path, field_name: "issue[label_names][]") } %span.dropdown-toggle-text {{ labelDropdownTitle }} = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default" - if can?(current_user, :admin_label, current_board_parent) - = render partial: "shared/issuable/label_page_create" + = render partial: "shared/issuable/label_page_create", locals: { show_add_list: true } diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml index fd413bd68c8..416b4a34651 100644 --- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml +++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml @@ -4,5 +4,5 @@ .dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - if can?(current_user, :admin_label, board.parent) - = render partial: "shared/issuable/label_page_create" + = render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' } = dropdown_loading diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index d5fb85ba0f3..f2c0c77a583 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -8,7 +8,7 @@ - classes = local_assigns.fetch(:classes, []) - selected = local_assigns.fetch(:selected, nil) - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") -- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"} +- dropdown_data = label_dropdown_data(@project, labels: labels_filter_path_with_defaults, default_label: "Labels") - dropdown_data.merge!(data_options) - label_name = local_assigns.fetch(:label_name, "Labels") - no_default_styles = local_assigns.fetch(:no_default_styles, false) diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index 55edaa7eda4..d173e3c0192 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -1,4 +1,7 @@ - show_close = local_assigns.fetch(:show_close, true) +- show_add_list = local_assigns.fetch(:show_add_list, false) +- add_list = local_assigns.fetch(:add_list, false) +- add_list_class = local_assigns.fetch(:add_list_class, '') - subject = @project || @group .dropdown-page-two.dropdown-new-label = dropdown_title(create_label_title(subject), options: { back: true, close: show_close }) @@ -12,6 +15,11 @@ .dropdown-label-color-input .dropdown-label-color-preview.js-dropdown-label-color-preview %input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') } + - if show_add_list + .dropdown-label-input{ class: add_list_class } + %label + %input.js-add-list{ type: "checkbox", name: "add_list", checked: add_list } + %span= _('Add list') .clearfix %button.btn.btn-primary.float-left.js-new-label-btn{ type: "button" } = _('Create') diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9596c1df20e..0798b1da4b7 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -105,10 +105,8 @@ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - - selected_labels.each do |label| - = link_to sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]) do - %span.badge.color-label.has-tooltip{ style: "background-color: #{label[:color]}; color: #{label[:text_color]}", title: label[:description], data: { container: "body" } } - = label[:title] + - selected_labels.each do |label_hash| + = render_label(label_from_hash(label_hash), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title])) - else %span.no-value = _('None') @@ -116,7 +114,7 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable_type}[label_names][]", ability_name: issuable_type, show_no: "true", show_any: "true", namespace_path: issuable_sidebar[:namespace_path], project_path: issuable_sidebar[:project_path], issue_update: issuable_sidebar[:issuable_json_path], labels: issuable_sidebar[:project_labels_path], display: 'static' } } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index 7619d0a2e9c..743ee1435e8 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -4,7 +4,9 @@ .form-group.row = f.label :title, class: 'col-form-label col-sm-2' .col-sm-10 - = f.text_field :title, class: "form-control qa-label-title", required: true, autofocus: true + = f.text_field :title, class: "form-control js-label-title qa-label-title", required: true, autofocus: true + = render_if_exists 'shared/labels/create_label_help_text' + .form-group.row = f.label :description, class: 'col-form-label col-sm-2' .col-sm-10 diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index eba64daaadc..5863f52aa78 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -21,8 +21,7 @@ %span.issuable-number= issuable.to_reference - labels.each do |label| - = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do - - render_colored_label(label) + = render_label(label, link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' })) %span.assignee-icon - assignees.each do |assignee| diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 6797520650d..6b0640bd8cb 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -5,8 +5,7 @@ %li.is-not-draggable %span.label-row %span.label-name - = link_to milestones_label_path(options) do - - render_colored_label(label, tooltip: false) + = render_label(label, tooltip: false, link: milestones_label_path(options)) %span.prepend-description-left = markdown_field(label, :description) diff --git a/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml b/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml new file mode 100644 index 00000000000..5dd25d0ffc1 --- /dev/null +++ b/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Fix long label overflow on metrics dashboard +merge_request: 26775 +author: +type: fixed diff --git a/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml new file mode 100644 index 00000000000..3e3784d5413 --- /dev/null +++ b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml @@ -0,0 +1,5 @@ +--- +title: Show error when namespace/svc account missing +merge_request: 26362 +author: +type: added diff --git a/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml b/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml new file mode 100644 index 00000000000..18bd51711d9 --- /dev/null +++ b/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml @@ -0,0 +1,5 @@ +--- +title: "Fix image diff swipe view on commit and compare pages" +merge_request: 26968 +author: ftab +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml b/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml new file mode 100644 index 00000000000..a6953a68a76 --- /dev/null +++ b/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml @@ -0,0 +1,5 @@ +--- +title: Resolve "Hide Kubernetes cluster warning if project has cluster related" +merge_request: 26749 +author: +type: fixed diff --git a/changelogs/unreleased/57364-improve-diff-nav-header.yml b/changelogs/unreleased/57364-improve-diff-nav-header.yml new file mode 100644 index 00000000000..95d119b949c --- /dev/null +++ b/changelogs/unreleased/57364-improve-diff-nav-header.yml @@ -0,0 +1,5 @@ +--- +title: Make stylistic improvements to diff nav header +merge_request: 26557 +author: +type: fixed diff --git a/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml b/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml new file mode 100644 index 00000000000..c188d59fe94 --- /dev/null +++ b/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml @@ -0,0 +1,5 @@ +--- +title: Create a shortcut for a new MR in the Web IDE +merge_request: 26792 +author: +type: added diff --git a/changelogs/unreleased/57493-add-limit-to-user-name.yml b/changelogs/unreleased/57493-add-limit-to-user-name.yml new file mode 100644 index 00000000000..e6c78572d23 --- /dev/null +++ b/changelogs/unreleased/57493-add-limit-to-user-name.yml @@ -0,0 +1,5 @@ +--- +title: Set user.name limit to 128 characters +merge_request: 26146 +author: +type: changed diff --git a/changelogs/unreleased/57602-create-cluster-validations.yml b/changelogs/unreleased/57602-create-cluster-validations.yml new file mode 100644 index 00000000000..35349c1e9f4 --- /dev/null +++ b/changelogs/unreleased/57602-create-cluster-validations.yml @@ -0,0 +1,5 @@ +--- +title: Display cluster form validation error messages inline +merge_request: 26502 +author: +type: changed diff --git a/changelogs/unreleased/57668-create-file-from-url.yml b/changelogs/unreleased/57668-create-file-from-url.yml new file mode 100644 index 00000000000..b6033fa24ca --- /dev/null +++ b/changelogs/unreleased/57668-create-file-from-url.yml @@ -0,0 +1,5 @@ +--- +title: Implemented support for creation of new files from URL in Web IDE +merge_request: 26622 +author: +type: added diff --git a/changelogs/unreleased/58375-api-controller.yml b/changelogs/unreleased/58375-api-controller.yml new file mode 100644 index 00000000000..60f21b37ae7 --- /dev/null +++ b/changelogs/unreleased/58375-api-controller.yml @@ -0,0 +1,5 @@ +--- +title: Add a Prometheus API per environment +merge_request: 26841 +author: +type: added diff --git a/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml b/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml new file mode 100644 index 00000000000..9f5881966c7 --- /dev/null +++ b/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml @@ -0,0 +1,5 @@ +--- +title: Allow task lists that follow a blockquote to work correctly +merge_request: 26937 +author: +type: fixed diff --git a/changelogs/unreleased/58835-button-run-pipeline.yml b/changelogs/unreleased/58835-button-run-pipeline.yml new file mode 100644 index 00000000000..39407a60780 --- /dev/null +++ b/changelogs/unreleased/58835-button-run-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Changed button label at /pipelines/new +merge_request: 26893 +author: antfobe,leonardofl +type: other diff --git a/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml b/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml new file mode 100644 index 00000000000..3df13dbb960 --- /dev/null +++ b/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml @@ -0,0 +1,5 @@ +--- +title: Migrate clusters tests to jest +merge_request: 27013 +author: +type: other diff --git a/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml b/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml new file mode 100644 index 00000000000..9ab8d2b8596 --- /dev/null +++ b/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml @@ -0,0 +1,5 @@ +--- +title: Fix multiple series queries on metrics dashboard +merge_request: 26514 +author: +type: fixed diff --git a/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml b/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml new file mode 100644 index 00000000000..8b82d757303 --- /dev/null +++ b/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml @@ -0,0 +1,5 @@ +--- +title: Order labels alphabetically in issue boards +merge_request: 26927 +author: +type: changed diff --git a/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml b/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml new file mode 100644 index 00000000000..bfea3ac52af --- /dev/null +++ b/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml @@ -0,0 +1,5 @@ +--- +title: "Make touch events work on image diff swipe view and onion skin" +merge_request: 26971 +author: ftab +type: added diff --git a/changelogs/unreleased/60068-avoid-null-domain-help-text.yml b/changelogs/unreleased/60068-avoid-null-domain-help-text.yml new file mode 100644 index 00000000000..5305b8584a8 --- /dev/null +++ b/changelogs/unreleased/60068-avoid-null-domain-help-text.yml @@ -0,0 +1,5 @@ +--- +title: Do not display Ingress IP help text when there isn’t an Ingress IP assigned +merge_request: 27057 +author: +type: fixed diff --git a/changelogs/unreleased/60116-fix-button-wrapping.yml b/changelogs/unreleased/60116-fix-button-wrapping.yml new file mode 100644 index 00000000000..d6df920b51d --- /dev/null +++ b/changelogs/unreleased/60116-fix-button-wrapping.yml @@ -0,0 +1,5 @@ +--- +title: Add to white-space nowrap to all buttons +merge_request: 27069 +author: +type: fixed diff --git a/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml b/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml new file mode 100644 index 00000000000..8c3a47cf62c --- /dev/null +++ b/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml @@ -0,0 +1,5 @@ +--- +title: Handle possible HTTP exception for Sentry client +merge_request: 27080 +author: +type: fixed diff --git a/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml b/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml new file mode 100644 index 00000000000..b773eb2720c --- /dev/null +++ b/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml @@ -0,0 +1,5 @@ +--- +title: Rewrite related MRs widget with Vue +merge_request: 27027 +author: +type: other diff --git a/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml b/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml new file mode 100644 index 00000000000..61236b9b82b --- /dev/null +++ b/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml @@ -0,0 +1,5 @@ +--- +title: Enable FindCommit caching for project and commits pages +merge_request: 27048 +author: +type: performance diff --git a/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml new file mode 100644 index 00000000000..10475824a75 --- /dev/null +++ b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml @@ -0,0 +1,5 @@ +--- +title: Change project avatar remove button to a link +merge_request: 26589 +author: +type: other diff --git a/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml b/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml new file mode 100644 index 00000000000..4bbbc706e62 --- /dev/null +++ b/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml @@ -0,0 +1,5 @@ +--- +title: Improve project merge request settings +merge_request: 26495 +author: +type: other diff --git a/changelogs/unreleased/create-label-and-list-checkbox.yml b/changelogs/unreleased/create-label-and-list-checkbox.yml new file mode 100644 index 00000000000..330372df1be --- /dev/null +++ b/changelogs/unreleased/create-label-and-list-checkbox.yml @@ -0,0 +1,5 @@ +--- +title: 'Added "Add List" checkbox to create label dropdown to make creation of list optional' +merge_request: 25716 +author: Tucker Chapman +type: fixed diff --git a/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml b/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml new file mode 100644 index 00000000000..d99187d8d41 --- /dev/null +++ b/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml @@ -0,0 +1,5 @@ +--- +title: Drop legacy artifacts usage as there are no leftovers +merge_request: 24294 +author: +type: performance diff --git a/changelogs/unreleased/duplicate-related-mrs.yml b/changelogs/unreleased/duplicate-related-mrs.yml new file mode 100644 index 00000000000..0f5f6ede9f8 --- /dev/null +++ b/changelogs/unreleased/duplicate-related-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Remove duplicates from issue related merge requests +merge_request: 27067 +author: +type: fixed diff --git a/changelogs/unreleased/extend-cte-optimisations-to-projects.yml b/changelogs/unreleased/extend-cte-optimisations-to-projects.yml new file mode 100644 index 00000000000..e5407127b2f --- /dev/null +++ b/changelogs/unreleased/extend-cte-optimisations-to-projects.yml @@ -0,0 +1,5 @@ +--- +title: Speed up filtering issues in a project when searching +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml b/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml new file mode 100644 index 00000000000..81cf5cb810d --- /dev/null +++ b/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml @@ -0,0 +1,5 @@ +--- +title: Use gitlabktl to build and deploy GitLab Serverless Functions +merge_request: 26926 +author: +type: added diff --git a/changelogs/unreleased/fix-UI-links-to-route-map-info.yml b/changelogs/unreleased/fix-UI-links-to-route-map-info.yml new file mode 100644 index 00000000000..bb506507080 --- /dev/null +++ b/changelogs/unreleased/fix-UI-links-to-route-map-info.yml @@ -0,0 +1,5 @@ +--- +title: Fix UI anchor links after docs refactor +merge_request: 26890 +author: +type: fixed diff --git a/changelogs/unreleased/fix-include-ci-yaml.yml b/changelogs/unreleased/fix-include-ci-yaml.yml new file mode 100644 index 00000000000..042413b89aa --- /dev/null +++ b/changelogs/unreleased/fix-include-ci-yaml.yml @@ -0,0 +1,5 @@ +--- +title: Fix single string values for the 'include' keyword validation of gitlab-ci.yml. +merge_request: 26998 +author: Paul Bonaud (@paulrbr) +type: fixed diff --git a/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml b/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml new file mode 100644 index 00000000000..9ccc79109d8 --- /dev/null +++ b/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml @@ -0,0 +1,5 @@ +--- +title: Fix MWPS does not work for merge request pipelines +merge_request: 26906 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-version-v1.32.0.yml b/changelogs/unreleased/gitaly-version-v1.32.0.yml new file mode 100644 index 00000000000..8413f31278e --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.32.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.32.0 +merge_request: 26989 +author: +type: changed diff --git a/changelogs/unreleased/gitaly-version-v1.33.0.yml b/changelogs/unreleased/gitaly-version-v1.33.0.yml new file mode 100644 index 00000000000..d21e521a0bb --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.33.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.33.0 +merge_request: 27065 +author: +type: changed diff --git a/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml b/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml new file mode 100644 index 00000000000..8f4f49896d7 --- /dev/null +++ b/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml @@ -0,0 +1,5 @@ +--- +title: Fix IDE detection of MR from fork with same branch name +merge_request: 26986 +author: +type: fixed diff --git a/changelogs/unreleased/knative-prometheus.yml b/changelogs/unreleased/knative-prometheus.yml new file mode 100644 index 00000000000..e24f53b7225 --- /dev/null +++ b/changelogs/unreleased/knative-prometheus.yml @@ -0,0 +1,5 @@ +--- +title: Add Knative metrics to Prometheus +merge_request: 24663 +author: Chris Baumbauer <cab@cabnetworks.net> +type: added diff --git a/changelogs/unreleased/minimized-multiple-queries-ce.yml b/changelogs/unreleased/minimized-multiple-queries-ce.yml new file mode 100644 index 00000000000..d8c20d492d6 --- /dev/null +++ b/changelogs/unreleased/minimized-multiple-queries-ce.yml @@ -0,0 +1,5 @@ +--- +title: Support multiple queries per chart on metrics dash +merge_request: 25758 +author: +type: added diff --git a/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml new file mode 100644 index 00000000000..34fd0c1b787 --- /dev/null +++ b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml @@ -0,0 +1,5 @@ +--- +title: Move allow developers to create projects in groups to Core +merge_request: 25975 +author: +type: added diff --git a/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml b/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml new file mode 100644 index 00000000000..0e090592101 --- /dev/null +++ b/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml @@ -0,0 +1,5 @@ +--- +title: Removes the undescriptive CI Charts header +merge_request: !26869 +author: +type: changed diff --git a/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml b/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml new file mode 100644 index 00000000000..a051c1f70a8 --- /dev/null +++ b/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml @@ -0,0 +1,5 @@ +--- +title: Enable Gitaly FindCommit caching for TreeController +merge_request: 27100 +author: +type: performance diff --git a/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml b/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml new file mode 100644 index 00000000000..3d1501cd667 --- /dev/null +++ b/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml @@ -0,0 +1,5 @@ +--- +title: Fix real-time updates for projects that contain a reserved word +merge_request: 27060 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-ref-name-caching.yml b/changelogs/unreleased/sh-fix-ref-name-caching.yml new file mode 100644 index 00000000000..6abd86688b4 --- /dev/null +++ b/changelogs/unreleased/sh-fix-ref-name-caching.yml @@ -0,0 +1,5 @@ +--- +title: Fix and expand Gitaly FindCommit caching +merge_request: 27018 +author: +type: performance diff --git a/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml b/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml new file mode 100644 index 00000000000..867d7e6b9df --- /dev/null +++ b/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml @@ -0,0 +1,5 @@ +--- +title: 'GitHub import: Run housekeeping after initial import' +merge_request: 26600 +author: +type: performance diff --git a/changelogs/unreleased/sh-improve-find-commit-caching.yml b/changelogs/unreleased/sh-improve-find-commit-caching.yml new file mode 100644 index 00000000000..1b38684d018 --- /dev/null +++ b/changelogs/unreleased/sh-improve-find-commit-caching.yml @@ -0,0 +1,5 @@ +--- +title: Expand FindCommit caching to blob and refs +merge_request: 27084 +author: +type: performance diff --git a/changelogs/unreleased/sh-update-rails-5-0-7-2.yml b/changelogs/unreleased/sh-update-rails-5-0-7-2.yml new file mode 100644 index 00000000000..b0bc08d4760 --- /dev/null +++ b/changelogs/unreleased/sh-update-rails-5-0-7-2.yml @@ -0,0 +1,5 @@ +--- +title: Update Rails to 5.0.7.2 +merge_request: 27022 +author: +type: security diff --git a/config.ru b/config.ru index 5cd79870d54..6f6fb85d8fa 100644 --- a/config.ru +++ b/config.ru @@ -13,10 +13,6 @@ if defined?(Unicorn) # Max memory size (RSS) per worker use Unicorn::WorkerKiller::Oom, min, max end - - # Monkey patch for fixing Rack 2.0.6 bug: - # https://gitlab.com/gitlab-org/gitlab-ee/issues/8539 - Unicorn::StreamInput.send(:public, :eof?) # rubocop:disable GitlabSecurity/PublicSend end require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/helpers/is_ee_env.js b/config/helpers/is_ee_env.js index 1fdbca591c0..3fe9bb891eb 100644 --- a/config/helpers/is_ee_env.js +++ b/config/helpers/is_ee_env.js @@ -4,6 +4,6 @@ const path = require('path'); const ROOT_PATH = path.resolve(__dirname, '../..'); module.exports = - process.env.EE !== undefined - ? JSON.parse(process.env.EE) + process.env.IS_GITLAB_EE !== undefined + ? JSON.parse(process.env.IS_GITLAB_EE) : fs.existsSync(path.join(ROOT_PATH, 'ee')); diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 01ffcade931..3c426cdb969 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,6 +126,7 @@ Settings['issues_tracker'] ||= {} # GitLab # Settings['gitlab'] ||= Settingslogic.new({}) +Settings.gitlab['default_project_creation'] ||= ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml index 9bdaf1575e9..884868c6336 100644 --- a/config/prometheus/common_metrics.yml +++ b/config/prometheus/common_metrics.yml @@ -259,3 +259,13 @@ label: Pod average unit: "cores" track: canary + - title: "Knative function invocations" + y_label: "Invocations" + required_metrics: + - istio_revision_request_count + weight: 1 + queries: + - id: system_metrics_knative_function_invocation_count + query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))' + label: invocations / minute + unit: requests diff --git a/config/routes/project.rb b/config/routes/project.rb index d60a5cc9ae8..93d168fc595 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -219,6 +219,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :metrics get :additional_metrics get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } + + get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy' end collection do @@ -250,7 +252,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end namespace :serverless do - get '/functions/:environment_id/:id', to: 'functions#show' + scope :functions do + get '/:environment_id/:id', to: 'functions#show' + get '/:environment_id/:id/metrics', to: 'functions#metrics', as: :metrics + end + resources :functions, only: [:index] end diff --git a/config/webpack.config.js b/config/webpack.config.js index 9a37856a99e..19b48845305 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -322,7 +322,7 @@ module.exports = { }), new webpack.DefinePlugin({ - 'process.env.EE': JSON.stringify(IS_EE), + 'process.env.IS_GITLAB_EE': JSON.stringify(IS_EE), }), ].filter(Boolean), diff --git a/db/fixtures/development/02_application_settings.rb b/db/fixtures/development/02_application_settings.rb new file mode 100644 index 00000000000..d604f0be3cd --- /dev/null +++ b/db/fixtures/development/02_application_settings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +puts "Creating the default ApplicationSetting record.".color(:green) +Gitlab::CurrentSettings.current_application_settings + +# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241 +puts "Enable hashed storage for every new projects.".color(:green) +ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true) + +print '.' diff --git a/db/fixtures/development/02_settings.rb b/db/fixtures/development/02_settings.rb deleted file mode 100644 index 3a4a5d436bf..00000000000 --- a/db/fixtures/development/02_settings.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Enable hashed storage, in development mode, for all projects by default. -Gitlab::Seeder.quiet do - ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache - ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true) - print '.' -end diff --git a/db/fixtures/development/08_settings.rb b/db/fixtures/development/08_settings.rb deleted file mode 100644 index 141465c06cf..00000000000 --- a/db/fixtures/development/08_settings.rb +++ /dev/null @@ -1,7 +0,0 @@ -# We want to enable hashed storage for every new project in development -# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241 -Gitlab::Seeder.quiet do - ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache - ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true) - print '.' -end diff --git a/db/fixtures/production/001_application_settings.rb b/db/fixtures/production/001_application_settings.rb index ab15717e9a9..cf647650142 100644 --- a/db/fixtures/production/001_application_settings.rb +++ b/db/fixtures/production/001_application_settings.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + puts "Creating the default ApplicationSetting record.".color(:green) Gitlab::CurrentSettings.current_application_settings diff --git a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb new file mode 100644 index 00000000000..87427ad2930 --- /dev/null +++ b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddDefaultProjectCreationApplicationSetting < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + unless column_exists?(:application_settings, :default_project_creation) + add_column(:application_settings, :default_project_creation, :integer, default: 2, null: false) + end + end + + def down + if column_exists?(:application_settings, :default_project_creation) + remove_column(:application_settings, :default_project_creation) + end + end +end diff --git a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb new file mode 100644 index 00000000000..159e0a95ace --- /dev/null +++ b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddProjectCreationLevelToNamespaces < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + unless column_exists?(:namespaces, :project_creation_level) + add_column :namespaces, :project_creation_level, :integer + end + end + + def down + unless column_exists?(:namespaces, :project_creation_level) + remove_column :namespaces, :project_creation_level, :integer + end + end +end diff --git a/db/migrate/20190325080727_truncate_user_fullname.rb b/db/migrate/20190325080727_truncate_user_fullname.rb new file mode 100644 index 00000000000..e5f88671eef --- /dev/null +++ b/db/migrate/20190325080727_truncate_user_fullname.rb @@ -0,0 +1,21 @@ +# rubocop:disable Migration/UpdateLargeTable +class TruncateUserFullname < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + truncated_name = Arel.sql('SUBSTRING(name from 1 for 128)') + where_clause = Arel.sql("LENGTH(name) > 128") + + update_column_in_batches(:users, :name, truncated_name) do |table, query| + query.where(where_clause) + end + end + + def down + # noop + end +end diff --git a/db/migrate/20190326164045_import_common_metrics_knative.rb b/db/migrate/20190326164045_import_common_metrics_knative.rb new file mode 100644 index 00000000000..340ec1e1f75 --- /dev/null +++ b/db/migrate/20190326164045_import_common_metrics_knative.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ImportCommonMetricsKnative < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + require Rails.root.join('db/importers/common_metrics_importer.rb') + + DOWNTIME = false + + def up + Importers::CommonMetricsImporter.new.execute + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb b/db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb new file mode 100644 index 00000000000..fcd63f42b0e --- /dev/null +++ b/db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanUpNoteableIdForNotesOnCommits < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + TEMP_INDEX_NAME = 'index_notes_on_commit_with_null_noteable_id' + + disable_ddl_transaction! + + def up + remove_concurrent_index_by_name(:notes, TEMP_INDEX_NAME) + + add_concurrent_index(:notes, :id, where: "noteable_type = 'Commit' AND noteable_id IS NOT NULL", name: TEMP_INDEX_NAME) + + # rubocop:disable Migration/UpdateLargeTable + update_column_in_batches(:notes, :noteable_id, nil, batch_size: 300) do |table, query| + query.where( + table[:noteable_type].eq('Commit').and(table[:noteable_id].not_eq(nil)) + ) + end + + remove_concurrent_index_by_name(:notes, TEMP_INDEX_NAME) + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index b20fe4b3d39..ca5b04e810a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190325165127) do +ActiveRecord::Schema.define(version: 20190326164045) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -177,6 +177,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do t.string "runners_registration_token_encrypted" t.integer "local_markdown_version", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false + t.integer "default_project_creation", default: 2, null: false t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree end @@ -1391,6 +1392,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do t.integer "cached_markdown_version" t.string "runners_token" t.string "runners_token_encrypted" + t.integer "project_creation_level" t.boolean "auto_devops_enabled" t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md index 345e8646f9b..1780e1babe9 100644 --- a/doc/administration/git_protocol.md +++ b/doc/administration/git_protocol.md @@ -31,7 +31,7 @@ From the client side, `git` `v2.18.0` or newer must be installed. From the server side, if we want to configure SSH we need to set the `sshd` server to accept the `GIT_PROTOCOL` environment. -In installations using [GitLab Helm Charts](../install/kubernetes/gitlab_chart.md) +In installations using [GitLab Helm Charts](https://docs.gitlab.com/charts/) and [All-in-one docker image](https://docs.gitlab.com/omnibus/docker/), the SSH service is already configured to accept the `GIT_PROTOCOL` environment and users need not do anything more. diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md index 63945506f3c..aa9d87562a3 100644 --- a/doc/administration/job_traces.md +++ b/doc/administration/job_traces.md @@ -10,13 +10,13 @@ In the following table you can see the phases a trace goes through. | Phase | State | Condition | Data flow | Stored path | | ----- | ----- | --------- | --------- | ----------- | -| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| -| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| -| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`| +| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| +| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| +| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/gitlab-rails/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`| | 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`| The `ROOT_PATH` varies per your environment. For Omnibus GitLab it -would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source +would be `/var/opt/gitlab`, whereas for installations from source it would be `/home/git/gitlab`. ## Changing the job traces local location diff --git a/doc/api/commits.md b/doc/api/commits.md index c8c282a71d9..e205307eeca 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -155,7 +155,7 @@ Example response: } ``` -GitLab supports [form encoding](../README.md#encoding-api-parameters-of-array-and-hash-types). The following is an example using Commit API with form encoding: +GitLab supports [form encoding](README.md#encoding-api-parameters-of-array-and-hash-types). The following is an example using Commit API with form encoding: ```bash curl --request POST \ diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md index 02334f0298e..f36e352da67 100644 --- a/doc/api/project_clusters.md +++ b/doc/api/project_clusters.md @@ -159,7 +159,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of the project owned by the authenticated user | | `name` | String | yes | The name of the cluster | -| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base_domain) of the cluster | +| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster | | `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true | | `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API | | `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes | @@ -250,7 +250,7 @@ Parameters: | `id` | integer | yes | The ID of the project owned by the authenticated user | | `cluster_id` | integer | yes | The ID of the cluster | | `name` | String | no | The name of the cluster | -| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base_domain) of the cluster | +| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster | | `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API | | `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes | | `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | diff --git a/doc/api/runners.md b/doc/api/runners.md index 0b7ef46888c..46f7b1d2a25 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -4,6 +4,29 @@ [ce-2640]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2640 +## Registration and authentication tokens + +There are two tokens to take into account when connecting a Runner with GitLab. + +| Token | Description | +| ----- | ----------- | +| Registration token | Token used to [register the Runner](https://docs.gitlab.com/runner/register/). It can be [obtained through GitLab](../ci/runners/README.md). | +| Authentication token | Token used to authenticate the Runner with the GitLab instance. It is obtained either automatically when [registering a Runner](https://docs.gitlab.com/runner/register/), or manually when [registering the Runner via the Runners API](#register-a-new-runner). | + +Here's an example of how the two tokens are used in Runner registration: + +1. You register the Runner via the GitLab API using a registration token, and an + authentication token is returned. +1. You use that authentication token and add it to the + [Runner's configuration file](https://docs.gitlab.com/runner/commands/#configuration-file): + + ```toml + [[runners]] + token = "<authentication_token>" + ``` + +GitLab and Runner are then connected. + ## List owned runners Get a list of specific runners available to the user. @@ -456,7 +479,7 @@ POST /runners | Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| -| `token` | string | yes | Registration token ([Read how to obtain a token](../ci/runners/README.md)) | +| `token` | string | yes | [Registration token](#registration-and-authentication-tokens). | | `description`| string | no | Runner's description| | `info` | hash | no | Runner's metadata | | `active` | boolean| no | Whether the Runner is active | @@ -466,7 +489,7 @@ POST /runners | `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job | ``` -curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=ipzXrMhuyyJPifUt6ANz" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" +curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=<registration_token>" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" ``` Response: @@ -494,10 +517,10 @@ DELETE /runners | Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| -| `token` | string | yes | Runner's authentication token | +| `token` | string | yes | Runner's [authentication token](#registration-and-authentication-tokens). | ``` -curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=ebb6fc00521627750c8bb750f2490e" +curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=<authentication_token>" ``` Response: @@ -516,10 +539,10 @@ POST /runners/verify | Attribute | Type | Required | Description | |-------------|---------|----------|---------------------| -| `token` | string | yes | Runner's authentication token | +| `token` | string | yes | Runner's [authentication token](#registration-and-authentication-tokens). | ``` -curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=ebb6fc00521627750c8bb750f2490e" +curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=<authentication_token>" ``` Response: diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 54493bc2922..eaafc7bc1c0 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -664,7 +664,7 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/* ### Scoping environments with specs **[PREMIUM]** Some GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) features can behave differently for each -environment. For example, you can [create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-variables-premium). +environment. For example, you can [create a secret variable to be injected only into a production environment](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium). In most cases, these features use the _environment specs_ mechanism, which offers an efficient way to implement scoping within each environment group. diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 908cf85980e..d6ad00a77da 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -511,7 +511,7 @@ Errors can be easily debugged through GitLab's build logs, and within minutes of you can see the changes live on your game. Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables -rapid but stable development. We can easily test changes in a separate [environment](../../../ci/environments.md#introduction-to-environments-and-deployments), +rapid but stable development. We can easily test changes in a separate [environment](../../environments.md), or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing and tedious, but having faith in a stable deployment with GitLab CI/CD allows a lot of breathing room in quickly getting changes to players. diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md index d505f2ae4ce..6055d8c282a 100644 --- a/doc/ci/introduction/index.md +++ b/doc/ci/introduction/index.md @@ -127,7 +127,7 @@ displayed by GitLab: ![pipeline status](img/pipeline_status.png) At the end, if anything goes wrong, you can easily -[roll back](../environments.md#rolling-back-changes) all the changes: +[roll back](../environments.md#retrying-and-rolling-back) all the changes: ![rollback button](img/rollback.png) diff --git a/doc/ci/merge_request_pipelines/img/merge_request.png b/doc/ci/merge_request_pipelines/img/merge_request.png Binary files differindex cf9c628e9a0..d03fdc6a885 100644 --- a/doc/ci/merge_request_pipelines/img/merge_request.png +++ b/doc/ci/merge_request_pipelines/img/merge_request.png diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png b/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png Binary files differnew file mode 100644 index 00000000000..58d5581f628 --- /dev/null +++ b/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png b/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png Binary files differnew file mode 100644 index 00000000000..0a84e61d284 --- /dev/null +++ b/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md index e8953d235a7..4f61e97bd8a 100644 --- a/doc/ci/merge_request_pipelines/index.md +++ b/doc/ci/merge_request_pipelines/index.md @@ -2,14 +2,16 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6. -Usually, when you create a new merge request, a pipeline runs on the +Usually, when you create a new merge request, a pipeline runs with the new change and checks if it's qualified to be merged into a target branch. This -pipeline should contain only necessary jobs for checking the new changes. +pipeline should contain only necessary jobs for validating the new changes. For example, unit tests, lint checks, and [Review Apps](../review_apps/index.md) are often used in this cycle. With pipelines for merge requests, you can design a specific pipeline structure -for merge requests. +for when you are running a pipeline in a merge request. This +could be either adding or removing steps in the pipeline, to make sure that +your pipelines are as efficient as possible. ## Configuring pipelines for merge requests @@ -30,9 +32,7 @@ build: stage: build script: ./build only: - - branches - - tags - - merge_requests + - master test: stage: test @@ -43,6 +43,8 @@ test: deploy: stage: deploy script: ./deploy + only: + - master ``` After the merge request is updated with new commits: @@ -50,18 +52,58 @@ After the merge request is updated with new commits: - GitLab detects that changes have occurred and creates a new pipeline for the merge request. - The pipeline fetches the latest code from the source branch and run tests against it. -In the above example, the pipeline contains only `build` and `test` jobs. -Since the `deploy` job doesn't have the `only: merge_requests` parameter, -deployment jobs will not happen in the merge request. +In the above example, the pipeline contains only a `test` job. +Since the `build` and `deploy` jobs don't have the `only: merge_requests` parameter, +they will not run in the merge request. -Pipelines tagged with the **merge request** badge indicate that they were triggered +Pipelines tagged with the **detached** badge indicate that they were triggered when a merge request was created or updated. For example: ![Merge request page](img/merge_request.png) -The same tag is shown on the pipeline's details: +## Combined ref pipelines **[PREMIUM]** + +> [GitLab Premium](https://about.gitlab.com/pricing/) 11.10. + +It's possible for your source and target branches to diverge, which can result +in the scenario that source branch's pipeline was green, the target's pipeline was green, +but the combined output fails. By having your merge request pipeline automatically +create a new ref that contains the merge result of the source and target branch +(then running a pipeline on that ref), we can better test that the combined result +is also valid. + +From GitLab 11.10, pipelines for merge requests run by default +on this merged result. That is, where the source and target branches are combined into a +new ref and a pipeline for this ref validates the result prior to merging. + +![Merge request pipeline as the head pipeline](img/merge_request_pipeline.png) + +There are some cases where creating a combined ref is not possible or not wanted. +For example, a source branch that has conflicts with the target branch +or a merge request that is still in WIP status. In this case, the merge request pipeline falls back to a "detached" state +and runs on the source branch ref as if it was a regular pipeline. + +The detached state serves to warn you that you are working in a situation +subjected to merge problems, and helps to highlight that you should +get out of WIP status or resolve merge conflicts as soon as possible. -![Pipeline's details](img/pipeline_detail.png) +### Enabling combined ref pipelines + +This feature disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186). It can be enabled at the project level: + +1. Visit your project's **Settings > General** and expand **Merge requests**. +1. Check **Merge pipelines will try to validate the post-merge result prior to merging**. +1. Click **Save changes** button. + +![Merge request pipeline config](img/merge_request_pipeline_config.png) + +### Combined ref pipeline's limitations + +- This feature requires [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer. +- This feature requires [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer. +- After the merge request pipeline succeeds, if the target branch has moved forward, the result of the pipeline is stale and must be retried. In busy repos, this can become a problem as it is highly probable that the target branch will have moved ahead. Improvements are [planned](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186) for future versions of GitLab. +- Forking/cross-repo workflows are not currently supported. To follow progress, see [#9713](https://gitlab.com/gitlab-org/gitlab-ee/issues/9713). +- This feature is not available for [fast forward merges](../../user/project/merge_requests/fast_forward_merge.md) yet. To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab-ce/issues/58226). ## Excluding certain jobs @@ -138,3 +180,12 @@ External users could steal secret variables from the parent project by modifying We're discussing a secure solution of running pipelines for merge requests that submitted from forked projects, see [the issue about the permission extension](https://gitlab.com/gitlab-org/gitlab-ce/issues/23902). + +## Additional predefined variables + +By using pipelines for merge requests, GitLab exposes additional predefined variables to the pipeline jobs. +Those variables contain information of the associated merge request, so that it's useful +to integrate your job with [GitLab Merge Request API](../../api/merge_requests.md). + +You can find the list of avilable variables in [the reference sheet](../variables/predefined_variables.md). +The variable names begin with the `CI_MERGE_REQUEST_` prefix. diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 38cd58f11ac..2ffa3d4edc7 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -295,7 +295,7 @@ You can do this straight from the pipeline graph. Just click on the play button to execute that particular job. For example, your pipeline start automatically, but require manual action to -[deploy to production](environments.md#manually-deploying-to-environments). In the example below, the `production` +[deploy to production](environments.md#configuring-manual-deployments). In the example below, the `production` stage has a job with a manual action. ![Pipelines example](img/pipelines.png) @@ -313,7 +313,7 @@ For example, if you start rolling out new code and: - Users do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%. - Users experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline - and [rolling](environments.md#rolling-back-changes) back to the last stable version. + and [rolling](environments.md#retrying-and-rolling-back) back to the last stable version. ![Pipelines example](img/pipeline_incremental_rollout.png) diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 53651a807c2..2fc1e14f02e 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -33,7 +33,7 @@ In this example, you can see a branch was: ## How do Review Apps work? -The basis of Review Apps in GitLab is [dynamic environments](../environments.md#dynamic-environments), which allow you to dynamically create a new environment for each branch. +The basis of Review Apps in GitLab is [dynamic environments](../environments.md#configuring-dynamic-environments), which allow you to dynamically create a new environment for each branch. Access to the Review App is made available as a link on the [merge request](../../user/project/merge_requests.md) relevant to the branch. Review Apps enable you to review all changes proposed by the merge request in live environment. @@ -60,14 +60,14 @@ To get a better understanding of Review Apps, review documentation on how enviro 1. Learn about [environments](../environments.md) and their role in the development workflow. 1. Learn about [CI variables](../variables/README.md) and how they can be used in your CI jobs. 1. Explore the [`environment` syntax](../yaml/README.md#environment) as defined in `.gitlab-ci.yml`. This will become a primary reference. -1. Additionally, find out about [manual actions](../environments.md#manually-deploying-to-environments) and how you can use them to deploy to critical environments like production with the push of a button. +1. Additionally, find out about [manual actions](../environments.md#configuring-manual-deployments) and how you can use them to deploy to critical environments like production with the push of a button. 1. Follow the [example tutorials](#examples). These will guide you through setting up infrastructure and using Review Apps. ### Configuring dynamic environments Configuring Review Apps dynamic environments depends on your technology stack and infrastructure. -For more information, see [dynamic environments](../environments.md#dynamic-environments) documentation to understand how to define and create them. +For more information, see [dynamic environments](../environments.md#configuring-dynamic-environments) documentation to understand how to define and create them. ### Creating and destroying Review Apps diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index 416627740d2..b429dc8c8be 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -23,6 +23,9 @@ future GitLab releases.** | `CHAT_INPUT` | 10.6 | all | Additional arguments passed in the [ChatOps](../chatops/README.md) command | | `CHAT_CHANNEL` | 10.6 | all | Source chat channel which triggered the [ChatOps](../chatops/README.md) command | | `CI` | all | 0.4 | Mark that job is executed in CI environment | +| `CI_BUILDS_DIR` | all | 11.10 | Top-level directory where builds are executed. | +| `CI_CONCURRENT_ID` | all | 11.10 | Unique ID of build execution within a single executor. | +| `CI_CONCURRENT_PROJECT_ID` | all | 11.10 | Unique ID of build execution within a single executor and project. | | `CI_COMMIT_BEFORE_SHA` | 11.2 | all | The previous latest commit present on a branch before a push request. | | `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. | | `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 686d36c50ee..cf2189cd00a 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -756,7 +756,7 @@ Manual actions are a special type of job that are not executed automatically, they need to be explicitly started by a user. An example usage of manual actions would be a deployment to a production environment. Manual actions can be started from the pipeline, job, environment, and deployment views. Read more at the -[environments documentation](../environments.md#manually-deploying-to-environments). +[environments documentation](../environments.md#configuring-manual-deployments). Manual actions can be either optional or blocking. Blocking manual actions will block the execution of the pipeline at the stage this action is defined in. It's @@ -2362,8 +2362,35 @@ variables: GIT_STRATEGY: clone GIT_CHECKOUT: "false" script: - - git checkout master - - git merge $CI_BUILD_REF_NAME + - git checkout -B master origin/master + - git merge $CI_COMMIT_SHA +``` + +#### Git clean flags + +> Introduced in GitLab Runner 11.10 + +The `GIT_CLEAN_FLAGS` variable is used to control the default behavior of +`git clean` after checking out the sources. You can set it globally or per-job in the +[`variables`](#variables) section. + +`GIT_CLEAN_FLAGS` accepts all possible options of the [git clean](https://git-scm.com/docs/git-clean) +command. + +`git clean` is disabled if `GIT_CHECKOUT: "false"` is specified. + +If `GIT_CLEAN_FLAGS` is: + +- Not specified, `git clean` flags default to `-ffdx`. +- Given the value `none`, `git clean` is not executed. + +For example: + +```yaml +variables: + GIT_CLEAN_FLAGS: -ffdx -e cache/ +script: + - ls -al cache/ ``` #### Job stages attempts @@ -2439,6 +2466,72 @@ CAUTION: **Deprecated:** `type` is deprecated, and could be removed in one of the future releases. Use [`stage`](#stage) instead. +## Custom build directories + +> [Introduced][https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1267] in Gitlab Runner 11.10 + +NOTE: **Note:** +This can only be used when `custom_build_dir` is enabled in the [Runner's +configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscustom_build_dir-section). +This is the default configuration for `docker` and `kubernetes` executor. + +By default, GitLab Runner clones the repository in a unique subpath of the `$CI_BUILDS_DIR` directory. +However, sometimes your project might require the code in a specific directory, +but sometimes your project might require to have the code in a specific directory, +like Go projects, for example. In that case, you can specify the `GIT_CLONE_PATH` variable +to tell the Runner in which directory to clone the repository: + +```yml +variables: + GIT_CLONE_PATH: $CI_BUILDS_DIR/project-name + +test: + script: + - pwd +``` + +The `GIT_CLONE_PATH` has to always be within `$CI_BUILDS_DIR`. The directory set in `$CI_BUILDS_DIR` +is dependent on executor and configuration of [runners.builds_dir](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) +setting. + +### Handling concurrency + +An executor using a concurrency greater than `1` might lead +to failures because multiple jobs might be working on the same directory if the `builds_dir` +is shared between jobs. +GitLab Runner does not try to prevent this situation. It is up to the administrator +and developers to comply with the requirements of Runner configuration. + +To avoid this scenario, you can use a unique path within `$CI_BUILDS_DIR`, because Runner +exposes two additional variables that provide a unique `ID` of concurrency: + +- `$CI_CONCURRENT_ID`: Unique ID for all jobs running within the given executor. +- `$CI_CONCURRENT_PROJECT_ID`: Unique ID for all jobs running within the given executor and project. + +The most stable configuration that should work well in any scenario and on any executor +is to use `$CI_CONCURRENT_ID` in the `GIT_CLONE_PATH`. For example: + +```yml +variables: + GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/project-name + +test: + script: + - pwd +``` + +The `$CI_CONCURRENT_PROJECT_ID` should be used in conjunction with `$CI_PROJECT_PATH` +as the `$CI_PROJECT_PATH` provides a path of a repository. That is, `group/subgroup/project`. For example: + +```yml +variables: + GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH + +test: + script: + - pwd +``` + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md index 6f6b361f423..548d72bea93 100644 --- a/doc/development/fe_guide/style_guide_scss.md +++ b/doc/development/fe_guide/style_guide_scss.md @@ -12,7 +12,15 @@ led by the [GitLab UI WG](https://gitlab.com/gitlab-com/www-gitlab-com/merge_req We have a few internal utility classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/framework/common.scss) and we use [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/) -New utility classes should be added to [`common.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/framework/common.scss). +New utility classes should be added to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/utilities.scss). Existing classes include: + +**Background color**: `.bg-variant-shade` e.g. `.bg-warning-400` +**Text color**: `.text-variant-shade` e.g. `.text-success-500` +- variant is one of 'primary', 'secondary', 'success', 'warning', 'error' +- shade is on of the shades listed on [colors](https://design.gitlab.com/foundations/colors/) + +**Font size**: `.text-size` e.g. `.text-2` +- **size** is number from 1-6 from our [Type scale](https://design.gitlab.com/foundations/typography) ### Naming diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index ffc71051377..55ca502f84a 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -130,9 +130,9 @@ secure note named **gitlab-{ce,ee} Review App's root password**. 1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`. 1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`. 1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`. -1. Replace `-c task-runner -- ls` with `-- /srv/gitlab/bin/rails c` from the +1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the default command or - - Run `kubectl exec --namespace review-apps-ce -it review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -- /srv/gitlab/bin/rails c` + - Run `kubectl exec --namespace review-apps-ce review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console` and - Replace `review-apps-ce` with `review-apps-ee` if the Review App is running EE, and diff --git a/doc/install/README.md b/doc/install/README.md index 52011526768..53778f7f0d3 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -55,9 +55,9 @@ need to be aware of: - It can be more expensive for smaller installations. The default installation requires more resources than a single node Omnibus deployment, as most services are deployed in a redundant fashion. -- There are some feature [limitations to be aware of](kubernetes/gitlab_chart.md#limitations). +- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations). -[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](kubernetes/index.md) +[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](https://docs.gitlab.com/charts/) ## Installing GitLab with Docker diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index 9db246b3eb3..43655767002 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,156 +1,5 @@ -# GitLab Helm Chart +--- +redirect_to: https://docs.gitlab.com/charts/ +--- -This is the official way to install GitLab on a cloud native environment. - -NOTE: **Kubernetes experience required:** -Our Helm charts are recommended for those who are familiar with Kubernetes. -If you're not sure if Kubernetes is for you, our -[Omnibus GitLab packages](../README.md#installing-gitlab-using-the-omnibus-gitlab-package-recommended) -are mature, scalable, support [high availability](../../administration/high_availability/README.md) -and are used today on GitLab.com. -It is not necessary to have GitLab installed on Kubernetes in order to use [GitLab Kubernetes integration](https://docs.gitlab.com/ee/user/project/clusters/index.html). - -## Introduction - -The `gitlab` chart is the best way to operate GitLab on Kubernetes. This chart -contains all the required components to get started, and can scale to large deployments. - -The default deployment includes: - -- Core GitLab components: Unicorn, Shell, Workhorse, Registry, Sidekiq, and Gitaly -- Optional dependencies: Postgres, Redis, Minio -- An auto-scaling, unprivileged [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor -- Automatically provisioned SSL via [Let's Encrypt](https://letsencrypt.org/). - -## Limitations - -Some features of GitLab are not currently available: - -- [GitLab Pages](https://gitlab.com/charts/gitlab/issues/37) -- [GitLab Geo](https://gitlab.com/charts/gitlab/issues/8) -- [No in-cluster HA database](https://gitlab.com/charts/gitlab/issues/48) -- MySQL will not be supported, as support is [deprecated within GitLab](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only) - -## Installing GitLab using the Helm Chart - -The `gitlab` chart includes all required dependencies, and takes a few minutes -to deploy. - -TIP: **Tip:** -For production deployments, we strongly recommend using the -[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/index.md) -utilizing [external Postgres, Redis, and object storage](https://gitlab.com/charts/gitlab/tree/master/doc/advanced) services. - -### Requirements - -In order to deploy GitLab on Kubernetes, the following are required: - -1. `helm` and `kubectl` [installed on your computer](preparation/tools_installation.md). -1. A Kubernetes cluster, version 1.8 or higher. 6vCPU and 16GB of RAM is recommended. - - [Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html) - - [Google GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-container-cluster) - - [IBM IKS](https://console.bluemix.net/docs/tutorials/scalable-webapp-kubernetes.html#create_kube_cluster) - - [Microsoft AKS](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal) -1. A [wildcard DNS entry and external IP address](preparation/networking.md) -1. [Authenticate and connect](preparation/connect.md) to the cluster -1. Configure and initialize [Helm Tiller](preparation/tiller.md). - -### Deployment of GitLab to Kubernetes - -To deploy GitLab, the following three parameters are required: - -- `global.hosts.domain`: the [base domain](preparation/networking.md) of the - wildcard host entry. For example, `example.com` if the wild card entry is - `*.example.com`. -- `global.hosts.externalIP`: the [external IP](preparation/networking.md) which - the wildcard DNS resolves to. -- `certmanager-issuer.email`: the email address to use when requesting new SSL - certificates from Let's Encrypt. - -NOTE: **Note:** -For deployments to Amazon EKS, there are -[additional configuration requirements](preparation/eks.md). A full list of -configuration options is [also available](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md). - -Once you have all of your configuration options collected, you can get any -dependencies and run helm. In this example, the helm release is named "gitlab": - -```sh -helm repo add gitlab https://charts.gitlab.io/ -helm repo update -helm upgrade --install gitlab gitlab/gitlab \ - --timeout 600 \ - --set global.hosts.domain=example.com \ - --set global.hosts.externalIP=10.10.10.10 \ - --set certmanager-issuer.email=email@example.com -``` - -### Monitoring the Deployment - -This will output the list of resources installed once the deployment finishes, -which may take 5-10 minutes. - -The status of the deployment can be checked by running `helm status gitlab` -which can also be done while the deployment is taking place if you run the -command in another terminal. - -### Initial login - -You can access the GitLab instance by visiting the domain name beginning with -`gitlab.` followed by the domain specified during installation. From the example -above, the URL would be `https://gitlab.example.com`. - -If you manually created the secret for initial root password, you -can use that to sign in as `root` user. If not, GitLab automatically -created a random password for `root` user. This can be extracted by the -following command (replace `<name>` by name of the release - which is `gitlab` -if you used the command above): - -```sh -kubectl get secret <name>-gitlab-initial-root-password -ojsonpath={.data.password} | base64 --decode ; echo -``` - -### Outgoing email - -By default outgoing email is disabled. To enable it, provide details for your SMTP server -using the `global.smtp` and `global.email` settings. You can find details for these settings in the -[command line options](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md#email-configuration). - -If your SMTP server requires authentication make sure to read the section on providing -your password in the [secrets documentation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md#smtp-password). -You can disable authentication settings with `--set global.smtp.authentication=""`. - -If your Kubernetes cluster is on GKE, be aware that SMTP port [25 is blocked](https://cloud.google.com/compute/docs/tutorials/sending-mail/#using_standard_email_ports). - -### Deploying the Community Edition - -To deploy the Community Edition, include these options in your `helm install` command: - -```sh ---set gitlab.migrations.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce ---set gitlab.sidekiq.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce ---set gitlab.unicorn.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-unicorn-ce ---set gitlab.unicorn.workhorse.image=registry.gitlab.com/gitlab-org/build/cng/gitlab-workhorse-ce ---set gitlab.task-runner.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-task-runner-ce -``` - -## Updating GitLab using the Helm Chart - -Once your GitLab Chart is installed, configuration changes and chart updates -should be done using `helm upgrade`: - -```sh -helm repo update -helm upgrade --reuse-values gitlab gitlab/gitlab -``` - -## Uninstalling GitLab using the Helm Chart - -To uninstall the GitLab Chart, run the following: - -```sh -helm delete gitlab -``` - -[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types -[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses +This document was moved to [another location](https://docs.gitlab.com/charts/). diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index c0cb7694e91..43655767002 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -1,246 +1,5 @@ -# GitLab-Omnibus Helm Chart +--- +redirect_to: https://docs.gitlab.com/charts/ +--- -CAUTION: **Caution:** -This chart is **deprecated**. We recommend using the [`gitlab` chart](gitlab_chart.md) -instead. A comparison of the two charts is available in [this video](https://youtu.be/Z6jWR8Z8dv8). - -For more information on available GitLab Helm Charts, see [Installing GitLab on Kubernetes](index.md). - -- This GitLab-Omnibus chart has been tested on Google Kubernetes Engine and Azure Container Service. -- This work is based partially on: <https://github.com/lwolf/kubernetes-gitlab/>. GitLab would like to thank Sergey Nuzhdin for his work. - -## Introduction - -This chart provides an easy way to get started with GitLab, provisioning an -installation with nearly all functionality enabled. SSL is automatically -provisioned via [Let's Encrypt](https://letsencrypt.org/). - -This Helm chart is suited for small to medium deployments and is **deprecated** -and replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). -Due to the significant architectural changes, migrating will require backing up -data out of this instance and importing it into the new deployment. - -The deployment includes: - -- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus -- An auto-scaling [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor -- [Redis](https://github.com/kubernetes/charts/tree/master/stable/redis) -- [PostgreSQL](https://github.com/kubernetes/charts/tree/master/stable/postgresql) -- [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) -- Persistent Volume Claims for Data, Registry, Postgres, and Redis - -## Limitations - -[High Availability](../../administration/high_availability/README.md) and -[Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) are not supported. - -## Requirements - -- _At least_ 4 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required. -- Kubernetes 1.4+ with Beta APIs enabled -- [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure -- A [wildcard DNS entry](#networking-requirements), which resolves to the external IP address -- The `kubectl` CLI installed locally and authenticated for the cluster -- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine - -### Networking requirements - -This chart configures a GitLab server and Kubernetes cluster which can support -dynamic [Review Apps](../../ci/review_apps/index.md), as well as services like -the integrated [Container Registry](../../user/project/container_registry.md) -and [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/). - -To support the GitLab services and dynamic environments, a wildcard DNS entry -is required which resolves to the [load balancer](#load-balancer-ip) or -[external IP](#external-ip-recommended). Configuration of the DNS entry will depend upon -the DNS service being used. - -#### External IP (recommended) - -To provision an external IP on GCP and Azure, simply request a new address from -the Networking section. Ensure that the region matches the region your container -cluster is created in. It is important that the IP is not assigned at this point -in time. It will be automatically assigned once the Helm chart is installed, -and assigned to the Load Balancer. - -Now that an external IP address has been allocated, ensure that the wildcard -DNS entry you would like to use resolves to this IP. Please consult the -documentation for your DNS service for more information on creating DNS records. - -Finally, set the `baseIP` setting to this IP address when -[deploying GitLab](#configuring-and-installing-gitlab). - -#### Load Balancer IP - -If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or -Ingress. You can retrieve this IP by running the following command *after* deploying GitLab: - -```sh -kubectl get svc -w --namespace nginx-ingress nginx -``` - -The IP address will be displayed in the `EXTERNAL-IP` field, and should be used -to configure the Wildcard DNS entry. For more information on creating a wildcard -DNS entry, consult the documentation for the DNS server you are using. - -For production deployments of GitLab, we strongly recommend using a -[external IP](#external-ip-recommended). - -## Configuring and Installing GitLab - -For most installations, two parameters are required: - -- `baseDomain`: the [base domain](#networking-requirements) of the wildcard host entry. For example, `mycompany.io` if the wild card entry is `*.mycompany.io`. -- `legoEmail`: Email address to use when requesting new SSL certificates from Let's Encrypt. - -Other common configuration options: - -- `baseIP`: the desired [external IP address](#external-ip-recommended) -- `gitlab`: Choose the [desired edition](https://about.gitlab.com/pricing), either `ee` or `ce`. `ce` is the default. -- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart -- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/). - -For additional configuration options, consult the -[`values.yaml`](https://gitlab.com/charts/gitlab-omnibus/blob/master/values.yaml). - -### Choosing a different GitLab release version - -The version of GitLab installed is based on the `gitlab` setting (see [section](#configuring-and-installing-gitLab) above), and -the value of the corresponding helm setting: `gitlabCEImage` or `gitabEEImage`. - -```yaml -gitlab: CE -gitlabCEImage: gitlab/gitlab-ce:9.5.2-ce.0 -gitlabEEImage: gitlab/gitlab-ee:9.5.2-ee.0 -``` - -The different images can be found in the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce/tags/) and [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee/tags/) -repositories on Docker Hub. - -### Persistent storage - -NOTE: **Note:** -If you are using a machine type with support for less than 4 attached disks, -like an Azure trial, you should disable dedicated storage for Postgres and Redis. - -By default, persistent storage is enabled for GitLab and the charts it depends -on (Redis and PostgreSQL). Components can have their claim size set from your -`values.yaml`, along with whether to provision separate storage for Postgres and Redis. - -Basic configuration: - -```yaml -redisImage: redis:3.2.10 -redisDedicatedStorage: true -redisStorageSize: 5Gi -postgresImage: postgres:9.6.3 -# If you disable postgresDedicatedStorage, you should consider bumping up gitlabRailsStorageSize -postgresDedicatedStorage: true -postgresStorageSize: 30Gi -gitlabRailsStorageSize: 30Gi -gitlabRegistryStorageSize: 30Gi -gitlabConfigStorageSize: 1Gi -``` - -### Routing and SSL - -Ingress routing and SSL are automatically configured within this Chart. An NGINX -ingress is provisioned and configured, and will route traffic to any service. -SSL certificates are automatically created and configured by -[kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). - -NOTE: **Note:** -Let's Encrypt limits a single TLD to five certificate requests within a single -week. This means that common DNS wildcard services like [nip.io](http://nip.io) -and [xip.io](http://xip.io) are unlikely to work. - -## Installing GitLab using the Helm Chart - -NOTE: **Note:** -You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` -while storage provisions. Once the storage provisions, the pods will automatically start. -This may take a couple minutes depending on your cloud provider. If the error persists, -please review the [requirements sections](#requirements) to ensure you have enough RAM, CPU, and storage. - -Add the GitLab Helm repository and initialize Helm: - -```bash -helm init -helm repo add gitlab https://charts.gitlab.io -``` - -Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab), -you can install the chart. We recommending saving your configuration options in a -`values.yaml` file for easier upgrades in the future: - -```bash -helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus -``` - -Or you can pass them on the command line: - -```bash -helm install --name gitlab --set baseDomain=gitlab.io,baseIP=192.0.2.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus -``` - -## Updating GitLab using the Helm Chart - -If you are upgrading from a previous version to 0.1.35 or above, you will need to -change the access mode values for GitLab's storage. To do this, set the following -in `values.yaml` or on the CLI: - -```sh -gitlabDataAccessMode=ReadWriteMany -gitlabRegistryAccessMode=ReadWriteMany -gitlabConfigAccessMode=ReadWriteMany -``` - -Once your GitLab Chart is installed, configuration changes and chart updates -should be done using `helm upgrade`: - -```sh -helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus -``` - -## Upgrading from CE to EE using the Helm Chart - -If you have installed the Community Edition using this chart, upgrading to -Enterprise Edition is easy. - -If you are using a `values.yaml` file to specify the configuration options, edit -the file and set `gitlab=ee`. If you would like to run a specific version of -GitLab EE, set `gitlabEEImage` to be the desired GitLab -[docker image](https://hub.docker.com/r/gitlab/gitlab-ee/tags/). Then you can -use `helm upgrade` to update your GitLab instance to EE: - -```bash -helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus -``` - -You can also upgrade and specify these options via the command line: - -```bash -helm upgrade gitlab --set gitlab=ee,gitlabEEImage=gitlab/gitlab-ee:9.5.5-ee.0 gitlab/gitlab-omnibus -``` - -## Uninstalling GitLab using the Helm Chart - -To uninstall the GitLab Chart, run the following: - -```bash -helm delete --purge gitlab -``` - -## Troubleshooting - -### Storage errors when updating `gitlab-omnibus` versions prior to 0.1.35 - -Users upgrading `gitlab-omnibus` from a version prior to 0.1.35, may see an error -like: `Error: UPGRADE FAILED: PersistentVolumeClaim "gitlab-gitlab-config-storage" is invalid: spec: Forbidden: field is immutable after creation`. - -This is due to a change in the access mode for GitLab storage in version 0.1.35. -To successfully upgrade, the access mode flags must be set to `ReadWriteMany` -as detailed in the [update section](#updating-gitlab-using-the-helm-chart). - -[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types -[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses +This document was moved to [another location](https://docs.gitlab.com/charts/). diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 68b2a146115..08ccf2cf9ad 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,269 +1,5 @@ -# GitLab Runner Helm Chart -> **Note:** -These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/gitlab-org/gitlab-runner/issues). +--- +redirect_to: https://docs.gitlab.com/runner/install/kubernetes.html +--- -The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your -Kubernetes cluster. - -This chart configures the Runner to: - -- Run using the GitLab Runner [Kubernetes executor](https://docs.gitlab.com/runner/install/kubernetes.html) -- For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a - new pod within the specified namespace to run it. - -For more information on available GitLab Helm Charts, please see our [overview](index.md). - -## Prerequisites - -- Your GitLab Server's API is reachable from the cluster -- Kubernetes 1.4+ with Beta APIs enabled -- The `kubectl` CLI installed locally and authenticated for the cluster -- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine - -## Configuring GitLab Runner using the Helm Chart - -Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md) -for information on how your values file will override the defaults. - -The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. - -### Required configuration - -In order for GitLab Runner to function, your config file **must** specify the following: - - - `gitlabUrl` - the GitLab Server URL (with protocol) to register the runner against - - `runnerRegistrationToken` - The Registration Token for adding new Runners to the GitLab Server. This must be - retrieved from your GitLab Instance. See the [GitLab Runner Documentation](../../ci/runners/README.md) for more information. - -Unless you need to specify additional configuration, you are [ready to install](#installing-gitlab-runner-using-the-helm-chart). - -### Other configuration - -The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. - -Here is a snippet of the important settings: - -```yaml -## The GitLab Server URL (with protocol) that want to register the runner against -## ref: https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-register -## -gitlabUrl: http://gitlab.your-domain.com/ - -## The Registration Token for adding new Runners to the GitLab Server. This must -## be retrieved from your GitLab Instance. -## ref: https://docs.gitlab.com/ee/ci/runners/README.html -## -runnerRegistrationToken: "" - -## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use -## Provide resource name for a Kubernetes Secret Object in the same namespace, -## this is used to populate the /etc/gitlab-runner/certs directory -## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates -## -#certsSecretName: - -## Configure the maximum number of concurrent jobs -## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section -## -concurrent: 10 - -## Defines in seconds how often to check GitLab for a new builds -## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section -## -checkInterval: 30 - -## For RBAC support: -rbac: - create: false - - ## Run the gitlab-bastion container with the ability to deploy/manage containers of jobs - ## cluster-wide or only within namespace - clusterWideAccess: false - - ## Use the following Kubernetes Service Account name if RBAC is disabled in this Helm chart (see rbac.create) - ## - # serviceAccountName: default - -## Configuration for the Pods that the runner launches for each new job -## -runners: - ## Default container image to use for builds when none is specified - ## - image: ubuntu:16.04 - - ## Run all containers with the privileged flag enabled - ## This will allow the docker:stable-dind image to run if you need to run Docker - ## commands. Please read the docs before turning this on: - ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind - ## - privileged: false - - ## Namespace to run Kubernetes jobs in (defaults to 'default') - ## - # namespace: - - ## Build Container specific configuration - ## - builds: - # cpuLimit: 200m - # memoryLimit: 256Mi - cpuRequests: 100m - memoryRequests: 128Mi - - ## Service Container specific configuration - ## - services: - # cpuLimit: 200m - # memoryLimit: 256Mi - cpuRequests: 100m - memoryRequests: 128Mi - - ## Helper Container specific configuration - ## - helpers: - # cpuLimit: 200m - # memoryLimit: 256Mi - cpuRequests: 100m - memoryRequests: 128Mi - -``` - -### Enabling RBAC support - -If your cluster has RBAC enabled, you can choose to either have the chart create its own service account or provide one. - -To have the chart create the service account for you, set `rbac.create` to true. - -### Controlling maximum Runner concurrency - -A single GitLab Runner deployed on Kubernetes is able to execute multiple jobs in parallel by automatically starting additional Runner pods. The [`concurrent` setting](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) controls the maximum number of pods allowed at a single time, and defaults to `10`. - -```yaml -## Configure the maximum number of concurrent jobs -## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section -## -concurrent: 10 -``` - -### Running Docker-in-Docker containers with GitLab Runners - -See [Running Privileged Containers for the Runners](#running-privileged-containers-for-the-runners) for how to enable it, -and the [GitLab CI Runner documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-in-your-builds) on running dind. - -### Running privileged containers for the Runners - -You can tell the GitLab Runner to run using privileged containers. You may need -this enabled if you need to use the Docker executable within your GitLab CI jobs. - -This comes with several risks that you can read about in the -[GitLab CI Runner documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-in-your-builds). - -If you are okay with the risks, and your GitLab CI Runner instance is registered -against a specific project in GitLab that you trust the CI jobs of, you can -enable privileged mode in `values.yaml`: - -```yaml -runners: - ## Run all containers with the privileged flag enabled - ## This will allow the docker:stable-dind image to run if you need to run Docker - ## commands. Please read the docs before turning this on: - ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind - ## - privileged: true -``` - -### Providing a custom certificate for accessing GitLab - -You can provide a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/) -to the GitLab Runner Helm Chart, which will be used to populate the container's -`/etc/gitlab-runner/certs` directory. - -Each key name in the Secret will be used as a filename in the directory, with the -file content being the value associated with the key. - -More information on how GitLab Runner uses these certificates can be found in the -[Runner Documentation](https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates). - - - The key/file name used should be in the format `<gitlab-hostname>.crt`. For example: `gitlab.your-domain.com.crt`. - - Any intermediate certificates need to be concatenated to your server certificate in the same file. - - The hostname used should be the one the certificate is registered for. - -The GitLab Runner Helm Chart does not create a secret for you. In order to create -the secret, you can prepare your certificate on you local machine, and then run -the `kubectl create secret` command from the directory with the certificate - -```bash -kubectl - --namespace <NAMESPACE> - create secret generic <SECRET_NAME> - --from-file=<CERTFICATE_FILENAME> -``` - -- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner. -- `<SECRET_NAME>` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert` -- `<CERTFICATE_FILENAME>` is the filename for the certificate in your current directory that will be imported into the secret - -You then need to provide the secret's name to the GitLab Runner chart. - -Add the following to your `values.yaml` - -```yaml -## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use -## Provide resource name for a Kubernetes Secret Object in the same namespace, -## this is used to populate the /etc/gitlab-runner/certs directory -## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates -## -certsSecretName: <SECRET NAME> -``` - -- `<SECRET_NAME>` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert` - -## Installing GitLab Runner using the Helm Chart - -Add the GitLab Helm repository and initialize Helm: - -```bash -helm repo add gitlab https://charts.gitlab.io -helm init -``` - -Once you [have configured](#configuring-gitlab-runner-using-the-helm-chart) GitLab Runner in your `values.yml` file, -run the following: - -```bash -helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner -``` - -- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner. -- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom configuration. See the - [Configuring GitLab Runner using the Helm Chart](#configuring-gitlab-runner-using-the-helm-chart) section to create it. - -## Updating GitLab Runner using the Helm Chart - -Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade` - -```bash -helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner -``` - -Where: - -- `<NAMESPACE>` is the Kubernetes namespace where GitLab Runner is installed -- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom configuration. See the - [Configuring GitLab Runner using the Helm Chart](#configuring-gitlab-runner-using-the-helm-chart) section to create it. -- `<RELEASE-NAME>` is the name you gave the chart when installing it. - In the [Installing GitLab Runner using the Helm Chart](#installing-gitlab-runner-using-the-helm-chart) section, we called it `gitlab-runner`. - -## Uninstalling GitLab Runner using the Helm Chart - -To uninstall the GitLab Runner Chart, run the following: - -```bash -helm delete --namespace <NAMESPACE> <RELEASE-NAME> -``` - -where: - -- `<NAMESPACE>` is the Kubernetes namespace where GitLab Runner is installed -- `<RELEASE-NAME>` is the name you gave the chart when installing it. - In the [Installing GitLab Runner using the Helm Chart](#installing-gitlab-runner-using-the-helm-chart) section, we called it `gitlab-runner`. +This document was moved to [another location](https://docs.gitlab.com/runner/install/kubernetes.html). diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index ecc956d04e9..7312bf2d4f7 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -21,16 +21,15 @@ of the application including how it should be deployed, upgraded, and configured ## GitLab Chart This chart contains all the required components to get started, and can scale to -large deployments. It offers a number of benefits: +large deployments. It offers a number of benefits, among others: -- Horizontal scaling of individual components -- No requirement for shared storage to scale -- Containers do not need `root` permissions -- Automatic SSL with Let's Encrypt -- An unprivileged GitLab Runner -- and plenty more. +- Horizontal scaling of individual components. +- No requirement for shared storage to scale. +- Containers do not need `root` permissions. +- Automatic SSL with Let's Encrypt. +- An unprivileged GitLab Runner. -Learn more about the [GitLab chart](gitlab_chart.md). +Learn more about the [GitLab chart](https://docs.gitlab.com/charts/). ## GitLab Runner Chart @@ -39,4 +38,4 @@ and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart. -Learn more about [gitlab-runner chart](gitlab_runner_chart.md). +Learn more about the [GitLab Runner chart](https://docs.gitlab.com/runner/install/kubernetes.html). diff --git a/doc/install/kubernetes/preparation/connect.md b/doc/install/kubernetes/preparation/connect.md index a3a0cba4bf2..db55e03d3d4 100644 --- a/doc/install/kubernetes/preparation/connect.md +++ b/doc/install/kubernetes/preparation/connect.md @@ -1,27 +1,5 @@ -# Connecting your computer to a cluster +--- +redirect_to: https://docs.gitlab.com/charts/installation/cloud/ +--- -In order to deploy software and settings to a cluster, you must connect and authenticate to it. - -## Connect to GKE cluster - -The command for connection to the cluster can be obtained from the -[Google Cloud Platform Console](https://console.cloud.google.com/kubernetes/list) -by the individual cluster. - -Look for the **Connect** button in the clusters list page or use the command below, -filling in your cluster's information: - -``` -gcloud container clusters get-credentials <cluster-name> --zone <zone> --project <project-id> -``` - -## Connect to EKS cluster - -For the most up to date instructions, follow the Amazon EKS documentation on -[connecting to a cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#eks-configure-kubectl). - -## Connect to local minikube cluster - -If you are doing local development, you can use `minikube` as your -local cluster. If `kubectl cluster-info` is not showing `minikube` as the current -cluster, use `kubectl config set-cluster minikube` to set the active cluster. +This document was moved to [another location](https://docs.gitlab.com/charts/installation/cloud/). diff --git a/doc/install/kubernetes/preparation/eks.md b/doc/install/kubernetes/preparation/eks.md index ea3b075dd82..975d35c11c6 100644 --- a/doc/install/kubernetes/preparation/eks.md +++ b/doc/install/kubernetes/preparation/eks.md @@ -1,45 +1,5 @@ -# Running GitLab on EKS +--- +redirect_to: https://docs.gitlab.com/charts/installation/cloud/eks.html +--- -There are a few nuances to Amazon EKS which are important to be aware of, when deploying GitLab. - -## Persistent volume management - -There are two methods to manage volume claims on Kubernetes: - -1. Manually creating each persistent volume (recommended on EKS) -1. Utilizing dynamic provisioning to automatically create the persistent volumes - -### Manual provisioning of volumes (Recommended) - -Manually creating the volumes allows you to control the zone of each volume, as well as all other details supported by the underlying storage. - -Follow our documentation on [manually creating persistent volumes](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#manually-creating-static-volumes). - -### Dynamic provisioning of volumes - -Dynamic provisioning utilizes a Kubernetes provisioner, like `aws-ebs`, to automatically create persistent volumes to fulfill each claim. - -With EKS, there are a few important details to keep in mind: - -1. Clusters are required to span multiple AZ's -1. Kubernetes volume provisioners create volumes across zones without regard to which pod they belong to. This leads to scenarios where a pod with multiple volumes being unable to start due to the volumes being in different zones. -1. There is no default Storage Class. - -The easiest way to solve this and still utilize dynamic provisioning is to utilize, or create, a Storage Class that is locked to a specific zone. - -> **Note**: Restricting volumes to specific zone will cause GitLab and any other application using this Storage Class to only reside in that zone. For multiple zone support, utilize [manually provisioned volumes](#manual-provisioning-of-volumes-recommended). - -To create the storage class, download and edit Amazon EKS's [sample Storage Class](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) and add the following parameter: - -```yaml -parameters: - zone: <desired-zone> -``` - -Then [specify the Storage Class](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#using-a-custom-storage-class) name when deploying GitLab. - -## External access to GitLab - -By default, GitLab will an deploy an ingress which will create an associated Elastic Load Balancer. Since the DNS names of ELB's cannot be known ahead of time, it is difficult to utilize Let's Encrypt to automatically provision HTTPS certificates. - -We recommend [using your own certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md#option-2-use-your-own-wildcard-certificate), and then mapping your desired DNS name to the created ELB using a CNAME record. +This document was moved to [another location](https://docs.gitlab.com/charts/installation/cloud/eks.html). diff --git a/doc/install/kubernetes/preparation/networking.md b/doc/install/kubernetes/preparation/networking.md index b9fb9a7399f..2af16a752dc 100644 --- a/doc/install/kubernetes/preparation/networking.md +++ b/doc/install/kubernetes/preparation/networking.md @@ -1,38 +1,5 @@ -# Networking Prerequisites +--- +redirect_to: https://docs.gitlab.com/charts/installation/deployment.html#networking-and-dns +--- -NOTE: **Note:** -Amazon EKS utilizes Elastic Load Balancers, which are addressed by DNS name and -cannot be known ahead of time. If you're using EKS, you can skip this section. - -The `gitlab` chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html). - -To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the external IP. - -## External IP - -To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, to the Load Balancer. - -Set `global.hosts.externalIP` to this IP address when [deploying GitLab](../gitlab_chart.md#installing-gitlab-using-the-helm-chart). - -Then, create a [wildcard DNS record](#wildcard-dns-entry) which resolves to this IP address. - -### Creating an external IP on GCP - -When creating the external IP, it is critical to create it in the same region as your cluster. Otherwise, the IP address will fail to bind to the Load Balancer. - -1. Open the [web console](https://console.cloud.google.com) -1. In the sidebar, browse to `VPC Network > External IP addresses` -1. Click `Reserve static address` -1. Choose `Regional` and select the region of your cluster -1. Leave `Attached to` blank, as it will be automatically assigned during deployment - -## Wildcard DNS entry - -Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Typically this would be an `A record` for `*`, resolving to the external IP above. - -Please consult the documentation for your DNS service for more information on creating DNS records: - -- [Google Domains](https://support.google.com/domains/answer/3290350?hl=en) -- [GoDaddy](https://www.godaddy.com/help/add-an-a-record-19238) - -Set `global.hosts.domain` to this DNS name when [deploying GitLab](../gitlab_chart.md#installing-gitlab-using-the-helm-chart). +This document was moved to [another location](https://docs.gitlab.com/charts/installation/deployment.html#networking-and-dns). diff --git a/doc/install/kubernetes/preparation/rbac.md b/doc/install/kubernetes/preparation/rbac.md index c5f8d7a7e9e..f94e7c24cdc 100644 --- a/doc/install/kubernetes/preparation/rbac.md +++ b/doc/install/kubernetes/preparation/rbac.md @@ -1,20 +1,5 @@ -# Role Based Access Control +--- +redirect_to: https://docs.gitlab.com/charts/installation/deployment.html#rbac +--- -Until Kubernetes 1.7, there were no permissions within a cluster. With the launch -of 1.7, there is now a [role based access control system (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) -which determines what services can perform actions within a cluster. - -RBAC affects a few different aspects of GitLab: - -- [Installation of GitLab using Helm](tiller.md#preparing-for-helm-with-rbac) -- Prometheus monitoring -- GitLab Runner - -## Checking that RBAC is enabled - -Try listing the current cluster roles, if it fails then `RBAC` is disabled. -The following command will output `false` if `RBAC` is disabled and `true` otherwise: - -```sh -kubectl get clusterroles > /dev/null 2>&1 && echo true || echo false -``` +This document was moved to [another location](https://docs.gitlab.com/charts/installation/deployment.html#rbac). diff --git a/doc/install/kubernetes/preparation/tiller.md b/doc/install/kubernetes/preparation/tiller.md index 684df14ac2c..66d6c8faece 100644 --- a/doc/install/kubernetes/preparation/tiller.md +++ b/doc/install/kubernetes/preparation/tiller.md @@ -1,109 +1,5 @@ -# Configuring and initializing Helm Tiller - -To make use of Helm, you must have a [Kubernetes][k8s-io] cluster. Ensure you can -access your cluster using `kubectl`. - -Helm consists of two parts, the `helm` client and a `tiller` server inside Kubernetes. - -NOTE: **Note:** -If you are not able to run Tiller in your cluster, for example on OpenShift, it -is possible to use [Tiller locally](https://docs.gitlab.com/charts/installation/tools.html#local-tiller) -and avoid deploying it into the cluster. This should only be used when Tiller -cannot be normally deployed. - -## Initialize Helm and Tiller - -Tiller is deployed into the cluster and interacts with the Kubernetes API to deploy your applications. If role based access control (RBAC) is enabled, Tiller will need to be [granted permissions](#preparing-for-helm-with-rbac) to allow it to talk to the Kubernetes API. - -If RBAC is not enabled, skip to [initializing Helm](#initialize-helm). - -If you are not sure whether RBAC is enabled in your cluster, or to learn more, read through our [RBAC documentation](rbac.md). - -## Preparing for Helm with RBAC - -Helm's Tiller will need to be granted permissions to perform operations. These instructions grant cluster wide permissions, however for more advanced deployments [permissions can be restricted to a single namespace](https://docs.helm.sh/using_helm/#example-deploy-tiller-in-a-namespace-restricted-to-deploying-resources-only-in-that-namespace). To grant access to the cluster, we will create a new `tiller` service account and bind it to the `cluster-admin` role. - -Create a file `rbac-config.yaml` with the following contents: - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: tiller - namespace: kube-system --- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: tiller -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: - - kind: ServiceAccount - name: tiller - namespace: kube-system -``` - -Next we need to connect to the cluster and upload the RBAC config. - -### Upload the RBAC config - -Some clusters require authentication to use `kubectl` to create the Tiller roles. - -#### Upload the RBAC config as an admin user (GKE) - -For GKE, you need to obtain the admin credentials. This command will output the admin password: - -``` -gcloud container clusters describe <cluster-name> --zone <zone> --project <project-id> --format='value(masterAuth.password)' -``` - -Use the admin password to set the admin credentials. Replace the password value below with the output value from the above step: - -``` -kubectl config set-credentials admin --username=admin --password=xxxxxxxxxxxxxx -``` - -Once credentials have been set, create the role: - -``` -kubectl --user=admin create -f rbac-config.yaml -``` - -#### Upload the RBAC config (Non-GKE clusters) - -For other clusters like Amazon EKS, you can directly upload the RBAC configuration. - -``` -kubectl create -f rbac-config.yaml -``` - -## Initialize Helm - -Deploy Helm Tiller with a service account: - -``` -helm init --service-account tiller -``` - -If your cluster previously had Helm/Tiller installed, -run the following to ensure that the deployed version of Tiller matches the local Helm version: - -``` -helm init --upgrade --service-account tiller -``` - -### Patching Helm Tiller for Amazon EKS - -Helm Tiller requires a flag to be enabled to work properly on Amazon EKS: - -``` -kubectl -n kube-system patch deployment tiller-deploy -p '{"spec": {"template": {"spec": {"automountServiceAccountToken": true}}}}' -``` +redirect_to: https://docs.gitlab.com/charts/installation/tools.html +--- -[helm]: https://helm.sh -[helm-using]: https://docs.helm.sh/using_helm -[k8s-io]: https://kubernetes.io/ -[gcp-k8s]: https://console.cloud.google.com/kubernetes/list +This document was moved to [another location](https://docs.gitlab.com/charts/installation/tools.html). diff --git a/doc/install/kubernetes/preparation/tools_installation.md b/doc/install/kubernetes/preparation/tools_installation.md index d2f7a69a0af..66d6c8faece 100644 --- a/doc/install/kubernetes/preparation/tools_installation.md +++ b/doc/install/kubernetes/preparation/tools_installation.md @@ -1,19 +1,5 @@ -# Installing kubectl and Helm on your computer +--- +redirect_to: https://docs.gitlab.com/charts/installation/tools.html +--- -In order to work with the GitLab Helm charts, `kubectl` and `helm` must be installed and configured on your computer. - -## Installing `kubectl` - -`kubectl` is the Kubernetes command line tool, which can be used to deploy settings to the cluster. - -Follow the [official documentation](https://kubernetes.io/docs/tasks/tools/install-kubectl/) for the most up to date instructions. - -## Installing `helm` - -Helm is a package management tool for Kubernetes, and is used to deploy charts. - -You can get Helm from the project's [releases page](https://github.com/kubernetes/helm/releases), or follow other options under the official documentation of [Installing Helm](https://docs.helm.sh/using_helm/#installing-helm). - -# Next steps - -Once installed, proceed to the next [installation step](../gitlab_chart.md#installing-gitlab-using-the-helm-chart). +This document was moved to [another location](https://docs.gitlab.com/charts/installation/tools.html). diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 7693109b3c4..9060360e6a2 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -955,7 +955,7 @@ required to go from `10%` to `100%`, you can jump to whatever job you want. You can also scale down by running a lower percentage job, just before hitting `100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll back by redeploying the old version using the -[rollback button](../../ci/environments.md#rolling-back-changes) in the +[rollback button](../../ci/environments.md#retrying-and-rolling-back) in the environment page. Below, you can see how the pipeline will look if the rollout or staging diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 3bcfd30079d..9c3f6fcec9b 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -151,6 +151,17 @@ There are two different ways to add a new project to a group: ![Select group](img/select_group_dropdown.png) +### Default project creation level + +Group owners or administrators can allow users with the +Developer role to create projects under groups. + +By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under agroup, but this can be changed either within the group settings for a group, or +be set globally by a GitLab administrator in the Admin area +at **Settings > General > Visibility and access controls**. + +Available settings are `No one`, `Maintainers`, or `Developers + Maintainers`. + ## Transfer projects into groups Learn how to [transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace). diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index c94a3f4d3b5..878d30dddaa 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -570,7 +570,7 @@ deployment jobs, immediately before the jobs starts. However, sometimes GitLab can not create them. In such instances, your job will fail with the message: ```text -The job failed to complete prerequisite tasks +This job failed because the necessary resources were not successfully created. ``` To find the cause of this error when creating a namespace and service account, check the [logs](../../../administration/logs.md#sidekiqlog). diff --git a/doc/user/project/clusters/serverless/img/function-details-loaded.png b/doc/user/project/clusters/serverless/img/function-details-loaded.png Binary files differnew file mode 100644 index 00000000000..34465c5c087 --- /dev/null +++ b/doc/user/project/clusters/serverless/img/function-details-loaded.png diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 96bf455116c..5b7e9ef906f 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -11,13 +11,12 @@ Run serverless workloads on Kubernetes using [Knative](https://cloud.google.com/ Knative extends Kubernetes to provide a set of middleware components that are useful to build modern, source-centric, container-based applications. Knative brings some significant benefits out of the box through its main components: -- [Build](https://github.com/knative/build): Source-to-container build orchestration. -- [Eventing](https://github.com/knative/eventing): Management and delivery of events. - [Serving](https://github.com/knative/serving): Request-driven compute that can scale to zero. +- [Eventing](https://github.com/knative/eventing): Management and delivery of events. For more information on Knative, visit the [Knative docs repo](https://github.com/knative/docs). -With GitLab serverless, you can deploy both functions-as-a-service (FaaS) and serverless applications. +With GitLab Serverless, you can deploy both functions-as-a-service (FaaS) and serverless applications. ## Prerequisites @@ -41,13 +40,16 @@ To run Knative on Gitlab, you will need: wildcard domain where your applications will be served. Configure your DNS server to use the external IP address or hostname for that domain. 1. **`.gitlab-ci.yml`:** GitLab uses [Kaniko](https://github.com/GoogleContainerTools/kaniko) - to build the application and the [TriggerMesh CLI](https://github.com/triggermesh/tm) to simplify the - deployment of knative services and functions. + to build the application. We also use [gitlabktl](https://gitlab.com/gitlab-org/gitlabktl) + and [TriggerMesh CLI](https://github.com/triggermesh/tm) CLIs to simplify the + deployment of services and functions to Knative. 1. **`serverless.yml`** (for [functions only](#deploying-functions)): When using serverless to deploy functions, the `serverless.yml` file will contain the information for all the functions being hosted in the repository as well as a reference to the runtime being used. -1. **`Dockerfile`** (for [applications only](#deploying-serverless-applications): Knative requires a `Dockerfile` in order to build your application. It should be included - at the root of your project's repo and expose port `8080`. +1. **`Dockerfile`** (for [applications only](#deploying-serverless-applications): Knative requires a + `Dockerfile` in order to build your applications. It should be included at the root of your + project's repo and expose port `8080`. `Dockerfile` is not require if you plan to build serverless functions + using our [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes). 1. **Prometheus** (optional): Installing Prometheus allows you to monitor the scale and traffic of your serverless function/application. See [Installing Applications](../index.md#installing-applications) for more information. @@ -89,10 +91,11 @@ Using functions is useful for dealing with independent events without needing to maintain a complex unified infrastructure. This allows you to focus on a single task that can be executed/scaled automatically and independently. -Currently the following [runtimes](https://gitlab.com/triggermesh/runtimes) are offered: +Currently the following [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes) are offered: +- ruby - node.js -- kaniko +- Dockerfile You can find and import all the files referenced in this doc in the **[functions example project](https://gitlab.com/knative-examples/functions)**. @@ -111,13 +114,17 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ include: template: Serverless.gitlab-ci.yml - functions: + functions:build: + extends: .serverless:build:functions + environment: production + + functions:deploy: extends: .serverless:deploy:functions environment: production ``` - This `.gitlab-ci.yml` creates a `functions` job that invokes some - predefined commands to deploy your functions to your cluster. + This `.gitlab-ci.yml` creates jobs that invoke some predefined commands to + build and deploy your functions to your cluster. `Serverless.gitlab-ci.yml` is a template that allows customization. You can either import it with `include` parameter and use `extends` to @@ -135,29 +142,40 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ You can find the relevant files for this project in the [functions example project](https://gitlab.com/knative-examples/functions). ```yaml - service: my-functions - description: "Deploying functions from GitLab using Knative" + service: functions + description: "GitLab Serverless functions using Knative" provider: name: triggermesh - registry-secret: gitlab-registry environment: - FOO: BAR + FOO: value functions: - echo: - handler: echo - runtime: https://gitlab.com/triggermesh/runtimes/raw/master/nodejs.yaml - description: "echo function using node.js runtime" - buildargs: - - DIRECTORY=echo + echo-js: + handler: echo-js + source: ./echo-js + runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs + description: "node.js runtime function" + environment: + MY_FUNCTION: echo-js + + echo-rb: + handler: MyEcho.my_function + source: ./echo-rb + runtime: https://gitlab.com/gitlab-org/serverless/runtimes/ruby + description: "Ruby runtime function" + environment: + MY_FUNCTION: echo-rb + + echo-docker: + handler: echo-docker + source: ./echo-docker + description: "Dockerfile runtime function" environment: - FUNCTION: echo + MY_FUNCTION: echo-docker ``` -The `serverless.yml` file references both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`), -which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it -contains three sections with distinct parameters: +Explanation of the fields used above: ### `service` @@ -171,7 +189,6 @@ contains three sections with distinct parameters: | Parameter | Description | |-----------|-------------| | `name` | Indicates which provider is used to execute the `serverless.yml` file. In this case, the TriggerMesh `tm` CLI. | -| `registry-secret` | Indicates which registry will be used to store docker images. The sample function is using the GitLab Registry (`gitlab-registry`). A different registry host may be specified using `registry` key in the `provider` object. If changing the default, update the permission and the secret value on the `gitlab-ci.yml` file | | `environment` | Includes the environment variables to be passed as part of function execution for **all** functions in the file, where `FOO` is the variable name and `BAR` are he variable contents. You may replace this with you own variables. | ### `functions` @@ -180,10 +197,10 @@ In the `serverless.yml` example above, the function name is `echo` and the subse | Parameter | Description | |-----------|-------------| -| `handler` | The function's file name. In the example above, both the function name and the handler are the same. | +| `handler` | The function's name. | +| `source` | Directory with sources of a functions. | | `runtime` | The runtime to be used to execute the function. | | `description` | A short description of the function. | -| `buildargs` | Pointer to the function file in the repo. In the sample the function is located in the `echo` directory. | | `environment` | Sets an environment variable for the specific function only. | After the `gitlab-ci.yml` template has been added and the `serverless.yml` file has been @@ -284,3 +301,23 @@ The second to last line, labeled **Service domain** contains the URL for the dep browser to see the app live. ![knative app](img/knative-app.png) + +## Function details + +Go to the **Operations > Serverless** page and click on one of the function +rows to bring up the function details page. + +![function_details](img/function-details-loaded.png) + +The pod count will give you the number of pods running the serverless function instances on a given cluster. + +### Prometheus support + +For the Knative function invocations to appear, +[Prometheus must be installed](../index.md#installing-applications). + +Once Prometheus is installed, a message may appear indicating that the metrics data _is +loading or is not available at this time._ It will appear upon the first access of the +page, but should go away after a few seconds. If the message does not disappear, then it +is possible that GitLab is unable to connect to the Prometheus instance running on the +cluster. diff --git a/jest.config.js b/jest.config.js index c7518be9e96..fdbbe977f0b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,10 +16,13 @@ module.exports = { testMatch: ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js'], moduleFileExtensions: ['js', 'json', 'vue'], moduleNameMapper: { - '^~(.*)$': '<rootDir>/app/assets/javascripts$1', - '^ee(.*)$': '<rootDir>/ee/app/assets/javascripts$1', - '^helpers(.*)$': '<rootDir>/spec/frontend/helpers$1', - '^vendor(.*)$': '<rootDir>/vendor/assets/javascripts$1', + '^~(/.*)$': '<rootDir>/app/assets/javascripts$1', + '^ee(/.*)$': '<rootDir>/ee/app/assets/javascripts$1', + '^ee_else_ce(/.*)$': IS_EE + ? '<rootDir>/ee/app/assets/javascripts$1' + : '<rootDir>/app/assets/javascripts$1', + '^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1', + '^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1', '\\.(jpg|jpeg|png|svg)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js', }, collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'], diff --git a/lib/api/internal.rb b/lib/api/internal.rb index cb9aa849eeb..9c7b9146c8f 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -87,7 +87,8 @@ module API gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, git_config_options: [], - gitaly: gitaly_payload(params[:action]) + gitaly: gitaly_payload(params[:action]), + gl_console_messages: check_result.console_messages } # Custom option for git-receive-pack command diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 3dd90502050..999a9cb5a82 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -307,7 +307,7 @@ module API merge_requests = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user) .execute(issue) - .flatten + .first present paginate(::Kaminari.paginate_array(merge_requests)), with: Entities::MergeRequest, diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 98dcc388f44..e4b21b7d1c4 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -111,6 +111,7 @@ module API desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :source_branch, type: String, desc: 'Return merge requests with the given source branch' + optional :source_project_id, type: Integer, desc: 'Return merge requests with the given source project id' optional :target_branch, type: String, desc: 'Return merge requests with the given target branch' optional :search, type: String, desc: 'Search merge requests for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' diff --git a/lib/api/settings.rb b/lib/api/settings.rb index d742c6c97c1..d96cdc31212 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -40,7 +40,8 @@ module API end optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" - optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' + optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' + optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 5f8aca104aa..44b151d01e7 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -195,15 +195,21 @@ module Banzai content = link_content || object_link_text(object, matches) - %(<a href="#{url}" #{data} - title="#{escape_once(title)}" - class="#{klass}">#{content}</a>) + link = %(<a href="#{url}" #{data} + title="#{escape_once(title)}" + class="#{klass}">#{content}</a>) + + wrap_link(link, object) else match end end end + def wrap_link(link, object) + link + end + def data_attributes_for(text, parent, object, link_content: false, link_reference: false) object_parent_type = parent.is_a?(Group) ? :group : :project diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb index ad367cc5efe..8f5ad9981e5 100644 --- a/lib/banzai/filter/blockquote_fence_filter.rb +++ b/lib/banzai/filter/blockquote_fence_filter.rb @@ -42,7 +42,9 @@ module Banzai def call @text.gsub(REGEX) do if $~[:quote] - $~[:quote].gsub(/^/, "> ").gsub(/^> $/, ">") + # keep the same number of source lines/positions by replacing the + # fence lines with newlines + "\n" + $~[:quote].gsub(/^/, "> ").gsub(/^> $/, ">") + "\n" else $~[0] end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index f90a35952e5..77e4c438bd0 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -91,7 +91,11 @@ module Banzai label_suffix = " <i>in #{reference}</i>" if reference.present? end - LabelsHelper.render_colored_label(object, label_suffix) + LabelsHelper.render_colored_label(object, label_suffix: label_suffix, title: tooltip_title(object)) + end + + def tooltip_title(label) + nil end def full_path_ref?(matches) diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 6c8ca8f219c..6eb08f674c2 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -24,6 +24,11 @@ module Gitlab PROTECTION_FULL = 2 PROTECTION_DEV_CAN_MERGE = 3 + # Default project creation level + NO_ONE_PROJECT_ACCESS = 0 + MAINTAINER_PROJECT_ACCESS = 1 + DEVELOPER_MAINTAINER_PROJECT_ACCESS = 2 + class << self delegate :values, to: :options @@ -85,6 +90,22 @@ module Gitlab def human_access_with_none(access) options_with_none.key(access) end + + def project_creation_options + { + s_('ProjectCreationLevel|No one') => NO_ONE_PROJECT_ACCESS, + s_('ProjectCreationLevel|Maintainers') => MAINTAINER_PROJECT_ACCESS, + s_('ProjectCreationLevel|Developers + Maintainers') => DEVELOPER_MAINTAINER_PROJECT_ACCESS + } + end + + def project_creation_values + project_creation_options.values + end + + def project_creation_level_name(name) + project_creation_options.key(name) + end end def human_access diff --git a/lib/gitlab/ci/config/entry/includes.rb b/lib/gitlab/ci/config/entry/includes.rb index 82b2b1ccf4b..43e74dfd628 100644 --- a/lib/gitlab/ci/config/entry/includes.rb +++ b/lib/gitlab/ci/config/entry/includes.rb @@ -11,7 +11,7 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable validations do - validates :config, type: Array + validates :config, array_or_string: true end def self.aspects diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index f7d0715e617..96d05842838 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -16,7 +16,8 @@ module Gitlab Status::Build::Skipped], [Status::Build::Cancelable, Status::Build::Retryable], - [Status::Build::Failed], + [Status::Build::FailedUnmetPrerequisites, + Status::Build::Failed], [Status::Build::FailedAllowed, Status::Build::Unschedule, Status::Build::Play, diff --git a/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb new file mode 100644 index 00000000000..eaad3969a4c --- /dev/null +++ b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Build + class FailedUnmetPrerequisites < Status::Extended + def illustration + { + image: 'illustrations/pipelines_failed.svg', + size: 'svg-430', + title: _('Failed to create resources'), + content: _('Retry this job in order to create the necessary resources.') + } + end + + def self.matches?(build, _) + build.unmet_prerequisites? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index f786022beb0..b3eec2b6210 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -78,8 +78,14 @@ include: - template: Jobs/Code-Quality.gitlab-ci.yml - template: Jobs/Deploy.gitlab-ci.yml - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml - - template: Jobs/DAST.gitlab-ci.yml + - template: Security/DAST.gitlab-ci.yml - template: Security/Container-Scanning.gitlab-ci.yml - template: Security/Dependency-Scanning.gitlab-ci.yml - template: Security/License-Management.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml + +# Override DAST job to exclude master branch +dast: + except: + refs: + - master
\ No newline at end of file diff --git a/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml deleted file mode 100644 index aedbbb21674..00000000000 --- a/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -dast: - stage: dast - image: docker:stable - variables: - DOCKER_DRIVER: overlay2 - allow_failure: true - services: - - docker:stable-dind - script: - - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} - - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} - - | - if ! docker info &>/dev/null; then - if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then - export DOCKER_HOST='tcp://localhost:2375' - fi - fi - - | - function dast_run() { - docker run \ - --env DAST_TARGET_AVAILABILITY_TIMEOUT \ - --volume "$PWD:/output" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - -w /output \ - "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ - /analyze -t $DAST_WEBSITE \ - "$@" - } - - | - if [ -n "$DAST_AUTH_URL" ] - then - dast_run \ - --auth-url $DAST_AUTH_URL \ - --auth-username $DAST_USERNAME \ - --auth-password $DAST_PASSWORD \ - --auth-username-field $DAST_USERNAME_FIELD \ - --auth-password-field $DAST_PASSWORD_FIELD - else - dast_run - fi - artifacts: - reports: - dast: gl-dast-report.json - only: - refs: - - branches - - tags - variables: - - $GITLAB_FEATURES =~ /\bdast\b/ - except: - refs: - - master - variables: - - $DAST_DISABLED diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml index 770340de16f..2a90cc9a06c 100644 --- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml @@ -4,12 +4,6 @@ # List of the variables: https://gitlab.com/gitlab-org/security-products/dast#settings # How to set: https://docs.gitlab.com/ee/ci/yaml/#variables -include: - - template: Jobs/DAST.gitlab-ci.yml - -variables: - DAST_WEBSITE: http://example.com # Please edit to be your website to scan for vulnerabilities - stages: - build - test @@ -17,10 +11,53 @@ stages: - dast dast: + stage: dast + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:stable-dind + script: + - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} + - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} + - | + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + - | + function dast_run() { + docker run \ + --env DAST_TARGET_AVAILABILITY_TIMEOUT \ + --volume "$PWD:/output" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + -w /output \ + "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ + /analyze -t $DAST_WEBSITE \ + "$@" + } + - | + if [ -n "$DAST_AUTH_URL" ] + then + dast_run \ + --auth-url $DAST_AUTH_URL \ + --auth-username $DAST_USERNAME \ + --auth-password $DAST_PASSWORD \ + --auth-username-field $DAST_USERNAME_FIELD \ + --auth-password-field $DAST_PASSWORD_FIELD + else + dast_run + fi + artifacts: + reports: + dast: gl-dast-report.json only: refs: - branches + variables: + - $GITLAB_FEATURES =~ /\bdast\b/ except: - refs: [] # Override default from template variables: - $DAST_DISABLED diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml index 4f3d08d98fe..4c92dcb7941 100644 --- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml @@ -31,11 +31,14 @@ stages: - echo "$CI_REGISTRY_IMAGE" - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait +.serverless:build:functions: + stage: build + environment: development + image: registry.gitlab.com/gitlab-org/gitlabktl:latest + script: /usr/bin/gitlabktl serverless build + .serverless:deploy:functions: stage: deploy environment: development - image: gcr.io/triggermesh/tm:v0.0.9 - script: - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull - - tm -n "$KUBE_NAMESPACE" deploy --wait + image: registry.gitlab.com/gitlab-org/gitlabktl:latest + script: /usr/bin/gitlabktl serverless deploy diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 746fe83f90f..df34d254c65 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -54,6 +54,14 @@ module Gitlab end end + class ArrayOrStringValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Array) || value.is_a?(String) + record.errors.add(attribute, 'should be an array or a string') + end + end + end + class BooleanValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 76c8b4ec5c2..fa06fb935f7 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -47,7 +47,7 @@ module Gitlab user: build.user.try(:hook_attrs), runner: build.runner && runner_hook_attrs(build.runner), artifacts_file: { - filename: build.artifacts_file.filename, + filename: build.artifacts_file&.filename, size: build.artifacts_size } } diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 3abd0600e9d..7f5eb1188fc 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -282,6 +282,7 @@ module Gitlab # Updates the value of a column in batches. # # This method updates the table in batches of 5% of the total row count. + # A `batch_size` option can also be passed to set this to a fixed number. # This method will continue updating rows until no rows remain. # # When given a block this method will yield two values to the block: @@ -320,7 +321,7 @@ module Gitlab # make things _more_ complex). # # rubocop: disable Metrics/AbcSize - def update_column_in_batches(table, column, value) + def update_column_in_batches(table, column, value, batch_size: nil) if transaction_open? raise 'update_column_in_batches can not be run inside a transaction, ' \ 'you can disable transactions by calling disable_ddl_transaction! ' \ @@ -336,14 +337,16 @@ module Gitlab return if total == 0 - # Update in batches of 5% until we run out of any rows to update. - batch_size = ((total / 100.0) * 5.0).ceil - max_size = 1000 + if batch_size.nil? + # Update in batches of 5% until we run out of any rows to update. + batch_size = ((total / 100.0) * 5.0).ceil + max_size = 1000 - # The upper limit is 1000 to ensure we don't lock too many rows. For - # example, for "merge_requests" even 1% of the table is around 35 000 - # rows for GitLab.com. - batch_size = max_size if batch_size > max_size + # The upper limit is 1000 to ensure we don't lock too many rows. For + # example, for "merge_requests" even 1% of the table is around 35 000 + # rows for GitLab.com. + batch_size = max_size if batch_size > max_size + end start_arel = table.project(table[:id]).order(table[:id].asc).take(1) start_arel = yield table, start_arel if block_given? diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index 0891f79198d..17fbecbd097 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -15,50 +15,51 @@ module Gitlab new environments].freeze RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape))) + RESERVED_WORDS_PREFIX = %Q(^(?!.*\/(#{RESERVED_WORDS_REGEX})\/).*) ROUTES = [ Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z), + %r(#{RESERVED_WORDS_PREFIX}/noteable/issue/\d+/notes\z), 'issue_notes' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/realtime_changes\z), + %r(#{RESERVED_WORDS_PREFIX}/issues/\d+/realtime_changes\z), 'issue_title' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/commit/\S+/pipelines\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/commit/\S+/pipelines\.json\z), 'commit_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/new\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/merge_requests/new\.json\z), 'new_merge_request_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/\d+/pipelines\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/merge_requests/\d+/pipelines\.json\z), 'merge_request_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/pipelines\.json\z), 'project_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines/\d+\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/pipelines/\d+\.json\z), 'project_pipeline' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/builds/\d+\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/builds/\d+\.json\z), 'project_build' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/environments\.json\z), 'environments' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/github/realtime_changes\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/import/github/realtime_changes\.json\z), 'realtime_changes_import_github' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/gitea/realtime_changes\.json\z), + %r(#{RESERVED_WORDS_PREFIX}/import/gitea/realtime_changes\.json\z), 'realtime_changes_import_gitea' ) ].freeze diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 010bd0e520c..cb80ed64eff 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -85,7 +85,7 @@ module Gitlab check_push_access! end - ::Gitlab::GitAccessResult::Success.new + ::Gitlab::GitAccessResult::Success.new(console_messages: check_for_console_messages(cmd)) end def guest_can_download_code? @@ -116,6 +116,10 @@ module Gitlab nil end + def check_for_console_messages(cmd) + [] + end + def check_valid_actor! return unless actor.is_a?(Key) diff --git a/lib/gitlab/git_access_result/success.rb b/lib/gitlab/git_access_result/success.rb index 7bb9f24cb0e..e950d727e2e 100644 --- a/lib/gitlab/git_access_result/success.rb +++ b/lib/gitlab/git_access_result/success.rb @@ -3,6 +3,11 @@ module Gitlab module GitAccessResult class Success + attr_reader :console_messages + + def initialize(console_messages: []) + @console_messages = console_messages + end end end end diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb index 6d48c6a15b4..6aad7955415 100644 --- a/lib/gitlab/github_import/importer/repository_importer.rb +++ b/lib/gitlab/github_import/importer/repository_importer.rb @@ -54,6 +54,11 @@ module Gitlab project.repository.fetch_as_mirror(project.import_url, refmap: refmap, forced: true, remote_name: 'github') project.change_head(default_branch) if default_branch + + # The initial fetch can bring in lots of loose refs and objects. + # Running a `git gc` will make importing pull requests faster. + Projects::HousekeepingService.new(project, :gc).execute + true rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e fail_import("Failed to import the repository: #{e.message}") diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index c2be7f3d63a..a56ca1e39e7 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -35,5 +35,9 @@ module Gitlab [project, type] end + + def self.default_type + PROJECT + end end end diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb index f2772c733c7..38b32770e90 100644 --- a/lib/gitlab/object_hierarchy.rb +++ b/lib/gitlab/object_hierarchy.rb @@ -5,6 +5,8 @@ module Gitlab # # This class uses recursive CTEs and as a result will only work on PostgreSQL. class ObjectHierarchy + DEPTH_COLUMN = :depth + attr_reader :ancestors_base, :descendants_base, :model # ancestors_base - An instance of ActiveRecord::Relation for which to @@ -27,6 +29,17 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord + # Returns the maximum depth starting from the base + # A base object with no children has a maximum depth of `1` + def max_descendants_depth + unless hierarchy_supported? + # This makes the return value consistent with the case where hierarchy is supported + return descendants_base.exists? ? 1 : nil + end + + base_and_descendants(with_depth: true).maximum(DEPTH_COLUMN) + end + # Returns the set of ancestors of a given relation, but excluding the given # relation # @@ -64,10 +77,15 @@ module Gitlab # Returns a relation that includes the descendants_base set of objects # and all their descendants (recursively). - def base_and_descendants - return descendants_base unless hierarchy_supported? - - read_only(base_and_descendants_cte.apply_to(model.all)) + # + # When `with_depth` is `true`, a `depth` column is included where it starts with `1` for the base objects + # and incremented as we go down the descendant tree + def base_and_descendants(with_depth: false) + unless hierarchy_supported? + return with_depth ? descendants_base.select("1 as #{DEPTH_COLUMN}", objects_table[Arel.star]) : descendants_base + end + + read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(model.all)) end # Returns a relation that includes the base objects, their ancestors, @@ -124,10 +142,9 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def base_and_ancestors_cte(stop_id = nil, hierarchy_order = nil) cte = SQL::RecursiveCTE.new(:base_and_ancestors) - depth_column = :depth base_query = ancestors_base.except(:order) - base_query = base_query.select("1 as #{depth_column}", objects_table[Arel.star]) if hierarchy_order + base_query = base_query.select("1 as #{DEPTH_COLUMN}", "ARRAY[id] AS tree_path", "false AS tree_cycle", objects_table[Arel.star]) if hierarchy_order cte << base_query @@ -137,7 +154,17 @@ module Gitlab .where(objects_table[:id].eq(cte.table[:parent_id])) .except(:order) - parent_query = parent_query.select(cte.table[depth_column] + 1, objects_table[Arel.star]) if hierarchy_order + if hierarchy_order + quoted_objects_table_name = model.connection.quote_table_name(objects_table.name) + + parent_query = parent_query.select( + cte.table[DEPTH_COLUMN] + 1, + "tree_path || #{quoted_objects_table_name}.id", + "#{quoted_objects_table_name}.id = ANY(tree_path)", + objects_table[Arel.star] + ).where(cte.table[:tree_cycle].eq(false)) + end + parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id cte << parent_query @@ -146,17 +173,32 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def base_and_descendants_cte + def base_and_descendants_cte(with_depth: false) cte = SQL::RecursiveCTE.new(:base_and_descendants) - cte << descendants_base.except(:order) + base_query = descendants_base.except(:order) + base_query = base_query.select("1 AS #{DEPTH_COLUMN}", "ARRAY[id] AS tree_path", "false AS tree_cycle", objects_table[Arel.star]) if with_depth + + cte << base_query # Recursively get all the descendants of the base set. - cte << model + descendants_query = model .from([objects_table, cte.table]) .where(objects_table[:parent_id].eq(cte.table[:id])) .except(:order) + if with_depth + quoted_objects_table_name = model.connection.quote_table_name(objects_table.name) + + descendants_query = descendants_query.select( + cte.table[DEPTH_COLUMN] + 1, + "tree_path || #{quoted_objects_table_name}.id", + "#{quoted_objects_table_name}.id = ANY(tree_path)", + objects_table[Arel.star] + ).where(cte.table[:tree_cycle].eq(false)) + end + + cte << descendants_query cte end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/prometheus/queries/knative_invocation_query.rb b/lib/gitlab/prometheus/queries/knative_invocation_query.rb new file mode 100644 index 00000000000..2691abe46d6 --- /dev/null +++ b/lib/gitlab/prometheus/queries/knative_invocation_query.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + module Prometheus + module Queries + class KnativeInvocationQuery < BaseQuery + include QueryAdditionalMetrics + + def query(serverless_function_id) + PrometheusMetric + .find_by_identifier(:system_metrics_knative_function_invocation_count) + .to_query_metric.tap do |q| + q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id)) + end + end + + protected + + def context(function_id) + function = Serverless::Function.find_by_id(function_id) + { + function_name: function.name, + kube_namespace: function.namespace + } + end + + def run_query(query, context) + query %= context + client_query_range(query, start: 8.hours.ago.to_f, stop: Time.now.to_f) + end + + def self.transform_reactive_result(result) + result[:metrics] = result.delete :data + result + end + end + end + end +end diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb index b4de7cd2bce..f13156f898e 100644 --- a/lib/gitlab/prometheus_client.rb +++ b/lib/gitlab/prometheus_client.rb @@ -24,6 +24,19 @@ module Gitlab json_api_get('query', query: '1') end + def proxy(type, args) + path = api_path(type) + get(path, args) + rescue RestClient::ExceptionWithResponse => ex + if ex.response + ex.response + else + raise PrometheusClient::Error, "Network connection error" + end + rescue RestClient::Exception + raise PrometheusClient::Error, "Network connection error" + end + def query(query, time: Time.now) get_result('vector') do json_api_get('query', query: query, time: time.to_f) @@ -64,22 +77,14 @@ module Gitlab private - def json_api_get(type, args = {}) - path = ['api', 'v1', type].join('/') - get(path, args) - rescue JSON::ParserError - raise PrometheusClient::Error, 'Parsing response failed' - rescue Errno::ECONNREFUSED - raise PrometheusClient::Error, 'Connection refused' + def api_path(type) + ['api', 'v1', type].join('/') end - def get(path, args) - response = rest_client[path].get(params: args) + def json_api_get(type, args = {}) + path = api_path(type) + response = get(path, args) handle_response(response) - rescue SocketError - raise PrometheusClient::Error, "Can't connect to #{rest_client.url}" - rescue OpenSSL::SSL::SSLError - raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data" rescue RestClient::ExceptionWithResponse => ex if ex.response handle_exception_response(ex.response) @@ -90,8 +95,18 @@ module Gitlab raise PrometheusClient::Error, "Network connection error" end + def get(path, args) + rest_client[path].get(params: args) + rescue SocketError + raise PrometheusClient::Error, "Can't connect to #{rest_client.url}" + rescue OpenSSL::SSL::SSLError + raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data" + rescue Errno::ECONNREFUSED + raise PrometheusClient::Error, 'Connection refused' + end + def handle_response(response) - json_data = JSON.parse(response.body) + json_data = parse_json(response.body) if response.code == 200 && json_data['status'] == 'success' json_data['data'] || {} else @@ -103,7 +118,7 @@ module Gitlab if response.code == 200 && response['status'] == 'success' response['data'] || {} elsif response.code == 400 - json_data = JSON.parse(response.body) + json_data = parse_json(response.body) raise PrometheusClient::QueryError, json_data['error'] || 'Bad data received' else raise PrometheusClient::Error, "#{response.code} - #{response.body}" @@ -114,5 +129,11 @@ module Gitlab data = yield data['result'] if data['resultType'] == expected_type end + + def parse_json(response_body) + JSON.parse(response_body) + rescue JSON::ParserError + raise PrometheusClient::Error, 'Parsing response failed' + end end end diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 207a80b7db2..b4f41b9cd9a 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -24,7 +24,10 @@ module Gitlab return [project, type, redirected_path] if project end - nil + # When a project did not exist, the parsed repo_type would be empty. + # In that case, we want to continue with a regular project repository. As we + # could create the project if the user pushing is allowed to do so. + [nil, Gitlab::GlRepository.default_type, nil] end def self.find_project(project_path) diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index bb1aa2a7a10..4022e8ff946 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -47,9 +47,11 @@ module Sentry end def http_get(url, params = {}) - resp = Gitlab::HTTP.get(url, **request_params.merge(params)) + response = handle_request_exceptions do + Gitlab::HTTP.get(url, **request_params.merge(params)) + end - handle_response(resp) + handle_response(response) end def get_issues(issue_status:, limit:) @@ -63,14 +65,36 @@ module Sentry http_get(projects_api_url) end + def handle_request_exceptions + yield + rescue HTTParty::Error => e + Gitlab::Sentry.track_acceptable_exception(e) + raise_error 'Error when connecting to Sentry' + rescue Net::OpenTimeout + raise_error 'Connection to Sentry timed out' + rescue SocketError + raise_error 'Received SocketError when trying to connect to Sentry' + rescue OpenSSL::SSL::SSLError + raise_error 'Sentry returned invalid SSL data' + rescue Errno::ECONNREFUSED + raise_error 'Connection refused' + rescue => e + Gitlab::Sentry.track_acceptable_exception(e) + raise_error "Sentry request failed due to #{e.class}" + end + def handle_response(response) unless response.code == 200 - raise Client::Error, "Sentry response status code: #{response.code}" + raise_error "Sentry response status code: #{response.code}" end response end + def raise_error(message) + raise Client::Error, message + end + def projects_api_url projects_url = URI(@url) projects_url.path = '/api/0/projects/' diff --git a/locale/ar_SA/gitlab.po b/locale/ar_SA/gitlab.po index af98becc19e..32dfea864fb 100644 --- a/locale/ar_SA/gitlab.po +++ b/locale/ar_SA/gitlab.po @@ -534,9 +534,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po index cf0ef977e47..746301e5c2c 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Ðабор от графики отноÑно непрекъÑнатата интеграциÑ" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/ca_ES/gitlab.po b/locale/ca_ES/gitlab.po index e70b8c43f9e..3bdf99cfe57 100644 --- a/locale/ca_ES/gitlab.po +++ b/locale/ca_ES/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/cs_CZ/gitlab.po b/locale/cs_CZ/gitlab.po index 97c0e8046a1..e7e75019fe7 100644 --- a/locale/cs_CZ/gitlab.po +++ b/locale/cs_CZ/gitlab.po @@ -464,9 +464,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/cy_GB/gitlab.po b/locale/cy_GB/gitlab.po index 1eabb89a94c..8cd9e9d2594 100644 --- a/locale/cy_GB/gitlab.po +++ b/locale/cy_GB/gitlab.po @@ -534,9 +534,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/da_DK/gitlab.po b/locale/da_DK/gitlab.po index 231c3569bb2..6f75ce3cb3a 100644 --- a/locale/da_DK/gitlab.po +++ b/locale/da_DK/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po index cc544400449..35c59b619df 100644 --- a/locale/de/gitlab.po +++ b/locale/de/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration" - msgid "A default branch cannot be chosen for an empty project." msgstr "Ein Default-Branch kann nicht für ein leeres Projekt ausgewählt werden." diff --git a/locale/el_GR/gitlab.po b/locale/el_GR/gitlab.po index e8c50215d7e..de854b2baa0 100644 --- a/locale/el_GR/gitlab.po +++ b/locale/el_GR/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index 5e14e94e6fc..151fbffeb02 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -52,9 +52,6 @@ msgid_plural "%d pipelines" msgstr[0] "" msgstr[1] "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "About auto deploy" msgstr "" diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po index 8704cc351fc..34f46e6f249 100644 --- a/locale/eo/gitlab.po +++ b/locale/eo/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Aro da diagramoj pri la seninterrompa integrado" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 7ac4b7c7eb1..da0a2a25adb 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Una colección de gráficos sobre Integración Continua" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/et_EE/gitlab.po b/locale/et_EE/gitlab.po index 16c4951d8a7..e32a2158987 100644 --- a/locale/et_EE/gitlab.po +++ b/locale/et_EE/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/fil_PH/gitlab.po b/locale/fil_PH/gitlab.po index 271ba2587b6..242c3c770f7 100644 --- a/locale/fil_PH/gitlab.po +++ b/locale/fil_PH/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po index 81d25244a74..6b4b35f4562 100644 --- a/locale/fr/gitlab.po +++ b/locale/fr/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Un ensemble de graphiques concernant l’intégration continue (CI)" - msgid "A default branch cannot be chosen for an empty project." msgstr "Une branche par défaut ne peut pas être choisie pour un projet vide." diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3a140aef0e7..f22b351ad78 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -141,6 +141,9 @@ msgstr "" msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" msgstr "" +msgid "%{mrText}, this issue will be closed automatically." +msgstr "" + msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgstr "" @@ -238,6 +241,9 @@ msgid_plural "%d closed merge requests" msgstr[0] "" msgstr[1] "" +msgid "1 day" +msgstr "" + msgid "1 merged merge request" msgid_plural "%d merged merge requests" msgstr[0] "" @@ -258,6 +264,9 @@ msgid_plural "%d pipelines" msgstr[0] "" msgstr[1] "" +msgid "1 week" +msgstr "" + msgid "1st contribution!" msgstr "" @@ -267,6 +276,15 @@ msgstr "" msgid "2FA enabled" msgstr "" +msgid "3 days" +msgstr "" + +msgid "3 hours" +msgstr "" + +msgid "30 minutes" +msgstr "" + msgid "403|Please contact your GitLab administrator to get permission." msgstr "" @@ -282,6 +300,9 @@ msgstr "" msgid "404|Please contact your GitLab administrator if you think this is a mistake." msgstr "" +msgid "8 hours" +msgstr "" + msgid "<code>\"johnsmith@example.com\": \"@johnsmith\"</code> will add \"By <a href=\"#\">@johnsmith</a>\" to all issues and comments originally created by johnsmith@example.com, and will set <a href=\"#\">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com." msgstr "" @@ -321,9 +342,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" @@ -333,6 +351,12 @@ msgstr "" msgid "A member of GitLab's abuse team will review your report as soon as possible." msgstr "" +msgid "A merge commit is created for every merge, and merging is allowed as long as there are no conflicts." +msgstr "" + +msgid "A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. This way you could make sure that if this merge request would build, after merging to target branch it would also build." +msgstr "" + msgid "A new branch will be created in your fork and a new merge request will be started." msgstr "" @@ -456,6 +480,9 @@ msgstr "" msgid "Add license" msgstr "" +msgid "Add list" +msgstr "" + msgid "Add new application" msgstr "" @@ -1047,6 +1074,9 @@ msgstr "" msgid "Automatically marked as default internal user" msgstr "" +msgid "Automatically resolve merge request diff discussions when they become outdated" +msgstr "" + msgid "Automatically resolved" msgstr "" @@ -1518,9 +1548,6 @@ msgstr "" msgid "Choose <strong>Next</strong> at the bottom of the page." msgstr "" -msgid "Choose File ..." -msgstr "" - msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request." msgstr "" @@ -1542,7 +1569,7 @@ msgstr "" msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code" msgstr "" -msgid "Choose file..." +msgid "Choose file…" msgstr "" msgid "Choose the top-level group for your repository imports." @@ -1731,6 +1758,9 @@ msgstr "" msgid "ClusterIntegration|API URL" msgstr "" +msgid "ClusterIntegration|API URL should be a valid http/https url." +msgstr "" + msgid "ClusterIntegration|Add Kubernetes cluster" msgstr "" @@ -1791,6 +1821,9 @@ msgstr "" msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgstr "" +msgid "ClusterIntegration|Cluster name is required." +msgstr "" + msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters." msgstr "" @@ -1812,7 +1845,7 @@ msgstr "" msgid "ClusterIntegration|Copy Kubernetes cluster name" msgstr "" -msgid "ClusterIntegration|Copy Token" +msgid "ClusterIntegration|Copy Service Token" msgstr "" msgid "ClusterIntegration|Create Kubernetes cluster" @@ -2004,6 +2037,9 @@ msgstr "" msgid "ClusterIntegration|Number of nodes" msgstr "" +msgid "ClusterIntegration|Number of nodes must be a numerical value." +msgstr "" + msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes" msgstr "" @@ -2016,9 +2052,6 @@ msgstr "" msgid "ClusterIntegration|Project cluster" msgstr "" -msgid "ClusterIntegration|Project namespace" -msgstr "" - msgid "ClusterIntegration|Project namespace (optional, unique)" msgstr "" @@ -2085,7 +2118,10 @@ msgstr "" msgid "ClusterIntegration|Select zone to choose machine type" msgstr "" -msgid "ClusterIntegration|Service token" +msgid "ClusterIntegration|Service Token" +msgstr "" + +msgid "ClusterIntegration|Service token is required." msgstr "" msgid "ClusterIntegration|Show" @@ -2118,9 +2154,6 @@ msgstr "" msgid "ClusterIntegration|Toggle Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|Token" -msgstr "" - msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgstr "" @@ -2522,9 +2555,6 @@ msgstr "" msgid "Create a new branch" msgstr "" -msgid "Create a new branch and merge request" -msgstr "" - msgid "Create a new issue" msgstr "" @@ -3178,6 +3208,9 @@ msgstr "" msgid "Ends at (UTC)" msgstr "" +msgid "Enter at least three characters to search" +msgstr "" + msgid "Enter in your Bitbucket Server URL and personal access token below" msgstr "" @@ -3577,6 +3610,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to create resources" +msgstr "" + msgid "Failed to deploy to" msgstr "" @@ -3616,6 +3652,9 @@ msgstr "" msgid "Failure" msgstr "" +msgid "Fast-forward merge" +msgstr "" + msgid "Fast-forward merge without a merge commit" msgstr "" @@ -4604,6 +4643,9 @@ msgstr "" msgid "Job|The artifacts will be removed" msgstr "" +msgid "Job|This job failed because the necessary resources were not successfully created." +msgstr "" + msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it." msgstr "" @@ -4986,9 +5028,15 @@ msgstr "" msgid "Merge Requests" msgstr "" +msgid "Merge commit" +msgstr "" + msgid "Merge commit message" msgstr "" +msgid "Merge commit with semi-linear history" +msgstr "" + msgid "Merge events" msgstr "" @@ -4998,6 +5046,9 @@ msgstr "" msgid "Merge in progress" msgstr "" +msgid "Merge method" +msgstr "" + msgid "Merge request" msgstr "" @@ -5100,6 +5151,12 @@ msgstr "" msgid "Metrics|No deployed environments" msgstr "" +msgid "Metrics|Not enough data to display" +msgstr "" + +msgid "Metrics|Show last" +msgstr "" + msgid "Metrics|There was an error fetching the environments data, please try again" msgstr "" @@ -5423,6 +5480,9 @@ msgstr "" msgid "No license. All rights reserved" msgstr "" +msgid "No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. When fast-forward merge is not possible, the user is given the option to rebase." +msgstr "" + msgid "No merge requests found" msgstr "" @@ -5483,6 +5543,9 @@ msgstr "" msgid "Not now" msgstr "" +msgid "Not ready yet. Try again later." +msgstr "" + msgid "Not started" msgstr "" @@ -5611,6 +5674,12 @@ msgstr "" msgid "Only admins" msgstr "" +msgid "Only allow merge requests to be merged if all discussions are resolved" +msgstr "" + +msgid "Only allow merge requests to be merged if the pipeline succeeds" +msgstr "" + msgid "Only mirror protected branches" msgstr "" @@ -5794,6 +5863,9 @@ msgstr "" msgid "Pipeline triggers" msgstr "" +msgid "Pipeline: %{status}" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "" @@ -5863,6 +5935,9 @@ msgstr "" msgid "Pipelines for last year" msgstr "" +msgid "Pipelines need to be configured to enable this feature." +msgstr "" + msgid "Pipelines settings for '%{project_name}' were successfully updated." msgstr "" @@ -5911,12 +5986,6 @@ msgstr "" msgid "Pipeline|Coverage" msgstr "" -msgid "Pipeline|Create for" -msgstr "" - -msgid "Pipeline|Create pipeline" -msgstr "" - msgid "Pipeline|Duration" msgstr "" @@ -5929,6 +5998,9 @@ msgstr "" msgid "Pipeline|Run Pipeline" msgstr "" +msgid "Pipeline|Run for" +msgstr "" + msgid "Pipeline|Search branches" msgstr "" @@ -6370,9 +6442,6 @@ msgstr "" msgid "Project avatar" msgstr "" -msgid "Project avatar in repository: %{link}" -msgstr "" - msgid "Project details" msgstr "" @@ -6409,6 +6478,21 @@ msgstr "" msgid "ProjectActivityRSS|Subscribe" msgstr "" +msgid "ProjectCreationLevel|Allowed to create projects" +msgstr "" + +msgid "ProjectCreationLevel|Default project creation protection" +msgstr "" + +msgid "ProjectCreationLevel|Developers + Maintainers" +msgstr "" + +msgid "ProjectCreationLevel|Maintainers" +msgstr "" + +msgid "ProjectCreationLevel|No one" +msgstr "" + msgid "ProjectFileTree|Name" msgstr "" @@ -6873,6 +6957,9 @@ msgstr "" msgid "Retry this job" msgstr "" +msgid "Retry this job in order to create the necessary resources." +msgstr "" + msgid "Retry verification" msgstr "" @@ -7019,6 +7106,9 @@ msgstr "" msgid "Scope not supported with disabled 'users_search' feature!" msgstr "" +msgid "Scoped label" +msgstr "" + msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right." msgstr "" @@ -7067,6 +7157,9 @@ msgstr "" msgid "Search users" msgstr "" +msgid "Search your projects" +msgstr "" + msgid "SearchAutocomplete|All GitLab" msgstr "" @@ -7184,9 +7277,24 @@ msgstr "" msgid "Serverless" msgstr "" +msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first." +msgstr "" + +msgid "ServerlessDetails|Install Prometheus" +msgstr "" + +msgid "ServerlessDetails|Invocation metrics loading or not available at this time." +msgstr "" + +msgid "ServerlessDetails|Invocations" +msgstr "" + msgid "ServerlessDetails|Kubernetes Pods" msgstr "" +msgid "ServerlessDetails|More information" +msgstr "" + msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity." msgstr "" @@ -7202,9 +7310,6 @@ msgstr "" msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster." msgstr "" -msgid "Serverless|An error occurred while retrieving serverless components" -msgstr "" - msgid "Serverless|Getting started with serverless" msgstr "" @@ -7337,6 +7442,9 @@ msgstr "" msgid "Show latest version" msgstr "" +msgid "Show link to create/view merge request when pushing from the command line" +msgstr "" + msgid "Show parent pages" msgstr "" @@ -7444,6 +7552,9 @@ msgstr "" msgid "Something went wrong while fetching comments. Please try again." msgstr "" +msgid "Something went wrong while fetching related merge requests." +msgstr "" + msgid "Something went wrong while fetching the environments for this merge request. Please try again." msgstr "" @@ -7462,9 +7573,15 @@ msgstr "" msgid "Something went wrong while resolving this discussion. Please try again." msgstr "" +msgid "Something went wrong, unable to search projects" +msgstr "" + msgid "Something went wrong. Please try again." msgstr "" +msgid "Sorry, no projects matched your search" +msgstr "" + msgid "Sorry, your filter produced no results" msgstr "" @@ -7657,6 +7774,9 @@ msgstr "" msgid "Start a %{new_merge_request} with these changes" msgstr "" +msgid "Start a new merge request" +msgstr "" + msgid "Start and due date" msgstr "" @@ -8322,9 +8442,6 @@ msgstr "" msgid "This option is disabled as you don't have write permissions for the current branch" msgstr "" -msgid "This option is disabled while you still have unstaged changes" -msgstr "" - msgid "This page is unavailable because you are not allowed to read information across multiple projects." msgstr "" @@ -9166,6 +9283,14 @@ msgstr "" msgid "When enabled, users cannot use GitLab until the terms have been accepted." msgstr "" +msgid "When fast-forward merge is not possible, the user is given the option to rebase." +msgstr "" + +msgid "When this merge request is accepted" +msgid_plural "When these merge requests are accepted" +msgstr[0] "" +msgstr[1] "" + msgid "When:" msgstr "" @@ -10040,6 +10165,9 @@ msgstr "" msgid "private" msgstr "" +msgid "processing" +msgstr "" + msgid "project" msgstr "" diff --git a/locale/gl_ES/gitlab.po b/locale/gl_ES/gitlab.po index 962e96995b6..9cd4eb0048a 100644 --- a/locale/gl_ES/gitlab.po +++ b/locale/gl_ES/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/he_IL/gitlab.po b/locale/he_IL/gitlab.po index 0819ef9afee..31b4e682982 100644 --- a/locale/he_IL/gitlab.po +++ b/locale/he_IL/gitlab.po @@ -464,9 +464,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/hi_IN/gitlab.po b/locale/hi_IN/gitlab.po index 779c2496e2e..fda92703ce6 100644 --- a/locale/hi_IN/gitlab.po +++ b/locale/hi_IN/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/hr_HR/gitlab.po b/locale/hr_HR/gitlab.po index 912929a31c4..f1886066b54 100644 --- a/locale/hr_HR/gitlab.po +++ b/locale/hr_HR/gitlab.po @@ -429,9 +429,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/hu_HU/gitlab.po b/locale/hu_HU/gitlab.po index 5fe58919064..12e1cd5c79b 100644 --- a/locale/hu_HU/gitlab.po +++ b/locale/hu_HU/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/id_ID/gitlab.po b/locale/id_ID/gitlab.po index 9c9444e6d46..16444a8f259 100644 --- a/locale/id_ID/gitlab.po +++ b/locale/id_ID/gitlab.po @@ -359,9 +359,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index b63371c5616..cd4681b0c5b 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Un insieme di grafici riguardo la Continuous Integration" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po index f8654721e31..d36cf65b1ea 100644 --- a/locale/ja/gitlab.po +++ b/locale/ja/gitlab.po @@ -359,9 +359,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po index 36474fb85d1..ca8e7294869 100644 --- a/locale/ko/gitlab.po +++ b/locale/ko/gitlab.po @@ -359,9 +359,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ" - msgid "A default branch cannot be chosen for an empty project." msgstr "빈 프로ì 트ì—서는 기본 브랜치를 ì„ íƒí• 수 없습니다." diff --git a/locale/mn_MN/gitlab.po b/locale/mn_MN/gitlab.po index 3942a250799..257017265f1 100644 --- a/locale/mn_MN/gitlab.po +++ b/locale/mn_MN/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/nb_NO/gitlab.po b/locale/nb_NO/gitlab.po index fca801a32a5..88bfd23eeea 100644 --- a/locale/nb_NO/gitlab.po +++ b/locale/nb_NO/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po index 44399927a09..c80b3e3c352 100644 --- a/locale/nl_NL/gitlab.po +++ b/locale/nl_NL/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/pa_IN/gitlab.po b/locale/pa_IN/gitlab.po index 2291dc0e557..a1d196bc86e 100644 --- a/locale/pa_IN/gitlab.po +++ b/locale/pa_IN/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po index f3266c9c227..9d6fb6ecd90 100644 --- a/locale/pl_PL/gitlab.po +++ b/locale/pl_PL/gitlab.po @@ -464,9 +464,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po index f5b206ba288..969dc528332 100644 --- a/locale/pt_BR/gitlab.po +++ b/locale/pt_BR/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Uma coleção de gráficos sobre Integração ContÃnua" - msgid "A default branch cannot be chosen for an empty project." msgstr "Um branch padrão não pode ser escolhido para um projeto vazio." diff --git a/locale/pt_PT/gitlab.po b/locale/pt_PT/gitlab.po index 2d43d0ebe4d..829cd4bbdac 100644 --- a/locale/pt_PT/gitlab.po +++ b/locale/pt_PT/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/ro_RO/gitlab.po b/locale/ro_RO/gitlab.po index e4b0be5e2a8..76895485520 100644 --- a/locale/ro_RO/gitlab.po +++ b/locale/ro_RO/gitlab.po @@ -429,9 +429,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po index ee94200a9f7..41c64745fc3 100644 --- a/locale/ru/gitlab.po +++ b/locale/ru/gitlab.po @@ -464,9 +464,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Графики непрерывной интеграции (CI)" - msgid "A default branch cannot be chosen for an empty project." msgstr "Ð”Ð»Ñ Ð¿ÑƒÑтого проекта Ð½ÐµÐ»ÑŒÐ·Ñ Ð²Ñ‹Ð±Ñ€Ð°Ñ‚ÑŒ ветку по умолчанию." diff --git a/locale/sk_SK/gitlab.po b/locale/sk_SK/gitlab.po index e1d860895f6..2179aabc6fd 100644 --- a/locale/sk_SK/gitlab.po +++ b/locale/sk_SK/gitlab.po @@ -464,9 +464,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/sq_AL/gitlab.po b/locale/sq_AL/gitlab.po index cb3bb5e42a1..f19f890be7e 100644 --- a/locale/sq_AL/gitlab.po +++ b/locale/sq_AL/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/sr_CS/gitlab.po b/locale/sr_CS/gitlab.po index 25f70503b18..c94d364a498 100644 --- a/locale/sr_CS/gitlab.po +++ b/locale/sr_CS/gitlab.po @@ -429,9 +429,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/sr_SP/gitlab.po b/locale/sr_SP/gitlab.po index f60a4196aec..5a9e09718a0 100644 --- a/locale/sr_SP/gitlab.po +++ b/locale/sr_SP/gitlab.po @@ -429,9 +429,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/sv_SE/gitlab.po b/locale/sv_SE/gitlab.po index ca342915099..d1d108c1b06 100644 --- a/locale/sv_SE/gitlab.po +++ b/locale/sv_SE/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/sw_KE/gitlab.po b/locale/sw_KE/gitlab.po index e4c780f147f..03a4884209f 100644 --- a/locale/sw_KE/gitlab.po +++ b/locale/sw_KE/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/tr_TR/gitlab.po b/locale/tr_TR/gitlab.po index dd5e8f6db76..510aec20155 100644 --- a/locale/tr_TR/gitlab.po +++ b/locale/tr_TR/gitlab.po @@ -394,9 +394,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Sürekli Entegrasyon için bir grafik derlemesi" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 86b7048ad3d..a45ab9df805 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -464,9 +464,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Ðабір графіків відноÑно безперервної інтеграції" - msgid "A default branch cannot be chosen for an empty project." msgstr "Гілку за замовчуваннÑм не може бути обрано Ð´Ð»Ñ Ð¿Ð¾Ñ€Ð¾Ð¶Ð½ÑŒÐ¾Ð³Ð¾ проекту." @@ -4648,9 +4645,6 @@ msgstr "Вузол не працює або зламаний." msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage." msgstr "Вузол працює повільно, перевантажений або тільки що відновивÑÑ Ð¿Ñ–ÑÐ»Ñ Ð·Ð±Ð¾ÑŽ." -msgid "GeoNodes|Alternate URL" -msgstr "" - msgid "GeoNodes|Checksummed" msgstr "Із контрольною Ñумою" @@ -4831,9 +4825,6 @@ msgstr "Ð’ÑÑ– проекти плануютьÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— пРmsgid "Geo|All projects are being scheduled for re-sync" msgstr "Ð’ÑÑ– проекти плануютьÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— Ñинхронізації" -msgid "Geo|Alternate URL" -msgstr "" - msgid "Geo|Batch operations" msgstr "Групові операції" @@ -12610,4 +12601,3 @@ msgstr[3] "протÑгом %d хвилин " msgid "yaml invalid" msgstr "" - diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index d89914c9775..9b43afb1d72 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -359,9 +359,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "æŒç»é›†æˆæ•°æ®å›¾" - msgid "A default branch cannot be chosen for an empty project." msgstr "æ— æ³•ä¸ºç©ºé¡¹ç›®é€‰æ‹©é»˜è®¤åˆ†æ”¯ã€‚" diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 7df18cc4d70..1d770d45c1a 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -359,9 +359,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ" - msgid "A default branch cannot be chosen for an empty project." msgstr "" diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index f374616a5fb..973492be822 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -359,9 +359,6 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "ä¸é–“æ–·æ•´åˆçš„圖表集åˆ" - msgid "A default branch cannot be chosen for an empty project." msgstr "無法å°ä¸€å€‹ç©ºå°ˆæ¡ˆé¸æ“‡é è¨åˆ†æ”¯ã€‚" diff --git a/package.json b/package.json index 711baf5d1a3..6d3e6764a46 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.58.0", - "@gitlab/ui": "^3.0.0", + "@gitlab/ui": "^3.2.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-upload-client": "^10.0.0", @@ -110,7 +110,7 @@ "sql.js": "^0.4.0", "stickyfilljs": "^2.0.5", "style-loader": "^0.23.1", - "stylelint-error-string-formatter": "^1.0.1", + "stylelint-error-string-formatter": "1.0.2", "svg4everybody": "2.1.9", "three": "^0.84.0", "three-orbit-controls": "^82.1.0", @@ -139,7 +139,7 @@ }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.2.0", - "@gitlab/eslint-config": "^1.4.0", + "@gitlab/eslint-config": "^1.5.0", "@vue/test-utils": "^1.0.0-beta.25", "axios-mock-adapter": "^1.15.0", "babel-jest": "^24.1.0", diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb index ffd5b36e1ae..63a230fbdb2 100644 --- a/qa/qa/page/project/operations/kubernetes/add_existing.rb +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -6,7 +6,7 @@ module QA class AddExisting < Page::Base view 'app/views/clusters/clusters/user/_form.html.haml' do element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern - element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern + element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index 1eada4a6c28..0ff71baed90 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/50 + context 'Create', :quarantine do describe 'Merge request creation' do it 'user creates a new merge request', :smoke do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb index b643468a664..f6f0468e76e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create', :smoke do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/49 + context 'Create', :smoke, :quarantine do describe 'Snippet creation' do it 'User creates a snippet' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 0837b720df1..33f342edb08 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -9,17 +9,17 @@ module QA Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' - resource.value = 'some CI variable' + resource.value = 'some_CI_variable' end Page::Project::Settings::CICD.perform do |settings| settings.expand_ci_variables do |page| expect(page).to have_field(with: 'VARIABLE_KEY') - expect(page).not_to have_field(with: 'some CI variable') + expect(page).not_to have_field(with: 'some_CI_variable') page.reveal_variables - expect(page).to have_field(with: 'some CI variable') + expect(page).to have_field(with: 'some_CI_variable') end end end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb index 9f34e4218c1..caa9be341b4 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb @@ -8,11 +8,11 @@ module QA Page::Main::Login.act { sign_in_using_credentials } deploy_token_name = 'deploy token name' - deploy_token_expires_at = Date.today + 7 # 1 Week from now + one_week_from_now = Date.today + 7 deploy_token = Resource::DeployToken.fabricate! do |resource| resource.name = deploy_token_name - resource.expires_at = deploy_token_expires_at + resource.expires_at = one_week_from_now end expect(deploy_token.username.length).to be > 0 diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb index 49a1af8e9f0..de8cfa1aed9 100644 --- a/qa/qa/tools/generate_perf_testdata.rb +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -33,6 +33,7 @@ module QA add_new_file methods_arr = [ method(:create_issues), + method(:create_labels), method(:create_todos), method(:create_merge_requests), method(:create_issue_with_500_discussions), @@ -80,6 +81,15 @@ module QA STDOUT.puts "Created todos" end + def create_labels + 30.times do |i| + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/labels").url, + "name=label#{i}&color=#{Faker::Color.hex_color}" + end + @urls[:labels_page] = @urls[:project_page] + "/labels" + STDOUT.puts "Created labels" + end + def create_merge_requests 30.times do |i| post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests").url, "source_branch=branch#{i}&target_branch=master&title=MR#{i}" @@ -108,36 +118,77 @@ module QA 500.times do post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues/#{issue_id}/discussions").url, "body=\"Let us discuss\"" end + + labels_list = (0..15).map {|i| "label#{i}"}.join(',') + # Add description and labels + put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues/#{issue_id}").url, "description=#{Faker::Lorem.sentences(500).join(" ")}&labels=#{labels_list}" @urls[:large_issue] = @urls[:project_page] + "/issues/#{issue_id}" STDOUT.puts "Created Issue with 500 Discussions" end def create_mr_with_large_files content_arr = [] - 20.times do |i| + 16.times do |i| faker_line_arr = Faker::Lorem.sentences(1500) content = faker_line_arr.join("\n\r") - post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=master&commit_message=\"Add hello#{i}.txt\"&content=#{content}" + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, + "branch=master&commit_message=\"Add hello#{i}.txt\"&content=#{content}" content_arr[i] = faker_line_arr end - post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/branches").url, "branch=performance&ref=master" + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/branches").url, + "branch=performance&ref=master" - 20.times do |i| + 16.times do |i| missed_line_array = content_arr[i].each_slice(2).map(&:first) content = missed_line_array.join("\n\rIm new!:D \n\r ") - put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=performance&commit_message=\"Update hello#{i}.txt\"&content=#{content}" + put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, + "branch=performance&commit_message=\"Update hello#{i}.txt\"&content=#{content}" end - create_mr_response = post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests").url, "source_branch=performance&target_branch=master&title=Large_MR" + create_mr_response = post Runtime::API::Request.new(@api_client, """/projects/#{@group_name}%2F#{@project_name}/merge_requests""").url, + "source_branch=performance&target_branch=master&title=Large_MR" iid = JSON.parse(create_mr_response.body)["iid"] - 500.times do - post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url, "body=\"Let us discuss\"" + diff_refs = JSON.parse(create_mr_response.body)["diff_refs"] + + # Add discussions to diff tab and resolve a few! + should_resolve = false + 16.times do |i| + 1.upto(9) do |j| + create_diff_note(iid, i, j, diff_refs["head_sha"], diff_refs["start_sha"], diff_refs["base_sha"], "new_line") + create_diff_note_response = create_diff_note(iid, i, j, diff_refs["head_sha"], diff_refs["start_sha"], diff_refs["base_sha"], "old_line") + + if should_resolve + discussion_id = JSON.parse(create_diff_note_response.body)["id"] + put Runtime::API::Request.new(@api_client, """/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions/#{discussion_id}""").url, + "resolved=true" + end + + should_resolve ^= true + end + end + + # Add discussions to main tab + 100.times do + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url, + "body=\"Let us discuss\"" end @urls[:large_mr] = JSON.parse(create_mr_response.body)["web_url"] STDOUT.puts "Created MR with 500 Discussions and 20 Very Large Files" end + + def create_diff_note(iid, file_count, line_count, head_sha, start_sha, base_sha, line_type) + post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url, + """body=\"Let us discuss\"& + position[position_type]=text& + position[new_path]=hello#{file_count}.txt& + position[old_path]=hello#{file_count}.txt& + position[#{line_type}]=#{line_count * 100}& + position[head_sha]=#{head_sha}& + position[start_sha]=#{start_sha}& + position[base_sha]=#{base_sha}""" + end end end end diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb index b1ada3c99db..efb620fe6dd 100644 --- a/spec/config/object_store_settings_spec.rb +++ b/spec/config/object_store_settings_spec.rb @@ -3,7 +3,7 @@ require Rails.root.join('config', 'object_store_settings.rb') describe ObjectStoreSettings do describe '.parse' do - it 'should set correct default values' do + it 'sets correct default values' do settings = described_class.parse(nil) expect(settings['enabled']).to be false diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 9af472df74e..1a7be4c9a85 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -85,6 +85,13 @@ describe Admin::ApplicationSettingsController do expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.receive_max_input_size).to eq(1024) end + + it 'updates the default_project_creation for string value' do + put :update, params: { application_setting: { default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } } + + expect(response).to redirect_to(admin_application_settings_path) + expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + end end describe 'PUT #reset_registration_token' do diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index 647fce0ecef..22165faa625 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -60,5 +60,11 @@ describe Admin::GroupsController do expect(response).to redirect_to(admin_group_path(group)) expect(group.users).not_to include group_user end + + it 'updates the project_creation_level successfully' do + expect do + post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS } } + end.to change { group.reload.project_creation_level }.to(::Gitlab::Access::NO_ONE_PROJECT_ACCESS) + end end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index ab40b4eb178..828de0e7ca5 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -75,7 +75,7 @@ describe Dashboard::MilestonesController do expect(response.body).not_to include(project_milestone.title) end - it 'should show counts of group and project milestones to which the user belongs to' do + it 'shows counts of group and project milestones to which the user belongs to' do get :index expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>") diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index ef23ffaa843..e5180ec5c5c 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -455,7 +455,7 @@ describe Groups::ClustersController do context 'when domain is invalid' do let(:domain) { 'http://not-a-valid-domain' } - it 'should not update cluster attributes' do + it 'does not update cluster attributes' do go cluster.reload diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index 15eb0a442a6..3290ed8b088 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -124,7 +124,7 @@ describe Groups::Settings::CiCdController do end context 'when explicitly enabling auto devops' do - it 'should update group attribute' do + it 'updates group attribute' do expect(group.auto_devops_enabled).to eq(true) end end @@ -132,7 +132,7 @@ describe Groups::Settings::CiCdController do context 'when explicitly disabling auto devops' do let(:auto_devops_param) { '0' } - it 'should update group attribute' do + it 'updates group attribute' do expect(group.auto_devops_enabled).to eq(false) end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 38d7240ea81..4a28a27da79 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -349,6 +349,13 @@ describe GroupsController do expect(assigns(:group).errors).not_to be_empty expect(assigns(:group).path).not_to eq('new_path') end + + it 'updates the project_creation_level successfully' do + post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } } + + expect(response).to have_gitlab_http_status(302) + expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + end end describe '#ensure_canonical_path' do @@ -566,11 +573,11 @@ describe GroupsController do } end - it 'should return a notice' do + it 'returns a notice' do expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") end - it 'should redirect to the new path' do + it 'redirects to the new path' do expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}") end end @@ -587,11 +594,11 @@ describe GroupsController do } end - it 'should return a notice' do + it 'returns a notice' do expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") end - it 'should redirect to the new path' do + it 'redirects to the new path' do expect(response).to redirect_to("/#{group.path}") end end @@ -611,11 +618,11 @@ describe GroupsController do } end - it 'should return an alert' do + it 'returns an alert' do expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved" end - it 'should redirect to the current path' do + it 'redirects to the current path' do expect(response).to redirect_to(edit_group_path(group)) end end @@ -633,7 +640,7 @@ describe GroupsController do } end - it 'should be denied' do + it 'is denied' do expect(response).to have_gitlab_http_status(404) end end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 06c6f49f7cc..b823a8d7463 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -55,7 +55,7 @@ describe OmniauthCallbacksController, type: :controller do allow(@routes).to receive(:generate_extras) { [path, []] } end - it 'it calls through to the failure handler' do + it 'calls through to the failure handler' do request.env['omniauth.error'] = OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch") request.env['omniauth.error.strategy'] = OmniAuth::Strategies::SAML.new(nil) stub_route_as('/users/auth/saml/callback') diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 3801fca09dc..485e3e21c4d 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -10,6 +10,8 @@ describe Projects::BlobController do context 'with file path' do before do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + get(:show, params: { namespace_id: project.namespace, @@ -285,7 +287,7 @@ describe Projects::BlobController do merge_request.update!(source_project: other_project, target_project: other_project) end - it "it redirect to blob" do + it "redirects to blob" do put :update, params: mr_params expect(response).to redirect_to(blob_after_edit_path) diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb index cfa010c2d1c..0b79484bbfa 100644 --- a/spec/controllers/projects/ci/lints_controller_spec.rb +++ b/spec/controllers/projects/ci/lints_controller_spec.rb @@ -16,15 +16,15 @@ describe Projects::Ci::LintsController do get :show, params: { namespace_id: project.namespace, project_id: project } end - it 'should be success' do + it 'is success' do expect(response).to be_success end - it 'should render show page' do + it 'renders show page' do expect(response).to render_template :show end - it 'should retrieve project' do + it 'retrieves project' do expect(assigns(:project)).to eq(project) end end @@ -36,7 +36,7 @@ describe Projects::Ci::LintsController do get :show, params: { namespace_id: project.namespace, project_id: project } end - it 'should respond with 404' do + it 'responds with 404' do expect(response).to have_gitlab_http_status(404) end end @@ -74,7 +74,7 @@ describe Projects::Ci::LintsController do post :create, params: { namespace_id: project.namespace, project_id: project, content: content } end - it 'should be success' do + it 'is success' do expect(response).to be_success end @@ -82,7 +82,7 @@ describe Projects::Ci::LintsController do expect(response).to render_template :show end - it 'should retrieve project' do + it 'retrieves project' do expect(assigns(:project)).to eq(project) end end @@ -102,7 +102,7 @@ describe Projects::Ci::LintsController do post :create, params: { namespace_id: project.namespace, project_id: project, content: content } end - it 'should assign errors' do + it 'assigns errors' do expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt') end end @@ -114,7 +114,7 @@ describe Projects::Ci::LintsController do post :create, params: { namespace_id: project.namespace, project_id: project, content: content } end - it 'should respond with 404' do + it 'responds with 404' do expect(response).to have_gitlab_http_status(404) end end diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index 8cb9130b834..9f753e5641f 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -15,7 +15,7 @@ describe Projects::CommitsController do describe "GET commits_root" do context "no ref is provided" do - it 'should redirect to the default branch of the project' do + it 'redirects to the default branch of the project' do get(:commits_root, params: { namespace_id: project.namespace, @@ -113,6 +113,8 @@ describe Projects::CommitsController do render_views before do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original unless id.include?(' ') + get(:signatures, params: { namespace_id: project.namespace, diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb new file mode 100644 index 00000000000..5a0b92c2514 --- /dev/null +++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::Environments::PrometheusApiController do + set(:project) { create(:project) } + set(:environment) { create(:environment, project: project) } + set(:user) { create(:user) } + + before do + project.add_reporter(user) + sign_in(user) + end + + describe 'GET #proxy' do + let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) } + let(:expected_params) do + ActionController::Parameters.new( + environment_params( + proxy_path: 'query', + controller: 'projects/environments/prometheus_api', + action: 'proxy' + ) + ).permit! + end + + context 'with valid requests' do + before do + allow(Prometheus::ProxyService).to receive(:new) + .with(environment, 'GET', 'query', expected_params) + .and_return(prometheus_proxy_service) + + allow(prometheus_proxy_service).to receive(:execute) + .and_return(service_result) + end + + context 'with success result' do + let(:service_result) { { status: :success, body: prometheus_body } } + let(:prometheus_body) { '{"status":"success"}' } + let(:prometheus_json_body) { JSON.parse(prometheus_body) } + + it 'returns prometheus response' do + get :proxy, params: environment_params + + expect(Prometheus::ProxyService).to have_received(:new) + .with(environment, 'GET', 'query', expected_params) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq(prometheus_json_body) + end + end + + context 'with nil result' do + let(:service_result) { nil } + + it 'returns 202 accepted' do + get :proxy, params: environment_params + + expect(json_response['status']).to eq('processing') + expect(json_response['message']).to eq('Not ready yet. Try again later.') + expect(response).to have_gitlab_http_status(:accepted) + end + end + + context 'with 404 result' do + let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } } + + it 'returns body' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['body']).to eq('value') + end + end + + context 'with error result' do + context 'with http_status' do + let(:service_result) do + { http_status: :service_unavailable, status: :error, message: 'error message' } + end + + it 'sets the http response status code' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + + context 'without http_status' do + let(:service_result) { { status: :error, message: 'error message' } } + + it 'returns bad_request' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + end + end + + context 'with inappropriate requests' do + context 'with anonymous user' do + before do + sign_out(user) + end + + it 'redirects to signin page' do + get :proxy, params: environment_params + + expect(response).to redirect_to(new_user_session_path) + end + end + + context 'without correct permissions' do + before do + project.team.truncate + end + + it 'returns 404' do + get :proxy, params: environment_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with invalid environment id' do + let(:other_environment) { create(:environment) } + + it 'returns 404' do + get :proxy, params: environment_params(id: other_environment.id) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + private + + def environment_params(params = {}) + { + id: environment.id.to_s, + namespace_id: project.namespace.name, + project_id: project.name, + proxy_path: 'query', + query: '1' + }.merge(params) + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c8fa93a74ee..017162519d8 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -60,6 +60,8 @@ describe Projects::MergeRequestsController do end it "renders merge request page" do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + go(format: :html) expect(response).to be_success diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb index 86a12a5e903..f2b73956e8d 100644 --- a/spec/controllers/projects/mirrors_controller_spec.rb +++ b/spec/controllers/projects/mirrors_controller_spec.rb @@ -65,7 +65,7 @@ describe Projects::MirrorsController do expect(flash[:notice]).to match(/successfully updated/) end - it 'should create a RemoteMirror object' do + it 'creates a RemoteMirror object' do expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1) end end @@ -82,7 +82,7 @@ describe Projects::MirrorsController do expect(flash[:alert]).to match(/Only allowed protocols are/) end - it 'should not create a RemoteMirror object' do + it 'does not create a RemoteMirror object' do expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count) end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index b64ae552efc..814100f7d5d 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -97,6 +97,8 @@ describe Projects::PipelinesController do RequestStore.clear! RequestStore.begin! + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + expect { get_pipelines_index_json } .to change { Gitlab::GitalyClient.get_request_count }.by(2) end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 3cc3fe69fba..33486edcdd1 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::ProjectMembersController do let(:project) { create(:project, :public, :access_requestable) } describe 'GET index' do - it 'should have the project_members address with a 200 status code' do + it 'has the project_members address with a 200 status code' do get :index, params: { namespace_id: project.namespace, project_id: project } expect(response).to have_gitlab_http_status(200) diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb index 62f2af947e4..0d0fa5d9f45 100644 --- a/spec/controllers/projects/refs_controller_spec.rb +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -44,11 +44,15 @@ describe Projects::RefsController do end it 'renders JS' do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + xhr_get(:js) expect(response).to be_success end it 'renders JSON' do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + xhr_get(:json) expect(response).to be_success diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index 276cf340962..782f5f272d9 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -76,6 +76,15 @@ describe Projects::Serverless::FunctionsController do end end + describe 'GET #metrics' do + context 'invalid data' do + it 'has a bad function name' do + get :metrics, params: params({ format: :json, environment_id: "*", id: "foo" }) + expect(response).to have_gitlab_http_status(204) + end + end + end + describe 'GET #index with data', :use_clean_rails_memory_store_caching do before do stub_kubeclient_service_pods diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 601a292bf54..d00d5bf579d 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -147,7 +147,7 @@ describe Projects::ServicesController do params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' } } end - it 'should not update the service' do + it 'does not update the service' do service.reload expect(service.namespace).not_to eq('updated_namespace') end @@ -172,7 +172,7 @@ describe Projects::ServicesController do context 'with approved services' do let(:service_id) { 'jira' } - it 'should render edit page' do + it 'renders edit page' do expect(response).to be_success end end @@ -180,7 +180,7 @@ describe Projects::ServicesController do context 'with a deprecated service' do let(:service_id) { 'kubernetes' } - it 'should render edit page' do + it 'renders edit page' do expect(response).to be_success end end diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index b15a2bc84a5..78201498eaa 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -16,6 +16,8 @@ describe Projects::TreeController do render_views before do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + get(:show, params: { namespace_id: project.namespace.to_param, diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 56d38b9475e..af437c5561b 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -77,6 +77,10 @@ describe ProjectsController do end context "user has access to project" do + before do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + end + context "and does not have notification setting" do it "initializes notification as disabled" do get :show, params: { namespace_id: public_project.namespace, id: public_project } diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb index c71d75a3e7f..3cbbba934b8 100644 --- a/spec/controllers/user_callouts_controller_spec.rb +++ b/spec/controllers/user_callouts_controller_spec.rb @@ -14,11 +14,11 @@ describe UserCalloutsController do let(:feature_name) { UserCallout.feature_names.keys.first } context 'when callout entry does not exist' do - it 'should create a callout entry with dismissed state' do + it 'creates a callout entry with dismissed state' do expect { subject }.to change { UserCallout.count }.by(1) end - it 'should return success' do + it 'returns success' do subject expect(response).to have_gitlab_http_status(:ok) @@ -28,7 +28,7 @@ describe UserCalloutsController do context 'when callout entry already exists' do let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) } - it 'should return success' do + it 'returns success' do subject expect(response).to have_gitlab_http_status(:ok) @@ -39,7 +39,7 @@ describe UserCalloutsController do context 'with invalid feature name' do let(:feature_name) { 'bogus_feature_name' } - it 'should return bad request' do + it 'returns bad request' do subject expect(response).to have_gitlab_http_status(:bad_request) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 067391c1179..f8c494c159e 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -336,6 +336,11 @@ FactoryBot.define do failure_reason 2 end + trait :prerequisite_failure do + failed + failure_reason 10 + end + trait :with_runner_session do after(:build) do |build| build.build_runner_session(url: 'https://localhost') diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index dcef8571f41..18a0c2ec731 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -4,6 +4,7 @@ FactoryBot.define do path { name.downcase.gsub(/\s/, '_') } type 'Group' owner nil + project_creation_level ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS after(:create) do |group| if group.owner diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ea69ec0319b..4c6175f5590 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -345,7 +345,7 @@ describe 'Issue Boards', :js do click_link 'Create project label' - fill_in('new_label_name', with: 'Testing New Label') + fill_in('new_label_name', with: 'Testing New Label - with list') first('.suggest-colors a').click diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index ee38e756f9e..dfdb8d589eb 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -343,6 +343,24 @@ describe 'Issue Boards', :js do expect(page).to have_link 'test label' end + expect(page).to have_selector('.board', count: 3) + end + + it 'creates project label and list' do + click_card(card) + + page.within('.labels') do + click_link 'Edit' + click_link 'Create project label' + fill_in 'new_label_name', with: 'test label' + first('.suggest-colors-dropdown a').click + first('.js-add-list').click + click_button 'Create' + wait_for_requests + + expect(page).to have_link 'test label' + end + expect(page).to have_selector('.board', count: 4) end end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 8d801161148..b2b3382666a 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -34,7 +34,7 @@ describe 'Expand and collapse diffs', :js do define_method(file.split('.').first) { file_container(file) } end - it 'should show the diff content with a highlighted line when linking to line' do + it 'shows the diff content with a highlighted line when linking to line' do expect(large_diff).not_to have_selector('.code') expect(large_diff).to have_selector('.nothing-here-block') @@ -48,7 +48,7 @@ describe 'Expand and collapse diffs', :js do expect(large_diff).to have_selector('.hll') end - it 'should show the diff content when linking to file' do + it 'shows the diff content when linking to file' do expect(large_diff).not_to have_selector('.code') expect(large_diff).to have_selector('.nothing-here-block') diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 8ed4051856e..56f6b1f7eaf 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -68,17 +68,17 @@ describe 'Explore Groups page', :js do end describe 'landing component' do - it 'should show a landing component' do + it 'shows a landing component' do expect(page).to have_content('Below you will find all the groups that are public.') end - it 'should be dismissable' do + it 'is dismissable' do find('.dismiss-button').click expect(page).not_to have_content('Below you will find all the groups that are public.') end - it 'should persistently not show once dismissed' do + it 'does not show persistently once dismissed' do find('.dismiss-button').click visit explore_groups_path diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb index 2410cd92e3f..b661b5cbaef 100644 --- a/spec/features/groups/clusters/user_spec.rb +++ b/spec/features/groups/clusters/user_spec.rb @@ -69,7 +69,7 @@ describe 'User Cluster', :js do end it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + expect(page).to have_css('.gl-field-error') end end end diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 378e4d5febc..5cef5f0521f 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -77,6 +77,14 @@ describe 'Edit group settings' do end end + describe 'project creation level menu' do + it 'shows the selection menu' do + visit edit_group_path(group) + + expect(page).to have_content('Allowed to create projects') + end + end + describe 'edit group avatar' do before do visit edit_group_path(group) diff --git a/spec/features/groups/settings/ci_cd_spec.rb b/spec/features/groups/settings/ci_cd_spec.rb index 0f793dbab6e..5b1a9512c55 100644 --- a/spec/features/groups/settings/ci_cd_spec.rb +++ b/spec/features/groups/settings/ci_cd_spec.rb @@ -43,7 +43,7 @@ describe 'Group CI/CD settings' do end context 'as owner first visiting group settings' do - it 'should see instance enabled badge' do + it 'sees instance enabled badge' do visit group_settings_ci_cd_path(group) page.within '#auto-devops-settings' do @@ -53,7 +53,7 @@ describe 'Group CI/CD settings' do end context 'when Auto DevOps group has been enabled' do - it 'should see group enabled badge' do + it 'sees group enabled badge' do group.update!(auto_devops_enabled: true) visit group_settings_ci_cd_path(group) @@ -65,7 +65,7 @@ describe 'Group CI/CD settings' do end context 'when Auto DevOps group has been disabled' do - it 'should not see a badge' do + it 'does not see a badge' do group.update!(auto_devops_enabled: false) visit group_settings_ci_cd_path(group) diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index c2f32c76422..8e7f78cab81 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -237,7 +237,7 @@ describe 'Group' do let!(:project) { create(:project, namespace: group) } let!(:path) { group_path(group) } - it 'it renders projects and groups on the page' do + it 'renders projects and groups on the page' do visit path wait_for_requests diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index e24b1f4349d..bcd2b90d3bb 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -82,15 +82,15 @@ describe 'Help Pages' do visit help_path end - it 'should display custom help page text' do + it 'displays custom help page text' do expect(page).to have_text "My Custom Text" end - it 'should hide marketing content when enabled' do + it 'hides marketing content when enabled' do expect(page).not_to have_link "Get a support subscription" end - it 'should use a custom support url' do + it 'uses a custom support url' do expect(page).to have_link "See our website for getting help", href: "http://example.com/help" end end diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb index 23385ba65fc..870e92b8de8 100644 --- a/spec/features/issuables/markdown_references/internal_references_spec.rb +++ b/spec/features/issuables/markdown_references/internal_references_spec.rb @@ -70,7 +70,7 @@ describe "Internal references", :js do page.within("#merge-requests ul") do expect(page).to have_content(private_project_merge_request.title) - expect(page).to have_css(".merge-request-status") + expect(page).to have_css(".ic-issue-open-m") end expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}") diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index e0b1e286dee..75313442b65 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -42,7 +42,7 @@ describe 'Dropdown assignee', :js do expect(page).to have_css(js_dropdown_assignee, visible: false) end - it 'should show loading indicator when opened' do + it 'shows loading indicator when opened' do slow_requests do # We aren't using `input_filtered_search` because we want to see the loading indicator filtered_search.set('assignee:') @@ -51,13 +51,13 @@ describe 'Dropdown assignee', :js do end end - it 'should hide loading indicator when loaded' do + it 'hides loading indicator when loaded' do input_filtered_search('assignee:', submit: false, extra_space: false) expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading') end - it 'should load all the assignees when opened' do + it 'loads all the assignees when opened' do input_filtered_search('assignee:', submit: false, extra_space: false) expect(dropdown_assignee_size).to eq(4) diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index bedc61b9eed..bc8d9bc8450 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -50,7 +50,7 @@ describe 'Dropdown author', :js do expect(page).to have_css(js_dropdown_author, visible: false) end - it 'should show loading indicator when opened' do + it 'shows loading indicator when opened' do slow_requests do filtered_search.set('author:') @@ -58,13 +58,13 @@ describe 'Dropdown author', :js do end end - it 'should hide loading indicator when loaded' do + it 'hides loading indicator when loaded' do send_keys_to_filtered_search('author:') expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') end - it 'should load all the authors when opened' do + it 'loads all the authors when opened' do send_keys_to_filtered_search('author:') expect(dropdown_author_size).to eq(4) diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index f36d4e8f23f..a5c3ab7e7d0 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -69,7 +69,7 @@ describe 'Dropdown emoji', :js do expect(page).to have_css(js_dropdown_emoji, visible: false) end - it 'should show loading indicator when opened' do + it 'shows loading indicator when opened' do slow_requests do filtered_search.set('my-reaction:') @@ -77,13 +77,13 @@ describe 'Dropdown emoji', :js do end end - it 'should hide loading indicator when loaded' do + it 'hides loading indicator when loaded' do send_keys_to_filtered_search('my-reaction:') expect(page).not_to have_css('#js-dropdown-my-reaction .filter-dropdown-loading') end - it 'should load all the emojis when opened' do + it 'loads all the emojis when opened' do send_keys_to_filtered_search('my-reaction:') expect(dropdown_emoji_size).to eq(4) diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index b330eafe1d1..7584339ccc0 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -49,7 +49,7 @@ describe 'Dropdown milestone', :js do expect(page).to have_css(js_dropdown_milestone, visible: false) end - it 'should show loading indicator when opened' do + it 'shows loading indicator when opened' do slow_requests do filtered_search.set('milestone:') @@ -57,13 +57,13 @@ describe 'Dropdown milestone', :js do end end - it 'should hide loading indicator when loaded' do + it 'hides loading indicator when loaded' do filtered_search.set('milestone:') expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading') end - it 'should load all the milestones when opened' do + it 'loads all the milestones when opened' do filtered_search.set('milestone:') expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6) diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index f2e4c5779df..26c781350e5 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -45,7 +45,7 @@ describe 'New/edit issue', :js do wait_for_requests end - it 'should display selected users even if they are not part of the original API call' do + it 'displays selected users even if they are not part of the original API call' do find('.dropdown-input-field').native.send_keys user2.name page.within '.dropdown-menu-user' do diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 76bc93e9766..791bd003597 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -26,7 +26,7 @@ describe 'Issue Detail', :js do wait_for_requests end - it 'should encode the description to prevent xss issues' do + it 'encodes the description to prevent xss issues' do page.within('.issuable-details .detail-page-description') do expect(page).to have_selector('img', count: 1) expect(find('img')['onerror']).to be_nil diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb index 426e205b30b..9938a4e781c 100644 --- a/spec/features/issues/user_uses_quick_actions_spec.rb +++ b/spec/features/issues/user_uses_quick_actions_spec.rb @@ -43,7 +43,7 @@ describe 'Issues > User uses quick actions', :js do describe 'issue-only commands' do let(:user) { create(:user) } let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) } before do project.add_maintainer(user) @@ -57,6 +57,8 @@ describe 'Issues > User uses quick actions', :js do end it_behaves_like 'confidential quick action' + it_behaves_like 'remove_due_date quick action' + it_behaves_like 'duplicate quick action' describe 'adding a due date from note' do let(:issue) { create(:issue, project: project) } @@ -76,24 +78,6 @@ describe 'Issues > User uses quick actions', :js do end end - describe 'removing a due date from note' do - let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) } - - it_behaves_like 'remove_due_date action available and due date can be removed' - - context 'when the current user cannot update the due date' do - let(:guest) { create(:user) } - before do - project.add_guest(guest) - gitlab_sign_out - sign_in(guest) - visit project_issue_path(project, issue) - end - - it_behaves_like 'remove_due_date action not available' - end - end - describe 'toggling the WIP prefix from the title from note' do let(:issue) { create(:issue, project: project) } @@ -104,42 +88,6 @@ describe 'Issues > User uses quick actions', :js do end end - describe 'mark issue as duplicate' do - let(:issue) { create(:issue, project: project) } - let(:original_issue) { create(:issue, project: project) } - - context 'when the current user can update issues' do - it 'does not create a note, and marks the issue as a duplicate' do - add_note("/duplicate ##{original_issue.to_reference}") - - expect(page).not_to have_content "/duplicate #{original_issue.to_reference}" - expect(page).to have_content 'Commands applied' - expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" - - expect(issue.reload).to be_closed - end - end - - context 'when the current user cannot update the issue' do - let(:guest) { create(:user) } - before do - project.add_guest(guest) - gitlab_sign_out - sign_in(guest) - visit project_issue_path(project, issue) - end - - it 'does not create a note, and does not mark the issue as a duplicate' do - add_note("/duplicate ##{original_issue.to_reference}") - - expect(page).not_to have_content 'Commands applied' - expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" - - expect(issue.reload).to be_open - end - end - end - describe 'move the issue to another project' do let(:issue) { create(:issue, project: project) } diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index 7c31e67a7fa..bac297de4a6 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -145,7 +145,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do visit new_project_issue_path(project_1) end - it 'should be able to assign ancestor group labels' do + it 'is able to assign ancestor group labels' do fill_in 'issue_title', with: 'new created issue' fill_in 'issue_description', with: 'new issue description' diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb index 0ccab5b2fac..b8c4a78e24f 100644 --- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb +++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb @@ -76,7 +76,7 @@ describe 'create a merge request, allowing commits from members who can merge to sign_in(member) end - it 'it hides the option from members' do + it 'hides the option from members' do visit edit_project_merge_request_path(target_project, merge_request) expect(page).not_to have_content('Allows commits from members who can merge to the target branch') diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 2609546990d..40ba676ff92 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -302,7 +302,7 @@ describe 'Merge request > User sees merge widget', :js do visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project) end - it 'should be allowed to merge' do + it 'is allowed to merge' do # Wait for the `ci_status` and `merge_check` requests wait_for_requests diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index 5c45e363997..6eae3fd4676 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -230,7 +230,7 @@ describe 'Merge request > User sees versions', :js do wait_for_requests end - it 'should only show diffs from the commit' do + it 'only shows diffs from the commit' do diff_commit_ids = find_all('.diff-file [data-commit-id]').map {|diff| diff['data-commit-id']} expect(diff_commit_ids).not_to be_empty diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb index 56774896795..8d308729f62 100644 --- a/spec/features/merge_request/user_uses_quick_actions_spec.rb +++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb @@ -57,130 +57,7 @@ describe 'Merge request > User uses quick actions', :js do end it_behaves_like 'merge quick action' - - describe 'toggling the WIP prefix in the title from note' do - context 'when the current user can toggle the WIP prefix' do - before do - sign_in(user) - visit project_merge_request_path(project, merge_request) - wait_for_requests - end - - it 'adds the WIP: prefix to the title' do - add_note("/wip") - - expect(page).not_to have_content '/wip' - expect(page).to have_content 'Commands applied' - - expect(merge_request.reload.work_in_progress?).to eq true - end - - it 'removes the WIP: prefix from the title' do - merge_request.title = merge_request.wip_title - merge_request.save - add_note("/wip") - - expect(page).not_to have_content '/wip' - expect(page).to have_content 'Commands applied' - - expect(merge_request.reload.work_in_progress?).to eq false - end - end - - context 'when the current user cannot toggle the WIP prefix' do - before do - project.add_guest(guest) - sign_in(guest) - visit project_merge_request_path(project, merge_request) - end - - it 'does not change the WIP prefix' do - add_note("/wip") - - expect(page).not_to have_content '/wip' - expect(page).not_to have_content 'Commands applied' - - expect(merge_request.reload.work_in_progress?).to eq false - end - end - end - - describe '/target_branch command in merge request' do - let(:another_project) { create(:project, :public, :repository) } - let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } - - before do - another_project.add_maintainer(user) - sign_in(user) - end - - it 'changes target_branch in new merge_request' do - visit project_new_merge_request_path(another_project, new_url_opts) - - fill_in "merge_request_title", with: 'My brand new feature' - fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" - click_button "Submit merge request" - - merge_request = another_project.merge_requests.first - expect(merge_request.description).to eq "le feature \nFeature description:" - expect(merge_request.target_branch).to eq 'fix' - end - - it 'does not change target branch when merge request is edited' do - new_merge_request = create(:merge_request, source_project: another_project) - - visit edit_project_merge_request_path(another_project, new_merge_request) - fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n" - click_button "Save changes" - - new_merge_request = another_project.merge_requests.first - expect(new_merge_request.description).to include('/target_branch') - expect(new_merge_request.target_branch).not_to eq('fix') - end - end - - describe '/target_branch command from note' do - context 'when the current user can change target branch' do - before do - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - it 'changes target branch from a note' do - add_note("message start \n/target_branch merge-test\n message end.") - - wait_for_requests - expect(page).not_to have_content('/target_branch') - expect(page).to have_content('message start') - expect(page).to have_content('message end.') - - expect(merge_request.reload.target_branch).to eq 'merge-test' - end - - it 'does not fail when target branch does not exists' do - add_note('/target_branch totally_not_existing_branch') - - expect(page).not_to have_content('/target_branch') - - expect(merge_request.target_branch).to eq 'feature' - end - end - - context 'when current user can not change target branch' do - before do - project.add_guest(guest) - sign_in(guest) - visit project_merge_request_path(project, merge_request) - end - - it 'does not change target branch' do - add_note('/target_branch merge-test') - - expect(page).not_to have_content '/target_branch merge-test' - - expect(merge_request.target_branch).to eq 'feature' - end - end - end + it_behaves_like 'target_branch quick action' + it_behaves_like 'wip quick action' end end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index a7aa63018fd..aa2e538cc8e 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -572,7 +572,7 @@ describe 'File blob', :js do visit_blob('files/ruby/test.rb', ref: 'feature') end - it 'should show the realtime pipeline status' do + it 'shows the realtime pipeline status' do page.within('.commit-actions') do expect(page).to have_css('.ci-status-icon') expect(page).to have_css('.ci-status-icon-running') diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index aa1c3902f0f..527508b3519 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -80,7 +80,7 @@ describe 'Clusters Applications', :js do context 'on an abac cluster' do let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) } - it 'should show info block and not be installable' do + it 'shows info block and not be installable' do page.within('.js-cluster-application-row-knative') do expect(page).to have_css('.rbac-notice') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') @@ -91,7 +91,7 @@ describe 'Clusters Applications', :js do context 'on an rbac cluster' do let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - it 'should not show callout block and be installable' do + it 'does not show callout block and be installable' do page.within('.js-cluster-application-row-knative') do expect(page).not_to have_css('.rbac-notice') expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 9322e29d744..83e582c34f0 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do end it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + expect(page).to have_css('.gl-field-error') end end end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 1f2f7592d8b..fe4f737a7da 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -53,7 +53,7 @@ describe 'User Cluster', :js do end it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') + expect(page).to have_css('.gl-field-error') end end end diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb index 19f6ebf2c1a..614f11c8392 100644 --- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb +++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb @@ -43,7 +43,7 @@ describe 'Mini Pipeline Graph in Commit View', :js do visit project_commit_path(project, project.commit.id) end - it 'should not display a mini pipeline graph' do + it 'does not display a mini pipeline graph' do expect(page).not_to have_selector('.mr-widget-pipeline-graph') end end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index fe71cb7661a..da4ef6428d4 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -159,7 +159,7 @@ describe 'Environment' do context 'for project maintainer' do let(:role) { :maintainer } - it 'it shows the terminal button' do + it 'shows the terminal button' do expect(page).to have_terminal_button end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index b2a435e554d..7b7e45312d9 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -30,7 +30,7 @@ describe 'Environments page', :js do end describe 'in available tab page' do - it 'should show one environment' do + it 'shows one environment' do visit_environments(project, scope: 'available') expect(page).to have_css('.environments-container') @@ -44,7 +44,7 @@ describe 'Environments page', :js do create_list(:environment, 4, project: project, state: :available) end - it 'should render second page of pipelines' do + it 'renders second page of pipelines' do visit_environments(project, scope: 'available') find('.js-next-button').click @@ -56,7 +56,7 @@ describe 'Environments page', :js do end describe 'in stopped tab page' do - it 'should show no environments' do + it 'shows no environments' do visit_environments(project, scope: 'stopped') expect(page).to have_css('.environments-container') @@ -72,7 +72,7 @@ describe 'Environments page', :js do allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) end - it 'should show one environment without error' do + it 'shows one environment without error' do visit_environments(project, scope: 'available') expect(page).to have_css('.environments-container') @@ -87,7 +87,7 @@ describe 'Environments page', :js do end describe 'in available tab page' do - it 'should show no environments' do + it 'shows no environments' do visit_environments(project, scope: 'available') expect(page).to have_css('.environments-container') @@ -96,7 +96,7 @@ describe 'Environments page', :js do end describe 'in stopped tab page' do - it 'should show one environment' do + it 'shows one environment' do visit_environments(project, scope: 'stopped') expect(page).to have_css('.environments-container') diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index b2d2dba55f1..7432c600c1e 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -159,7 +159,7 @@ describe 'Project > Members > Invite group', :js do open_select2 '#link_group_id' end - it 'should infinitely scroll' do + it 'infinitely scrolls' do expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1) scroll_select2_to_bottom('.select2-drop .select2-results:visible') diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 75c72a68069..b54ea929978 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -252,4 +252,23 @@ describe 'New project' do end end end + + context 'Namespace selector' do + context 'with group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } + + before do + group.add_developer(user) + visit new_project_path(namespace_id: group.id) + end + + it 'selects the group namespace' do + page.within('#blank-project-pane') do + namespace = find('#project_namespace_id option[selected]') + + expect(namespace.text).to eq group.full_path + end + end + end + end end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index ee6b67b2188..b1a705f09ce 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -93,14 +93,14 @@ describe 'Pipeline Schedules', :js do expect(page).to have_button('UTC') end - it 'it creates a new scheduled pipeline' do + it 'creates a new scheduled pipeline' do fill_in_schedule_form save_pipeline_schedule expect(page).to have_content('my fancy description') end - it 'it prevents an invalid form from being submitted' do + it 'prevents an invalid form from being submitted' do save_pipeline_schedule expect(page).to have_content('This field is required') @@ -112,7 +112,7 @@ describe 'Pipeline Schedules', :js do edit_pipeline_schedule end - it 'it displays existing properties' do + it 'displays existing properties' do description = find_field('schedule_description').value expect(description).to eq('pipeline schedule') expect(page).to have_button('master') diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index b197557039d..cf334e1e4da 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -154,7 +154,7 @@ describe 'Pipeline', :js do end end - it 'should be possible to retry the success job' do + it 'is possible to retry the success job' do find('#ci-badge-build .ci-action-icon-container').click expect(page).not_to have_content('Retry job') @@ -194,13 +194,13 @@ describe 'Pipeline', :js do end end - it 'should be possible to retry the failed build' do + it 'is possible to retry the failed build' do find('#ci-badge-test .ci-action-icon-container').click expect(page).not_to have_content('Retry job') end - it 'should include the failure reason' do + it 'includes the failure reason' do page.within('#ci-badge-test') do build_link = page.find('.js-pipeline-graph-job-link') expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)') @@ -220,7 +220,7 @@ describe 'Pipeline', :js do end end - it 'should be possible to play the manual job' do + it 'is possible to play the manual job' do find('#ci-badge-manual-build .ci-action-icon-container').click expect(page).not_to have_content('Play job') @@ -454,7 +454,7 @@ describe 'Pipeline', :js do expect(page).to have_content('Cancel running') end - it 'should not link to job' do + it 'does not link to job' do expect(page).not_to have_selector('.js-pipeline-graph-job-link') end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 7ca3b3d8edd..de780f13681 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -542,19 +542,19 @@ describe 'Pipelines', :js do visit_project_pipelines end - it 'should render a mini pipeline graph' do + it 'renders a mini pipeline graph' do expect(page).to have_selector('.js-mini-pipeline-graph') expect(page).to have_selector('.js-builds-dropdown-button') end context 'when clicking a stage badge' do - it 'should open a dropdown' do + it 'opens a dropdown' do find('.js-builds-dropdown-button').click expect(page).to have_link build.name end - it 'should be possible to cancel pending build' do + it 'is possible to cancel pending build' do find('.js-builds-dropdown-button').click find('.js-ci-action').click wait_for_requests @@ -570,7 +570,7 @@ describe 'Pipelines', :js do name: 'build') end - it 'should display the failure reason' do + it 'displays the failure reason' do find('.js-builds-dropdown-button').click within('.js-builds-dropdown-list') do @@ -587,21 +587,21 @@ describe 'Pipelines', :js do create(:ci_empty_pipeline, project: project) end - it 'should render pagination' do + it 'renders pagination' do visit project_pipelines_path(project) wait_for_requests expect(page).to have_selector('.gl-pagination') end - it 'should render second page of pipelines' do + it 'renders second page of pipelines' do visit project_pipelines_path(project, page: '2') wait_for_requests expect(page).to have_selector('.gl-pagination .page', count: 2) end - it 'should show updated content' do + it 'shows updated content' do visit project_pipelines_path(project) wait_for_requests page.find('.js-next-button .page-link').click @@ -685,7 +685,7 @@ describe 'Pipelines', :js do end it 'creates a new pipeline' do - expect { click_on 'Create pipeline' } + expect { click_on 'Run Pipeline' } .to change { Ci::Pipeline.count }.by(1) expect(Ci::Pipeline.last).to be_web @@ -698,7 +698,7 @@ describe 'Pipelines', :js do fill_in "Input variable value", with: "value" end - expect { click_on 'Create pipeline' } + expect { click_on 'Run Pipeline' } .to change { Ci::Pipeline.count }.by(1) expect(Ci::Pipeline.last.variables.map { |var| var.slice(:key, :secret_value) }) @@ -709,7 +709,7 @@ describe 'Pipelines', :js do context 'without gitlab-ci.yml' do before do - click_on 'Create pipeline' + click_on 'Run Pipeline' end it { expect(page).to have_content('Missing .gitlab-ci.yml file') } @@ -722,14 +722,14 @@ describe 'Pipelines', :js do click_link 'master' end - expect { click_on 'Create pipeline' } + expect { click_on 'Run Pipeline' } .to change { Ci::Pipeline.count }.by(1) end end end end - describe 'Create pipelines' do + describe 'Run Pipelines' do let(:project) { create(:project, :repository) } before do @@ -740,7 +740,7 @@ describe 'Pipelines', :js do it 'has field to add a new pipeline' do expect(page).to have_selector('.js-branch-select') expect(find('.js-branch-select')).to have_content project.default_branch - expect(page).to have_content('Create for') + expect(page).to have_content('Run for') end end diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb index aa71669de98..e14934b1672 100644 --- a/spec/features/projects/serverless/functions_spec.rb +++ b/spec/features/projects/serverless/functions_spec.rb @@ -50,7 +50,7 @@ describe 'Functions', :js do end it 'sees an empty listing of serverless functions' do - expect(page).to have_selector('.gl-responsive-table-row') + expect(page).to have_selector('.empty-state') end end end diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb index 84de6858d5f..b1c2bab08c0 100644 --- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb @@ -93,11 +93,13 @@ describe 'Projects > Settings > User manages merge request settings' do it 'when unchecked sets :printing_merge_request_link_enabled to false' do uncheck('project_printing_merge_request_link_enabled') within('.merge-request-settings-form') do + find('.qa-save-merge-request-changes') click_on('Save changes') end - # Wait for save to complete and page to reload + find('.flash-notice') checkbox = find_field('project_printing_merge_request_link_enabled') + expect(checkbox).not_to be_checked project.reload diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb index 9c1ef78b0ca..4e1e2f330ec 100644 --- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb +++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb @@ -23,14 +23,14 @@ describe 'Projects > Snippets > User comments on a snippet', :js do expect(page).to have_content('Good snippet!') end - it 'should have autocomplete' do + it 'has autocomplete' do find('#note_note').native.send_keys('') fill_in 'note[note]', with: '@' expect(page).to have_selector('.atwho-view') end - it 'should have zen mode' do + it 'has zen mode' do find('.js-zen-enter').click expect(page).to have_selector('.fullscreen') end diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb index 8d7e2883b2a..c0932539131 100644 --- a/spec/features/projects/user_creates_project_spec.rb +++ b/spec/features/projects/user_creates_project_spec.rb @@ -54,4 +54,31 @@ describe 'User creates a project', :js do expect(project.namespace).to eq(subgroup) end end + + context 'in a group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } + + before do + group.add_developer(user) + end + + it 'creates a new project' do + visit(new_project_path) + + fill_in :project_name, with: 'a-new-project' + fill_in :project_path, with: 'a-new-project' + + page.find('.js-select-namespace').click + page.find("div[role='option']", text: group.full_path).click + + page.within('#content-body') do + click_button('Create project') + end + + expect(page).to have_content("Project 'a-new-project' was successfully created") + + project = Project.find_by(name: 'a-new-project') + expect(project.namespace).to eq(group) + end + end end diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb index b0923b451ee..9a049764dec 100644 --- a/spec/features/raven_js_spec.rb +++ b/spec/features/raven_js_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' describe 'RavenJS' do let(:raven_path) { '/raven.chunk.js' } - it 'should not load raven if sentry is disabled' do + it 'does not load raven if sentry is disabled' do visit new_user_session_path expect(has_requested_raven).to eq(false) end - it 'should load raven if sentry is enabled' do + it 'loads raven if sentry is enabled' do stub_application_setting(clientside_sentry_dsn: 'https://key@domain.com/id', clientside_sentry_enabled: true) visit new_user_session_path diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index fc6726985ae..78e0a43ce6d 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -83,7 +83,7 @@ describe 'Comments on personal snippets', :js do expect(find('div#notes')).to have_content('This is awesome!') end - it 'should not have autocomplete' do + it 'does not have autocomplete' do wait_for_requests find('#note_note').native.send_keys('') diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb index 3db9ae7a951..bfa85696e19 100644 --- a/spec/features/users/overview_spec.rb +++ b/spec/features/users/overview_spec.rb @@ -93,7 +93,7 @@ describe 'Overview tab on a user profile', :js do describe 'user has no personal projects' do include_context 'visit overview tab' - it 'it shows an empty project list with an info message' do + it 'shows an empty project list with an info message' do page.within('.projects-block') do expect(page).to have_selector('.loading', visible: false) expect(page).to have_content('You haven\'t created any personal projects.') @@ -113,7 +113,7 @@ describe 'Overview tab on a user profile', :js do include_context 'visit overview tab' - it 'it shows one entry in the list of projects' do + it 'shows one entry in the list of projects' do page.within('.projects-block') do expect(page).to have_selector('.project-row', count: 1) end @@ -139,7 +139,7 @@ describe 'Overview tab on a user profile', :js do include_context 'visit overview tab' - it 'it shows max. ten entries in the list of projects' do + it 'shows max. ten entries in the list of projects' do page.within('.projects-block') do expect(page).to have_selector('.project-row', count: 10) end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 00b6cad1a66..fe53fabe54c 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -719,7 +719,7 @@ describe IssuesFinder do end end - describe '#use_subquery_for_search?' do + describe '#use_cte_for_search?' do let(:finder) { described_class.new(nil, params) } before do @@ -731,7 +731,7 @@ describe IssuesFinder do let(:params) { { attempt_group_search_optimizations: true } } it 'returns false' do - expect(finder.use_subquery_for_search?).to be_falsey + expect(finder.use_cte_for_search?).to be_falsey end end @@ -743,15 +743,15 @@ describe IssuesFinder do end it 'returns false' do - expect(finder.use_subquery_for_search?).to be_falsey + expect(finder.use_cte_for_search?).to be_falsey end end - context 'when the attempt_group_search_optimizations param is falsey' do + context 'when the force_cte param is falsey' do let(:params) { { search: 'foo' } } it 'returns false' do - expect(finder.use_subquery_for_search?).to be_falsey + expect(finder.use_cte_for_search?).to be_falsey end end @@ -763,80 +763,39 @@ describe IssuesFinder do end it 'returns false' do - expect(finder.use_subquery_for_search?).to be_falsey + expect(finder.use_cte_for_search?).to be_falsey end end - context 'when force_cte? is true' do - let(:params) { { search: 'foo', attempt_group_search_optimizations: true, force_cte: true } } - - it 'returns false' do - expect(finder.use_subquery_for_search?).to be_falsey - end - end - - context 'when all conditions are met' do - let(:params) { { search: 'foo', attempt_group_search_optimizations: true } } - - it 'returns true' do - expect(finder.use_subquery_for_search?).to be_truthy - end - end - end + context 'when attempt_group_search_optimizations is unset and attempt_project_search_optimizations is set' do + let(:params) { { search: 'foo', attempt_project_search_optimizations: true } } - describe '#use_cte_for_count?' do - let(:finder) { described_class.new(nil, params) } - - before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - stub_feature_flags(attempt_group_search_optimizations: true) - end - - context 'when there is no search param' do - let(:params) { { attempt_group_search_optimizations: true, force_cte: true } } - - it 'returns false' do - expect(finder.use_cte_for_count?).to be_falsey - end - end - - context 'when the database is not Postgres' do - let(:params) { { search: 'foo', force_cte: true, attempt_group_search_optimizations: true } } - - before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(false) - end - - it 'returns false' do - expect(finder.use_cte_for_count?).to be_falsey - end - end - - context 'when the force_cte param is falsey' do - let(:params) { { search: 'foo' } } + context 'and the corresponding feature flag is disabled' do + before do + stub_feature_flags(attempt_project_search_optimizations: false) + end - it 'returns false' do - expect(finder.use_cte_for_count?).to be_falsey + it 'returns false' do + expect(finder.use_cte_for_search?).to be_falsey + end end - end - context 'when the attempt_group_search_optimizations flag is disabled' do - let(:params) { { search: 'foo', force_cte: true, attempt_group_search_optimizations: true } } - - before do - stub_feature_flags(attempt_group_search_optimizations: false) - end + context 'and the corresponding feature flag is enabled' do + before do + stub_feature_flags(attempt_project_search_optimizations: true) + end - it 'returns false' do - expect(finder.use_cte_for_count?).to be_falsey + it 'returns true' do + expect(finder.use_cte_for_search?).to be_truthy + end end end context 'when all conditions are met' do - let(:params) { { search: 'foo', force_cte: true, attempt_group_search_optimizations: true } } + let(:params) { { search: 'foo', attempt_group_search_optimizations: true } } it 'returns true' do - expect(finder.use_cte_for_count?).to be_truthy + expect(finder.use_cte_for_search?).to be_truthy end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 56136eb84bc..f508b9bdb6f 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -83,6 +83,14 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request2) end + it 'filters by source project id' do + params = { source_project_id: merge_request2.source_project_id } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3) + end + it 'filters by state' do params = { state: 'locked' } diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index ecffbb9e197..34c7b508c56 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -9,7 +9,7 @@ describe MilestonesFinder do let!(:milestone_3) { create(:milestone, project: project_1, state: 'active', due_date: Date.tomorrow) } let!(:milestone_4) { create(:milestone, project: project_2, state: 'active') } - it 'it returns milestones for projects' do + it 'returns milestones for projects' do result = described_class.new(project_ids: [project_1.id, project_2.id], state: 'all').execute expect(result).to contain_exactly(milestone_3, milestone_4) diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb index 35279906854..3ad38207da4 100644 --- a/spec/finders/projects/serverless/functions_finder_spec.rb +++ b/spec/finders/projects/serverless/functions_finder_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Projects::Serverless::FunctionsFinder do include KubernetesHelpers + include PrometheusHelpers include ReactiveCachingHelpers let(:user) { create(:user) } @@ -24,12 +25,12 @@ describe Projects::Serverless::FunctionsFinder do describe 'retrieve data from knative' do it 'does not have knative installed' do - expect(described_class.new(project.clusters).execute).to be_empty + expect(described_class.new(project).execute).to be_empty end context 'has knative installed' do let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } - let(:finder) { described_class.new(project.clusters) } + let(:finder) { described_class.new(project) } it 'there are no functions' do expect(finder.execute).to be_empty @@ -58,13 +59,36 @@ describe Projects::Serverless::FunctionsFinder do expect(result).not_to be_empty expect(result["metadata"]["name"]).to be_eql(cluster.project.name) end + + it 'has metrics', :use_clean_rails_memory_store_caching do + end + end + + context 'has prometheus' do + let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } + let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } + let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let(:finder) { described_class.new(project) } + + before do + allow(finder).to receive(:prometheus_adapter).and_return(prometheus_adapter) + allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix')) + end + + it 'is available' do + expect(finder.has_prometheus?("*")).to be true + end + + it 'has query data' do + expect(finder.invocation_metrics("*", cluster.project.name)).not_to be_nil + end end end describe 'verify if knative is installed' do context 'knative is not installed' do it 'does not have knative installed' do - expect(described_class.new(project.clusters).installed?).to be false + expect(described_class.new(project).installed?).to be false end end @@ -72,7 +96,7 @@ describe Projects::Serverless::FunctionsFinder do let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } it 'does have knative installed' do - expect(described_class.new(project.clusters).installed?).to be true + expect(described_class.new(project).installed?).to be true end end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json index 7e9e048a9fd..214b67a9a0f 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json +++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json @@ -51,6 +51,5 @@ "toggle_subscription_path": { "type": "string" }, "move_issue_path": { "type": "string" }, "projects_autocomplete_path": { "type": "string" } - }, - "additionalProperties": false + } } diff --git a/spec/fixtures/blockquote_fence_after.md b/spec/fixtures/blockquote_fence_after.md index 2652a842c0e..555905bf07e 100644 --- a/spec/fixtures/blockquote_fence_after.md +++ b/spec/fixtures/blockquote_fence_after.md @@ -18,10 +18,13 @@ Double `>>>` inside code block: Blockquote outside code block: + > Quote + Code block inside blockquote: + > Quote > > ``` @@ -30,8 +33,10 @@ Code block inside blockquote: > > Quote + Single `>>>` inside code block inside blockquote: + > Quote > > ``` @@ -42,8 +47,10 @@ Single `>>>` inside code block inside blockquote: > > Quote + Double `>>>` inside code block inside blockquote: + > Quote > > ``` @@ -56,6 +63,7 @@ Double `>>>` inside code block inside blockquote: > > Quote + Single `>>>` inside HTML: <pre> @@ -76,10 +84,13 @@ Double `>>>` inside HTML: Blockquote outside HTML: + > Quote + HTML inside blockquote: + > Quote > > <pre> @@ -88,8 +99,10 @@ HTML inside blockquote: > > Quote + Single `>>>` inside HTML inside blockquote: + > Quote > > <pre> @@ -100,8 +113,10 @@ Single `>>>` inside HTML inside blockquote: > > Quote + Double `>>>` inside HTML inside blockquote: + > Quote > > <pre> @@ -113,3 +128,4 @@ Double `>>>` inside HTML inside blockquote: > </pre> > > Quote + diff --git a/spec/fixtures/valid.po b/spec/fixtures/valid.po index dbe2f952bad..155b6cbb95d 100644 --- a/spec/fixtures/valid.po +++ b/spec/fixtures/valid.po @@ -35,9 +35,6 @@ msgid_plural "%d pipelines" msgstr[0] "1 pipeline" msgstr[1] "%d pipelines" -msgid "A collection of graphs regarding Continuous Integration" -msgstr "Una colección de gráficos sobre Integración Continua" - msgid "About auto deploy" msgstr "Acerca del auto despliegue" diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index 0d3dcc29f22..eea7bd87257 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -5,19 +5,41 @@ import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX, } from '~/clusters/constants'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { loadHTMLFixture } from 'helpers/fixtures'; +import { setTestTimeout } from 'helpers/timeout'; +import $ from 'jquery'; describe('Clusters', () => { + setTestTimeout(500); + let cluster; - preloadFixtures('clusters/show_cluster.html'); + let mock; + + const mockGetClusterStatusRequest = () => { + const { statusPath } = document.querySelector('.js-edit-cluster-form').dataset; + + mock = new MockAdapter(axios); + + mock.onGet(statusPath).reply(200); + }; + + beforeEach(() => { + loadHTMLFixture('clusters/show_cluster.html'); + }); + + beforeEach(() => { + mockGetClusterStatusRequest(); + }); beforeEach(() => { - loadFixtures('clusters/show_cluster.html'); cluster = new Clusters(); }); afterEach(() => { cluster.destroy(); + mock.restore(); }); describe('toggle', () => { @@ -29,16 +51,13 @@ describe('Clusters', () => { '.js-cluster-enable-toggle-area .js-project-feature-toggle-input', ); - toggleButton.click(); - - getSetTimeoutPromise() - .then(() => { - expect(toggleButton.classList).not.toContain('is-checked'); + $(toggleInput).one('trigger-change', () => { + expect(toggleButton.classList).not.toContain('is-checked'); + expect(toggleInput.getAttribute('value')).toEqual('false'); + done(); + }); - expect(toggleInput.getAttribute('value')).toEqual('false'); - }) - .then(done) - .catch(done.fail); + toggleButton.click(); }); }); @@ -197,7 +216,7 @@ describe('Clusters', () => { describe('installApplication', () => { it('tries to install helm', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); + jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); @@ -209,7 +228,7 @@ describe('Clusters', () => { }); it('tries to install ingress', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); + jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null); @@ -221,7 +240,7 @@ describe('Clusters', () => { }); it('tries to install runner', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); + jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); expect(cluster.store.state.applications.runner.requestStatus).toEqual(null); @@ -233,7 +252,7 @@ describe('Clusters', () => { }); it('tries to install jupyter', () => { - spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); + jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce(); expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null); cluster.installApplication({ @@ -248,35 +267,32 @@ describe('Clusters', () => { }); }); - it('sets error request status when the request fails', done => { - spyOn(cluster.service, 'installApplication').and.returnValue( - Promise.reject(new Error('STUBBED ERROR')), - ); + it('sets error request status when the request fails', () => { + jest + .spyOn(cluster.service, 'installApplication') + .mockRejectedValueOnce(new Error('STUBBED ERROR')); expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); - cluster.installApplication({ id: 'helm' }); + const promise = cluster.installApplication({ id: 'helm' }); expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.service.installApplication).toHaveBeenCalled(); - getSetTimeoutPromise() - .then(() => { - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE); - expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); - }) - .then(done) - .catch(done.fail); + return promise.then(() => { + expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE); + expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); + }); }); }); describe('handleSuccess', () => { beforeEach(() => { - spyOn(cluster.store, 'updateStateFromServer'); - spyOn(cluster, 'toggleIngressDomainHelpText'); - spyOn(cluster, 'checkForNewInstalls'); - spyOn(cluster, 'updateContainer'); + jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis(); + jest.spyOn(cluster, 'toggleIngressDomainHelpText').mockReturnThis(); + jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis(); + jest.spyOn(cluster, 'updateContainer').mockReturnThis(); cluster.handleSuccess({ data: {} }); }); @@ -300,9 +316,13 @@ describe('Clusters', () => { describe('toggleIngressDomainHelpText', () => { const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS; + let ingressPreviousState; + let ingressNewState; - const ingressPreviousState = { status: INSTALLABLE }; - const ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' }; + beforeEach(() => { + ingressPreviousState = { status: INSTALLABLE }; + ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' }; + }); describe(`when ingress application new status is ${INSTALLED}`, () => { beforeEach(() => { @@ -333,7 +353,7 @@ describe('Clusters', () => { }); describe('when ingress application new status and old status are the same', () => { - it('does not modify custom domain help text', () => { + it('does not display custom domain help text', () => { ingressPreviousState.status = INSTALLED; ingressNewState.status = ingressPreviousState.status; @@ -342,5 +362,15 @@ describe('Clusters', () => { expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); }); }); + + describe(`when ingress new status is ${INSTALLED} and there isn’t an ip assigned`, () => { + it('does not display custom domain help text', () => { + ingressNewState.externalIp = null; + + cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); + + expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); + }); + }); }); }); diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js index a2dd4e93daf..b28d0075d06 100644 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ b/spec/frontend/clusters/components/application_row_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import eventHub from '~/clusters/event_hub'; import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants'; import applicationRow from '~/clusters/components/application_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; describe('Application Row', () => { @@ -160,7 +160,7 @@ describe('Application Row', () => { }); it('clicking install button emits event', () => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit'); vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.INSTALLABLE, @@ -176,7 +176,7 @@ describe('Application Row', () => { }); it('clicking install button when installApplicationRequestParams are provided emits event', () => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit'); vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.INSTALLABLE, @@ -193,7 +193,7 @@ describe('Application Row', () => { }); it('clicking disabled install button emits nothing', () => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit'); vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.INSTALLING, @@ -255,7 +255,7 @@ describe('Application Row', () => { }); it('clicking upgrade button emits event', () => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit'); vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.UPDATE_ERRORED, @@ -271,7 +271,7 @@ describe('Application Row', () => { }); it('clicking disabled upgrade button emits nothing', () => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit'); vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.UPDATING, diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js index 0f8153ad493..7c54a27d950 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/frontend/clusters/components/applications_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import applications from '~/clusters/components/applications.vue'; import { CLUSTER_TYPE } from '~/clusters/constants'; import eventHub from '~/clusters/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; describe('Applications', () => { @@ -314,7 +314,7 @@ describe('Applications', () => { }); it('emits event when clicking Save changes button', () => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit'); vm = mountComponent(Applications, props); const saveButton = vm.$el.querySelector('.js-knative-save-domain-button'); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js index b4d1bb710e0..b4d1bb710e0 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/frontend/clusters/services/mock_data.js diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index 161722ec571..161722ec571 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js new file mode 100644 index 00000000000..2e8bff298c4 --- /dev/null +++ b/spec/frontend/helpers/monitor_helper_spec.js @@ -0,0 +1,45 @@ +import * as monitorHelper from '~/helpers/monitor_helper'; + +describe('monitor helper', () => { + const defaultConfig = { default: true, name: 'default name' }; + const name = 'data name'; + const series = [[1, 1], [2, 2], [3, 3]]; + const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }]; + + describe('makeDataSeries', () => { + const expectedDataSeries = [ + { + ...defaultConfig, + data: series, + }, + ]; + + it('converts query results to data series', () => { + expect(monitorHelper.makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual( + expectedDataSeries, + ); + }); + + it('returns an empty array if no query results exist', () => { + expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]); + }); + + it('handles multi-series query results', () => { + const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' }; + + expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([ + expectedData, + expectedData, + ]); + }); + + it('excludes NaN values', () => { + expect( + monitorHelper.makeDataSeries( + data({ metric: {}, values: [[1, 1], [2, NaN]] }), + defaultConfig, + ), + ).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]); + }); + }); +}); diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js index 5de7a281d34..40d47aaad03 100644 --- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js @@ -18,7 +18,7 @@ describe('IDE commit module mutations', () => { describe('UPDATE_COMMIT_ACTION', () => { it('updates commitAction', () => { - mutations.UPDATE_COMMIT_ACTION(state, 'testing'); + mutations.UPDATE_COMMIT_ACTION(state, { commitAction: 'testing' }); expect(state.commitAction).toBe('testing'); }); @@ -39,4 +39,20 @@ describe('IDE commit module mutations', () => { expect(state.submitCommitLoading).toBeTruthy(); }); }); + + describe('TOGGLE_SHOULD_CREATE_MR', () => { + it('changes shouldCreateMR to true when initial state is false', () => { + state.shouldCreateMR = false; + mutations.TOGGLE_SHOULD_CREATE_MR(state); + + expect(state.shouldCreateMR).toBe(true); + }); + + it('changes shouldCreateMR to false when initial state is true', () => { + state.shouldCreateMR = true; + mutations.TOGGLE_SHOULD_CREATE_MR(state); + + expect(state.shouldCreateMR).toBe(false); + }); + }); }); diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js index acfdc885032..d54e0eab845 100644 --- a/spec/frontend/labels_select_spec.js +++ b/spec/frontend/labels_select_spec.js @@ -13,40 +13,104 @@ const mockLabels = [ }, ]; +const mockScopedLabels = [ + { + id: 27, + title: 'Foo::Bar', + description: 'Foobar', + color: '#333ABC', + text_color: '#FFFFFF', + }, +]; + describe('LabelsSelect', () => { describe('getLabelTemplate', () => { - const label = mockLabels[0]; - let $labelEl; - - beforeEach(() => { - $labelEl = $( - LabelsSelect.getLabelTemplate({ - labels: mockLabels, - issueUpdateURL: mockUrl, - }), - ); - }); + describe('when normal label is present', () => { + const label = mockLabels[0]; + let $labelEl; - it('generated label item template has correct label URL', () => { - expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label'); - }); + beforeEach(() => { + $labelEl = $( + LabelsSelect.getLabelTemplate({ + labels: mockLabels, + issueUpdateURL: mockUrl, + enableScopedLabels: true, + scopedLabelsDocumentationLink: 'docs-link', + }), + ); + }); - it('generated label item template has correct label title', () => { - expect($labelEl.find('span.label').text()).toBe(label.title); - }); + it('generated label item template has correct label URL', () => { + expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label'); + }); - it('generated label item template has label description as title attribute', () => { - expect($labelEl.find('span.label').attr('title')).toBe(label.description); - }); + it('generated label item template has correct label title', () => { + expect($labelEl.find('span.label').text()).toBe(label.title); + }); + + it('generated label item template has label description as title attribute', () => { + expect($labelEl.find('span.label').attr('title')).toBe(label.description); + }); - it('generated label item template has correct label styles', () => { - expect($labelEl.find('span.label').attr('style')).toBe( - `background-color: ${label.color}; color: ${label.text_color};`, - ); + it('generated label item template has correct label styles', () => { + expect($labelEl.find('span.label').attr('style')).toBe( + `background-color: ${label.color}; color: ${label.text_color};`, + ); + }); + + it('generated label item has a badge class', () => { + expect($labelEl.find('span').hasClass('badge')).toEqual(true); + }); + + it('generated label item template does not have scoped-label class', () => { + expect($labelEl.find('.scoped-label')).toHaveLength(0); + }); }); - it('generated label item has a badge class', () => { - expect($labelEl.find('span').hasClass('badge')).toEqual(true); + describe('when scoped label is present', () => { + const label = mockScopedLabels[0]; + let $labelEl; + + beforeEach(() => { + $labelEl = $( + LabelsSelect.getLabelTemplate({ + labels: mockScopedLabels, + issueUpdateURL: mockUrl, + enableScopedLabels: true, + scopedLabelsDocumentationLink: 'docs-link', + }), + ); + }); + + it('generated label item template has correct label URL', () => { + expect($labelEl.find('a').attr('href')).toBe('/foo/bar?label_name[]=Foo%3A%3ABar'); + }); + + it('generated label item template has correct label title', () => { + expect($labelEl.find('span.label').text()).toBe(label.title); + }); + + it('generated label item template has html flag as true', () => { + expect($labelEl.find('span.label').attr('data-html')).toBe('true'); + }); + + it('generated label item template has question icon', () => { + expect($labelEl.find('i.fa-question-circle')).toHaveLength(1); + }); + + it('generated label item template has scoped-label class', () => { + expect($labelEl.find('.scoped-label')).toHaveLength(1); + }); + + it('generated label item template has correct label styles', () => { + expect($labelEl.find('span.label').attr('style')).toBe( + `background-color: ${label.color}; color: ${label.text_color};`, + ); + }); + + it('generated label item has a badge class', () => { + expect($labelEl.find('span').hasClass('badge')).toEqual(true); + }); }); }); }); diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 0a266b19ea5..3f331055a32 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -151,4 +151,31 @@ describe('text_utility', () => { ); }); }); + + describe('truncateNamespace', () => { + it(`should return the root namespace if the namespace only includes one level`, () => { + expect(textUtils.truncateNamespace('a / b')).toBe('a'); + }); + + it(`should return the first 2 namespaces if the namespace inlcudes exactly 2 levels`, () => { + expect(textUtils.truncateNamespace('a / b / c')).toBe('a / b'); + }); + + it(`should return the first and last namespaces, separated by "...", if the namespace inlcudes more than 2 levels`, () => { + expect(textUtils.truncateNamespace('a / b / c / d')).toBe('a / ... / c'); + expect(textUtils.truncateNamespace('a / b / c / d / e / f / g / h / i')).toBe('a / ... / h'); + }); + + it(`should return an empty string for invalid inputs`, () => { + [undefined, null, 4, {}, true, new Date()].forEach(input => { + expect(textUtils.truncateNamespace(input)).toBe(''); + }); + }); + + it(`should not alter strings that aren't formatted as namespaces`, () => { + ['', ' ', '\t', 'a', 'a \\ b'].forEach(input => { + expect(textUtils.truncateNamespace(input)).toBe(input); + }); + }); + }); }); diff --git a/spec/frontend/serverless/components/area_spec.js b/spec/frontend/serverless/components/area_spec.js new file mode 100644 index 00000000000..62005e1981a --- /dev/null +++ b/spec/frontend/serverless/components/area_spec.js @@ -0,0 +1,122 @@ +import { shallowMount } from '@vue/test-utils'; +import Area from '~/serverless/components/area.vue'; +import { mockNormalizedMetrics } from '../mock_data'; + +describe('Area component', () => { + const mockWidgets = 'mockWidgets'; + const mockGraphData = mockNormalizedMetrics; + let areaChart; + + beforeEach(() => { + areaChart = shallowMount(Area, { + propsData: { + graphData: mockGraphData, + containerWidth: 0, + }, + slots: { + default: mockWidgets, + }, + sync: false, + }); + }); + + afterEach(() => { + areaChart.destroy(); + }); + + it('renders chart title', () => { + expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title); + }); + + it('contains graph widgets from slot', () => { + expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + const mockDate = mockNormalizedMetrics.queries[0].result[0].values[0].time; + const generateSeriesData = type => ({ + seriesData: [ + { + componentSubType: type, + value: [mockDate, 4], + }, + ], + value: mockDate, + }); + + describe('series is of line type', () => { + beforeEach(() => { + areaChart.vm.formatTooltipText(generateSeriesData('line')); + }); + + it('formats tooltip title', () => { + expect(areaChart.vm.tooltipPopoverTitle).toBe('28 Feb 2019, 11:11AM'); + }); + + it('formats tooltip content', () => { + expect(areaChart.vm.tooltipPopoverContent).toBe('Invocations (requests): 4'); + }); + }); + + it('verify default interval value of 1', () => { + expect(areaChart.vm.getInterval).toBe(1); + }); + }); + + describe('onResize', () => { + const mockWidth = 233; + + beforeEach(() => { + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ + width: mockWidth, + })); + areaChart.vm.onResize(); + }); + + it('sets area chart width', () => { + expect(areaChart.vm.width).toBe(mockWidth); + }); + }); + }); + + describe('computed', () => { + describe('chartData', () => { + it('utilizes all data points', () => { + expect(Object.keys(areaChart.vm.chartData)).toEqual(['requests']); + expect(areaChart.vm.chartData.requests.length).toBe(2); + }); + + it('creates valid data', () => { + const data = areaChart.vm.chartData.requests; + + expect( + data.filter( + datum => new Date(datum.time).getTime() > 0 && typeof datum.value === 'number', + ).length, + ).toBe(data.length); + }); + }); + + describe('generateSeries', () => { + it('utilizes correct time data', () => { + expect(areaChart.vm.generateSeries.data).toEqual([ + ['2019-02-28T11:11:38.756Z', 0], + ['2019-02-28T11:12:38.756Z', 0], + ]); + }); + }); + + describe('xAxisLabel', () => { + it('constructs a label for the chart x-axis', () => { + expect(areaChart.vm.xAxisLabel).toBe('invocations / minute'); + }); + }); + + describe('yAxisLabel', () => { + it('constructs a label for the chart y-axis', () => { + expect(areaChart.vm.yAxisLabel).toBe('Invocations (requests)'); + }); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js index bdf7a714910..161a637dd75 100644 --- a/spec/javascripts/serverless/components/environment_row_spec.js +++ b/spec/frontend/serverless/components/environment_row_spec.js @@ -1,81 +1,70 @@ -import Vue from 'vue'; - import environmentRowComponent from '~/serverless/components/environment_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import ServerlessStore from '~/serverless/stores/serverless_store'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; +import { translate } from '~/serverless/utils'; -const createComponent = (env, envName) => - mountComponent(Vue.extend(environmentRowComponent), { env, envName }); +const createComponent = (localVue, env, envName) => + shallowMount(environmentRowComponent, { localVue, propsData: { env, envName }, sync: false }).vm; describe('environment row component', () => { describe('default global cluster case', () => { + let localVue; let vm; beforeEach(() => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctions); - vm = createComponent(store.state.functions['*'], '*'); + localVue = createLocalVue(); + vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*'); }); + afterEach(() => vm.$destroy()); + it('has the correct envId', () => { expect(vm.envId).toEqual('env-global'); - vm.$destroy(); }); it('is open by default', () => { expect(vm.isOpenClass).toEqual({ 'is-open': true }); - vm.$destroy(); }); it('generates correct output', () => { - expect(vm.$el.querySelectorAll('li').length).toEqual(2); expect(vm.$el.id).toEqual('env-global'); expect(vm.$el.classList.contains('is-open')).toBe(true); expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*'); - - vm.$destroy(); }); it('opens and closes correctly', () => { expect(vm.isOpen).toBe(true); vm.toggleOpen(); - Vue.nextTick(() => { - expect(vm.isOpen).toBe(false); - }); - vm.$destroy(); + expect(vm.isOpen).toBe(false); }); }); describe('default named cluster case', () => { let vm; + let localVue; beforeEach(() => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv); - vm = createComponent(store.state.functions.test, 'test'); + localVue = createLocalVue(); + vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test'); }); + afterEach(() => vm.$destroy()); + it('has the correct envId', () => { expect(vm.envId).toEqual('env-test'); - vm.$destroy(); }); it('is open by default', () => { expect(vm.isOpenClass).toEqual({ 'is-open': true }); - vm.$destroy(); }); it('generates correct output', () => { - expect(vm.$el.querySelectorAll('li').length).toEqual(1); expect(vm.$el.id).toEqual('env-test'); expect(vm.$el.classList.contains('is-open')).toBe(true); expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test'); - - vm.$destroy(); }); }); }); diff --git a/spec/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js new file mode 100644 index 00000000000..31348ff1194 --- /dev/null +++ b/spec/frontend/serverless/components/function_details_spec.js @@ -0,0 +1,117 @@ +import Vuex from 'vuex'; + +import functionDetailsComponent from '~/serverless/components/function_details.vue'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createStore } from '~/serverless/store'; + +describe('functionDetailsComponent', () => { + let localVue; + let component; + let store; + + beforeEach(() => { + localVue = createLocalVue(); + localVue.use(Vuex); + + store = createStore(); + }); + + afterEach(() => { + component.vm.$destroy(); + }); + + describe('Verify base functionality', () => { + const serviceStub = { + name: 'test', + description: 'a description', + environment: '*', + url: 'http://service.com/test', + namespace: 'test-ns', + podcount: 0, + metricsUrl: '/metrics', + }; + + it('has a name, description, URL, and no pods loaded', () => { + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + sync: false, + }); + + expect( + component.vm.$el.querySelector('.serverless-function-name').innerHTML.trim(), + ).toContain('test'); + + expect( + component.vm.$el.querySelector('.serverless-function-description').innerHTML.trim(), + ).toContain('a description'); + + expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain( + 'No pods loaded at this time.', + ); + }); + + it('has a pods loaded', () => { + serviceStub.podcount = 1; + + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + sync: false, + }); + + expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('1 pod in use'); + }); + + it('has multiple pods loaded', () => { + serviceStub.podcount = 3; + + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + sync: false, + }); + + expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('3 pods in use'); + }); + + it('can support a missing description', () => { + serviceStub.description = null; + + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + sync: false, + }); + + expect( + component.vm.$el.querySelector('.serverless-function-description').querySelector('div') + .innerHTML.length, + ).toEqual(0); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/frontend/serverless/components/function_row_spec.js index 6933a8f6c87..414fdc5cd82 100644 --- a/spec/javascripts/serverless/components/function_row_spec.js +++ b/spec/frontend/serverless/components/function_row_spec.js @@ -1,11 +1,10 @@ -import Vue from 'vue'; - import functionRowComponent from '~/serverless/components/function_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import { mockServerlessFunction } from '../mock_data'; -const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func }); +const createComponent = func => + shallowMount(functionRowComponent, { propsData: { func }, sync: false }).vm; describe('functionRowComponent', () => { it('Parses the function details correctly', () => { @@ -13,10 +12,7 @@ describe('functionRowComponent', () => { expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name); expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image); - expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null); - expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual( - mockServerlessFunction.url, - ); + expect(vm.$el.querySelector('timeago-stub').getAttribute('time')).not.toBe(null); vm.$destroy(); }); @@ -25,8 +21,6 @@ describe('functionRowComponent', () => { const vm = createComponent(mockServerlessFunction); expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row - expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image - expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar vm.$destroy(); }); diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js new file mode 100644 index 00000000000..5533de1a70a --- /dev/null +++ b/spec/frontend/serverless/components/functions_spec.js @@ -0,0 +1,106 @@ +import Vuex from 'vuex'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import functionsComponent from '~/serverless/components/functions.vue'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createStore } from '~/serverless/store'; +import { mockServerlessFunctions } from '../mock_data'; + +describe('functionsComponent', () => { + let component; + let store; + let localVue; + + beforeEach(() => { + localVue = createLocalVue(); + localVue.use(Vuex); + + store = createStore(); + }); + + afterEach(() => { + component.vm.$destroy(); + }); + + it('should render empty state when Knative is not installed', () => { + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: false, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); + + expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null); + }); + + it('should render a loading component', () => { + store.dispatch('requestFunctionsLoading'); + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: true, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); + + expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null); + }); + + it('should render empty state when there is no function data', () => { + store.dispatch('receiveFunctionsNoDataSuccess'); + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: true, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); + + expect( + component.vm.$el + .querySelector('.empty-state, .js-empty-state') + .classList.contains('js-empty-state'), + ).toBe(true); + + expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual( + 'No functions available', + ); + }); + + fit('should render the functions list', () => { + const statusPath = 'statusPath'; + const axiosMock = new AxiosMockAdapter(axios); + axiosMock.onGet(statusPath).reply(200); + + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: true, + clustersPath: 'clustersPath', + helpPath: 'helpPath', + statusPath, + }, + sync: false, + }); + + component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions); + + return component.vm.$nextTick().then(() => { + expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null); + }); + }); +}); diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js new file mode 100644 index 00000000000..d0df6125290 --- /dev/null +++ b/spec/frontend/serverless/components/missing_prometheus_spec.js @@ -0,0 +1,38 @@ +import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue'; +import { shallowMount } from '@vue/test-utils'; + +const createComponent = missingData => + shallowMount(missingPrometheusComponent, { + propsData: { + clustersPath: '/clusters', + helpPath: '/help', + missingData, + }, + sync: false, + }).vm; + +describe('missingPrometheusComponent', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + it('should render missing prometheus message', () => { + vm = createComponent(false); + + expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( + 'Function invocation metrics require Prometheus to be installed first.', + ); + + expect(vm.$el.querySelector('glbutton-stub').getAttribute('variant')).toEqual('success'); + }); + + it('should render no prometheus data message', () => { + vm = createComponent(true); + + expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( + 'Invocation metrics loading or not available at this time.', + ); + }); +}); diff --git a/spec/frontend/serverless/components/pod_box_spec.js b/spec/frontend/serverless/components/pod_box_spec.js new file mode 100644 index 00000000000..d82825d8f62 --- /dev/null +++ b/spec/frontend/serverless/components/pod_box_spec.js @@ -0,0 +1,23 @@ +import podBoxComponent from '~/serverless/components/pod_box.vue'; +import { shallowMount } from '@vue/test-utils'; + +const createComponent = count => + shallowMount(podBoxComponent, { + propsData: { + count, + }, + sync: false, + }).vm; + +describe('podBoxComponent', () => { + it('should render three boxes', () => { + const count = 3; + const vm = createComponent(count); + const rects = vm.$el.querySelectorAll('rect'); + + expect(rects.length).toEqual(3); + expect(parseInt(rects[2].getAttribute('x'), 10)).toEqual(40); + + vm.$destroy(); + }); +}); diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/frontend/serverless/components/url_spec.js index 21a879a49bb..d05a9bba103 100644 --- a/spec/javascripts/serverless/components/url_spec.js +++ b/spec/frontend/serverless/components/url_spec.js @@ -1,15 +1,14 @@ import Vue from 'vue'; - import urlComponent from '~/serverless/components/url.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -const createComponent = uri => { - const component = Vue.extend(urlComponent); +import { shallowMount } from '@vue/test-utils'; - return mountComponent(component, { - uri, - }); -}; +const createComponent = uri => + shallowMount(Vue.extend(urlComponent), { + propsData: { + uri, + }, + sync: false, + }).vm; describe('urlComponent', () => { it('should render correctly', () => { @@ -17,9 +16,7 @@ describe('urlComponent', () => { const vm = createComponent(uri); expect(vm.$el.classList.contains('clipboard-group')).toBe(true); - expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual( - uri, - ); + expect(vm.$el.querySelector('clipboardbutton-stub').getAttribute('text')).toEqual(uri); expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri); diff --git a/spec/javascripts/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js index ecd393b174c..a2c18616324 100644 --- a/spec/javascripts/serverless/mock_data.js +++ b/spec/frontend/serverless/mock_data.js @@ -77,3 +77,60 @@ export const mockMultilineServerlessFunction = { description: 'testfunc1\nA test service line\\nWith additional services', image: 'knative-test-container-buildtemplate', }; + +export const mockMetrics = { + success: true, + last_update: '2019-02-28T19:11:38.926Z', + metrics: { + id: 22, + title: 'Knative function invocations', + required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'], + weight: 0, + y_label: 'Invocations', + queries: [ + { + query_range: + 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))', + unit: 'requests', + label: 'invocations / minute', + result: [ + { + metric: {}, + values: [[1551352298.756, '0'], [1551352358.756, '0']], + }, + ], + }, + ], + }, +}; + +export const mockNormalizedMetrics = { + id: 22, + title: 'Knative function invocations', + required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'], + weight: 0, + y_label: 'Invocations', + queries: [ + { + query_range: + 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))', + unit: 'requests', + label: 'invocations / minute', + result: [ + { + metric: {}, + values: [ + { + time: '2019-02-28T11:11:38.756Z', + value: 0, + }, + { + time: '2019-02-28T11:12:38.756Z', + value: 0, + }, + ], + }, + ], + }, + ], +}; diff --git a/spec/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js new file mode 100644 index 00000000000..aac57c75a4f --- /dev/null +++ b/spec/frontend/serverless/store/actions_spec.js @@ -0,0 +1,90 @@ +import MockAdapter from 'axios-mock-adapter'; +import statusCodes from '~/lib/utils/http_status'; +import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions'; +import { mockServerlessFunctions, mockMetrics } from '../mock_data'; +import axios from '~/lib/utils/axios_utils'; +import testAction from '../../helpers/vuex_action_helper'; +import { adjustMetricQuery } from '../utils'; + +describe('ServerlessActions', () => { + describe('fetchFunctions', () => { + it('should successfully fetch functions', done => { + const endpoint = '/functions'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions)); + + testAction( + fetchFunctions, + { functionsPath: endpoint }, + {}, + [], + [ + { type: 'requestFunctionsLoading' }, + { type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions }, + ], + () => { + mock.restore(); + done(); + }, + ); + }); + + it('should successfully retry', done => { + const endpoint = '/functions'; + const mock = new MockAdapter(axios); + mock + .onGet(endpoint) + .reply(() => new Promise(resolve => setTimeout(() => resolve(200), Infinity))); + + testAction( + fetchFunctions, + { functionsPath: endpoint }, + {}, + [], + [{ type: 'requestFunctionsLoading' }], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('fetchMetrics', () => { + it('should return no prometheus', done => { + const endpoint = '/metrics'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.NO_CONTENT); + + testAction( + fetchMetrics, + { metricsPath: endpoint, hasPrometheus: false }, + {}, + [], + [{ type: 'receiveMetricsNoPrometheus' }], + () => { + mock.restore(); + done(); + }, + ); + }); + + it('should successfully fetch metrics', done => { + const endpoint = '/metrics'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics)); + + testAction( + fetchMetrics, + { metricsPath: endpoint, hasPrometheus: true }, + {}, + [], + [{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }], + () => { + mock.restore(); + done(); + }, + ); + }); + }); +}); diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js new file mode 100644 index 00000000000..fb549c8f153 --- /dev/null +++ b/spec/frontend/serverless/store/getters_spec.js @@ -0,0 +1,43 @@ +import serverlessState from '~/serverless/store/state'; +import * as getters from '~/serverless/store/getters'; +import { mockServerlessFunctions } from '../mock_data'; + +describe('Serverless Store Getters', () => { + let state; + + beforeEach(() => { + state = serverlessState; + }); + + describe('hasPrometheusMissingData', () => { + it('should return false if Prometheus is not installed', () => { + state.hasPrometheus = false; + + expect(getters.hasPrometheusMissingData(state)).toEqual(false); + }); + + it('should return false if Prometheus is installed and there is data', () => { + state.hasPrometheusData = true; + + expect(getters.hasPrometheusMissingData(state)).toEqual(false); + }); + + it('should return true if Prometheus is installed and there is no data', () => { + state.hasPrometheus = true; + state.hasPrometheusData = false; + + expect(getters.hasPrometheusMissingData(state)).toEqual(true); + }); + }); + + describe('getFunctions', () => { + it('should translate the raw function array to group the functions per environment scope', () => { + state.functions = mockServerlessFunctions; + + const funcs = getters.getFunctions(state); + + expect(Object.keys(funcs)).toContain('*'); + expect(funcs['*'].length).toEqual(2); + }); + }); +}); diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js new file mode 100644 index 00000000000..ca3053e5c38 --- /dev/null +++ b/spec/frontend/serverless/store/mutations_spec.js @@ -0,0 +1,86 @@ +import mutations from '~/serverless/store/mutations'; +import * as types from '~/serverless/store/mutation_types'; +import { mockServerlessFunctions, mockMetrics } from '../mock_data'; + +describe('ServerlessMutations', () => { + describe('Functions List Mutations', () => { + it('should ensure loading is true', () => { + const state = {}; + + mutations[types.REQUEST_FUNCTIONS_LOADING](state); + + expect(state.isLoading).toEqual(true); + }); + + it('should set proper state once functions are loaded', () => { + const state = {}; + + mutations[types.RECEIVE_FUNCTIONS_SUCCESS](state, mockServerlessFunctions); + + expect(state.isLoading).toEqual(false); + expect(state.hasFunctionData).toEqual(true); + expect(state.functions).toEqual(mockServerlessFunctions); + }); + + it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => { + const state = {}; + + mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state); + + expect(state.isLoading).toEqual(false); + expect(state.hasFunctionData).toEqual(false); + expect(state.functions).toBe(undefined); + }); + + it('should ensure loading has stopped, and an error is raised', () => { + const state = {}; + + mutations[types.RECEIVE_FUNCTIONS_ERROR](state, 'sample error'); + + expect(state.isLoading).toEqual(false); + expect(state.hasFunctionData).toEqual(false); + expect(state.functions).toBe(undefined); + expect(state.error).not.toBe(undefined); + }); + }); + + describe('Function Details Metrics Mutations', () => { + it('should ensure isLoading and hasPrometheus data flags indicate data is loaded', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_SUCCESS](state, mockMetrics); + + expect(state.isLoading).toEqual(false); + expect(state.hasPrometheusData).toEqual(true); + expect(state.graphData).toEqual(mockMetrics); + }); + + it('should ensure isLoading and hasPrometheus data flags are cleared indicating no functions available', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_NODATA_SUCCESS](state); + + expect(state.isLoading).toEqual(false); + expect(state.hasPrometheusData).toEqual(false); + expect(state.graphData).toBe(undefined); + }); + + it('should properly indicate an error', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_ERROR](state, 'sample error'); + + expect(state.hasPrometheusData).toEqual(false); + expect(state.error).not.toBe(undefined); + }); + + it('should properly indicate when prometheus is installed', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_NO_PROMETHEUS](state); + + expect(state.hasPrometheus).toEqual(false); + expect(state.hasPrometheusData).toEqual(false); + }); + }); +}); diff --git a/spec/frontend/serverless/utils.js b/spec/frontend/serverless/utils.js new file mode 100644 index 00000000000..5ce2e37d493 --- /dev/null +++ b/spec/frontend/serverless/utils.js @@ -0,0 +1,20 @@ +export const adjustMetricQuery = data => { + const updatedMetric = data.metrics; + + const queries = data.metrics.queries.map(query => ({ + ...query, + result: query.result.map(result => ({ + ...result, + values: result.values.map(([timestamp, value]) => ({ + time: new Date(timestamp * 1000).toISOString(), + value: Number(value), + })), + })), + })); + + updatedMetric.queries = queries; + return updatedMetric; +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap new file mode 100644 index 00000000000..add0c36a120 --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Resizable Chart Container renders the component 1`] = ` +<div> + <div + class="slot" + > + <span + class="width" + > + 0 + </span> + + <span + class="height" + > + 0 + </span> + </div> +</div> +`; diff --git a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js new file mode 100644 index 00000000000..8f533e8ab24 --- /dev/null +++ b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue'; +import $ from 'jquery'; + +jest.mock('~/lib/utils/common_utils', () => ({ + debounceByAnimationFrame(callback) { + return jest.spyOn({ callback }, 'callback'); + }, +})); + +describe('Resizable Chart Container', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(ResizableChartContainer, { + attachToDocument: true, + scopedSlots: { + default: ` + <div class="slot" slot-scope="{ width, height }"> + <span class="width">{{width}}</span> + <span class="height">{{height}}</span> + </div> + `, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the component', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('updates the slot width and height props', () => { + const width = 1920; + const height = 1080; + + // JSDOM mocks and sets clientWidth/clientHeight to 0 so we set manually + wrapper.vm.$refs.chartWrapper = { clientWidth: width, clientHeight: height }; + + $(document).trigger('content.resize'); + + return Vue.nextTick().then(() => { + const widthNode = wrapper.find('.slot > .width'); + const heightNode = wrapper.find('.slot > .height'); + + expect(parseInt(widthNode.text(), 10)).toEqual(width); + expect(parseInt(heightNode.text(), 10)).toEqual(height); + }); + }); + + it('calls onResize on manual resize', () => { + $(document).trigger('content.resize'); + expect(wrapper.vm.debouncedResize).toHaveBeenCalled(); + }); + + it('calls onResize on page resize', () => { + window.dispatchEvent(new Event('resize')); + expect(wrapper.vm.debouncedResize).toHaveBeenCalled(); + }); +}); diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 4b40d523287..37e9ddadb8c 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -59,19 +59,19 @@ describe IconsHelper do describe 'non existing icon' do non_existing = 'non_existing_icon_sprite' - it 'should raise in development mode' do + it 'raises in development mode' do allow(Rails.env).to receive(:development?).and_return(true) expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/) end - it 'should raise in test mode' do + it 'raises in test mode' do allow(Rails.env).to receive(:test?).and_return(true) expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/) end - it 'should not raise in production mode' do + it 'does not raise in production mode' do allow(Rails.env).to receive(:test?).and_return(false) allow(Rails.env).to receive(:development?).and_return(false) diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 012678db9c2..a049b5a6133 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -249,4 +249,24 @@ describe LabelsHelper do .to match_array([label2, label4, label1, label3]) end end + + describe 'label_from_hash' do + it 'builds a group label with whitelisted attributes' do + label = label_from_hash({ title: 'foo', color: 'bar', id: 1, group_id: 1 }) + + expect(label).to be_a(GroupLabel) + expect(label.id).to be_nil + expect(label.title).to eq('foo') + expect(label.color).to eq('bar') + end + + it 'builds a project label with whitelisted attributes' do + label = label_from_hash({ title: 'foo', color: 'bar', id: 1, project_id: 1 }) + + expect(label).to be_a(ProjectLabel) + expect(label.id).to be_nil + expect(label.title).to eq('foo') + expect(label.color).to eq('bar') + end + end end diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb index 7ccbdcd1332..601f864ef36 100644 --- a/spec/helpers/namespaces_helper_spec.rb +++ b/spec/helpers/namespaces_helper_spec.rb @@ -1,10 +1,38 @@ require 'spec_helper' -describe NamespacesHelper do +describe NamespacesHelper, :postgresql do let!(:admin) { create(:admin) } - let!(:admin_group) { create(:group, :private) } + let!(:admin_project_creation_level) { nil } + let!(:admin_group) do + create(:group, + :private, + project_creation_level: admin_project_creation_level) + end let!(:user) { create(:user) } - let!(:user_group) { create(:group, :private) } + let!(:user_project_creation_level) { nil } + let!(:user_group) do + create(:group, + :private, + project_creation_level: user_project_creation_level) + end + let!(:subgroup1) do + create(:group, + :private, + parent: admin_group, + project_creation_level: nil) + end + let!(:subgroup2) do + create(:group, + :private, + parent: admin_group, + project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + end + let!(:subgroup3) do + create(:group, + :private, + parent: admin_group, + project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + end before do admin_group.add_owner(admin) @@ -105,5 +133,43 @@ describe NamespacesHelper do helper.namespaces_options end end + + describe 'include_groups_with_developer_maintainer_access parameter' do + context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set for a project' do + let!(:admin_project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS } + + it 'returns groups where user is a developer' do + allow(helper).to receive(:current_user).and_return(user) + stub_application_setting(default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + admin_group.add_user(user, GroupMember::DEVELOPER) + + options = helper.namespaces_options_with_developer_maintainer_access + + expect(options).to include(admin_group.name) + expect(options).not_to include(subgroup1.name) + expect(options).to include(subgroup2.name) + expect(options).not_to include(subgroup3.name) + expect(options).to include(user_group.name) + expect(options).to include(user.name) + end + end + + context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set globally' do + it 'return groups where default is not overridden' do + allow(helper).to receive(:current_user).and_return(user) + stub_application_setting(default_project_creation: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) + admin_group.add_user(user, GroupMember::DEVELOPER) + + options = helper.namespaces_options_with_developer_maintainer_access + + expect(options).to include(admin_group.name) + expect(options).to include(subgroup1.name) + expect(options).to include(subgroup2.name) + expect(options).not_to include(subgroup3.name) + expect(options).to include(user_group.name) + expect(options).to include(user.name) + end + end + end end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 9cff0291250..2f59cfda0a0 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -12,7 +12,7 @@ describe SearchHelper do allow(self).to receive(:current_user).and_return(nil) end - it "it returns nil" do + it "returns nil" do expect(search_autocomplete_opts("q")).to be_nil end end diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb index bfec7ad4bba..e384e2bf9a0 100644 --- a/spec/helpers/version_check_helper_spec.rb +++ b/spec/helpers/version_check_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe VersionCheckHelper do describe '#version_status_badge' do - it 'should return nil if not dev environment and not enabled' do + it 'returns nil if not dev environment and not enabled' do allow(Rails.env).to receive(:production?) { false } allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false } @@ -16,16 +16,16 @@ describe VersionCheckHelper do allow(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' } end - it 'should return an image tag' do + it 'returns an image tag' do expect(helper.version_status_badge).to start_with('<img') end - it 'should have a js prefixed css class' do + it 'has a js prefixed css class' do expect(helper.version_status_badge) .to match(/class="js-version-status-badge lazy"/) end - it 'should have a VersionCheck url as the src' do + it 'has a VersionCheck url as the src' do expect(helper.version_status_badge) .to include(%{src="https://version.host.com/check.svg?gitlab_info=xxx"}) end diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 54fb0e8228b..e4ff3eb381f 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -178,6 +178,7 @@ describe('Issue model', () => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { expect(data.issue.assignee_ids).toEqual([1]); done(); + return Promise.resolve(); }); issue.update('url'); @@ -187,6 +188,7 @@ describe('Issue model', () => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { expect(data.issue.assignee_ids).toEqual([0]); done(); + return Promise.resolve(); }); issue.removeAllAssignees(); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 8d7c52a2876..3ce69bc3c20 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -57,6 +57,24 @@ describe('diffs/components/app', () => { wrapper.destroy(); }); + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = false; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true); + }); + + it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { + createComponent({}, ({ state }) => { + state.diffs.showTreeList = true; + state.diffs.isParallelView = false; + }); + + expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false); + }); + it('displays loading icon on loading', () => { createComponent({}, ({ state }) => { state.diffs.isLoading = true; diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index e886f962d2f..77f8352047c 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -66,6 +66,26 @@ describe('CompareVersions', () => { expect(inlineBtn.innerHTML).toContain('Inline'); expect(parallelBtn.innerHTML).toContain('Side-by-side'); }); + + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { + vm.isLimitedContainer = true; + + vm.$nextTick(() => { + const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width'); + + expect(limitedContainer).not.toBeNull(); + }); + }); + + it('does not add container-limiting classes when showFileTree is false with inline diffs', () => { + vm.isLimitedContainer = false; + + vm.$nextTick(() => { + const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width'); + + expect(limitedContainer).toBeNull(); + }); + }); }); describe('setInlineDiffViewType', () => { diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 6614069f44d..e1170c9762e 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -672,7 +672,7 @@ describe('diff_file_header', () => { vm = mountComponentWithStore(Component, { props, store }); - expect(vm.$el.querySelector('.js-expand-file').textContent).toContain('Show changes only'); + expect(vm.$el.querySelector('.ic-doc-changes')).not.toBeNull(); }); it('shows expand text', () => { @@ -680,7 +680,7 @@ describe('diff_file_header', () => { vm = mountComponentWithStore(Component, { props, store }); - expect(vm.$el.querySelector('.js-expand-file').textContent).toContain('Show full file'); + expect(vm.$el.querySelector('.ic-doc-expand')).not.toBeNull(); }); it('renders loading icon', () => { diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 645b3aa788a..0f3f9a10f94 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -65,3 +65,61 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller store_frontend_fixture(response, fixture_file_name) end end + +describe API::Issues, '(JavaScript fixtures)', type: :request do + include ApiHelpers + include JavaScriptFixturesHelpers + + def get_related_merge_requests(project_id, issue_iid, user = nil) + get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user) + end + + def create_referencing_mr(user, project, issue) + attributes = { + author: user, + source_project: project, + target_project: project, + source_branch: "master", + target_branch: "test", + assignee: user, + description: "See #{issue.to_reference}" + } + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + end + end + + it 'issues/related_merge_requests.json' do |example| + user = create(:user) + project = create(:project, :public, creator_id: user.id, namespace: user.namespace) + issue_title = 'foo' + issue_description = 'closed' + milestone = create(:milestone, title: '1.0.0', project: project) + issue = create :issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: generate(:past_time), + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + + project.add_reporter(user) + create_referencing_mr(user, project, issue) + + create(:merge_request, + :simple, + author: user, + source_project: project, + target_project: project, + description: "Some description") + project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) + create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline)) + + get_related_merge_requests(project.id, issue.iid, user) + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js index 7deed985219..f00bc2eeb6d 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js @@ -1,25 +1,31 @@ import Vue from 'vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here const createComponent = () => { const Component = Vue.extend(frequentItemsListItemComponent); - return mountComponent(Component, { - itemId: mockProject.id, - itemName: mockProject.name, - namespace: mockProject.namespace, - webUrl: mockProject.webUrl, - avatarUrl: mockProject.avatarUrl, + return shallowMount(Component, { + propsData: { + itemId: mockProject.id, + itemName: mockProject.name, + namespace: mockProject.namespace, + webUrl: mockProject.webUrl, + avatarUrl: mockProject.avatarUrl, + }, }); }; describe('FrequentItemsListItemComponent', () => { + let wrapper; let vm; beforeEach(() => { - vm = createComponent(); + wrapper = createComponent(); + + ({ vm } = wrapper); }); afterEach(() => { @@ -29,11 +35,11 @@ describe('FrequentItemsListItemComponent', () => { describe('computed', () => { describe('hasAvatar', () => { it('should return `true` or `false` if whether avatar is present or not', () => { - vm.avatarUrl = 'path/to/avatar.png'; + wrapper.setProps({ avatarUrl: 'path/to/avatar.png' }); expect(vm.hasAvatar).toBe(true); - vm.avatarUrl = null; + wrapper.setProps({ avatarUrl: null }); expect(vm.hasAvatar).toBe(false); }); @@ -41,41 +47,49 @@ describe('FrequentItemsListItemComponent', () => { describe('highlightedItemName', () => { it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => { - vm.matcher = 'lab'; + wrapper.setProps({ matcher: 'lab' }); - expect(vm.highlightedItemName).toContain('<b>Lab</b>'); + expect(wrapper.find('.js-frequent-items-item-title').html()).toContain( + '<b>L</b><b>a</b><b>b</b>', + ); }); it('should return project name as it is if `matcher` is not available', () => { - vm.matcher = null; + wrapper.setProps({ matcher: null }); - expect(vm.highlightedItemName).toBe(mockProject.name); + expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe( + mockProject.name, + ); }); }); describe('truncatedNamespace', () => { it('should truncate project name from namespace string', () => { - vm.namespace = 'platform / nokia-3310'; + wrapper.setProps({ namespace: 'platform / nokia-3310' }); - expect(vm.truncatedNamespace).toBe('platform'); + expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform'); }); it('should truncate namespace string from the middle if it includes more than two groups in path', () => { - vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310'; + wrapper.setProps({ + namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310', + }); - expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset'); + expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe( + 'platform / ... / Mobile Chipset', + ); }); }); }); describe('template', () => { it('should render component element', () => { - expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy(); - expect(vm.$el.querySelectorAll('a').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1); - expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1); + expect(wrapper.classes()).toContain('frequent-items-list-item-container'); + expect(wrapper.findAll('a').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1); + expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1); }); }); }); diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js index d564292f1ba..ddbbc5c2d29 100644 --- a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js +++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js @@ -1,19 +1,22 @@ import Vue from 'vue'; import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue'; import eventHub from '~/frequent_items/event_hub'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; const createComponent = (namespace = 'projects') => { const Component = Vue.extend(searchComponent); - return mountComponent(Component, { namespace }); + return shallowMount(Component, { propsData: { namespace } }); }; describe('FrequentItemsSearchInputComponent', () => { + let wrapper; let vm; beforeEach(() => { - vm = createComponent(); + wrapper = createComponent(); + + ({ vm } = wrapper); }); afterEach(() => { @@ -35,7 +38,7 @@ describe('FrequentItemsSearchInputComponent', () => { describe('mounted', () => { it('should listen `dropdownOpen` event', done => { spyOn(eventHub, '$on'); - const vmX = createComponent(); + const vmX = createComponent().vm; Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith( @@ -49,7 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => { describe('beforeDestroy', () => { it('should unbind event listeners on eventHub', done => { - const vmX = createComponent(); + const vmX = createComponent().vm; spyOn(eventHub, '$off'); vmX.$mount(); @@ -67,12 +70,12 @@ describe('FrequentItemsSearchInputComponent', () => { describe('template', () => { it('should render component element', () => { - const inputEl = vm.$el.querySelector('input.form-control'); - - expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); - expect(inputEl).not.toBe(null); - expect(inputEl.getAttribute('placeholder')).toBe('Search your projects'); - expect(vm.$el.querySelector('.search-icon')).toBeDefined(); + expect(wrapper.classes()).toContain('search-input-container'); + expect(wrapper.contains('input.form-control')).toBe(true); + expect(wrapper.contains('.search-icon')).toBe(true); + expect(wrapper.find('input.form-control').attributes('placeholder')).toBe( + 'Search your projects', + ); }); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js index 3a5d6c8a90b..23e6a055518 100644 --- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import store from '~/ide/stores'; +import consts from '~/ide/stores/modules/commit/constants'; import commitActions from '~/ide/components/commit_sidebar/actions.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from 'spec/ide/helpers'; @@ -7,20 +8,33 @@ import { projectData } from 'spec/ide/mock_data'; describe('IDE commit sidebar actions', () => { let vm; - - beforeEach(done => { + const createComponent = ({ + hasMR = false, + commitAction = consts.COMMIT_TO_NEW_BRANCH, + mergeRequestsEnabled = true, + currentBranchId = 'master', + shouldCreateMR = false, + } = {}) => { const Component = Vue.extend(commitActions); vm = createComponentWithStore(Component, store); - vm.$store.state.currentBranchId = 'master'; + vm.$store.state.currentBranchId = currentBranchId; vm.$store.state.currentProjectId = 'abcproject'; + vm.$store.state.commit.commitAction = commitAction; Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData }); + vm.$store.state.projects.abcproject.merge_requests_enabled = mergeRequestsEnabled; + vm.$store.state.commit.shouldCreateMR = shouldCreateMR; - vm.$mount(); + if (hasMR) { + vm.$store.state.currentMergeRequestId = '1'; + vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ + store.state.currentMergeRequestId + ] = { foo: 'bar' }; + } - Vue.nextTick(done); - }); + return vm.$mount(); + }; afterEach(() => { vm.$destroy(); @@ -28,16 +42,20 @@ describe('IDE commit sidebar actions', () => { resetStore(vm.$store); }); - it('renders 3 groups', () => { - expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3); + it('renders 2 groups', () => { + createComponent(); + + expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); }); it('renders current branch text', () => { + createComponent(); + expect(vm.$el.textContent).toContain('Commit to master branch'); }); it('hides merge request option when project merge requests are disabled', done => { - vm.$store.state.projects.abcproject.merge_requests_enabled = false; + createComponent({ mergeRequestsEnabled: false }); vm.$nextTick(() => { expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); @@ -49,9 +67,53 @@ describe('IDE commit sidebar actions', () => { describe('commitToCurrentBranchText', () => { it('escapes current branch', () => { - vm.$store.state.currentBranchId = '<img src="x" />'; + const injectedSrc = '<img src="x" />'; + createComponent({ currentBranchId: injectedSrc }); + + expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc); + }); + }); + + describe('create new MR checkbox', () => { + it('disables `createMR` button when an MR already exists and committing to current branch', () => { + createComponent({ hasMR: true, commitAction: consts.COMMIT_TO_CURRENT_BRANCH }); + + expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(true); + }); + + it('does not disable checkbox if MR does not exist', () => { + createComponent({ hasMR: false }); + + expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false); + }); + + it('does not disable checkbox when creating a new branch', () => { + createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH }); + + expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false); + }); + + it('toggles off new MR when switching back to commit to current branch and MR exists', () => { + createComponent({ + commitAction: consts.COMMIT_TO_NEW_BRANCH, + shouldCreateMR: true, + }); + const currentBranchRadio = vm.$el.querySelector( + `input[value="${consts.COMMIT_TO_CURRENT_BRANCH}"`, + ); + currentBranchRadio.click(); + + vm.$nextTick(() => { + expect(vm.$store.state.commit.shouldCreateMR).toBe(false); + }); + }); + + it('toggles `shouldCreateMR` when clicking checkbox', () => { + createComponent(); + const el = vm.$el.querySelector('input[type="checkbox"]'); + el.dispatchEvent(new Event('change')); - expect(vm.commitToCurrentBranchText).not.toContain('<img src="x" />'); + expect(vm.$store.state.commit.shouldCreateMR).toBe(true); }); }); }); diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js index c93a939ad71..d7fed3f0681 100644 --- a/spec/javascripts/ide/components/file_row_extra_spec.js +++ b/spec/javascripts/ide/components/file_row_extra_spec.js @@ -20,7 +20,7 @@ describe('IDE extra file row component', () => { file: { ...file('test'), }, - mouseOver: false, + dropdownOpen: false, }); spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount); diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index 83e530f0a6a..aaebe88f314 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -56,11 +56,11 @@ describe('new dropdown component', () => { }); }); - describe('dropdownOpen', () => { + describe('isOpen', () => { it('scrolls dropdown into view', done => { spyOn(vm.$refs.dropdownMenu, 'scrollIntoView'); - vm.dropdownOpen = true; + vm.isOpen = true; setTimeout(() => { expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({ diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index a5839630657..4dd0c1150eb 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import actions, { - getMergeRequestsForBranch, getMergeRequestData, getMergeRequestChanges, getMergeRequestVersions, @@ -12,13 +11,17 @@ import service from '~/ide/services'; import { activityBarViews } from '~/ide/constants'; import { resetStore } from '../../helpers'; +const TEST_PROJECT = 'abcproject'; +const TEST_PROJECT_ID = 17; + describe('IDE store merge request actions', () => { let mock; beforeEach(() => { mock = new MockAdapter(axios); - store.state.projects.abcproject = { + store.state.projects[TEST_PROJECT] = { + id: TEST_PROJECT_ID, mergeRequests: {}, }; }); @@ -41,10 +44,11 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequests service method', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { - expect(service.getProjectMergeRequests).toHaveBeenCalledWith('abcproject', { + expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, { source_branch: 'bar', + source_project_id: TEST_PROJECT_ID, order_by: 'created_at', per_page: 1, }); @@ -56,13 +60,11 @@ describe('IDE store merge request actions', () => { it('sets the "Merge Request" Object', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { - expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(1); - expect(Object.keys(store.state.projects.abcproject.mergeRequests)[0]).toEqual('2'); - expect(store.state.projects.abcproject.mergeRequests[2]).toEqual( - jasmine.objectContaining(mrData), - ); + expect(store.state.projects.abcproject.mergeRequests).toEqual({ + '2': jasmine.objectContaining(mrData), + }); done(); }) .catch(done.fail); @@ -70,7 +72,7 @@ describe('IDE store merge request actions', () => { it('sets "Current Merge Request" object to the most recent MR', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { expect(store.state.currentMergeRequestId).toEqual('2'); done(); @@ -87,9 +89,9 @@ describe('IDE store merge request actions', () => { it('does not fail if there are no merge requests for current branch', done => { store - .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'foo' }) + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' }) .then(() => { - expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(0); + expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({}); expect(store.state.currentMergeRequestId).toEqual(''); done(); }) @@ -106,7 +108,8 @@ describe('IDE store merge request actions', () => { it('flashes message, if error', done => { const flashSpy = spyOnDependency(actions, 'flash'); - getMergeRequestsForBranch({ commit() {} }, { projectId: 'abcproject', branchId: 'bar' }) + store + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { fail('Expected getMergeRequestsForBranch to throw an error'); }) @@ -132,9 +135,9 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequestData service method', done => { store - .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, { + expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1, { render_html: true, }); @@ -145,10 +148,12 @@ describe('IDE store merge request actions', () => { it('sets the Merge Request Object', done => { store - .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest'); expect(store.state.currentMergeRequestId).toBe(1); + expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe( + 'mergerequest', + ); done(); }) @@ -170,7 +175,7 @@ describe('IDE store merge request actions', () => { dispatch, state: store.state, }, - { projectId: 'abcproject', mergeRequestId: 1 }, + { projectId: TEST_PROJECT, mergeRequestId: 1 }, ) .then(done.fail) .catch(() => { @@ -179,7 +184,7 @@ describe('IDE store merge request actions', () => { action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { - projectId: 'abcproject', + projectId: TEST_PROJECT, mergeRequestId: 1, force: false, }, @@ -193,7 +198,7 @@ describe('IDE store merge request actions', () => { describe('getMergeRequestChanges', () => { beforeEach(() => { - store.state.projects.abcproject.mergeRequests['1'] = { changes: [] }; + store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] }; }); describe('success', () => { @@ -207,9 +212,9 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequestChanges service method', done => { store - .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1); + expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1); done(); }) @@ -218,9 +223,9 @@ describe('IDE store merge request actions', () => { it('sets the Merge Request Changes Object', done => { store - .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe( + expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe( 'mergerequest', ); done(); @@ -243,7 +248,7 @@ describe('IDE store merge request actions', () => { dispatch, state: store.state, }, - { projectId: 'abcproject', mergeRequestId: 1 }, + { projectId: TEST_PROJECT, mergeRequestId: 1 }, ) .then(done.fail) .catch(() => { @@ -252,7 +257,7 @@ describe('IDE store merge request actions', () => { action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { - projectId: 'abcproject', + projectId: TEST_PROJECT, mergeRequestId: 1, force: false, }, @@ -266,7 +271,7 @@ describe('IDE store merge request actions', () => { describe('getMergeRequestVersions', () => { beforeEach(() => { - store.state.projects.abcproject.mergeRequests['1'] = { versions: [] }; + store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] }; }); describe('success', () => { @@ -279,9 +284,9 @@ describe('IDE store merge request actions', () => { it('calls getProjectMergeRequestVersions service method', done => { store - .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1); + expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1); done(); }) @@ -290,9 +295,9 @@ describe('IDE store merge request actions', () => { it('sets the Merge Request Versions Object', done => { store - .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 }) + .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 }) .then(() => { - expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1); + expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1); done(); }) .catch(done.fail); @@ -313,7 +318,7 @@ describe('IDE store merge request actions', () => { dispatch, state: store.state, }, - { projectId: 'abcproject', mergeRequestId: 1 }, + { projectId: TEST_PROJECT, mergeRequestId: 1 }, ) .then(done.fail) .catch(() => { @@ -322,7 +327,7 @@ describe('IDE store merge request actions', () => { action: jasmine.any(Function), actionText: 'Please try again', actionPayload: { - projectId: 'abcproject', + projectId: TEST_PROJECT, mergeRequestId: 1, force: false, }, @@ -336,7 +341,7 @@ describe('IDE store merge request actions', () => { describe('openMergeRequest', () => { const mr = { - projectId: 'abcproject', + projectId: TEST_PROJECT, targetProjectId: 'defproject', mergeRequestId: 2, }; diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 7b0963713fb..cd519eaed7c 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -279,5 +279,22 @@ describe('IDE store project actions', () => { .then(done) .catch(done.fail); }); + + it('creates a new file supplied via URL if the file does not exist yet', done => { + openBranch(store, { ...branch, basePath: 'not-existent.md' }) + .then(() => { + expect(store.dispatch).not.toHaveBeenCalledWith( + 'handleTreeEntryAction', + jasmine.anything(), + ); + + expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { + name: 'not-existent.md', + type: 'blob', + }); + }) + .then(done) + .catch(done.fail); + }); }); }); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index fbb676aab33..5ed9b9003a7 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'spec/helpers/vuex_action_helper'; -import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree'; +import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree'; import * as types from '~/ide/stores/mutation_types'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; @@ -206,4 +206,35 @@ describe('Multi-file store tree actions', () => { ); }); }); + + describe('setDirectoryData', () => { + it('sets tree correctly if there are no opened files yet', done => { + const treeFile = file({ name: 'README.md' }); + store.state.trees['abcproject/master'] = {}; + + testAction( + setDirectoryData, + { projectId: 'abcproject', branchId: 'master', treeList: [treeFile] }, + store.state, + [ + { + type: types.SET_DIRECTORY_DATA, + payload: { + treePath: 'abcproject/master', + data: [treeFile], + }, + }, + { + type: types.TOGGLE_LOADING, + payload: { + entry: {}, + forceValue: false, + }, + }, + ], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 34d97347438..abc97f3072c 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -3,7 +3,7 @@ import store from '~/ide/stores'; import service from '~/ide/services'; import router from '~/ide/ide_router'; import eventHub from '~/ide/eventhub'; -import * as consts from '~/ide/stores/modules/commit/constants'; +import consts from '~/ide/stores/modules/commit/constants'; import { resetStore, file } from 'spec/ide/helpers'; describe('IDE commit module actions', () => { @@ -389,7 +389,8 @@ describe('IDE commit module actions', () => { it('redirects to new merge request page', done => { spyOn(eventHub, '$on'); - store.state.commit.commitAction = '3'; + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; + store.state.commit.shouldCreateMR = true; store .dispatch('commit/commitChanges') @@ -405,6 +406,21 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); + it('does not redirect to new merge request page when shouldCreateMR is not checked', done => { + spyOn(eventHub, '$on'); + + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; + store.state.commit.shouldCreateMR = false; + + store + .dispatch('commit/commitChanges') + .then(() => { + expect(visitUrl).not.toHaveBeenCalled(); + done(); + }) + .catch(done.fail); + }); + it('resets changed files before redirecting', done => { spyOn(eventHub, '$on'); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index 702e78ef773..e00fd7199d7 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -1,5 +1,5 @@ import commitState from '~/ide/stores/modules/commit/state'; -import * as consts from '~/ide/stores/modules/commit/constants'; +import consts from '~/ide/stores/modules/commit/constants'; import * as getters from '~/ide/stores/modules/commit/getters'; describe('IDE commit module getters', () => { @@ -46,7 +46,7 @@ describe('IDE commit module getters', () => { currentBranchId: 'master', }; const localGetters = { - placeholderBranchName: 'newBranchName', + placeholderBranchName: 'placeholder-branch-name', }; beforeEach(() => { @@ -59,25 +59,28 @@ describe('IDE commit module getters', () => { expect(getters.branchName(state, null, rootState)).toBe('master'); }); - ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => { - describe(type, () => { - beforeEach(() => { - Object.assign(state, { - commitAction: consts[type], - }); + describe('COMMIT_TO_NEW_BRANCH', () => { + beforeEach(() => { + Object.assign(state, { + commitAction: consts.COMMIT_TO_NEW_BRANCH, }); + }); - it('uses newBranchName when not empty', () => { - expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName'); + it('uses newBranchName when not empty', () => { + const newBranchName = 'nonempty-branch-name'; + Object.assign(state, { + newBranchName, }); - it('uses placeholderBranchName when state newBranchName is empty', () => { - Object.assign(state, { - newBranchName: '', - }); + expect(getters.branchName(state, localGetters, rootState)).toBe(newBranchName); + }); - expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName'); + it('uses placeholderBranchName when state newBranchName is empty', () => { + Object.assign(state, { + newBranchName: '', }); + + expect(getters.branchName(state, localGetters, rootState)).toBe('placeholder-branch-name'); }); }); }); @@ -141,4 +144,33 @@ describe('IDE commit module getters', () => { }); }); }); + + describe('shouldDisableNewMrOption', () => { + it('returns false if commitAction `COMMIT_TO_NEW_BRANCH`', () => { + state.commitAction = consts.COMMIT_TO_NEW_BRANCH; + const rootState = { + currentMergeRequest: { foo: 'bar' }, + }; + + expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy(); + }); + + it('returns false if there is no current merge request', () => { + state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; + const rootState = { + currentMergeRequest: null, + }; + + expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy(); + }); + + it('returns true an MR exists and commit action is `COMMIT_TO_CURRENT_BRANCH`', () => { + state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; + const rootState = { + currentMergeRequest: { foo: 'bar' }, + }; + + expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeTruthy(); + }); + }); }); diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js index 67e9f7509da..7f9c978aa46 100644 --- a/spec/javascripts/ide/stores/mutations/tree_spec.js +++ b/spec/javascripts/ide/stores/mutations/tree_spec.js @@ -26,17 +26,11 @@ describe('Multi-file store tree mutations', () => { }); describe('SET_DIRECTORY_DATA', () => { - const data = [ - { - name: 'tree', - }, - { - name: 'submodule', - }, - { - name: 'blob', - }, - ]; + let data; + + beforeEach(() => { + data = [file('tree'), file('foo'), file('blob')]; + }); it('adds directory data', () => { localState.trees['project/master'] = { @@ -52,7 +46,7 @@ describe('Multi-file store tree mutations', () => { expect(tree.tree.length).toBe(3); expect(tree.tree[0].name).toBe('tree'); - expect(tree.tree[1].name).toBe('submodule'); + expect(tree.tree[1].name).toBe('foo'); expect(tree.tree[2].name).toBe('blob'); }); @@ -65,6 +59,49 @@ describe('Multi-file store tree mutations', () => { expect(localState.trees['project/master'].loading).toBe(true); }); + + it('does not override tree already in state, but merges the two with correct order', () => { + const openedFile = file('new'); + + localState.trees['project/master'] = { + loading: true, + tree: [openedFile], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const { tree } = localState.trees['project/master']; + + expect(tree.length).toBe(4); + expect(tree[0].name).toBe('blob'); + expect(tree[1].name).toBe('foo'); + expect(tree[2].name).toBe('new'); + expect(tree[3].name).toBe('tree'); + }); + + it('returns tree unchanged if the opened file is already in the tree', () => { + const openedFile = file('foo'); + localState.trees['project/master'] = { + loading: true, + tree: [openedFile], + }; + + mutations.SET_DIRECTORY_DATA(localState, { + data, + treePath: 'project/master', + }); + + const { tree } = localState.trees['project/master']; + + expect(tree.length).toBe(3); + + expect(tree[0].name).toBe('tree'); + expect(tree[1].name).toBe('foo'); + expect(tree[2].name).toBe('blob'); + }); }); describe('REMOVE_ALL_CHANGES_FILES', () => { diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index 9f18034f8a3..c4f122efdda 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -235,4 +235,129 @@ describe('Multi-file store utils', () => { ]); }); }); + + describe('mergeTrees', () => { + let fromTree; + let toTree; + + beforeEach(() => { + fromTree = [file('foo')]; + toTree = [file('bar')]; + }); + + it('merges simple trees with sorting the result', () => { + toTree = [file('beta'), file('alpha'), file('gamma')]; + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(4); + expect(res[0].name).toEqual('alpha'); + expect(res[1].name).toEqual('beta'); + expect(res[2].name).toEqual('foo'); + expect(res[3].name).toEqual('gamma'); + expect(res[2]).toBe(fromTree[0]); + }); + + it('handles edge cases', () => { + expect(utils.mergeTrees({}, []).length).toEqual(0); + + let res = utils.mergeTrees({}, toTree); + + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('bar'); + + res = utils.mergeTrees(fromTree, []); + + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('foo'); + expect(res[0]).toBe(fromTree[0]); + }); + + it('merges simple trees without producing duplicates', () => { + toTree.push(file('foo')); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(2); + expect(res[0].name).toEqual('bar'); + expect(res[1].name).toEqual('foo'); + expect(res[1]).not.toBe(fromTree[0]); + }); + + it('merges nested tree into the main one without duplicates', () => { + fromTree[0].tree.push({ + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('beta.md'), + path: 'foo/alpha/beta.md', + }, + ], + }); + + toTree.push({ + ...file('foo'), + tree: [ + { + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('gamma.md'), + path: 'foo/alpha/gamma.md', + }, + ], + }, + ], + }); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res.length).toEqual(2); + expect(res[1].name).toEqual('foo'); + + const finalBranch = res[1].tree[0].tree; + + expect(finalBranch.length).toEqual(2); + expect(finalBranch[0].name).toEqual('beta.md'); + expect(finalBranch[1].name).toEqual('gamma.md'); + }); + + it('marks correct folders as opened as the parsing goes on', () => { + fromTree[0].tree.push({ + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('beta.md'), + path: 'foo/alpha/beta.md', + }, + ], + }); + + toTree.push({ + ...file('foo'), + tree: [ + { + ...file('alpha'), + path: 'foo/alpha', + tree: [ + { + ...file('gamma.md'), + path: 'foo/alpha/gamma.md', + }, + ], + }, + ], + }); + + const res = utils.mergeTrees(fromTree, toTree); + + expect(res[1].name).toEqual('foo'); + expect(res[1].opened).toEqual(true); + + expect(res[1].tree[0].name).toEqual('alpha'); + expect(res[1].tree[0].opened).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index ba5d672f189..cef40117304 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -17,6 +17,7 @@ describe('Job App ', () => { const props = { endpoint: `${gl.TEST_HOST}jobs/123.json`, runnerHelpUrl: 'help/runner', + deploymentHelpUrl: 'help/deployment', runnerSettingsUrl: 'settings/ci-cd/runners', terminalPath: 'jobs/123/terminal', pagePath: `${gl.TEST_HOST}jobs/123`, @@ -253,6 +254,41 @@ describe('Job App ', () => { }); }); + describe('unmet prerequisites block', () => { + it('renders unmet prerequisites block when there is an unmet prerequisites failure', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + details_path: 'path', + illustration: { + content: 'Retry this job in order to create the necessary resources.', + image: 'path', + size: 'svg-430', + title: 'Failed to create resources', + }, + }, + failure_reason: 'unmet_prerequisites', + has_trace: false, + runners: { + available: true, + }, + tags: [], + }), + ); + vm = mountComponentWithStore(Component, { props, store }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull(); + done(); + }, 0); + }); + }); + describe('environments block', () => { it('renders environment block when job has environment', done => { mock.onGet(props.endpoint).replyOnce( diff --git a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js new file mode 100644 index 00000000000..68fcb321214 --- /dev/null +++ b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import component from '~/jobs/components/unmet_prerequisites_block.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Unmet Prerequisites Block Job component', () => { + const Component = Vue.extend(component); + let vm; + const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs'; + + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: true, + helpPath, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an alert with the correct message', () => { + const container = vm.$el.querySelector('.js-failed-unmet-prerequisites'); + const alertMessage = + 'This job failed because the necessary resources were not successfully created.'; + + expect(container).not.toBeNull(); + expect(container.innerHTML).toContain(alertMessage); + }); + + it('renders link to help page', () => { + const helpLink = vm.$el.querySelector('.js-help-path'); + + expect(helpLink).not.toBeNull(); + expect(helpLink.innerHTML).toContain('More information'); + expect(helpLink.getAttribute('href')).toEqual(helpPath); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 2084c36e484..da012e1d5f7 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -2,7 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; -import BreakpointInstance from '~/breakpoints'; +import breakpointInstance from '~/breakpoints'; const PIXEL_TOLERANCE = 0.2; @@ -383,7 +383,7 @@ describe('common_utils', () => { describe('contentTop', () => { it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => { - spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('sm'); + spyOn(breakpointInstance, 'isDesktop').and.returnValue(false); setFixtures(` <div class="diff-file file-title-flex-parent"> @@ -398,7 +398,7 @@ describe('common_utils', () => { }); it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => { - spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('lg'); + spyOn(breakpointInstance, 'isDesktop').and.returnValue(true); setFixtures(` <div class="diff-file file-title-flex-parent"> diff --git a/spec/javascripts/lib/utils/higlight_spec.js b/spec/javascripts/lib/utils/higlight_spec.js new file mode 100644 index 00000000000..638bbf65ae9 --- /dev/null +++ b/spec/javascripts/lib/utils/higlight_spec.js @@ -0,0 +1,43 @@ +import highlight from '~/lib/utils/highlight'; + +describe('highlight', () => { + it(`should appropriately surround substring matches`, () => { + const expected = 'g<b>i</b><b>t</b>lab'; + + expect(highlight('gitlab', 'it')).toBe(expected); + }); + + it(`should return an empty string in the case of invalid inputs`, () => { + [null, undefined].forEach(input => { + expect(highlight(input, 'match')).toBe(''); + }); + }); + + it(`should return the original value if match is null, undefined, or ''`, () => { + [null, undefined].forEach(match => { + expect(highlight('gitlab', match)).toBe('gitlab'); + }); + }); + + it(`should highlight matches in non-string inputs`, () => { + const expected = '123<b>4</b><b>5</b>6'; + + expect(highlight(123456, 45)).toBe(expected); + }); + + it(`should sanitize the input string before highlighting matches`, () => { + const expected = 'hello <b>w</b>orld'; + + expect(highlight('hello <b>world</b>', 'w')).toBe(expected); + }); + + it(`should not highlight anything if no matches are found`, () => { + expect(highlight('gitlab', 'hello')).toBe('gitlab'); + }); + + it(`should allow wrapping elements to be customized`, () => { + const expected = '1<hello>2</hello>3'; + + expect(highlight('123', '2', '<hello>', '</hello>')).toBe(expected); + }); +}); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index 549a7935c0f..41a6c04efb9 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; import Area from '~/monitoring/components/charts/area.vue'; import MonitoringStore from '~/monitoring/stores/monitoring_store'; @@ -65,7 +65,7 @@ describe('Area component', () => { expect(props.data).toBe(areaChart.vm.chartData); expect(props.option).toBe(areaChart.vm.chartOptions); expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText); - expect(props.thresholds).toBe(areaChart.props('alertData')); + expect(props.thresholds).toBe(areaChart.vm.thresholds); }); it('recieves a tooltip title', () => { @@ -105,12 +105,13 @@ describe('Area component', () => { seriesName: areaChart.vm.chartData[0].name, componentSubType: type, value: [mockDate, 5.55555], + seriesIndex: 0, }, ], value: mockDate, }); - describe('series is of line type', () => { + describe('when series is of line type', () => { beforeEach(() => { areaChart.vm.formatTooltipText(generateSeriesData('line')); }); @@ -120,18 +121,20 @@ describe('Area component', () => { }); it('formats tooltip content', () => { - expect(areaChart.vm.tooltip.content).toEqual([{ name: 'Core Usage', value: '5.556' }]); + const name = 'Core Usage'; + const value = '5.556'; + const seriesLabel = areaChart.find(GlChartSeriesLabel); + + expect(seriesLabel.vm.color).toBe(''); + expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true); + expect(areaChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]); expect( - shallowWrapperContainsSlotText( - areaChart.find(GlAreaChart), - 'tooltipContent', - 'Core Usage 5.556', - ), + shallowWrapperContainsSlotText(areaChart.find(GlAreaChart), 'tooltipContent', value), ).toBe(true); }); }); - describe('series is of scatter type', () => { + describe('when series is of scatter type', () => { beforeEach(() => { areaChart.vm.formatTooltipText(generateSeriesData('scatter')); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 454777fa912..ce2c6c43c0f 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import Dashboard from '~/monitoring/components/dashboard.vue'; +import { timeWindows } from '~/monitoring/constants'; import axios from '~/lib/utils/axios_utils'; import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data'; @@ -50,7 +51,7 @@ describe('Dashboard', () => { it('shows a getting started empty state when no metrics are present', () => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData, + propsData: { ...propsData, showTimeWindowDropdown: false }, }); expect(component.$el.querySelector('.prometheus-graphs')).toBe(null); @@ -66,7 +67,7 @@ describe('Dashboard', () => { it('shows up a loading state', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true }, + propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: false }, }); Vue.nextTick(() => { @@ -78,7 +79,12 @@ describe('Dashboard', () => { it('hides the legend when showLegend is false', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showLegend: false }, + propsData: { + ...propsData, + hasMetrics: true, + showLegend: false, + showTimeWindowDropdown: false, + }, }); setTimeout(() => { @@ -92,7 +98,12 @@ describe('Dashboard', () => { it('hides the group panels when showPanels is false', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); setTimeout(() => { @@ -106,7 +117,12 @@ describe('Dashboard', () => { it('renders the environments dropdown with a number of environments', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); component.store.storeEnvironmentsData(environmentData); @@ -124,7 +140,12 @@ describe('Dashboard', () => { it('hides the environments dropdown list when there is no environments', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); component.store.storeEnvironmentsData([]); @@ -142,7 +163,12 @@ describe('Dashboard', () => { it('renders the environments dropdown with a single is-active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); component.store.storeEnvironmentsData(environmentData); @@ -166,6 +192,7 @@ describe('Dashboard', () => { hasMetrics: true, showPanels: false, environmentsEndpoint: '', + showTimeWindowDropdown: false, }, }); @@ -176,6 +203,51 @@ describe('Dashboard', () => { done(); }); }); + + it('does not show the time window dropdown when the feature flag is not set', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, + }); + + setTimeout(() => { + const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown'); + + expect(timeWindowDropdown).toBeNull(); + + done(); + }); + }); + + it('renders the time window dropdown with a set of options', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: true, + }, + }); + const numberOfTimeWindows = Object.keys(timeWindows).length; + + setTimeout(() => { + const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown'); + const timeWindowDropdownEls = component.$el.querySelectorAll( + '.js-time-window-dropdown .dropdown-item', + ); + + expect(timeWindowDropdown).not.toBeNull(); + expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows); + + done(); + }); + }); }); describe('when the window resizes', () => { @@ -191,7 +263,12 @@ describe('Dashboard', () => { it('sets elWidth to page width when the sidebar is resized', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), - propsData: { ...propsData, hasMetrics: true, showPanels: false }, + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + showTimeWindowDropdown: false, + }, }); expect(component.elWidth).toEqual(0); diff --git a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js new file mode 100644 index 00000000000..29760f79c3c --- /dev/null +++ b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js @@ -0,0 +1,89 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; +import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue'; +import createStore from '~/related_merge_requests/store/index'; + +const FIXTURE_PATH = 'issues/related_merge_requests.json'; +const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests'; +const localVue = createLocalVue(); + +describe('RelatedMergeRequests', () => { + let wrapper; + let mock; + let mockData; + + beforeEach(done => { + loadFixtures(FIXTURE_PATH); + mockData = getJSONFixture(FIXTURE_PATH); + mock = new MockAdapter(axios); + mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 }); + + wrapper = mount(RelatedMergeRequests, { + localVue, + sync: false, + store: createStore(), + propsData: { + endpoint: API_ENDPOINT, + projectNamespace: 'gitlab-org', + projectPath: 'gitlab-ce', + }, + }); + + setTimeout(done); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('methods', () => { + describe('getAssignees', () => { + const assignees = [{ name: 'foo' }, { name: 'bar' }]; + + describe('when there is assignees array', () => { + it('should return assignees array', () => { + const mr = { assignees }; + + expect(wrapper.vm.getAssignees(mr)).toEqual(assignees); + }); + }); + + it('should return an array with single assingee', () => { + const mr = { assignee: assignees[0] }; + + expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]); + }); + + it('should return empty array when assignee is not set', () => { + expect(wrapper.vm.getAssignees({})).toEqual([]); + expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]); + }); + }); + }); + + describe('template', () => { + it('should render related merge request items', () => { + expect(wrapper.find('.js-items-count').text()).toEqual('2'); + expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2); + + const props = wrapper + .findAll(RelatedIssuableItem) + .at(1) + .props(); + const data = mockData[1]; + + expect(props.idKey).toEqual(data.id); + expect(props.pathIdSeparator).toEqual('!'); + expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status); + expect(props.assignees).toEqual([data.assignee]); + expect(props.isMergeRequest).toBe(true); + expect(props.confidential).toEqual(false); + expect(props.title).toEqual(data.title); + expect(props.state).toEqual(data.state); + expect(props.createdAt).toEqual(data.created_at); + }); + }); +}); diff --git a/spec/javascripts/related_merge_requests/store/actions_spec.js b/spec/javascripts/related_merge_requests/store/actions_spec.js new file mode 100644 index 00000000000..65e436fbb17 --- /dev/null +++ b/spec/javascripts/related_merge_requests/store/actions_spec.js @@ -0,0 +1,110 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import * as types from '~/related_merge_requests/store/mutation_types'; +import actionsModule, * as actions from '~/related_merge_requests/store/actions'; +import testAction from 'spec/helpers/vuex_action_helper'; + +describe('RelatedMergeRequest store actions', () => { + let state; + let flashSpy; + let mock; + + beforeEach(() => { + state = { + apiEndpoint: '/api/related_merge_requests', + }; + flashSpy = spyOnDependency(actionsModule, 'createFlash'); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('setInitialState', () => { + it('commits types.SET_INITIAL_STATE with given props', done => { + const props = { a: 1, b: 2 }; + + testAction( + actions.setInitialState, + props, + {}, + [{ type: types.SET_INITIAL_STATE, payload: props }], + [], + done, + ); + }); + }); + + describe('requestData', () => { + it('commits types.REQUEST_DATA', done => { + testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done); + }); + }); + + describe('receiveDataSuccess', () => { + it('commits types.RECEIVE_DATA_SUCCESS with data', done => { + const data = { a: 1, b: 2 }; + + testAction( + actions.receiveDataSuccess, + data, + {}, + [{ type: types.RECEIVE_DATA_SUCCESS, payload: data }], + [], + done, + ); + }); + }); + + describe('receiveDataError', () => { + it('commits types.RECEIVE_DATA_ERROR', done => { + testAction( + actions.receiveDataError, + null, + {}, + [{ type: types.RECEIVE_DATA_ERROR }], + [], + done, + ); + }); + }); + + describe('fetchMergeRequests', () => { + describe('for a successful request', () => { + it('should dispatch success action', done => { + const data = { a: 1 }; + mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 }); + + testAction( + actions.fetchMergeRequests, + null, + state, + [], + [{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }], + done, + ); + }); + }); + + describe('for a failing request', () => { + it('should dispatch error action', done => { + mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400); + + testAction( + actions.fetchMergeRequests, + null, + state, + [], + [{ type: 'requestData' }, { type: 'receiveDataError' }], + () => { + expect(flashSpy).toHaveBeenCalledTimes(1); + expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong')); + + done(); + }, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/related_merge_requests/store/mutations_spec.js b/spec/javascripts/related_merge_requests/store/mutations_spec.js new file mode 100644 index 00000000000..21b6e26376b --- /dev/null +++ b/spec/javascripts/related_merge_requests/store/mutations_spec.js @@ -0,0 +1,49 @@ +import mutations from '~/related_merge_requests/store/mutations'; +import * as types from '~/related_merge_requests/store/mutation_types'; + +describe('RelatedMergeRequests Store Mutations', () => { + describe('SET_INITIAL_STATE', () => { + it('should set initial state according to given data', () => { + const apiEndpoint = '/api'; + const state = {}; + + mutations[types.SET_INITIAL_STATE](state, { apiEndpoint }); + + expect(state.apiEndpoint).toEqual(apiEndpoint); + }); + }); + + describe('REQUEST_DATA', () => { + it('should set loading flag', () => { + const state = {}; + + mutations[types.REQUEST_DATA](state); + + expect(state.isFetchingMergeRequests).toEqual(true); + }); + }); + + describe('RECEIVE_DATA_SUCCESS', () => { + it('should set loading flag and data', () => { + const state = {}; + const mrs = [1, 2, 3]; + + mutations[types.RECEIVE_DATA_SUCCESS](state, { data: mrs, total: mrs.length }); + + expect(state.isFetchingMergeRequests).toEqual(false); + expect(state.mergeRequests).toEqual(mrs); + expect(state.totalCount).toEqual(mrs.length); + }); + }); + + describe('RECEIVE_DATA_ERROR', () => { + it('should set loading and error flags', () => { + const state = {}; + + mutations[types.RECEIVE_DATA_ERROR](state); + + expect(state.isFetchingMergeRequests).toEqual(false); + expect(state.hasErrorFetchingMergeRequests).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js deleted file mode 100644 index 85cfe71281f..00000000000 --- a/spec/javascripts/serverless/components/functions_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import Vue from 'vue'; - -import functionsComponent from '~/serverless/components/functions.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import ServerlessStore from '~/serverless/stores/serverless_store'; - -import { mockServerlessFunctions } from '../mock_data'; - -const createComponent = ( - functions, - installed = true, - loadingData = true, - hasFunctionData = true, -) => { - const component = Vue.extend(functionsComponent); - - return mountComponent(component, { - functions, - installed, - clustersPath: '/testClusterPath', - helpPath: '/helpPath', - loadingData, - hasFunctionData, - }); -}; - -describe('functionsComponent', () => { - it('should render empty state when Knative is not installed', () => { - const vm = createComponent({}, false); - - expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true); - expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( - 'Getting started with serverless', - ); - - vm.$destroy(); - }); - - it('should render a loading component', () => { - const vm = createComponent({}); - - expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null); - expect(vm.$el.querySelector('div.animation-container')).not.toBe(null); - }); - - it('should render empty state when there is no function data', () => { - const vm = createComponent({}, true, false, false); - - expect( - vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'), - ).toBe(true); - - expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( - 'No functions available', - ); - - vm.$destroy(); - }); - - it('should render the functions list', () => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctions); - const vm = createComponent(store.state.functions, true, false); - - expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null); - expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true); - }); -}); diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js deleted file mode 100644 index 72fd903d7d1..00000000000 --- a/spec/javascripts/serverless/stores/serverless_store_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import ServerlessStore from '~/serverless/stores/serverless_store'; -import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; - -describe('Serverless Functions Store', () => { - let store; - - beforeEach(() => { - store = new ServerlessStore(false, '/cluster_path', 'help_path'); - }); - - describe('#updateFunctionsFromServer', () => { - it('should pass an empty hash object', () => { - store.updateFunctionsFromServer(); - - expect(store.state.functions).toEqual({}); - }); - - it('should group functions to one global environment', () => { - const mockServerlessData = mockServerlessFunctions; - store.updateFunctionsFromServer(mockServerlessData); - - expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); - expect(store.state.functions['*'].length).toEqual(2); - }); - - it('should group functions to multiple environments', () => { - const mockServerlessData = mockServerlessFunctionsDiffEnv; - store.updateFunctionsFromServer(mockServerlessData); - - expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); - expect(store.state.functions['*'].length).toEqual(1); - expect(store.state.functions.test.length).toEqual(1); - expect(store.state.functions.test[0].name).toEqual('testfunc2'); - }); - }); -}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 235a17d13b0..87ef0885d8c 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -69,7 +69,7 @@ window.gl = window.gl || {}; window.gl.TEST_HOST = TEST_HOST; window.gon = window.gon || {}; window.gon.test_env = true; -window.gon.ee = process.env.EE; +window.gon.ee = process.env.IS_GITLAB_EE; gon.relative_url_root = ''; let hasUnhandledPromiseRejections = false; @@ -124,7 +124,7 @@ const axiosDefaultAdapter = getDefaultAdapter(); // render all of our tests const testContexts = [require.context('spec', true, /_spec$/)]; -if (process.env.EE) { +if (process.env.IS_GITLAB_EE) { testContexts.push(require.context('ee_spec', true, /_spec$/)); } @@ -213,7 +213,7 @@ if (process.env.BABEL_ENV === 'coverage') { describe('Uncovered files', function() { const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)]; - if (process.env.EE) { + if (process.env.IS_GITLAB_EE) { sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/)); } diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js index d1fd899c1a8..7da69e3fa84 100644 --- a/spec/javascripts/vue_shared/components/file_row_spec.js +++ b/spec/javascripts/vue_shared/components/file_row_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import FileRow from '~/vue_shared/components/file_row.vue'; +import FileRowExtra from '~/ide/components/file_row_extra.vue'; import { file } from 'spec/ide/helpers'; import mountComponent from '../../helpers/vue_mount_component_helper'; @@ -16,6 +17,10 @@ describe('File row component', () => { vm.$destroy(); }); + const findNewDropdown = () => vm.$el.querySelector('.ide-new-btn .dropdown'); + const findNewDropdownButton = () => vm.$el.querySelector('.ide-new-btn .dropdown button'); + const findFileRow = () => vm.$el.querySelector('.file-row'); + it('renders name', () => { createComponent({ file: file('t4'), @@ -84,4 +89,59 @@ describe('File row component', () => { expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null); }); + + describe('new dropdown', () => { + beforeEach(() => { + createComponent({ + file: file('t5'), + level: 1, + extraComponent: FileRowExtra, + }); + }); + + it('renders in extra component', () => { + expect(findNewDropdown()).not.toBe(null); + }); + + it('is hidden at start', () => { + expect(findNewDropdown()).not.toHaveClass('show'); + }); + + it('is opened when button is clicked', done => { + expect(vm.dropdownOpen).toBe(false); + findNewDropdownButton().dispatchEvent(new Event('click')); + + vm.$nextTick() + .then(() => { + expect(vm.dropdownOpen).toBe(true); + expect(findNewDropdown()).toHaveClass('show'); + }) + .then(done) + .catch(done.fail); + }); + + describe('when opened', () => { + beforeEach(() => { + vm.dropdownOpen = true; + }); + + it('stays open when button triggers mouseout', () => { + findNewDropdownButton().dispatchEvent(new Event('mouseout')); + + expect(vm.dropdownOpen).toBe(true); + }); + + it('stays open when button triggers mouseleave', () => { + findNewDropdownButton().dispatchEvent(new Event('mouseleave')); + + expect(vm.dropdownOpen).toBe(true); + }); + + it('closes when row triggers mouseleave', () => { + findFileRow().dispatchEvent(new Event('mouseleave')); + + expect(vm.dropdownOpen).toBe(false); + }); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js new file mode 100644 index 00000000000..b95183747bb --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js @@ -0,0 +1,110 @@ +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +const localVue = createLocalVue(); + +describe('ProjectListItem component', () => { + const Component = localVue.extend(ProjectListItem); + let wrapper; + let vm; + let options; + loadJSONFixtures('projects.json'); + const project = getJSONFixture('projects.json')[0]; + + beforeEach(() => { + options = { + propsData: { + project, + selected: false, + }, + sync: false, + localVue, + }; + }); + + afterEach(() => { + wrapper.vm.$destroy(); + }); + + it('does not render a check mark icon if selected === false', () => { + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true); + }); + + it('renders a check mark icon if selected === true', () => { + options.propsData.selected = true; + + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true); + }); + + it(`emits a "clicked" event when clicked`, () => { + wrapper = shallowMount(Component, options); + ({ vm } = wrapper); + + spyOn(vm, '$emit'); + wrapper.vm.onClick(); + + expect(wrapper.vm.$emit).toHaveBeenCalledWith('click'); + }); + + it(`renders the project avatar`, () => { + wrapper = shallowMount(Component, options); + + expect(wrapper.contains('.js-project-avatar')).toBe(true); + }); + + it(`renders a simple namespace name with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b'; + + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + + expect(renderedNamespace).toBe('a /'); + }); + + it(`renders a properly truncated namespace with a trailing slash`, () => { + options.propsData.project.name_with_namespace = 'a / b / c / d / e / f'; + + wrapper = shallowMount(Component, options); + const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text()); + + expect(renderedNamespace).toBe('a / ... / e /'); + }); + + it(`renders the project name`, () => { + options.propsData.project.name = 'my-test-project'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').text()); + + expect(renderedName).toBe('my-test-project'); + }); + + it(`renders the project name with highlighting in the case of a search query match`, () => { + options.propsData.project.name = 'my-test-project'; + options.propsData.matcher = 'pro'; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject'; + + expect(renderedName).toContain(expected); + }); + + it('prevents search query and project name XSS', () => { + const alertSpy = spyOn(window, 'alert'); + options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject"; + options.propsData.matcher = "pro<script>alert('XSS');</script>"; + + wrapper = shallowMount(Component, options); + const renderedName = trimText(wrapper.find('.js-project-name').html()); + const expected = 'my-xss-project'; + + expect(renderedName).toContain(expected); + expect(alertSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js new file mode 100644 index 00000000000..ba9ec8f2f19 --- /dev/null +++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js @@ -0,0 +1,132 @@ +import Vue from 'vue'; +import _ from 'underscore'; +import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue'; +import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { shallowMount } from '@vue/test-utils'; +import { trimText } from 'spec/helpers/vue_component_helper'; + +describe('ProjectSelector component', () => { + let wrapper; + let vm; + loadJSONFixtures('projects.json'); + const allProjects = getJSONFixture('projects.json'); + const searchResults = allProjects.slice(0, 5); + let selected = []; + selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8)); + + beforeEach(() => { + jasmine.clock().install(); + + wrapper = shallowMount(Vue.extend(ProjectSelector), { + propsData: { + projectSearchResults: searchResults, + selectedProjects: selected, + showNoResultsMessage: false, + showMinimumSearchQueryMessage: false, + showLoadingIndicator: false, + showSearchErrorMessage: false, + }, + attachToDocument: true, + }); + + ({ vm } = wrapper); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + vm.$destroy(); + }); + + it('renders the search results', () => { + expect(wrapper.findAll('.js-project-list-item').length).toBe(5); + }); + + it(`triggers a (debounced) search when the search input value changes`, () => { + spyOn(vm, '$emit'); + const query = 'my test query!'; + const searchInput = wrapper.find('.js-project-selector-input'); + searchInput.setValue(query); + searchInput.trigger('input'); + + expect(vm.$emit).not.toHaveBeenCalledWith(); + jasmine.clock().tick(501); + + expect(vm.$emit).toHaveBeenCalledWith('searched', query); + }); + + it(`debounces the search input`, () => { + spyOn(vm, '$emit'); + const searchInput = wrapper.find('.js-project-selector-input'); + + const updateSearchQuery = (count = 0) => { + if (count === 10) { + jasmine.clock().tick(101); + + expect(vm.$emit).toHaveBeenCalledTimes(1); + expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`); + } else { + searchInput.setValue(`search query #${count}`); + searchInput.trigger('input'); + + jasmine.clock().tick(400); + updateSearchQuery(count + 1); + } + }; + + updateSearchQuery(); + }); + + it(`includes a placeholder in the search box`, () => { + expect(wrapper.find('.js-project-selector-input').attributes('placeholder')).toBe( + 'Search your projects', + ); + }); + + it(`triggers a "projectClicked" event when a project is clicked`, () => { + spyOn(vm, '$emit'); + wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults)); + + expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults)); + }); + + it(`shows a "no results" message if showNoResultsMessage === true`, () => { + wrapper.setProps({ showNoResultsMessage: true }); + + expect(wrapper.contains('.js-no-results-message')).toBe(true); + + const noResultsEl = wrapper.find('.js-no-results-message'); + + expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search'); + }); + + it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, () => { + wrapper.setProps({ showMinimumSearchQueryMessage: true }); + + expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true); + + const minimumSearchEl = wrapper.find('.js-minimum-search-query-message'); + + expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search'); + }); + + it(`shows a error message if showSearchErrorMessage === true`, () => { + wrapper.setProps({ showSearchErrorMessage: true }); + + expect(wrapper.contains('.js-search-error-message')).toBe(true); + + const errorMessageEl = wrapper.find('.js-search-error-message'); + + expect(trimText(errorMessageEl.text())).toEqual( + 'Something went wrong, unable to search projects', + ); + }); + + it(`focuses the input element when the focusSearchInput() method is called`, () => { + const input = wrapper.find('.js-project-selector-input'); + + expect(document.activeElement).not.toBe(input.element); + vm.focusSearchInput(); + + expect(document.activeElement).toBe(input.element); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js index 5cf6afebd7e..0689fc1cf1f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => { }); const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more'); + expect(vmMoreLabels.dropdownToggleText).toBe( + `Foo Label +${mockMoreLabels.labels.length - 1} more`, + ); vmMoreLabels.$destroy(); }); it('returns first label name when `labels` prop has only one item present', () => { - expect(vm.dropdownToggleText).toBe('Foo Label'); + const singleLabel = Object.assign({}, componentConfig, { + labels: [mockLabels[0]], + }); + const vmSingleLabel = createComponent(singleLabel); + + expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title); + + vmSingleLabel.$destroy(); }); }); }); @@ -73,7 +82,7 @@ describe('DropdownButtonComponent', () => { const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); expect(dropdownToggleTextEl).not.toBeNull(); - expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label'); + expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more'); }); it('renders dropdown button icon', () => { diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js index 804b33422bd..cb49fa31d20 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -35,9 +35,12 @@ describe('DropdownValueCollapsedComponent', () => { }); it('returns labels names separated by coma when `labels` prop has more than one item', () => { - const vmMoreLabels = createComponent(mockLabels.concat(mockLabels)); + const labels = mockLabels.concat(mockLabels); + const vmMoreLabels = createComponent(labels); - expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label'); + const expectedText = labels.map(label => label.title).join(', '); + + expect(vmMoreLabels.labelsList).toBe(expectedText); vmMoreLabels.$destroy(); }); @@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => { const vmMoreLabels = createComponent(mockMoreLabels); - expect(vmMoreLabels.labelsList).toBe( - 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more', - ); + const expectedText = `${mockMoreLabels + .slice(0, 5) + .map(label => label.title) + .join(', ')}, and ${mockMoreLabels.length - 5} more`; + + expect(vmMoreLabels.labelsList).toBe(expectedText); vmMoreLabels.$destroy(); }); it('returns first label name when `labels` prop has only one item present', () => { - expect(vm.labelsList).toBe('Foo Label'); + const text = mockLabels.map(label => label.title).join(', '); + + expect(vm.labelsList).toBe(text); }); }); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 3fff781594f..35a9c300953 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import $ from 'jquery'; import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; @@ -15,6 +16,7 @@ const createComponent = ( return mountComponent(Component, { labels, labelFilterBasePath, + enableScopedLabels: true, }); }; @@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => { expect(styleObj.backgroundColor).toBe(label.color); }); }); + + describe('scopedLabelsDescription', () => { + it('returns html for tooltip', () => { + const html = vm.scopedLabelsDescription(mockLabels[1]); + const $el = $.parseHTML(html); + + expect($el[0]).toHaveClass('scoped-label-tooltip-title'); + expect($el[2].textContent).toEqual(mockLabels[1].description); + }); + }); + + describe('showScopedLabels', () => { + it('returns true if the label is scoped label', () => { + expect(vm.showScopedLabels(mockLabels[1])).toBe(true); + }); + + it('returns false when label is a regular label', () => { + expect(vm.showScopedLabels(mockLabels[0])).toBe(false); + }); + }); }); describe('template', () => { @@ -91,15 +113,25 @@ describe('DropdownValueComponent', () => { ); }); - it('renders label element with tooltip and styles based on label details', () => { + it('renders label element and styles based on label details', () => { const labelEl = vm.$el.querySelector('a span.badge.color-label'); expect(labelEl).not.toBeNull(); - expect(labelEl.dataset.placement).toBe('bottom'); - expect(labelEl.dataset.container).toBe('body'); - expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description); expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);'); expect(labelEl.innerText.trim()).toBe(mockLabels[0].title); }); + + describe('label is of scoped-label type', () => { + it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => { + expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull(); + }); + + it('renders anchor tag containing question icon', () => { + const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label'); + + expect(anchor).not.toBeNull(); + expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull(); + }); + }); }); }); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js index 3fcb91b6f5e..70025f041a7 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js @@ -6,6 +6,13 @@ export const mockLabels = [ color: '#BADA55', text_color: '#FFFFFF', }, + { + id: 27, + title: 'Foo::Bar', + description: 'Foobar', + color: '#0033CC', + text_color: '#FFFFFF', + }, ]; export const mockSuggestedColors = [ diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb index 9945d598a14..aed86b21cb7 100644 --- a/spec/lib/api/helpers/custom_validators_spec.rb +++ b/spec/lib/api/helpers/custom_validators_spec.rb @@ -21,7 +21,7 @@ describe API::Helpers::CustomValidators do end context 'invalid parameters' do - it 'should raise a validation error' do + it 'raises a validation error' do expect_validation_error({ 'test' => 'some_value' }) end end @@ -44,7 +44,7 @@ describe API::Helpers::CustomValidators do end context 'invalid parameters' do - it 'should raise a validation error' do + it 'raises a validation error' do expect_validation_error({ 'test' => 'some_other_string' }) end end @@ -67,7 +67,7 @@ describe API::Helpers::CustomValidators do end context 'invalid parameters' do - it 'should raise a validation error' do + it 'raises a validation error' do expect_validation_error({ 'test' => 'some_other_string' }) end end diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index b645e49bd43..5b3f679084e 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -13,6 +13,6 @@ describe Banzai::Filter::BlockquoteFenceFilter do end it 'allows trailing whitespace on blockquote fence lines' do - expect(filter(">>> \ntest\n>>> ")).to eq("> test") + expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n") end end diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index 8235c411eb7..6f7acfe7072 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Banzai::Filter::PlantumlFilter do include FilterSpecHelper - it 'should replace plantuml pre tag with img tag' do + it 'replaces plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' @@ -12,7 +12,7 @@ describe Banzai::Filter::PlantumlFilter do expect(doc.to_s).to eq output end - it 'should not replace plantuml pre tag with img tag if disabled' do + it 'does not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' @@ -21,7 +21,7 @@ describe Banzai::Filter::PlantumlFilter do expect(doc.to_s).to eq output end - it 'should not replace plantuml pre tag with img tag if url is invalid' do + it 'does not replace plantuml pre tag with img tag if url is invalid' do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb index 494c0561975..b9ffe895bf0 100644 --- a/spec/lib/forever_spec.rb +++ b/spec/lib/forever_spec.rb @@ -5,7 +5,7 @@ describe Forever do subject { described_class.date } context 'when using PostgreSQL' do - it 'should return Postgresql future date' do + it 'returns Postgresql future date' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) expect(subject).to eq(described_class::POSTGRESQL_DATE) @@ -13,7 +13,7 @@ describe Forever do end context 'when using MySQL' do - it 'should return MySQL future date' do + it 'returns MySQL future date' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) expect(subject).to eq(described_class::MYSQL_DATE) diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb index 812e0cc6947..128e118ac17 100644 --- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : end shared_examples 'consistent kubernetes namespace attributes' do - it 'should populate namespace and service account information' do + it 'populates namespace and service account information' do migration.perform clusters_with_namespace.each do |cluster| @@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do let(:cluster_projects) { cluster_projects_table.all } - it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do + it 'creates a Clusters::KubernetesNamespace per Clusters::Project' do expect do migration.perform end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects_table.count) @@ -57,7 +57,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : create_kubernetes_namespace(clusters_table.all) end - it 'should not create any Clusters::KubernetesNamespace' do + it 'does not create any Clusters::KubernetesNamespace' do expect do migration.perform end.not_to change(Clusters::KubernetesNamespace, :count) @@ -78,7 +78,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count) end - it 'should not modify clusters with Clusters::KubernetesNamespace' do + it 'does not modify clusters with Clusters::KubernetesNamespace' do migration.perform with_kubernetes_namespace.each do |cluster| diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index 8582af96199..0e73c8c59c9 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch migration.perform(1, 3) end - it 'it creates the fork network' do + it 'creates the fork network' do expect(fork_network1).not_to be_nil expect(fork_network2).not_to be_nil end diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index fa39b32d7ab..dd536a241bd 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::Ci::Config::External::File::Base do context 'when a location is present' do let(:location) { 'some-location' } - it 'should return true' do + it 'returns true' do expect(subject).to be_matching end end @@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::File::Base do context 'with a location is missing' do let(:location) { nil } - it 'should return false' do + it 'returns false' do expect(subject).not_to be_matching end end diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index dc14b07287e..9451db9522a 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'when a local is specified' do let(:params) { { local: 'file' } } - it 'should return true' do + it 'returns true' do expect(local_file).to be_matching end end @@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'with a missing local' do let(:params) { { local: nil } } - it 'should return false' do + it 'returns false' do expect(local_file).not_to be_matching end end @@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'with a missing local key' do let(:params) { {} } - it 'should return false' do + it 'returns false' do expect(local_file).not_to be_matching end end @@ -45,7 +45,7 @@ describe Gitlab::Ci::Config::External::File::Local do allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("image: 'ruby2:2'") end - it 'should return true' do + it 'returns true' do expect(local_file.valid?).to be_truthy end end @@ -53,7 +53,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'when is not a valid local path' do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } - it 'should return false' do + it 'returns false' do expect(local_file.valid?).to be_falsy end end @@ -61,7 +61,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'when is not a yaml file' do let(:location) { '/config/application.rb' } - it 'should return false' do + it 'returns false' do expect(local_file.valid?).to be_falsy end end @@ -84,7 +84,7 @@ describe Gitlab::Ci::Config::External::File::Local do allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return(local_file_content) end - it 'should return the content of the file' do + it 'returns the content of the file' do expect(local_file.content).to eq(local_file_content) end end @@ -92,7 +92,7 @@ describe Gitlab::Ci::Config::External::File::Local do context 'with an invalid file' do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } - it 'should be nil' do + it 'is nil' do expect(local_file.content).to be_nil end end @@ -101,7 +101,7 @@ describe Gitlab::Ci::Config::External::File::Local do describe '#error_message' do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } - it 'should return an error message' do + it 'returns an error message' do expect(local_file.error_message).to eq("Local file `#{location}` does not exist!") end end diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 6e89bb1b30f..4acb4f324d3 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::Ci::Config::External::File::Project do context 'when a file and project is specified' do let(:params) { { file: 'file.yml', project: 'project' } } - it 'should return true' do + it 'returns true' do expect(project_file).to be_matching end end @@ -27,7 +27,7 @@ describe Gitlab::Ci::Config::External::File::Project do context 'with only file is specified' do let(:params) { { file: 'file.yml' } } - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_matching end end @@ -35,7 +35,7 @@ describe Gitlab::Ci::Config::External::File::Project do context 'with only project is specified' do let(:params) { { project: 'project' } } - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_matching end end @@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::External::File::Project do context 'with a missing local key' do let(:params) { {} } - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_matching end end @@ -61,14 +61,14 @@ describe Gitlab::Ci::Config::External::File::Project do stub_project_blob(root_ref_sha, '/file.yml') { 'image: ruby:2.1' } end - it 'should return true' do + it 'returns true' do expect(project_file).to be_valid end context 'when user does not have permission to access file' do let(:context_user) { create(:user) } - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_valid expect(project_file.error_message).to include("Project `#{project.full_path}` not found or access denied!") end @@ -86,7 +86,7 @@ describe Gitlab::Ci::Config::External::File::Project do stub_project_blob(ref_sha, '/file.yml') { 'image: ruby:2.1' } end - it 'should return true' do + it 'returns true' do expect(project_file).to be_valid end end @@ -102,7 +102,7 @@ describe Gitlab::Ci::Config::External::File::Project do stub_project_blob(root_ref_sha, '/file.yml') { '' } end - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_valid expect(project_file.error_message).to include("Project `#{project.full_path}` file `/file.yml` is empty!") end @@ -113,7 +113,7 @@ describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, ref: 'I-Do-Not-Exist', file: '/file.yml' } end - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_valid expect(project_file.error_message).to include("Project `#{project.full_path}` reference `I-Do-Not-Exist` does not exist!") end @@ -124,7 +124,7 @@ describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, file: '/invalid-file.yml' } end - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_valid expect(project_file.error_message).to include("Project `#{project.full_path}` file `/invalid-file.yml` does not exist!") end @@ -135,7 +135,7 @@ describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, file: '/invalid-file' } end - it 'should return false' do + it 'returns false' do expect(project_file).not_to be_valid expect(project_file.error_message).to include('Included file `/invalid-file` does not have YAML extension!') end diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index c5b32c29759..d8a61618e77 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when a remote is specified' do let(:params) { { remote: 'http://remote' } } - it 'should return true' do + it 'returns true' do expect(remote_file).to be_matching end end @@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with a missing remote' do let(:params) { { remote: nil } } - it 'should return false' do + it 'returns false' do expect(remote_file).not_to be_matching end end @@ -37,7 +37,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with a missing remote key' do let(:params) { {} } - it 'should return false' do + it 'returns false' do expect(remote_file).not_to be_matching end end @@ -49,7 +49,7 @@ describe Gitlab::Ci::Config::External::File::Remote do WebMock.stub_request(:get, location).to_return(body: remote_file_content) end - it 'should return true' do + it 'returns true' do expect(remote_file.valid?).to be_truthy end end @@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with an irregular url' do let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } - it 'should return false' do + it 'returns false' do expect(remote_file.valid?).to be_falsy end end @@ -67,7 +67,7 @@ describe Gitlab::Ci::Config::External::File::Remote do allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) end - it 'should be falsy' do + it 'is falsy' do expect(remote_file.valid?).to be_falsy end end @@ -75,7 +75,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when is not a yaml file' do let(:location) { 'https://asdasdasdaj48ggerexample.com' } - it 'should be falsy' do + it 'is falsy' do expect(remote_file.valid?).to be_falsy end end @@ -83,7 +83,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with an internal url' do let(:location) { 'http://localhost:8080' } - it 'should be falsy' do + it 'is falsy' do expect(remote_file.valid?).to be_falsy end end @@ -95,7 +95,7 @@ describe Gitlab::Ci::Config::External::File::Remote do WebMock.stub_request(:get, location).to_return(body: remote_file_content) end - it 'should return the content of the file' do + it 'returns the content of the file' do expect(remote_file.content).to eql(remote_file_content) end end @@ -105,7 +105,7 @@ describe Gitlab::Ci::Config::External::File::Remote do allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) end - it 'should be falsy' do + it 'is falsy' do expect(remote_file.content).to be_falsy end end @@ -117,7 +117,7 @@ describe Gitlab::Ci::Config::External::File::Remote do WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error')) end - it 'should be nil' do + it 'is nil' do expect(remote_file.content).to be_nil end end @@ -125,7 +125,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'with an internal url' do let(:location) { 'http://localhost:8080' } - it 'should be nil' do + it 'is nil' do expect(remote_file.content).to be_nil end end @@ -147,7 +147,7 @@ describe Gitlab::Ci::Config::External::File::Remote do WebMock.stub_request(:get, location).to_timeout end - it 'should returns error message about a timeout' do + it 'returns error message about a timeout' do expect(subject).to match /could not be fetched because of a timeout error!/ end end @@ -157,7 +157,7 @@ describe Gitlab::Ci::Config::External::File::Remote do WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error) end - it 'should returns error message about a HTTP error' do + it 'returns error message about a HTTP error' do expect(subject).to match /could not be fetched because of HTTP error!/ end end @@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::External::File::Remote do WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404) end - it 'should returns error message about a timeout' do + it 'returns error message about a timeout' do expect(subject).to match /could not be fetched because of HTTP code `404` error!/ end end @@ -175,7 +175,7 @@ describe Gitlab::Ci::Config::External::File::Remote do context 'when the URL is blocked' do let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' } - it 'should include details about blocked URL' do + it 'includes details about blocked URL' do expect(subject).to eq "Remote file could not be fetched because URL '#{location}' " \ 'is blocked: Requests to localhost are not allowed!' end diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb index 8ecaf4800f8..1609b8fd66b 100644 --- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::Ci::Config::External::File::Template do context 'when a template is specified' do let(:params) { { template: 'some-template' } } - it 'should return true' do + it 'returns true' do expect(template_file).to be_matching end end @@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::External::File::Template do context 'with a missing template' do let(:params) { { template: nil } } - it 'should return false' do + it 'returns false' do expect(template_file).not_to be_matching end end @@ -32,7 +32,7 @@ describe Gitlab::Ci::Config::External::File::Template do context 'with a missing template key' do let(:params) { {} } - it 'should return false' do + it 'returns false' do expect(template_file).not_to be_matching end end @@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::External::File::Template do context 'when is a valid template name' do let(:template) { 'Auto-DevOps.gitlab-ci.yml' } - it 'should return true' do + it 'returns true' do expect(template_file).to be_valid end end @@ -50,7 +50,7 @@ describe Gitlab::Ci::Config::External::File::Template do context 'with invalid template name' do let(:template) { 'Template.yml' } - it 'should return false' do + it 'returns false' do expect(template_file).not_to be_valid expect(template_file.error_message).to include('Template file `Template.yml` is not a valid location!') end @@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::External::File::Template do context 'with a non-existing template' do let(:template) { 'I-Do-Not-Have-This-Template.gitlab-ci.yml' } - it 'should return false' do + it 'returns false' do expect(template_file).not_to be_valid expect(template_file.error_message).to include('Included file `I-Do-Not-Have-This-Template.gitlab-ci.yml` is empty or does not exist!') end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 3f6f6d7c5d9..e94bb44f990 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::External::Processor do context 'when no external files defined' do let(:values) { { image: 'ruby:2.2' } } - it 'should return the same values' do + it 'returns the same values' do expect(processor.perform).to eq(values) end end @@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::External::Processor do context 'when an invalid local file is defined' do let(:values) { { include: '/lib/gitlab/ci/templates/non-existent-file.yml', image: 'ruby:2.2' } } - it 'should raise an error' do + it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, "Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!" @@ -45,7 +45,7 @@ describe Gitlab::Ci::Config::External::Processor do WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error')) end - it 'should raise an error' do + it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, "Remote file `#{remote_file}` could not be fetched because of a socket error!" @@ -78,12 +78,12 @@ describe Gitlab::Ci::Config::External::Processor do WebMock.stub_request(:get, remote_file).to_return(body: external_file_content) end - it 'should append the file to the values' do + it 'appends the file to the values' do output = processor.perform expect(output.keys).to match_array([:image, :before_script, :rspec, :rubocop]) end - it "should remove the 'include' keyword" do + it "removes the 'include' keyword" do expect(processor.perform[:include]).to be_nil end end @@ -105,12 +105,12 @@ describe Gitlab::Ci::Config::External::Processor do .to receive(:fetch_local_content).and_return(local_file_content) end - it 'should append the file to the values' do + it 'appends the file to the values' do output = processor.perform expect(output.keys).to match_array([:image, :before_script]) end - it "should remove the 'include' keyword" do + it "removes the 'include' keyword" do expect(processor.perform[:include]).to be_nil end end @@ -148,11 +148,11 @@ describe Gitlab::Ci::Config::External::Processor do WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) end - it 'should append the files to the values' do + it 'appends the files to the values' do expect(processor.perform.keys).to match_array([:image, :stages, :before_script, :rspec]) end - it "should remove the 'include' keyword" do + it "removes the 'include' keyword" do expect(processor.perform[:include]).to be_nil end end @@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::External::Processor do .to receive(:fetch_local_content).and_return(local_file_content) end - it 'should raise an error' do + it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, "Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!" @@ -190,7 +190,7 @@ describe Gitlab::Ci::Config::External::Processor do HEREDOC end - it 'should take precedence' do + it 'takes precedence' do WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) expect(processor.perform[:image]).to eq('ruby:2.2') end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 00b2753c5fc..fd2a29e4ddb 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -225,7 +225,7 @@ describe Gitlab::Ci::Config do end context "when gitlab_ci_yml has valid 'include' defined" do - it 'should return a composed hash' do + it 'returns a composed hash' do before_script_values = [ "apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v", "which ruby", @@ -316,7 +316,7 @@ describe Gitlab::Ci::Config do HEREDOC end - it 'should take precedence' do + it 'takes precedence' do expect(config.to_hash).to eq({ image: 'ruby:2.2' }) end end @@ -341,7 +341,7 @@ describe Gitlab::Ci::Config do HEREDOC end - it 'should merge the variables dictionaries' do + it 'merges the variables dictionaries' do expect(config.to_hash).to eq({ variables: { A: 'alpha', B: 'beta', C: 'gamma', D: 'delta' } }) end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb index dc13cae961c..c7f4fc98ca3 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do step.perform! end - it 'should break the chain' do + it 'breaks the chain' do expect(step.break?).to be true end @@ -37,11 +37,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do step.perform! end - it 'should not break the chain' do + it 'does not break the chain' do expect(step.break?).to be false end - it 'should not skip a pipeline chain' do + it 'does not skip a pipeline chain' do expect(pipeline.reload).not_to be_skipped end end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index b379b08ad62..b6231510b91 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -123,6 +123,35 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.action_path).to include 'retry' end end + + context 'when build has unmet prerequisites' do + let(:build) { create(:ci_build, :prerequisite_failure) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Build::Retryable, + Gitlab::Ci::Status::Build::FailedUnmetPrerequisites] + end + + it 'fabricates a failed with unmet prerequisites build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::FailedUnmetPrerequisites + end + + it 'fabricates status with correct details' do + expect(status.text).to eq 'failed' + expect(status.icon).to eq 'status_failed' + expect(status.favicon).to eq 'favicon_status_failed' + expect(status.label).to eq 'failed' + expect(status).to have_details + expect(status).to have_action + expect(status.action_title).to include 'Retry' + expect(status.action_path).to include 'retry' + end + end end context 'when build is a canceled' do diff --git a/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb new file mode 100644 index 00000000000..a4854bdc6b9 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Status::Build::FailedUnmetPrerequisites do + describe '#illustration' do + subject { described_class.new(double).illustration } + + it { is_expected.to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + let(:build) { create(:ci_build, :created) } + + subject { described_class.matches?(build, double) } + + context 'when build has not failed' do + it { is_expected.to be_falsey } + end + + context 'when build has failed' do + before do + build.drop!(failure_reason) + end + + context 'with unmet prerequisites' do + let(:failure_reason) { :unmet_prerequisites } + + it { is_expected.to be_truthy } + end + + context 'with a different error' do + let(:failure_reason) { :runner_system_failure } + + it { is_expected.to be_falsey } + end + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 63a0d54dcfc..8b39c4e4dd0 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -615,6 +615,14 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } context "when validating a ci config file with no project context" do + context "when a single string is provided" do + let(:include_content) { "/local.gitlab-ci.yml" } + + it "does not return any error" do + expect { subject }.not_to raise_error + end + end + context "when an array is provided" do let(:include_content) { ["/local.gitlab-ci.yml"] } diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index b7924302014..51e5bdc6307 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -150,13 +150,13 @@ describe Gitlab::ContributionsCalendar do end describe '#starting_year' do - it "should be the start of last year" do + it "is the start of last year" do expect(calendar.starting_year).to eq(last_year.year) end end describe '#starting_month' do - it "should be the start of this month" do + it "is the start of this month" do expect(calendar.starting_month).to eq(today.month) end end diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb index 1d31f96159c..ddd54a669a3 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do describe '#rename_wildcard_paths' do it_behaves_like 'renames child namespaces' - it 'should rename projects' do + it 'renames projects' do rename_projects = double expect(described_class::RenameProjects) .to receive(:new).with(['the-path'], subject) @@ -40,7 +40,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do end describe '#rename_root_paths' do - it 'should rename namespaces' do + it 'renames namespaces' do rename_namespaces = double expect(described_class::RenameNamespaces) .to receive(:new).with(['the-path'], subject) diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 256166dbad3..0697594c725 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do let(:diffable) { merge_request.merge_request_diff } end - it 'it uses a different cache key if diff line keys change' do + it 'uses a different cache key if diff line keys change' do mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil) key = mr_diff.cache_key diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index f69cb502ca6..a7cb0bb2a87 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -19,6 +19,24 @@ describe Gitlab::EtagCaching::Router do expect(result.name).to eq 'issue_title' end + it 'matches with a project name that includes a suffix of create' do + result = described_class.match( + '/group/test-create/issues/123/realtime_changes' + ) + + expect(result).to be_present + expect(result.name).to eq 'issue_title' + end + + it 'matches with a project name that includes a prefix of create' do + result = described_class.match( + '/group/create-test/issues/123/realtime_changes' + ) + + expect(result).to be_present + expect(result.name).to eq 'issue_title' + end + it 'matches project pipelines endpoint' do result = described_class.match( '/my-group/my-project/pipelines.json' diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 4a4ac833e39..507bf222810 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -113,13 +113,13 @@ describe Gitlab::Git::Commit, :seed_helper do context 'Class methods' do shared_examples '.find' do - it "should return first head commit if without params" do + it "returns first head commit if without params" do expect(described_class.last(repository).id).to eq( rugged_repo.head.target.oid ) end - it "should return valid commit" do + it "returns valid commit" do expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit end @@ -127,21 +127,21 @@ describe Gitlab::Git::Commit, :seed_helper do expect(described_class.find(repository, SeedRepo::Commit::ID).parent_ids).to be_an(Array) end - it "should return valid commit for tag" do + it "returns valid commit for tag" do expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') end - it "should return nil for non-commit ids" do + it "returns nil for non-commit ids" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") expect(described_class.find(repository, blob.id)).to be_nil end - it "should return nil for parent of non-commit object" do + it "returns nil for parent of non-commit object" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") expect(described_class.find(repository, "#{blob.id}^")).to be_nil end - it "should return nil for nonexisting ids" do + it "returns nil for nonexisting ids" do expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil end @@ -328,7 +328,7 @@ describe Gitlab::Git::Commit, :seed_helper do end describe '.find_all' do - it 'should return a return a collection of commits' do + it 'returns a return a collection of commits' do commits = described_class.find_all(repository) expect(commits).to all( be_a_kind_of(described_class) ) diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 1d22329b670..9ab669ad488 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -182,7 +182,7 @@ EOT context "without default options" do let(:filtered_options) { described_class.filter_diff_options(options) } - it "should filter invalid options" do + it "filters invalid options" do expect(filtered_options).not_to have_key(:invalid_opt) end end @@ -193,16 +193,16 @@ EOT described_class.filter_diff_options(options, default_options) end - it "should filter invalid options" do + it "filters invalid options" do expect(filtered_options).not_to have_key(:invalid_opt) expect(filtered_options).not_to have_key(:bad_opt) end - it "should merge with default options" do + it "merges with default options" do expect(filtered_options).to have_key(:ignore_whitespace_change) end - it "should override default options" do + it "overrides default options" do expect(filtered_options).to have_key(:max_files) expect(filtered_options[:max_files]).to eq(100) end diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb index 6fd2b33486b..de81dcd227d 100644 --- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::GitmodulesParser do - it 'should parse a .gitmodules file correctly' do + it 'parses a .gitmodules file correctly' do data = <<~GITMODULES [submodule "vendor/libgit2"] path = vendor/libgit2 diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index fc8f590068a..fdb43d1221a 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -450,20 +450,20 @@ describe Gitlab::Git::Repository, :seed_helper do ensure_seeds end - it "should create a new branch" do + it "creates a new branch" do expect(repository.create_branch('new_branch', 'master')).not_to be_nil end - it "should create a new branch with the right name" do + it "creates a new branch with the right name" do expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch') end - it "should fail if we create an existing branch" do + it "fails if we create an existing branch" do repository.create_branch('duplicated_branch', 'master') expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") end - it "should fail if we create a branch from a non existing ref" do + it "fails if we create a branch from a non existing ref" do expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") end end @@ -522,7 +522,7 @@ describe Gitlab::Git::Repository, :seed_helper do describe "#refs_hash" do subject { repository.refs_hash } - it "should have as many entries as branches and tags" do + it "has as many entries as branches and tags" do expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS # We flatten in case a commit is pointed at by more than one branch and/or tag expect(subject.values.flatten.size).to eq(expected_refs.size) @@ -613,11 +613,11 @@ describe Gitlab::Git::Repository, :seed_helper do end shared_examples 'search files by content' do - it 'should have 2 items' do + it 'has 2 items' do expect(search_results.size).to eq(2) end - it 'should have the correct matching line' do + it 'has the correct matching line' do expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n", "search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n") end @@ -850,7 +850,7 @@ describe Gitlab::Git::Repository, :seed_helper do context "where provides 'after' timestamp" do options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or after that timestamp" do + it "returns commits on or after that timestamp" do commits = repository.log(options) expect(commits.size).to be > 0 @@ -863,7 +863,7 @@ describe Gitlab::Git::Repository, :seed_helper do context "where provides 'before' timestamp" do options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or before that timestamp" do + it "returns commits on or before that timestamp" do commits = repository.log(options) expect(commits.size).to be > 0 @@ -1064,14 +1064,14 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#find_branch' do - it 'should return a Branch for master' do + it 'returns a Branch for master' do branch = repository.find_branch('master') expect(branch).to be_a_kind_of(Gitlab::Git::Branch) expect(branch.name).to eq('master') end - it 'should handle non-existent branch' do + it 'handles non-existent branch' do branch = repository.find_branch('this-is-garbage') expect(branch).to eq(nil) diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 41810a8ec03..705df1f4fe7 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -197,6 +197,11 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do .to receive(:fetch_as_mirror) .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github') + service = double + expect(Projects::HousekeepingService) + .to receive(:new).with(project, :gc).and_return(service) + expect(service).to receive(:execute) + expect(importer.import_repository).to eq(true) end diff --git a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb index 4a669408025..e1106f7496a 100644 --- a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb +++ b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ClusterRoleBinding do subject { cluster_role_binding.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb index fe65d03875f..911d6024804 100644 --- a/spec/lib/gitlab/kubernetes/config_map_spec.rb +++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Kubernetes::ConfigMap do let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) } subject { config_map.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index aacae78be43..78a4eb44e38 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do describe '#pod_resource' do subject { base_command.pod_resource } - it 'should returns a kubeclient resoure with pod content for application' do + it 'returns a kubeclient resoure with pod content for application' do is_expected.to be_an_instance_of ::Kubeclient::Resource end diff --git a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb index 167bee22fc3..04649353976 100644 --- a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Kubernetes::Helm::Certificate do describe '.generate_root' do subject { described_class.generate_root } - it 'should generate a root CA that expires a long way in the future' do + it 'generates a root CA that expires a long way in the future' do expect(subject.cert.not_after).to be > 999.years.from_now end end @@ -13,14 +13,14 @@ describe Gitlab::Kubernetes::Helm::Certificate do describe '#issue' do subject { described_class.generate_root.issue } - it 'should generate a cert that expires soon' do + it 'generates a cert that expires soon' do expect(subject.cert.not_after).to be < 60.minutes.from_now end context 'passing in INFINITE_EXPIRY' do subject { described_class.generate_root.issue(expires_in: described_class::INFINITE_EXPIRY) } - it 'should generate a cert that expires a long way in the future' do + it 'generates a cert that expires a long way in the future' do expect(subject.cert.not_after).to be > 999.years.from_now end end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 95b6b3fd953..06c8d127951 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -10,11 +10,11 @@ describe Gitlab::Kubernetes::Helm::Pod do subject { described_class.new(command, namespace, service_account_name: service_account_name) } context 'with a command' do - it 'should generate a Kubeclient::Resource' do + it 'generates a Kubeclient::Resource' do expect(subject.generate).to be_a_kind_of(Kubeclient::Resource) end - it 'should generate the appropriate metadata' do + it 'generates the appropriate metadata' do metadata = subject.generate.metadata expect(metadata.name).to eq("install-#{app.name}") expect(metadata.namespace).to eq('gitlab-managed-apps') @@ -22,12 +22,12 @@ describe Gitlab::Kubernetes::Helm::Pod do expect(metadata.labels['gitlab.org/application']).to eq(app.name) end - it 'should generate a container spec' do + it 'generates a container spec' do spec = subject.generate.spec expect(spec.containers.count).to eq(1) end - it 'should generate the appropriate specifications for the container' do + it 'generates the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7') @@ -37,30 +37,30 @@ describe Gitlab::Kubernetes::Helm::Pod do expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"]) end - it 'should include a never restart policy' do + it 'includes a never restart policy' do spec = subject.generate.spec expect(spec.restartPolicy).to eq('Never') end - it 'should include volumes for the container' do + it 'includes volumes for the container' do container = subject.generate.spec.containers.first expect(container.volumeMounts.first['name']).to eq('configuration-volume') expect(container.volumeMounts.first['mountPath']).to eq("/data/helm/#{app.name}/config") end - it 'should include a volume inside the specification' do + it 'includes a volume inside the specification' do spec = subject.generate.spec expect(spec.volumes.first['name']).to eq('configuration-volume') end - it 'should mount configMap specification in the volume' do + it 'mounts configMap specification in the volume' do volume = subject.generate.spec.volumes.first expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}") expect(volume.configMap['items'].first['key']).to eq(:'values.yaml') expect(volume.configMap['items'].first['path']).to eq(:'values.yaml') end - it 'should have no serviceAccountName' do + it 'has no serviceAccountName' do spec = subject.generate.spec expect(spec.serviceAccountName).to be_nil end @@ -68,7 +68,7 @@ describe Gitlab::Kubernetes::Helm::Pod do context 'with a service_account_name' do let(:service_account_name) { 'sa' } - it 'should use the serviceAccountName provided' do + it 'uses the serviceAccountName provided' do spec = subject.generate.spec expect(spec.serviceAccountName).to eq(service_account_name) end diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb index a1a59533bfb..50acee254cb 100644 --- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb +++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do ).generate end - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/service_account_spec.rb b/spec/lib/gitlab/kubernetes/service_account_spec.rb index 8da9e932dc3..0d525966d18 100644 --- a/spec/lib/gitlab/kubernetes/service_account_spec.rb +++ b/spec/lib/gitlab/kubernetes/service_account_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::Kubernetes::ServiceAccount do subject { service_account.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb index 0773d3d9aec..0d334bed45f 100644 --- a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb +++ b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb @@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ServiceAccountToken do subject { service_account_token.generate } - it 'should build a Kubeclient Resource' do + it 'builds a Kubeclient Resource' do is_expected.to eq(resource) end end diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb index 4700a7ad2e1..e6e9ae3223e 100644 --- a/spec/lib/gitlab/object_hierarchy_spec.rb +++ b/spec/lib/gitlab/object_hierarchy_spec.rb @@ -81,6 +81,24 @@ describe Gitlab::ObjectHierarchy, :postgresql do expect { relation.update_all(share_with_group_lock: false) } .to raise_error(ActiveRecord::ReadOnlyRecord) end + + context 'when with_depth is true' do + let(:relation) do + described_class.new(Group.where(id: parent.id)).base_and_descendants(with_depth: true) + end + + it 'includes depth in the results' do + object_depths = { + parent.id => 1, + child1.id => 2, + child2.id => 3 + } + + relation.each do |object| + expect(object.depth).to eq(object_depths[object.id]) + end + end + end end describe '#descendants' do @@ -91,6 +109,28 @@ describe Gitlab::ObjectHierarchy, :postgresql do end end + describe '#max_descendants_depth' do + subject { described_class.new(base_relation).max_descendants_depth } + + context 'when base relation is empty' do + let(:base_relation) { Group.where(id: nil) } + + it { expect(subject).to be_nil } + end + + context 'when base has no children' do + let(:base_relation) { Group.where(id: child2) } + + it { expect(subject).to eq(1) } + end + + context 'when base has grandchildren' do + let(:base_relation) { Group.where(id: parent) } + + it { expect(subject).to eq(3) } + end + end + describe '#ancestors' do it 'includes only the ancestors' do relation = described_class.new(Group.where(id: child2)).ancestors diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb new file mode 100644 index 00000000000..7f6283715f2 --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do + include PrometheusHelpers + + let(:project) { create(:project) } + let(:serverless_func) { Serverless::Function.new(project, 'test-name', 'test-ns') } + + let(:client) { double('prometheus_client') } + subject { described_class.new(client) } + + context 'verify queries' do + before do + allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns'))) + allow(client).to receive(:query_range) + end + + it 'has the query, but no data' do + results = subject.query(serverless_func.id) + + expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))') + end + end +end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 2517ee71f24..f15ae83a02c 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -60,15 +60,13 @@ describe Gitlab::PrometheusClient do end describe 'failure to reach a provided prometheus url' do - let(:prometheus_url) {"https://prometheus.invalid.example.com"} + let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"} - subject { described_class.new(RestClient::Resource.new(prometheus_url)) } - - context 'exceptions are raised' do + shared_examples 'exceptions are raised' do it 'raises a Gitlab::PrometheusClient::Error error when a SocketError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) - expect { subject.send(:get, '/', {}) } + expect { subject } .to raise_error(Gitlab::PrometheusClient::Error, "Can't connect to #{prometheus_url}") expect(req_stub).to have_been_requested end @@ -76,7 +74,7 @@ describe Gitlab::PrometheusClient do it 'raises a Gitlab::PrometheusClient::Error error when a SSLError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) - expect { subject.send(:get, '/', {}) } + expect { subject } .to raise_error(Gitlab::PrometheusClient::Error, "#{prometheus_url} contains invalid SSL data") expect(req_stub).to have_been_requested end @@ -84,11 +82,23 @@ describe Gitlab::PrometheusClient do it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception) - expect { subject.send(:get, '/', {}) } + expect { subject } .to raise_error(Gitlab::PrometheusClient::Error, "Network connection error") expect(req_stub).to have_been_requested end end + + context 'ping' do + subject { described_class.new(RestClient::Resource.new(prometheus_url)).ping } + + it_behaves_like 'exceptions are raised' + end + + context 'proxy' do + subject { described_class.new(RestClient::Resource.new(prometheus_url)).proxy('query', { query: '1' }) } + + it_behaves_like 'exceptions are raised' + end end describe '#query' do @@ -258,4 +268,59 @@ describe Gitlab::PrometheusClient do it { is_expected.to eq(step) } end end + + describe 'proxy' do + context 'get API' do + let(:prometheus_query) { prometheus_cpu_query('env-slug') } + let(:query_url) { prometheus_query_url(prometheus_query) } + + around do |example| + Timecop.freeze { example.run } + end + + context 'when response status code is 200' do + it 'returns response object' do + req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector')) + + response = subject.proxy('query', { query: prometheus_query }) + json_response = JSON.parse(response.body) + + expect(response.code).to eq(200) + expect(json_response).to eq({ + 'status' => 'success', + 'data' => { + 'resultType' => 'vector', + 'result' => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] + } + }) + expect(req_stub).to have_been_requested + end + end + + context 'when response status code is not 200' do + it 'returns response object' do + req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'error' }) + + response = subject.proxy('query', { query: prometheus_query }) + json_response = JSON.parse(response.body) + + expect(req_stub).to have_been_requested + expect(response.code).to eq(400) + expect(json_response).to eq('error' => 'error') + end + end + + context 'when RestClient::Exception is raised' do + before do + stub_prometheus_request_with_exception(query_url, RestClient::Exception) + end + + it 'raises PrometheusClient::Error' do + expect { subject.proxy('query', { query: prometheus_query }) }.to( + raise_error(Gitlab::PrometheusClient::Error, 'Network connection error') + ) + end + end + end + end end diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 4c7ca4e2b57..8fbda929064 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -44,8 +44,10 @@ describe ::Gitlab::RepoPath do end end - it "returns nil for non existent paths" do - expect(described_class.parse("path/non-existent.git")).to eq(nil) + it "returns the default type for non existent paths" do + _project, type, _redirected = described_class.parse("path/non-existent.git") + + expect(type).to eq(Gitlab::GlRepository.default_type) end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 4b57eecff93..312aa3be490 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -97,7 +97,7 @@ describe Gitlab::SearchResults do results.objects('merge_requests') end - it 'it skips project filter if default project context is used' do + it 'skips project filter if default project context is used' do allow(results).to receive(:default_project_filter).and_return(true) expect(results).not_to receive(:project_ids_relation) @@ -113,7 +113,7 @@ describe Gitlab::SearchResults do results.objects('issues') end - it 'it skips project filter if default project context is used' do + it 'skips project filter if default project context is used' do allow(results).to receive(:default_project_filter).and_return(true) expect(results).not_to receive(:project_ids_relation) diff --git a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb index c9d1a06b3e6..0bbaf5968ed 100644 --- a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb +++ b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb @@ -7,19 +7,19 @@ describe Gitlab::Tracing::Rails::ActionViewSubscriber do using RSpec::Parameterized::TableSyntax shared_examples 'an actionview notification' do - it 'should notify the tracer when the hash contains null values' do + it 'notifies the tracer when the hash contains null values' do expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception) subject.public_send(notify_method, start, finish, payload) end - it 'should notify the tracer when the payload is missing values' do + it 'notifies the tracer when the payload is missing values' do expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception) subject.public_send(notify_method, start, finish, payload.compact) end - it 'should not throw exceptions when with the default tracer' do + it 'does not throw exceptions when with the default tracer' do expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error end end diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb index 3d066843148..7bd0875fa68 100644 --- a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb +++ b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb @@ -53,19 +53,19 @@ describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do } end - it 'should notify the tracer when the hash contains null values' do + it 'notifies the tracer when the hash contains null values' do expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception) subject.notify(start, finish, payload) end - it 'should notify the tracer when the payload is missing values' do + it 'notifies the tracer when the payload is missing values' do expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception) subject.notify(start, finish, payload.compact) end - it 'should not throw exceptions when with the default tracer' do + it 'does not throw exceptions when with the default tracer' do expect { subject.notify(start, finish, payload) }.not_to raise_error end end diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb index 566b5050e47..db75ce2a998 100644 --- a/spec/lib/gitlab/tracing_spec.rb +++ b/spec/lib/gitlab/tracing_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Tracing do end with_them do - it 'should return the correct state for .enabled?' do + it 'returns the correct state for .enabled?' do expect(described_class).to receive(:connection_string).and_return(connection_string) expect(described_class.enabled?).to eq(enabled_state) @@ -33,7 +33,7 @@ describe Gitlab::Tracing do end with_them do - it 'should return the correct state for .tracing_url_enabled?' do + it 'returns the correct state for .tracing_url_enabled?' do expect(described_class).to receive(:enabled?).and_return(enabled?) allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template) @@ -56,7 +56,7 @@ describe Gitlab::Tracing do end with_them do - it 'should return the correct state for .tracing_url' do + it 'returns the correct state for .tracing_url' do expect(described_class).to receive(:tracing_url_enabled?).and_return(tracing_url_enabled?) allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template) allow(Gitlab::CorrelationId).to receive(:current_id).and_return(correlation_id) diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 6e98a999766..5861e6955a6 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -161,7 +161,7 @@ describe Gitlab::UrlSanitizer do end context 'when credentials contains special chars' do - it 'should parse the URL without errors' do + it 'parses the URL without errors' do url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index 3333f8307ae..cb14204b99a 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -61,13 +61,37 @@ describe Sentry::Client do end end + shared_examples 'maps exceptions' do + exceptions = { + HTTParty::Error => 'Error when connecting to Sentry', + Net::OpenTimeout => 'Connection to Sentry timed out', + SocketError => 'Received SocketError when trying to connect to Sentry', + OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data', + Errno::ECONNREFUSED => 'Connection refused', + StandardError => 'Sentry request failed due to StandardError' + } + + exceptions.each do |exception, message| + context "#{exception}" do + before do + stub_request(:get, sentry_request_url).to_raise(exception) + end + + it do + expect { subject } + .to raise_exception(Sentry::Client::Error, message) + end + end + end + end + describe '#list_issues' do let(:issue_status) { 'unresolved' } let(:limit) { 20 } - let(:sentry_api_response) { issues_sample_response } + let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' } - let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sentry_api_response) } + let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) } subject { client.list_issues(issue_status: issue_status, limit: limit) } @@ -121,16 +145,14 @@ describe Sentry::Client do # Sentry API returns 404 if there are extra slashes in the URL! context 'extra slashes in URL' do let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' } - let(:client) { described_class.new(sentry_url, token) } - let!(:valid_req_stub) do - stub_sentry_request( - 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \ + let(:sentry_request_url) do + 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \ 'issues/?limit=20&query=is:unresolved' - ) end it 'removes extra slashes in api url' do + expect(client.url).to eq(sentry_url) expect(Gitlab::HTTP).to receive(:get).with( URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'), anything @@ -138,7 +160,7 @@ describe Sentry::Client do subject - expect(valid_req_stub).to have_been_requested + expect(sentry_api_request).to have_been_requested end end @@ -169,6 +191,8 @@ describe Sentry::Client do expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') end end + + it_behaves_like 'maps exceptions' end describe '#list_projects' do @@ -260,12 +284,18 @@ describe Sentry::Client do expect(valid_req_stub).to have_been_requested end end + + context 'when exception is raised' do + let(:sentry_request_url) { sentry_list_projects_url } + + it_behaves_like 'maps exceptions' + end end private def stub_sentry_request(url, body: {}, status: 200, headers: {}) - WebMock.stub_request(:get, url) + stub_request(:get, url) .to_return( status: status, headers: { 'Content-Type' => 'application/json' }.merge(headers), diff --git a/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb new file mode 100644 index 00000000000..572b7dfd0c8 --- /dev/null +++ b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190313092516_clean_up_noteable_id_for_notes_on_commits.rb') + +describe CleanUpNoteableIdForNotesOnCommits, :migration do + let(:notes) { table(:notes) } + + before do + notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test') + notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test') + notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test') + + notes.create!(noteable_type: 'Issue', noteable_id: 1, note: 'Test') + notes.create!(noteable_type: 'MergeRequest', noteable_id: 1, note: 'Test') + notes.create!(noteable_type: 'Snippet', noteable_id: 1, note: 'Test') + end + + it 'clears noteable_id for notes on commits' do + expect { migrate! }.to change { dirty_notes_on_commits.count }.from(3).to(0) + end + + it 'does not clear noteable_id for other notes' do + expect { migrate! }.not_to change { other_notes.count } + end + + def dirty_notes_on_commits + notes.where(noteable_type: 'Commit').where('noteable_id IS NOT NULL') + end + + def other_notes + notes.where("noteable_type != 'Commit' AND noteable_id IS NOT NULL") + end +end diff --git a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb index b1ff3cfd355..349cffea70e 100644 --- a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb +++ b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb @@ -25,7 +25,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do context 'with ProjectAutoDevOps with no domain' do let(:domain) { nil } - it 'should not update cluster project' do + it 'does not update cluster project' do migrate! expect(clusters_without_domain.count).to eq(clusters_table.count) @@ -35,7 +35,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do context 'with ProjectAutoDevOps with domain' do let(:domain) { 'example-domain.com' } - it 'should update all cluster projects' do + it 'updates all cluster projects' do migrate! expect(clusters_with_domain.count).to eq(clusters_table.count) @@ -49,7 +49,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do setup_cluster_projects_with_domain(quantity: 25, domain: nil) end - it 'should only update specific cluster projects' do + it 'only updates specific cluster projects' do migrate! expect(clusters_with_domain.count).to eq(20) diff --git a/spec/migrations/truncate_user_fullname_spec.rb b/spec/migrations/truncate_user_fullname_spec.rb new file mode 100644 index 00000000000..17fd4d9f688 --- /dev/null +++ b/spec/migrations/truncate_user_fullname_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20190325080727_truncate_user_fullname.rb') + +describe TruncateUserFullname, :migration do + let(:users) { table(:users) } + + let(:user_short) { create_user(name: 'abc', email: 'test_short@example.com') } + let(:user_long) { create_user(name: 'a' * 200 + 'z', email: 'test_long@example.com') } + + def create_user(params) + users.create!(params.merge(projects_limit: 0)) + end + + it 'truncates user full name to the first 128 characters' do + expect { migrate! }.to change { user_long.reload.name }.to('a' * 128) + end + + it 'does not truncate short names' do + expect { migrate! }.not_to change { user_short.reload.name.length } + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index c5579dafb4a..c81572d739e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -6,6 +6,7 @@ describe ApplicationSetting do let(:setting) { described_class.create_from_defaults } it { include(CacheableAttributes) } + it { include(ApplicationSettingImplementation) } it { expect(described_class.current_without_cache).to eq(described_class.last) } it { expect(setting).to be_valid } @@ -286,12 +287,10 @@ describe ApplicationSetting do end context 'restrict creating duplicates' do - before do - described_class.create_from_defaults - end + let!(:current_settings) { described_class.create_from_defaults } - it 'raises an record creation violation if already created' do - expect { described_class.create_from_defaults }.to raise_error(ActiveRecord::RecordNotUnique) + it 'returns the current settings' do + expect(described_class.create_from_defaults).to eq(current_settings) end end diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb index 314d7d1e9f4..c661f5384ea 100644 --- a/spec/models/badge_spec.rb +++ b/spec/models/badge_spec.rb @@ -61,7 +61,7 @@ describe Badge do end shared_examples 'rendered_links' do - it 'should use the project information to populate the url placeholders' do + it 'uses the project information to populate the url placeholders' do stub_project_commit_info(project) expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever" diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb index e683d110252..d41c5cf2ca1 100644 --- a/spec/models/badges/project_badge_spec.rb +++ b/spec/models/badges/project_badge_spec.rb @@ -14,7 +14,7 @@ describe ProjectBadge do end shared_examples 'rendered_links' do - it 'should use the badge project information to populate the url placeholders' do + it 'uses the badge project information to populate the url placeholders' do stub_project_commit_info(project) expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever" diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2c41dfa65cd..1352a2de2d7 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -117,6 +117,16 @@ describe Ci::Build do it 'returns the job' do is_expected.to include(job) end + + context 'when ci_enable_legacy_artifacts feature flag is disabled' do + before do + stub_feature_flags(ci_enable_legacy_artifacts: false) + end + + it 'does not return the job' do + is_expected.not_to include(job) + end + end end context 'when job has a job artifact archive' do @@ -471,6 +481,14 @@ describe Ci::Build do let(:build) { create(:ci_build, :legacy_artifacts) } it { is_expected.to be_truthy } + + context 'when ci_enable_legacy_artifacts feature flag is disabled' do + before do + stub_feature_flags(ci_enable_legacy_artifacts: false) + end + + it { is_expected.to be_falsy } + end end end end @@ -2708,13 +2726,13 @@ describe Ci::Build do project.deploy_tokens << deploy_token end - it 'should include deploy token variables' do + it 'includes deploy token variables' do is_expected.to include(*deploy_token_variables) end end context 'when gitlab-deploy-token does not exist' do - it 'should not include deploy token variables' do + it 'does not include deploy token variables' do expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil end @@ -3198,7 +3216,7 @@ describe Ci::Build do it 'does not try to create a todo' do project.add_developer(user) - expect(service).not_to receive(:commit_status_merge_requests) + expect(service).not_to receive(:pipeline_merge_requests) subject.drop! end @@ -3234,7 +3252,23 @@ describe Ci::Build do end context 'when build is not configured to be retried' do - subject { create(:ci_build, :running, project: project, user: user) } + subject { create(:ci_build, :running, project: project, user: user, pipeline: pipeline) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + ref: 'feature', + sha: merge_request.diff_head_sha, + merge_requests_as_head_pipeline: [merge_request]) + end + + let(:merge_request) do + create(:merge_request, :opened, + source_branch: 'feature', + source_project: project, + target_branch: 'master', + target_project: project) + end it 'does not retry build' do expect(described_class).not_to receive(:retry) @@ -3253,7 +3287,10 @@ describe Ci::Build do it 'creates a todo' do project.add_developer(user) - expect(service).to receive(:commit_status_merge_requests) + expect_next_instance_of(TodoService) do |todo_service| + expect(todo_service) + .to receive(:merge_request_build_failed).with(merge_request) + end subject.drop! end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index b3ab63925dd..2cb581696a0 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -72,7 +72,7 @@ describe Ci::Runner do expect(instance_runner.errors.full_messages).to include('Runner cannot have projects assigned') end - it 'should fail to save a group assigned to a project runner even if the runner is already saved' do + it 'fails to save a group assigned to a project runner even if the runner is already saved' do group_runner expect { create(:group, runners: [project_runner]) } diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb index af7eadfc74c..5cd80edb3a1 100644 --- a/spec/models/clusters/applications/cert_manager_spec.rb +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -36,7 +36,7 @@ describe Clusters::Applications::CertManager do it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } - it 'should be initialized with cert_manager arguments' do + it 'is initialized with cert_manager arguments' do expect(subject.name).to eq('certmanager') expect(subject.chart).to eq('stable/cert-manager') expect(subject.version).to eq('v0.5.2') @@ -52,7 +52,7 @@ describe Clusters::Applications::CertManager do cert_manager.email = cert_email end - it 'should use his/her email to register issuer with certificate provider' do + it 'uses his/her email to register issuer with certificate provider' do expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file)) end end @@ -68,7 +68,7 @@ describe Clusters::Applications::CertManager do context 'application failed to install previously' do let(:cert_manager) { create(:clusters_applications_cert_managers, :errored, version: '0.0.1') } - it 'should be initialized with the locked version' do + it 'is initialized with the locked version' do expect(subject.version).to eq('v0.5.2') end end @@ -80,7 +80,7 @@ describe Clusters::Applications::CertManager do subject { application.files } - it 'should include cert_manager specific keys in the values.yaml file' do + it 'includes cert_manager specific keys in the values.yaml file' do expect(values).to include('ingressShim') end end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index f97d126d918..f177d493a2e 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -36,11 +36,11 @@ describe Clusters::Applications::Helm do it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) } - it 'should be initialized with 1 arguments' do + it 'is initialized with 1 arguments' do expect(subject.name).to eq('helm') end - it 'should have cert files' do + it 'has cert files' do expect(subject.files[:'ca.pem']).to be_present expect(subject.files[:'ca.pem']).to eq(helm.ca_cert) diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 09e60b9a206..113d29b5551 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -73,7 +73,7 @@ describe Clusters::Applications::Ingress do it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } - it 'should be initialized with ingress arguments' do + it 'is initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') expect(subject.version).to eq('1.1.2') @@ -92,7 +92,7 @@ describe Clusters::Applications::Ingress do context 'application failed to install previously' do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } - it 'should be initialized with the locked version' do + it 'is initialized with the locked version' do expect(subject.version).to eq('1.1.2') end end @@ -104,7 +104,7 @@ describe Clusters::Applications::Ingress do subject { application.files } - it 'should include ingress valid keys in values' do + it 'includes ingress valid keys in values' do expect(values).to include('image') expect(values).to include('repository') expect(values).to include('stats') diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 5970a1959b5..1a7363b64f9 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -45,7 +45,7 @@ describe Clusters::Applications::Jupyter do it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } - it 'should be initialized with 4 arguments' do + it 'is initialized with 4 arguments' do expect(subject.name).to eq('jupyter') expect(subject.chart).to eq('jupyter/jupyterhub') expect(subject.version).to eq('0.9-174bbd5') @@ -65,7 +65,7 @@ describe Clusters::Applications::Jupyter do context 'application failed to install previously' do let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') } - it 'should be initialized with the locked version' do + it 'is initialized with the locked version' do expect(subject.version).to eq('0.9-174bbd5') end end @@ -77,7 +77,7 @@ describe Clusters::Applications::Jupyter do subject { application.files } - it 'should include valid values' do + it 'includes valid values' do expect(values).to include('ingress') expect(values).to include('hub') expect(values).to include('rbac') diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 25493689fbc..5e68f2634da 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -77,17 +77,17 @@ describe Clusters::Applications::Knative do end shared_examples 'a command' do - it 'should be an instance of Helm::InstallCommand' do + it 'is an instance of Helm::InstallCommand' do expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) end - it 'should be initialized with knative arguments' do + it 'is initialized with knative arguments' do expect(subject.name).to eq('knative') expect(subject.chart).to eq('knative/knative') expect(subject.files).to eq(knative.files) end - it 'should not install metrics for prometheus' do + it 'does not install metrics for prometheus' do expect(subject.postinstall).to be_nil end @@ -97,7 +97,7 @@ describe Clusters::Applications::Knative do subject { knative.install_command } - it 'should install metrics' do + it 'installs metrics' do expect(subject.postinstall).not_to be_nil expect(subject.postinstall.length).to be(1) expect(subject.postinstall[0]).to eql("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}") @@ -108,7 +108,7 @@ describe Clusters::Applications::Knative do describe '#install_command' do subject { knative.install_command } - it 'should be initialized with latest version' do + it 'is initialized with latest version' do expect(subject.version).to eq('0.3.0') end @@ -119,7 +119,7 @@ describe Clusters::Applications::Knative do let!(:current_installed_version) { knative.version = '0.1.0' } subject { knative.update_command } - it 'should be initialized with current version' do + it 'is initialized with current version' do expect(subject.version).to eq(current_installed_version) end @@ -132,7 +132,7 @@ describe Clusters::Applications::Knative do subject { application.files } - it 'should include knative specific keys in the values.yaml file' do + it 'includes knative specific keys in the values.yaml file' do expect(values).to include('domain') end end @@ -165,7 +165,7 @@ describe Clusters::Applications::Knative do synchronous_reactive_cache(knative) end - it 'should be able k8s core for pod details' do + it 'is able k8s core for pod details' do expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil end end @@ -190,7 +190,7 @@ describe Clusters::Applications::Knative do stub_kubeclient_service_pods end - it 'should have an unintialized cache' do + it 'has an unintialized cache' do is_expected.to be_nil end @@ -204,11 +204,11 @@ describe Clusters::Applications::Knative do synchronous_reactive_cache(knative) end - it 'should have cached services' do + it 'has cached services' do is_expected.not_to be_nil end - it 'should match our namespace' do + it 'matches our namespace' do expect(knative.services_for(ns: namespace)).not_to be_nil end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 82a502addd4..e8ba9737c23 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -94,7 +94,7 @@ describe Clusters::Applications::Prometheus do it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } - it 'should be initialized with 3 arguments' do + it 'is initialized with 3 arguments' do expect(subject.name).to eq('prometheus') expect(subject.chart).to eq('stable/prometheus') expect(subject.version).to eq('6.7.3') @@ -113,12 +113,12 @@ describe Clusters::Applications::Prometheus do context 'application failed to install previously' do let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') } - it 'should be initialized with the locked version' do + it 'is initialized with the locked version' do expect(subject.version).to eq('6.7.3') end end - it 'should not install knative metrics' do + it 'does not install knative metrics' do expect(subject.postinstall).to be_nil end @@ -128,7 +128,7 @@ describe Clusters::Applications::Prometheus do subject { prometheus.install_command } - it 'should install knative metrics' do + it 'installs knative metrics' do expect(subject.postinstall).to include("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}") end end @@ -142,7 +142,7 @@ describe Clusters::Applications::Prometheus do expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::InstallCommand) end - it 'should be initialized with 3 arguments' do + it 'is initialized with 3 arguments' do command = prometheus.upgrade_command(values) expect(command.name).to eq('prometheus') @@ -180,7 +180,7 @@ describe Clusters::Applications::Prometheus do subject { application.files } - it 'should include prometheus valid values' do + it 'includes prometheus valid values' do expect(values).to include('alertmanager') expect(values).to include('kubeStateMetrics') expect(values).to include('nodeExporter') @@ -204,7 +204,7 @@ describe Clusters::Applications::Prometheus do expect(subject[:'values.yaml']).to eq({ hello: :world }) end - it 'should include cert files' do + it 'includes cert files' do expect(subject[:'ca.pem']).to be_present expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) @@ -220,7 +220,7 @@ describe Clusters::Applications::Prometheus do application.cluster.application_helm.ca_cert = nil end - it 'should not include cert files' do + it 'does not include cert files' do expect(subject[:'ca.pem']).not_to be_present expect(subject[:'cert.pem']).not_to be_present expect(subject[:'key.pem']).not_to be_present diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 7e2f5835279..399a13f82cb 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -21,7 +21,7 @@ describe Clusters::Applications::Runner do it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } - it 'should be initialized with 4 arguments' do + it 'is initialized with 4 arguments' do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') expect(subject.version).to eq('0.3.0') @@ -41,7 +41,7 @@ describe Clusters::Applications::Runner do context 'application failed to install previously' do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } - it 'should be initialized with the locked version' do + it 'is initialized with the locked version' do expect(subject.version).to eq('0.3.0') end end @@ -53,7 +53,7 @@ describe Clusters::Applications::Runner do subject { application.files } - it 'should include runner valid values' do + it 'includes runner valid values' do expect(values).to include('concurrent') expect(values).to include('checkInterval') expect(values).to include('rbac') @@ -131,7 +131,7 @@ describe Clusters::Applications::Runner do allow(application).to receive(:chart_values).and_return(stub_values) end - it 'should overwrite values.yaml' do + it 'overwrites values.yaml' do expect(values).to match(/privileged: '?#{application.privileged}/) end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index fabd2806d9a..894ef3fb956 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -269,7 +269,7 @@ describe Clusters::Cluster do context 'when cluster is not a valid hostname' do let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') } - it 'should add an error on domain' do + it 'adds an error on domain' do expect(subject).not_to be_valid expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])') end @@ -599,7 +599,7 @@ describe Clusters::Cluster do stub_application_setting(auto_devops_domain: 'global_domain.com') end - it 'should include KUBE_INGRESS_BASE_DOMAIN' do + it 'includes KUBE_INGRESS_BASE_DOMAIN' do expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'global_domain.com') end end @@ -607,7 +607,7 @@ describe Clusters::Cluster do context 'with a cluster domain' do let(:cluster) { create(:cluster, :provided_by_gcp, domain: 'example.com') } - it 'should include KUBE_INGRESS_BASE_DOMAIN' do + it 'includes KUBE_INGRESS_BASE_DOMAIN' do expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'example.com') end end @@ -615,7 +615,7 @@ describe Clusters::Cluster do context 'with no domain' do let(:cluster) { create(:cluster, :provided_by_gcp, :project) } - it 'should return an empty array' do + it 'returns an empty array' do expect(subject.to_hash).to be_empty end end diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb index 579f486f99f..b5cba80b806 100644 --- a/spec/models/clusters/kubernetes_namespace_spec.rb +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -60,7 +60,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do context 'when platform has a namespace assigned' do let(:namespace) { 'platform-namespace' } - it 'should copy the namespace' do + it 'copies the namespace' do subject expect(kubernetes_namespace.namespace).to eq('platform-namespace') @@ -72,7 +72,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do let(:namespace) { nil } let(:project_slug) { "#{project.path}-#{project.id}" } - it 'should fallback to project namespace' do + it 'fallbacks to project namespace' do subject expect(kubernetes_namespace.namespace).to eq(project_slug) @@ -83,7 +83,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do describe '#service_account_name' do let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" } - it 'should set a service account name based on namespace' do + it 'sets a service account name based on namespace' do subject expect(kubernetes_namespace.service_account_name).to eq(service_account_name) diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 14bec17a2bd..0281dd2c303 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -447,7 +447,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching let(:platform) { cluster.platform } context 'when namespace is updated' do - it 'should call ConfigureWorker' do + it 'calls ConfigureWorker' do expect(ClusterConfigureWorker).to receive(:perform_async).with(cluster.id).once platform.namespace = 'new-namespace' @@ -456,7 +456,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end context 'when namespace is not updated' do - it 'should not call ConfigureWorker' do + it 'does not call ConfigureWorker' do expect(ClusterConfigureWorker).not_to receive(:perform_async) platform.username = "new-username" diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 259ac6852a8..27ed298ae08 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -659,7 +659,7 @@ describe Issuable do end context 'adding time' do - it 'should update the total time spent' do + it 'updates the total time spent' do spend_time(1800) expect(issue.total_time_spent).to eq(1800) @@ -679,7 +679,7 @@ describe Issuable do spend_time(1800) end - it 'should update the total time spent' do + it 'updates the total time spent' do spend_time(-900) expect(issue.total_time_spent).to eq(900) diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb index 650d49e41a1..67353475251 100644 --- a/spec/models/concerns/spammable_spec.rb +++ b/spec/models/concerns/spammable_spec.rb @@ -12,7 +12,7 @@ describe Spammable do end describe 'ClassMethods' do - it 'should return correct attr_spammable' do + it 'returns correct attr_spammable' do expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}") end end @@ -20,7 +20,7 @@ describe Spammable do describe 'InstanceMethods' do let(:issue) { build(:issue, spam: true) } - it 'should be invalid if spam' do + it 'is invalid if spam' do expect(issue.valid?).to be_falsey end diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 05320703e25..2fe82eaa778 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -9,7 +9,7 @@ describe DeployToken do it { is_expected.to have_many(:projects).through(:project_deploy_tokens) } describe '#ensure_token' do - it 'should ensure a token' do + it 'ensures a token' do deploy_token.token = nil deploy_token.save @@ -19,13 +19,13 @@ describe DeployToken do describe '#ensure_at_least_one_scope' do context 'with at least one scope' do - it 'should be valid' do + it 'is valid' do is_expected.to be_valid end end context 'with no scopes' do - it 'should be invalid' do + it 'is invalid' do deploy_token = build(:deploy_token, read_repository: false, read_registry: false) expect(deploy_token).not_to be_valid @@ -36,13 +36,13 @@ describe DeployToken do describe '#scopes' do context 'with all the scopes' do - it 'should return scopes assigned to DeployToken' do + it 'returns scopes assigned to DeployToken' do expect(deploy_token.scopes).to eq([:read_repository, :read_registry]) end end context 'with only one scope' do - it 'should return scopes assigned to DeployToken' do + it 'returns scopes assigned to DeployToken' do deploy_token = create(:deploy_token, read_registry: false) expect(deploy_token.scopes).to eq([:read_repository]) end @@ -50,7 +50,7 @@ describe DeployToken do end describe '#revoke!' do - it 'should update revoke attribute' do + it 'updates revoke attribute' do deploy_token.revoke! expect(deploy_token.revoked?).to be_truthy end @@ -58,20 +58,20 @@ describe DeployToken do describe "#active?" do context "when it has been revoked" do - it 'should return false' do + it 'returns false' do deploy_token.revoke! expect(deploy_token.active?).to be_falsy end end context "when it hasn't been revoked and is not expired" do - it 'should return true' do + it 'returns true' do expect(deploy_token.active?).to be_truthy end end context "when it hasn't been revoked and is expired" do - it 'should return true' do + it 'returns true' do deploy_token.update_attribute(:expires_at, Date.today - 5.days) expect(deploy_token.active?).to be_falsy end @@ -80,7 +80,7 @@ describe DeployToken do context "when it hasn't been revoked and has no expiry" do let(:deploy_token) { create(:deploy_token, expires_at: nil) } - it 'should return true' do + it 'returns true' do expect(deploy_token.active?).to be_truthy end end @@ -126,7 +126,7 @@ describe DeployToken do context 'when using Forever.date' do let(:deploy_token) { create(:deploy_token, expires_at: nil) } - it 'should return nil' do + it 'returns nil' do expect(deploy_token.expires_at).to be_nil end end @@ -135,7 +135,7 @@ describe DeployToken do let(:expires_at) { Date.today + 5.months } let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } - it 'should return the personalized date' do + it 'returns the personalized date' do expect(deploy_token.expires_at).to eq(expires_at) end end @@ -145,7 +145,7 @@ describe DeployToken do context 'when passing nil' do let(:deploy_token) { create(:deploy_token, expires_at: nil) } - it 'should assign Forever.date' do + it 'assigns Forever.date' do expect(deploy_token.read_attribute(:expires_at)).to eq(Forever.date) end end @@ -154,7 +154,7 @@ describe DeployToken do let(:expires_at) { Date.today + 5.months } let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } - it 'should respect the value' do + it 'respects the value' do expect(deploy_token.read_attribute(:expires_at)).to eq(expires_at) end end @@ -166,14 +166,14 @@ describe DeployToken do subject { project.deploy_tokens.gitlab_deploy_token } context 'with a gitlab deploy token associated' do - it 'should return the gitlab deploy token' do + it 'returns the gitlab deploy token' do deploy_token = create(:deploy_token, :gitlab_deploy_token, projects: [project]) is_expected.to eq(deploy_token) end end context 'with no gitlab deploy token associated' do - it 'should return nil' do + it 'returns nil' do is_expected.to be_nil end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index b2ffd5812ab..e6e7298a043 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -788,14 +788,14 @@ describe Group do describe '#has_parent?' do context 'when the group has a parent' do - it 'should be truthy' do + it 'is truthy' do group = create(:group, :nested) expect(group.has_parent?).to be_truthy end end context 'when the group has no parent' do - it 'should be falsy' do + it 'is falsy' do group = create(:group, parent: nil) expect(group.has_parent?).to be_falsy end @@ -959,4 +959,12 @@ describe Group do end end end + + describe 'project_creation_level' do + it 'outputs the default one if it is nil' do + group = create(:group, project_creation_level: nil) + + expect(group.project_creation_level).to eq(Gitlab::CurrentSettings.default_project_creation) + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 62e7dd3231b..387d1221c76 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -740,14 +740,14 @@ describe Namespace do describe '#full_path_was' do context 'when the group has no parent' do - it 'should return the path was' do + it 'returns the path was' do group = create(:group, parent: nil) expect(group.full_path_was).to eq(group.path_was) end end context 'when a parent is assigned to a group with no previous parent' do - it 'should return the path was' do + it 'returns the path was' do group = create(:group, parent: nil) parent = create(:group) @@ -758,7 +758,7 @@ describe Namespace do end context 'when a parent is removed from the group' do - it 'should return the parent full path' do + it 'returns the parent full path' do parent = create(:group) group = create(:group, parent: parent) group.parent = nil @@ -768,7 +768,7 @@ describe Namespace do end context 'when changing parents' do - it 'should return the previous parent full path' do + it 'returns the previous parent full path' do parent = create(:group) group = create(:group, parent: parent) new_parent = create(:group) diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb index d1a2bedf542..232172fde76 100644 --- a/spec/models/network/graph_spec.rb +++ b/spec/models/network/graph_spec.rb @@ -22,7 +22,7 @@ describe Network::Graph do expect(commits).to all( be_kind_of(Network::Commit) ) end - it 'it the commits by commit date (descending)' do + it 'sorts commits by commit date (descending)' do # Remove duplicate timestamps because they make it harder to # assert that the commits are sorted as expected. commits = graph.commits.uniq(&:date) diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 8ad28ce68cc..b81e5610e2c 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -117,7 +117,7 @@ describe ProjectAutoDevops do context 'when the project is public' do let(:project) { create(:project, :repository, :public) } - it 'should not create a gitlab deploy token' do + it 'does not create a gitlab deploy token' do expect do auto_devops.save end.not_to change { DeployToken.count } @@ -127,7 +127,7 @@ describe ProjectAutoDevops do context 'when the project is internal' do let(:project) { create(:project, :repository, :internal) } - it 'should create a gitlab deploy token' do + it 'creates a gitlab deploy token' do expect do auto_devops.save end.to change { DeployToken.count }.by(1) @@ -137,7 +137,7 @@ describe ProjectAutoDevops do context 'when the project is private' do let(:project) { create(:project, :repository, :private) } - it 'should create a gitlab deploy token' do + it 'creates a gitlab deploy token' do expect do auto_devops.save end.to change { DeployToken.count }.by(1) @@ -148,7 +148,7 @@ describe ProjectAutoDevops do let(:project) { create(:project, :repository, :internal) } let(:auto_devops) { build(:project_auto_devops, project: project) } - it 'should create a deploy token' do + it 'creates a deploy token' do expect do auto_devops.save end.to change { DeployToken.count }.by(1) @@ -159,7 +159,7 @@ describe ProjectAutoDevops do let(:project) { create(:project, :repository, :internal) } let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) } - it 'should create a deploy token' do + it 'creates a deploy token' do allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true) expect do @@ -172,7 +172,7 @@ describe ProjectAutoDevops do let(:project) { create(:project, :repository, :internal) } let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) } - it 'should not create a deploy token' do + it 'does not create a deploy token' do expect do auto_devops.save end.not_to change { DeployToken.count } @@ -184,7 +184,7 @@ describe ProjectAutoDevops do let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) } let(:auto_devops) { build(:project_auto_devops, project: project) } - it 'should not create a deploy token' do + it 'does not create a deploy token' do expect do auto_devops.save end.not_to change { DeployToken.count } @@ -196,7 +196,7 @@ describe ProjectAutoDevops do let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) } let(:auto_devops) { build(:project_auto_devops, project: project) } - it 'should not create a deploy token' do + it 'does not create a deploy token' do expect do auto_devops.save end.not_to change { DeployToken.count } diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 7bf093b71e7..3a381cb405d 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -70,11 +70,11 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do kubernetes_service.properties['namespace'] = "foo" end - it 'should not update attributes' do + it 'does not update attributes' do expect(kubernetes_service.save).to be_falsy end - it 'should include an error with a deprecation message' do + it 'includes an error with a deprecation message' do kubernetes_service.valid? expect(kubernetes_service.errors[:base].first).to match(/Kubernetes service integration has been deprecated/) end @@ -83,7 +83,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'with a non-deprecated service' do let(:kubernetes_service) { create(:kubernetes_service) } - it 'should update attributes' do + it 'updates attributes' do kubernetes_service.properties['namespace'] = 'foo' expect(kubernetes_service.save).to be_truthy end @@ -98,15 +98,15 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do kubernetes_service.save end - it 'should deactive the service' do + it 'deactivates the service' do expect(kubernetes_service.active?).to be_falsy end - it 'should not include a deprecation message as error' do + it 'does not include a deprecation message as error' do expect(kubernetes_service.errors.messages.count).to eq(0) end - it 'should update attributes' do + it 'updates attributes' do expect(kubernetes_service.properties['namespace']).to eq("foo") end end @@ -118,7 +118,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do kubernetes_service.properties['namespace'] = 'foo' end - it 'should update attributes' do + it 'updates attributes' do expect(kubernetes_service.save).to be_truthy expect(kubernetes_service.properties['namespace']).to eq('foo') end @@ -392,13 +392,13 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do let(:kubernetes_service) { create(:kubernetes_service) } context 'with an active kubernetes service' do - it 'should return false' do + it 'returns false' do expect(kubernetes_service.deprecated?).to be_falsy end end context 'with a inactive kubernetes service' do - it 'should return true' do + it 'returns true' do kubernetes_service.update_attribute(:active, false) expect(kubernetes_service.deprecated?).to be_truthy end @@ -408,18 +408,18 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe "#deprecation_message" do let(:kubernetes_service) { create(:kubernetes_service) } - it 'should indicate the service is deprecated' do + it 'indicates the service is deprecated' do expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/) end context 'if the services is active' do - it 'should return a message' do + it 'returns a message' do expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/) end end context 'if the service is not active' do - it 'should return a message' do + it 'returns a message' do kubernetes_service.update_attribute(:active, false) expect(kubernetes_service.deprecation_message).to match(/Fields on this page are now uneditable/) end diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb index de2c8790405..773b8b7890f 100644 --- a/spec/models/project_services/pivotaltracker_service_spec.rb +++ b/spec/models/project_services/pivotaltracker_service_spec.rb @@ -56,7 +56,7 @@ describe PivotaltrackerService do WebMock.stub_request(:post, url) end - it 'should post correct message' do + it 'posts correct message' do service.execute(push_data) expect(WebMock).to have_requested(:post, url).with( body: { @@ -81,14 +81,14 @@ describe PivotaltrackerService do end end - it 'should post message if branch is in the list' do + it 'posts message if branch is in the list' do service.execute(push_data(branch: 'master')) service.execute(push_data(branch: 'v10')) expect(WebMock).to have_requested(:post, url).twice end - it 'should not post message if branch is not in the list' do + it 'does not post message if branch is not in the list' do service.execute(push_data(branch: 'mas')) service.execute(push_data(branch: 'v11')) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 33e514cd7b9..5eb31430ccd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -415,7 +415,7 @@ describe Project do project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) end - it 'should return .external pipelines' do + it 'returns .external pipelines' do expect(project.all_pipelines).to all(have_attributes(source: 'external')) expect(project.all_pipelines.size).to eq(1) end @@ -439,7 +439,7 @@ describe Project do project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) end - it 'should return .external pipelines' do + it 'returns .external pipelines' do expect(project.ci_pipelines).to all(have_attributes(source: 'external')) expect(project.ci_pipelines.size).to eq(1) end @@ -1910,7 +1910,7 @@ describe Project do tags: %w[latest rc1]) end - it 'should have image tags' do + it 'has image tags' do expect(project).to have_container_registry_tags end end @@ -1921,7 +1921,7 @@ describe Project do tags: %w[latest rc1 pre1]) end - it 'should have image tags' do + it 'has image tags' do expect(project).to have_container_registry_tags end end @@ -1931,7 +1931,7 @@ describe Project do stub_container_registry_tags(repository: :any, tags: []) end - it 'should not have image tags' do + it 'does not have image tags' do expect(project).not_to have_container_registry_tags end end @@ -1942,16 +1942,16 @@ describe Project do stub_container_registry_config(enabled: false) end - it 'should not have image tags' do + it 'does not have image tags' do expect(project).not_to have_container_registry_tags end - it 'should not check root repository tags' do + it 'does not check root repository tags' do expect(project).not_to receive(:full_path) expect(project).not_to have_container_registry_tags end - it 'should iterate through container repositories' do + it 'iterates through container repositories' do expect(project).to receive(:container_repositories) expect(project).not_to have_container_registry_tags end @@ -2638,7 +2638,7 @@ describe Project do let!(:cluster) { kubernetes_namespace.cluster } let(:project) { kubernetes_namespace.project } - it 'should return token from kubernetes namespace' do + it 'returns token from kubernetes namespace' do expect(project.deployment_variables).to include( { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true } ) diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 0478094034a..f743dfed31f 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -7,14 +7,14 @@ describe RemoteMirror, :mailer do describe 'URL validation' do context 'with a valid URL' do - it 'should be valid' do + it 'is valid' do remote_mirror = build(:remote_mirror) expect(remote_mirror).to be_valid end end context 'with an invalid URL' do - it 'should not be valid' do + it 'is not valid' do remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid') expect(remote_mirror).not_to be_valid diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 2578208659a..3f5d285bc2c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -50,7 +50,7 @@ describe Repository do it { is_expected.not_to include('fix') } describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error do broken_repository.branch_names_contains(sample_commit.id) end @@ -225,7 +225,7 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error do broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore') end @@ -249,7 +249,7 @@ describe Repository do end describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error do broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id end @@ -390,7 +390,7 @@ describe Repository do end describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') } end end @@ -726,7 +726,7 @@ describe Repository do end describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error do broken_repository.search_files_by_content('feature', 'master') end @@ -775,7 +775,7 @@ describe Repository do end describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') } end end @@ -817,7 +817,7 @@ describe Repository do let(:broken_repository) { create(:project, :broken_storage).repository } describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error do broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2') end @@ -1018,7 +1018,7 @@ describe Repository do repository.add_branch(project.creator, ref, 'master') end - it 'should be true' do + it 'is true' do is_expected.to eq(true) end end @@ -1028,7 +1028,7 @@ describe Repository do repository.add_tag(project.creator, ref, 'master') end - it 'should be false' do + it 'is false' do is_expected.to eq(false) end end @@ -1152,7 +1152,7 @@ describe Repository do end context 'with broken storage', :broken_storage do - it 'should raise a storage error' do + it 'raises a storage error' do expect_to_raise_storage_error { broken_repository.exists? } end end @@ -2249,11 +2249,11 @@ describe Repository do let(:commit) { repository.commit } let(:ancestor) { commit.parents.first } - it 'it is an ancestor' do + it 'is an ancestor' do expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true) end - it 'it is not an ancestor' do + it 'is not an ancestor' do expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false) end diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb new file mode 100644 index 00000000000..1854d5f9415 --- /dev/null +++ b/spec/models/serverless/function_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Serverless::Function do + let(:project) { create(:project) } + let(:func) { described_class.new(project, 'test', 'test-ns') } + + it 'has a proper id' do + expect(func.id).to eql("#{project.id}/test/test-ns") + expect(func.name).to eql("test") + expect(func.namespace).to eql("test-ns") + end + + it 'can decode an identifier' do + f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns") + + expect(f.name).to eql("testfunc") + expect(f.namespace).to eql("dummy-ns") + end +end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 2f025038bab..64db32781fe 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -291,7 +291,7 @@ describe Service do describe "#deprecated?" do let(:project) { create(:project, :repository) } - it 'should return false by default' do + it 'returns false by default' do service = create(:service, project: project) expect(service.deprecated?).to be_falsy end @@ -300,7 +300,7 @@ describe Service do describe "#deprecation_message" do let(:project) { create(:project, :repository) } - it 'should be empty by default' do + it 'is empty by default' do service = create(:service, project: project) expect(service.deprecation_message).to be_nil end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b7e36748fa2..a45a2737b13 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -98,6 +98,11 @@ describe User do end describe 'validations' do + describe 'name' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_length_of(:name).is_at_most(128) } + end + describe 'username' do it 'validates presence' do expect(subject).to validate_presence_of(:username) diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index dc98baca6dc..59f3a961d50 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -347,6 +347,120 @@ describe GroupPolicy do end end + context "create_projects" do + context 'when group has no project creation level set' do + let(:group) { create(:group, project_creation_level: nil) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:create_projects) } + end + end + + context 'when group has project creation level set to no one' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_disallowed(:create_projects) } + end + end + + context 'when group has project creation level set to maintainer only' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:create_projects) } + end + end + + context 'when group has project creation level set to developers + maintainer' do + let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:create_projects) } + end + end + end + it_behaves_like 'clusterable policies' do let(:clusterable) { create(:group) } let(:cluster) do diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 676835b3880..e202f7a9b5f 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -180,7 +180,7 @@ describe Ci::BuildPresenter do context 'When build has failed and retried' do let(:build) { create(:ci_build, :script_failure, :retried, pipeline: pipeline) } - it 'should include the reason of failure and the retried title' do + it 'includes the reason of failure and the retried title' do tooltip = subject.tooltip_message expect(tooltip).to eq("#{build.name} - failed - (script failure) (retried)") @@ -190,7 +190,7 @@ describe Ci::BuildPresenter do context 'When build has failed and is allowed to' do let(:build) { create(:ci_build, :script_failure, :allowed_to_fail, pipeline: pipeline) } - it 'should include the reason of failure' do + it 'includes the reason of failure' do tooltip = subject.tooltip_message expect(tooltip).to eq("#{build.name} - failed - (script failure) (allowed to fail)") @@ -200,7 +200,7 @@ describe Ci::BuildPresenter do context 'For any other build (no retried)' do let(:build) { create(:ci_build, :success, pipeline: pipeline) } - it 'should include build name and status' do + it 'includes build name and status' do tooltip = subject.tooltip_message expect(tooltip).to eq("#{build.name} - passed") @@ -210,7 +210,7 @@ describe Ci::BuildPresenter do context 'For any other build (retried)' do let(:build) { create(:ci_build, :success, :retried, pipeline: pipeline) } - it 'should include build name and status' do + it 'includes build name and status' do tooltip = subject.tooltip_message expect(tooltip).to eq("#{build.name} - passed (retried)") @@ -269,7 +269,7 @@ describe Ci::BuildPresenter do context 'when is a script or missing dependency failure' do let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) } - it 'should return false' do + it 'returns false' do failure_reasons.each do |failure_reason| build.update_attribute(:failure_reason, failure_reason) expect(presenter.recoverable?).to be_falsy @@ -280,7 +280,7 @@ describe Ci::BuildPresenter do context 'when is any other failure type' do let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) } - it 'should return true' do + it 'returns true' do failure_reasons.each do |failure_reason| build.update_attribute(:failure_reason, failure_reason) expect(presenter.recoverable?).to be_truthy diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 537194b8e11..0919540e4ba 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -498,6 +498,40 @@ describe API::Internal do end end + context "console message" do + before do + project.add_developer(user) + end + + context "git pull" do + context "with no console message" do + it "has the correct payload" do + pull(key, project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gl_console_messages']).to eq([]) + end + end + + context "with a console message" do + let(:console_messages) { ['message for the console'] } + + it "has the correct payload" do + expect_next_instance_of(Gitlab::GitAccess) do |access| + expect(access).to receive(:check_for_console_messages) + .with('git-upload-pack') + .and_return(console_messages) + end + + pull(key, project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gl_console_messages']).to eq(console_messages) + end + end + end + end + context "blocked user" do let(:personal_project) { create(:project, namespace: user.namespace) } @@ -610,6 +644,22 @@ describe API::Internal do expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end + + it 'returns a 200 response when using a project path that does not exist' do + post( + api("/internal/allowed"), + params: { + key_id: key.id, + project: 'project/does-not-exist.git', + action: 'git-upload-pack', + secret_token: secret_token, + protocol: 'ssh' + } + ) + + expect(response).to have_gitlab_http_status(404) + expect(json_response["status"]).to be_falsey + end end context 'user does not exist' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index a5434d3ea80..86484ce62f8 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -2189,6 +2189,18 @@ describe API::Issues do expect_paginated_array_response(related_mr.id) end + context 'merge request closes an issue' do + let!(:closing_issue_mr_rel) do + create(:merge_requests_closing_issues, issue: issue, merge_request: related_mr) + end + + it 'returns closing MR only once' do + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response([related_mr.id]) + end + end + context 'no merge request mentioned a issue' do it 'returns empty array' do get_related_merge_requests(project.id, closed_issue.iid, user) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index c26d31c5e0d..9fed07cae82 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -435,7 +435,7 @@ describe API::Pipelines do end context 'unauthorized user' do - it 'should not return a project pipeline' do + it 'does not return a project pipeline' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) expect(response).to have_gitlab_http_status(404) @@ -481,7 +481,7 @@ describe API::Pipelines do context 'unauthorized user' do context 'when user is not member' do - it 'should return a 404' do + it 'returns a 404' do delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) expect(response).to have_gitlab_http_status(404) @@ -496,7 +496,7 @@ describe API::Pipelines do project.add_developer(developer) end - it 'should return a 403' do + it 'returns a 403' do delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer) expect(response).to have_gitlab_http_status(403) @@ -526,7 +526,7 @@ describe API::Pipelines do end context 'unauthorized user' do - it 'should not return a project pipeline' do + it 'does not return a project pipeline' do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) expect(response).to have_gitlab_http_status(404) diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index 81442125a1c..94e6ca2c07c 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -22,7 +22,7 @@ describe API::ProjectClusters do end context 'non-authorized user' do - it 'should respond with 404' do + it 'responds with 404' do get api("/projects/#{project.id}/clusters", non_member) expect(response).to have_gitlab_http_status(404) @@ -34,15 +34,15 @@ describe API::ProjectClusters do get api("/projects/#{project.id}/clusters", current_user) end - it 'should respond with 200' do + it 'responds with 200' do expect(response).to have_gitlab_http_status(200) end - it 'should include pagination headers' do + it 'includes pagination headers' do expect(response).to include_pagination_headers end - it 'should only include authorized clusters' do + it 'onlies include authorized clusters' do cluster_ids = json_response.map { |cluster| cluster['id'] } expect(cluster_ids).to match_array(clusters.pluck(:id)) @@ -67,7 +67,7 @@ describe API::ProjectClusters do end context 'non-authorized user' do - it 'should respond with 404' do + it 'responds with 404' do get api("/projects/#{project.id}/clusters/#{cluster_id}", non_member) expect(response).to have_gitlab_http_status(404) @@ -132,7 +132,7 @@ describe API::ProjectClusters do projects: [project]) end - it 'should not include GCP provider info' do + it 'does not include GCP provider info' do expect(json_response['provider_gcp']).not_to be_present end end @@ -194,7 +194,7 @@ describe API::ProjectClusters do end context 'non-authorized user' do - it 'should respond with 404' do + it 'responds with 404' do post api("/projects/#{project.id}/clusters/user", non_member), params: cluster_params expect(response).to have_gitlab_http_status(404) @@ -207,11 +207,11 @@ describe API::ProjectClusters do end context 'with valid params' do - it 'should respond with 201' do + it 'responds with 201' do expect(response).to have_gitlab_http_status(201) end - it 'should create a new Cluster::Cluster' do + it 'creates a new Cluster::Cluster' do cluster_result = Clusters::Cluster.find(json_response["id"]) platform_kubernetes = cluster_result.platform @@ -246,7 +246,7 @@ describe API::ProjectClusters do context 'when user sets authorization type as ABAC' do let(:authorization_type) { 'abac' } - it 'should create an ABAC cluster' do + it 'creates an ABAC cluster' do cluster_result = Clusters::Cluster.find(json_response['id']) expect(cluster_result.platform.abac?).to be_truthy @@ -256,15 +256,15 @@ describe API::ProjectClusters do context 'with invalid params' do let(:namespace) { 'invalid_namespace' } - it 'should respond with 400' do + it 'responds with 400' do expect(response).to have_gitlab_http_status(400) end - it 'should not create a new Clusters::Cluster' do + it 'does not create a new Clusters::Cluster' do expect(project.reload.clusters).to be_empty end - it 'should return validation errors' do + it 'returns validation errors' do expect(json_response['message']['platform_kubernetes.namespace'].first).to be_present end end @@ -278,11 +278,11 @@ describe API::ProjectClusters do post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params end - it 'should respond with 403' do + it 'responds with 403' do expect(response).to have_gitlab_http_status(403) end - it 'should return an appropriate message' do + it 'returns an appropriate message' do expect(json_response['message']).to include('Instance does not support multiple Kubernetes clusters') end end @@ -314,7 +314,7 @@ describe API::ProjectClusters do end context 'non-authorized user' do - it 'should respond with 404' do + it 'responds with 404' do put api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: update_params expect(response).to have_gitlab_http_status(404) @@ -329,11 +329,11 @@ describe API::ProjectClusters do end context 'with valid params' do - it 'should respond with 200' do + it 'responds with 200' do expect(response).to have_gitlab_http_status(200) end - it 'should update cluster attributes' do + it 'updates cluster attributes' do expect(cluster.domain).to eq('new-domain.com') expect(cluster.platform_kubernetes.namespace).to eq('new-namespace') end @@ -342,17 +342,17 @@ describe API::ProjectClusters do context 'with invalid params' do let(:namespace) { 'invalid_namespace' } - it 'should respond with 400' do + it 'responds with 400' do expect(response).to have_gitlab_http_status(400) end - it 'should not update cluster attributes' do + it 'does not update cluster attributes' do expect(cluster.domain).not_to eq('new_domain.com') expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace') expect(cluster.kubernetes_namespace.namespace).not_to eq('invalid_namespace') end - it 'should return validation errors' do + it 'returns validation errors' do expect(json_response['message']['platform_kubernetes.namespace'].first).to match('can contain only lowercase letters') end end @@ -366,11 +366,11 @@ describe API::ProjectClusters do } end - it 'should respond with 400' do + it 'responds with 400' do expect(response).to have_gitlab_http_status(400) end - it 'should return validation error' do + it 'returns validation error' do expect(json_response['message']['platform_kubernetes.base'].first).to eq('Cannot modify managed Kubernetes cluster') end end @@ -378,7 +378,7 @@ describe API::ProjectClusters do context 'when user tries to change namespace' do let(:namespace) { 'new-namespace' } - it 'should respond with 200' do + it 'responds with 200' do expect(response).to have_gitlab_http_status(200) end end @@ -407,11 +407,11 @@ describe API::ProjectClusters do } end - it 'should respond with 200' do + it 'responds with 200' do expect(response).to have_gitlab_http_status(200) end - it 'should update platform kubernetes attributes' do + it 'updates platform kubernetes attributes' do platform_kubernetes = cluster.platform_kubernetes expect(cluster.name).to eq('new-name') @@ -424,7 +424,7 @@ describe API::ProjectClusters do context 'with a cluster that does not belong to user' do let(:cluster) { create(:cluster, :project, :provided_by_user) } - it 'should respond with 404' do + it 'responds with 404' do expect(response).to have_gitlab_http_status(404) end end @@ -440,7 +440,7 @@ describe API::ProjectClusters do end context 'non-authorized user' do - it 'should respond with 404' do + it 'responds with 404' do delete api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: cluster_params expect(response).to have_gitlab_http_status(404) @@ -452,18 +452,18 @@ describe API::ProjectClusters do delete api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: cluster_params end - it 'should respond with 204' do + it 'responds with 204' do expect(response).to have_gitlab_http_status(204) end - it 'should delete the cluster' do + it 'deletes the cluster' do expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy end context 'with a cluster that does not belong to user' do let(:cluster) { create(:cluster, :project, :provided_by_user) } - it 'should respond with 404' do + it 'responds with 404' do expect(response).to have_gitlab_http_status(404) end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index f33eb5b9e02..f869325e892 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -44,6 +44,7 @@ describe API::Settings, 'Settings' do put api("/application/settings", admin), params: { default_projects_limit: 3, + default_project_creation: 2, password_authentication_enabled_for_web: false, repository_storages: ['custom'], plantuml_enabled: true, @@ -64,12 +65,13 @@ describe API::Settings, 'Settings' do performance_bar_allowed_group_path: group.full_path, instance_statistics_visibility_private: true, diff_max_patch_bytes: 150_000, - default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE, + default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE, local_markdown_version: 3 } expect(response).to have_gitlab_http_status(200) expect(json_response['default_projects_limit']).to eq(3) + expect(json_response['default_project_creation']).to eq(::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) expect(json_response['password_authentication_enabled_for_web']).to be_falsey expect(json_response['repository_storages']).to eq(['custom']) expect(json_response['plantuml_enabled']).to be_truthy diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index be6aa7c65c3..dbfb3eace83 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -14,7 +14,7 @@ describe AnalyticsStageSerializer do allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) end - it 'it generates payload for single object' do + it 'generates payload for single object' do expect(subject).to be_kind_of Hash end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb index 236c244b402..8fa0574bfd6 100644 --- a/spec/serializers/analytics_summary_serializer_spec.rb +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -18,7 +18,7 @@ describe AnalyticsSummarySerializer do .to receive(:value).and_return(1.12) end - it 'it generates payload for single object' do + it 'generates payload for single object' do expect(subject).to be_kind_of Hash end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index f6bd6e9ede4..1edf69dc290 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -112,5 +112,15 @@ describe BuildDetailsEntity do expect(subject['merge_request_path']).to be_nil end end + + context 'when the build has failed' do + let(:build) { create(:ci_build, :created) } + + before do + build.drop!(:unmet_prerequisites) + end + + it { is_expected.to include(failure_reason: 'unmet_prerequisites') } + end end end diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index 791b64dc356..c2312734042 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -54,7 +54,7 @@ describe EnvironmentEntity do projects: [project]) end - it 'should include cluster_type' do + it 'includes cluster_type' do expect(subject).to include(:cluster_type) expect(subject[:cluster_type]).to eq('project_type') end @@ -65,7 +65,7 @@ describe EnvironmentEntity do create(:kubernetes_service, project: project) end - it 'should not include cluster_type' do + it 'does not include cluster_type' do expect(subject).not_to include(:cluster_type) end end diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 851b41a7f7e..8de61d4d466 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -154,15 +154,15 @@ describe JobEntity do expect(subject[:status][:label]).to eq('failed') end - it 'should indicate the failure reason on tooltip' do + it 'indicates the failure reason on tooltip' do expect(subject[:status][:tooltip]).to eq('failed - (API failure)') end - it 'should include a callout message with a verbose output' do + it 'includes a callout message with a verbose output' do expect(subject[:callout_message]).to eq('There has been an API failure, please try again') end - it 'should state that it is not recoverable' do + it 'states that it is not recoverable' do expect(subject[:recoverable]).to be_truthy end end @@ -178,15 +178,15 @@ describe JobEntity do expect(subject[:status][:label]).to eq('failed (allowed to fail)') end - it 'should indicate the failure reason on tooltip' do + it 'indicates the failure reason on tooltip' do expect(subject[:status][:tooltip]).to eq('failed - (API failure) (allowed to fail)') end - it 'should include a callout message with a verbose output' do + it 'includes a callout message with a verbose output' do expect(subject[:callout_message]).to eq('There has been an API failure, please try again') end - it 'should state that it is not recoverable' do + it 'states that it is not recoverable' do expect(subject[:recoverable]).to be_truthy end end @@ -194,7 +194,7 @@ describe JobEntity do context 'when the job failed with a script failure' do let(:job) { create(:ci_build, :failed, :script_failure) } - it 'should not include callout message or recoverable keys' do + it 'does not include callout message or recoverable keys' do expect(subject).not_to include('callout_message') expect(subject).not_to include('recoverable') end @@ -203,7 +203,7 @@ describe JobEntity do context 'when job failed and is recoverable' do let(:job) { create(:ci_build, :api_failure) } - it 'should state it is recoverable' do + it 'states it is recoverable' do expect(subject[:recoverable]).to be_truthy end end @@ -211,7 +211,7 @@ describe JobEntity do context 'when job passed' do let(:job) { create(:ci_build, :success) } - it 'should not include callout message or recoverable keys' do + it 'does not include callout message or recoverable keys' do expect(subject).not_to include('callout_message') expect(subject).not_to include('recoverable') end diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 0e99ef38d2f..b89898f26f7 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -287,7 +287,7 @@ describe MergeRequestWidgetEntity do resource.commits.find { |c| c.short_id == short_id } end - it 'should not include merge commits' do + it 'does not include merge commits' do commits_in_widget = subject[:commits_without_merge_commits] expect(commits_in_widget.length).to be < resource.commits.length diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb index 11a65d0c300..382b9043566 100644 --- a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb @@ -89,7 +89,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do it_behaves_like 'creates service account and token' - it 'should create a cluster role binding with cluster-admin access' do + it 'creates a cluster role binding with cluster-admin access' do subject expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with( diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb index 3a2bbf1ecd1..1bd0356a73b 100644 --- a/spec/services/deploy_tokens/create_service_spec.rb +++ b/spec/services/deploy_tokens/create_service_spec.rb @@ -9,11 +9,11 @@ describe DeployTokens::CreateService do subject { described_class.new(project, user, deploy_token_params).execute } context 'when the deploy token is valid' do - it 'should create a new DeployToken' do + it 'creates a new DeployToken' do expect { subject }.to change { DeployToken.count }.by(1) end - it 'should create a new ProjectDeployToken' do + it 'creates a new ProjectDeployToken' do expect { subject }.to change { ProjectDeployToken.count }.by(1) end @@ -25,7 +25,7 @@ describe DeployTokens::CreateService do context 'when expires at date is not passed' do let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') } - it 'should set Forever.date' do + it 'sets Forever.date' do expect(subject.read_attribute(:expires_at)).to eq(Forever.date) end end @@ -33,11 +33,11 @@ describe DeployTokens::CreateService do context 'when the deploy token is invalid' do let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) } - it 'should not create a new DeployToken' do + it 'does not create a new DeployToken' do expect { subject }.not_to change { DeployToken.count } end - it 'should not create a new ProjectDeployToken' do + it 'does not create a new ProjectDeployToken' do expect { subject }.not_to change { ProjectDeployToken.count } end end diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb index a92d3376f7b..730fccc599e 100644 --- a/spec/services/error_tracking/list_projects_service_spec.rb +++ b/spec/services/error_tracking/list_projects_service_spec.rb @@ -51,14 +51,28 @@ describe ErrorTracking::ListProjectsService do end context 'sentry client raises exception' do - before do - expect(error_tracking_setting).to receive(:list_sentry_projects) - .and_raise(Sentry::Client::Error, 'Sentry response status code: 500') + context 'Sentry::Client::Error' do + before do + expect(error_tracking_setting).to receive(:list_sentry_projects) + .and_raise(Sentry::Client::Error, 'Sentry response status code: 500') + end + + it 'returns error response' do + expect(result[:message]).to eq('Sentry response status code: 500') + expect(result[:http_status]).to eq(:bad_request) + end end - it 'returns error response' do - expect(result[:message]).to eq('Sentry response status code: 500') - expect(result[:http_status]).to eq(:bad_request) + context 'Sentry::Client::MissingKeysError' do + before do + expect(error_tracking_setting).to receive(:list_sentry_projects) + .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') + end + + it 'returns error response' do + expect(result[:message]).to eq('Sentry API response is missing keys. key not found: "id"') + expect(result[:http_status]).to eq(:internal_server_error) + end end end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 79d504b9b45..0bc67dbb4a1 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -12,11 +12,11 @@ describe Groups::TransferService, :postgresql do allow(Group).to receive(:supports_nested_objects?).and_return(false) end - it 'should return false' do + it 'returns false' do expect(transfer_service.execute(new_parent_group)).to be_falsy end - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq('Transfer failed: Database is not supported.') end @@ -30,11 +30,11 @@ describe Groups::TransferService, :postgresql do create(:group_member, :owner, group: new_parent_group, user: user) end - it 'should return false' do + it 'returns false' do expect(transfer_service.execute(new_parent_group)).to be_falsy end - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq('Transfer failed: namespace directory cannot be moved') end @@ -50,7 +50,7 @@ describe Groups::TransferService, :postgresql do context 'when the group is already a root group' do let(:group) { create(:group, :public) } - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(nil) expect(transfer_service.error).to eq('Transfer failed: Group is already a root group.') end @@ -59,11 +59,11 @@ describe Groups::TransferService, :postgresql do context 'when the user does not have the right policies' do let!(:group_member) { create(:group_member, :guest, group: group, user: user) } - it "should return false" do + it "returns false" do expect(transfer_service.execute(nil)).to be_falsy end - it "should add an error on group" do + it "adds an error on group" do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.") end @@ -76,11 +76,11 @@ describe Groups::TransferService, :postgresql do create(:group, path: 'not-unique') end - it 'should return false' do + it 'returns false' do expect(transfer_service.execute(nil)).to be_falsy end - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(nil) expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.') end @@ -96,17 +96,17 @@ describe Groups::TransferService, :postgresql do group.reload end - it 'should update group attributes' do + it 'updates group attributes' do expect(group.parent).to be_nil end - it 'should update group children path' do + it 'updates group children path' do group.children.each do |subgroup| expect(subgroup.full_path).to eq("#{group.path}/#{subgroup.path}") end end - it 'should update group projects path' do + it 'updates group projects path' do group.projects.each do |project| expect(project.full_path).to eq("#{group.path}/#{project.path}") end @@ -122,11 +122,11 @@ describe Groups::TransferService, :postgresql do context 'when the new parent group is the same as the previous parent group' do let(:group) { create(:group, :public, :nested, parent: new_parent_group) } - it 'should return false' do + it 'returns false' do expect(transfer_service.execute(new_parent_group)).to be_falsy end - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq('Transfer failed: Group is already associated to the parent group.') end @@ -135,11 +135,11 @@ describe Groups::TransferService, :postgresql do context 'when the user does not have the right policies' do let!(:group_member) { create(:group_member, :guest, group: group, user: user) } - it "should return false" do + it "returns false" do expect(transfer_service.execute(new_parent_group)).to be_falsy end - it "should add an error on group" do + it "adds an error on group" do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.") end @@ -152,11 +152,11 @@ describe Groups::TransferService, :postgresql do create(:group, path: "not-unique", parent: new_parent_group) end - it 'should return false' do + it 'returns false' do expect(transfer_service.execute(new_parent_group)).to be_falsy end - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.') end @@ -171,11 +171,11 @@ describe Groups::TransferService, :postgresql do group.update_attribute(:path, 'foo') end - it 'should return false' do + it 'returns false' do expect(transfer_service.execute(new_parent_group)).to be_falsy end - it 'should add an error on group' do + it 'adds an error on group' do transfer_service.execute(new_parent_group) expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken') end @@ -191,7 +191,7 @@ describe Groups::TransferService, :postgresql do let(:new_parent_group) { create(:group, :public) } let(:group) { create(:group, :private, :nested) } - it 'should not update the visibility for the group' do + it 'does not update the visibility for the group' do group.reload expect(group.private?).to be_truthy expect(group.visibility_level).not_to eq(new_parent_group.visibility_level) @@ -202,27 +202,27 @@ describe Groups::TransferService, :postgresql do let(:new_parent_group) { create(:group, :private) } let(:group) { create(:group, :public, :nested) } - it 'should update visibility level based on the parent group' do + it 'updates visibility level based on the parent group' do group.reload expect(group.private?).to be_truthy expect(group.visibility_level).to eq(new_parent_group.visibility_level) end end - it 'should update visibility for the group based on the parent group' do + it 'updates visibility for the group based on the parent group' do expect(group.visibility_level).to eq(new_parent_group.visibility_level) end - it 'should update parent group to the new parent ' do + it 'updates parent group to the new parent' do expect(group.parent).to eq(new_parent_group) end - it 'should return the group as children of the new parent' do + it 'returns the group as children of the new parent' do expect(new_parent_group.children.count).to eq(1) expect(new_parent_group.children.first).to eq(group) end - it 'should create a redirect for the group' do + it 'creates a redirect for the group' do expect(group.redirect_routes.count).to eq(1) end end @@ -236,21 +236,21 @@ describe Groups::TransferService, :postgresql do transfer_service.execute(new_parent_group) end - it 'should update subgroups path' do + it 'updates subgroups path' do new_parent_path = new_parent_group.path group.children.each do |subgroup| expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}") end end - it 'should create redirects for the subgroups' do + it 'creates redirects for the subgroups' do expect(group.redirect_routes.count).to eq(1) expect(subgroup1.redirect_routes.count).to eq(1) expect(subgroup2.redirect_routes.count).to eq(1) end context 'when the new parent has a higher visibility than the children' do - it 'should not update the children visibility' do + it 'does not update the children visibility' do expect(subgroup1.private?).to be_truthy expect(subgroup2.internal?).to be_truthy end @@ -261,7 +261,7 @@ describe Groups::TransferService, :postgresql do let!(:subgroup2) { create(:group, :public, parent: group) } let(:new_parent_group) { create(:group, :private) } - it 'should update children visibility to match the new parent' do + it 'updates children visibility to match the new parent' do group.children.each do |subgroup| expect(subgroup.private?).to be_truthy end @@ -279,21 +279,21 @@ describe Groups::TransferService, :postgresql do transfer_service.execute(new_parent_group) end - it 'should update projects path' do + it 'updates projects path' do new_parent_path = new_parent_group.path group.projects.each do |project| expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}") end end - it 'should create permanent redirects for the projects' do + it 'creates permanent redirects for the projects' do expect(group.redirect_routes.count).to eq(1) expect(project1.redirect_routes.count).to eq(1) expect(project2.redirect_routes.count).to eq(1) end context 'when the new parent has a higher visibility than the projects' do - it 'should not update projects visibility' do + it 'does not update projects visibility' do expect(project1.private?).to be_truthy expect(project2.internal?).to be_truthy end @@ -304,7 +304,7 @@ describe Groups::TransferService, :postgresql do let!(:project2) { create(:project, :repository, :public, namespace: group) } let(:new_parent_group) { create(:group, :private) } - it 'should update projects visibility to match the new parent' do + it 'updates projects visibility to match the new parent' do group.projects.each do |project| expect(project.private?).to be_truthy end @@ -324,21 +324,21 @@ describe Groups::TransferService, :postgresql do transfer_service.execute(new_parent_group) end - it 'should update subgroups path' do + it 'updates subgroups path' do new_parent_path = new_parent_group.path group.children.each do |subgroup| expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}") end end - it 'should update projects path' do + it 'updates projects path' do new_parent_path = new_parent_group.path group.projects.each do |project| expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}") end end - it 'should create redirect for the subgroups and projects' do + it 'creates redirect for the subgroups and projects' do expect(group.redirect_routes.count).to eq(1) expect(subgroup1.redirect_routes.count).to eq(1) expect(subgroup2.redirect_routes.count).to eq(1) @@ -360,7 +360,7 @@ describe Groups::TransferService, :postgresql do transfer_service.execute(new_parent_group) end - it 'should update subgroups path' do + it 'updates subgroups path' do new_base_path = "#{new_parent_group.path}/#{group.path}" group.children.each do |children| expect(children.full_path).to eq("#{new_base_path}/#{children.path}") @@ -372,7 +372,7 @@ describe Groups::TransferService, :postgresql do end end - it 'should update projects path' do + it 'updates projects path' do new_parent_path = "#{new_parent_group.path}/#{group.path}" subgroup1.projects.each do |project| project_full_path = "#{new_parent_path}/#{project.namespace.path}/#{project.name}" @@ -380,7 +380,7 @@ describe Groups::TransferService, :postgresql do end end - it 'should create redirect for the subgroups and projects' do + it 'creates redirect for the subgroups and projects' do expect(group.redirect_routes.count).to eq(1) expect(project1.redirect_routes.count).to eq(1) expect(subgroup1.redirect_routes.count).to eq(1) @@ -402,7 +402,7 @@ describe Groups::TransferService, :postgresql do transfer_service.execute(new_parent_group) end - it 'should restore group and projects visibility' do + it 'restores group and projects visibility' do subgroup1.reload project1.reload expect(subgroup1.public?).to be_truthy diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 86e58fe06b9..74f1e83b362 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -58,8 +58,10 @@ describe Issues::BuildService do "> That has a quote\n"\ ">>>\n" note_result = " > This is a string\n"\ + " > \n"\ " > > with a blockquote\n"\ - " > > > That has a quote\n" + " > > > That has a quote\n"\ + " > \n" discussion = create(:diff_note_on_merge_request, note: note_text).to_discussion expect(service.item_for_discussion(discussion)).to include(note_result) end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index af0a214c00f..39a2ef579dd 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -77,6 +77,22 @@ describe MergeRequests::AddTodoWhenBuildFailsService do service.execute(commit_status) end end + + context 'when build belongs to a merge request pipeline' do + let(:pipeline) do + create(:ci_pipeline, source: :merge_request_event, + ref: merge_request.merge_ref_path, + merge_request: merge_request, + merge_requests_as_head_pipeline: [merge_request]) + end + + let(:commit_status) { create(:ci_build, ref: merge_request.merge_ref_path, pipeline: pipeline) } + + it 'notifies the todo service' do + expect(todo_service).to receive(:merge_request_build_failed).with(merge_request) + service.execute(commit_status) + end + end end describe '#close' do @@ -106,6 +122,22 @@ describe MergeRequests::AddTodoWhenBuildFailsService do service.close(commit_status) end end + + context 'when build belongs to a merge request pipeline' do + let(:pipeline) do + create(:ci_pipeline, source: :merge_request_event, + ref: merge_request.merge_ref_path, + merge_request: merge_request, + merge_requests_as_head_pipeline: [merge_request]) + end + + let(:commit_status) { create(:ci_build, ref: merge_request.merge_ref_path, pipeline: pipeline) } + + it 'notifies the todo service' do + expect(todo_service).to receive(:merge_request_build_retried).with(merge_request) + service.close(commit_status) + end + end end describe '#close_all' do diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index 52bbd4e794d..8b7db1b2f1f 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -95,7 +95,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do sha: '1234abcdef', status: 'success') end - it 'it does not merge request' do + it 'does not merge request' do expect(MergeWorker).not_to receive(:perform_async) service.trigger(old_pipeline) end @@ -112,6 +112,21 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.trigger(unrelated_pipeline) end end + + context 'when pipeline is merge request pipeline' do + let(:pipeline) do + create(:ci_pipeline, :success, + source: :merge_request_event, + ref: mr_merge_if_green_enabled.merge_ref_path, + merge_request: mr_merge_if_green_enabled, + merge_requests_as_head_pipeline: [mr_merge_if_green_enabled]) + end + + it 'merges the associated merge request' do + expect(MergeWorker).to receive(:perform_async) + service.trigger(pipeline) + end + end end describe "#cancel" do diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 5cf3577f01f..bd10523bc94 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -454,35 +454,35 @@ describe MergeRequests::RefreshService do end let(:force_push_commit) { @project.commit('feature').id } - it 'should reload a new diff for a push to the forked project' do + it 'reloads a new diff for a push to the forked project' do expect do service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end - it 'should reload a new diff for a force push to the source branch' do + it 'reloads a new diff for a force push to the source branch' do expect do service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end - it 'should reload a new diff for a force push to the target branch' do + it 'reloads a new diff for a force push to the target branch' do expect do service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end - it 'should reload a new diff for a push to the target project that contains a commit in the MR' do + it 'reloads a new diff for a push to the target project that contains a commit in the MR' do expect do service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end - it 'should not increase the diff count for a new push to target branch' do + it 'does not increase the diff count for a new push to target branch' do new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file', message: 'This is a test', branch_name: 'master') diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 7d2b6d5b8a7..9efdf96bc64 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -185,6 +185,7 @@ describe Notes::QuickActionsService do end before do + stub_licensed_features(multiple_issue_assignees: false) project.add_maintainer(maintainer) project.add_maintainer(assignee) end diff --git a/spec/services/projects/auto_devops/disable_service_spec.rb b/spec/services/projects/auto_devops/disable_service_spec.rb index 76977d7a1a7..fb1ab3f9949 100644 --- a/spec/services/projects/auto_devops/disable_service_spec.rb +++ b/spec/services/projects/auto_devops/disable_service_spec.rb @@ -46,7 +46,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do create(:ci_pipeline, :failed, :auto_devops_source, project: project) end - it 'should disable Auto DevOps for project' do + it 'disables Auto DevOps for project' do subject expect(auto_devops.enabled).to eq(false) @@ -58,7 +58,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do create_list(:ci_pipeline, 2, :failed, :auto_devops_source, project: project) end - it 'should explicitly disable Auto DevOps for project' do + it 'explicitly disables Auto DevOps for project' do subject expect(auto_devops.reload.enabled).to eq(false) @@ -70,7 +70,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do create(:ci_pipeline, :success, :auto_devops_source, project: project) end - it 'should not disable Auto DevOps for project' do + it 'does not disable Auto DevOps for project' do subject expect(auto_devops.reload.enabled).to be_nil @@ -85,14 +85,14 @@ describe Projects::AutoDevops::DisableService, '#execute' do create(:ci_pipeline, :failed, :auto_devops_source, project: project) end - it 'should disable Auto DevOps for project' do + it 'disables Auto DevOps for project' do subject auto_devops = project.reload.auto_devops expect(auto_devops.enabled).to eq(false) end - it 'should create a ProjectAutoDevops record' do + it 'creates a ProjectAutoDevops record' do expect { subject }.to change { ProjectAutoDevops.count }.from(0).to(1) end end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 4b6d0c51363..8455b9bc3cf 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -41,12 +41,12 @@ describe Projects::ParticipantsService do group.add_owner(user) end - it 'should return an url for the avatar' do + it 'returns an url for the avatar' do expect(service.groups.size).to eq 1 expect(service.groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png") end - it 'should return an url for the avatar with relative url' do + it 'returns an url for the avatar with relative url' do stub_config_setting(relative_url_root: '/gitlab') stub_config_setting(url: Settings.send(:build_gitlab_url)) diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb new file mode 100644 index 00000000000..4bdb20de4c9 --- /dev/null +++ b/spec/services/prometheus/proxy_service_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Prometheus::ProxyService do + include ReactiveCachingHelpers + + set(:project) { create(:project) } + set(:environment) { create(:environment, project: project) } + + describe '#initialize' do + let(:params) { ActionController::Parameters.new(query: '1').permit! } + + it 'initializes attributes' do + result = described_class.new(environment, 'GET', 'query', params) + + expect(result.proxyable).to eq(environment) + expect(result.method).to eq('GET') + expect(result.path).to eq('query') + expect(result.params).to eq('query' => '1') + end + + it 'converts ActionController::Parameters into hash' do + result = described_class.new(environment, 'GET', 'query', params) + + expect(result.params).to be_an_instance_of(Hash) + end + + context 'with unknown params' do + let(:params) { ActionController::Parameters.new(query: '1', other_param: 'val').permit! } + + it 'filters unknown params' do + result = described_class.new(environment, 'GET', 'query', params) + + expect(result.params).to eq('query' => '1') + end + end + end + + describe '#execute' do + let(:prometheus_adapter) { instance_double(PrometheusService) } + let(:params) { ActionController::Parameters.new(query: '1').permit! } + + subject { described_class.new(environment, 'GET', 'query', params) } + + context 'when prometheus_adapter is nil' do + before do + allow(environment).to receive(:prometheus_adapter).and_return(nil) + end + + it 'returns error' do + expect(subject.execute).to eq( + status: :error, + message: 'No prometheus server found', + http_status: :service_unavailable + ) + end + end + + context 'when prometheus_adapter cannot query' do + before do + allow(environment).to receive(:prometheus_adapter).and_return(prometheus_adapter) + allow(prometheus_adapter).to receive(:can_query?).and_return(false) + end + + it 'returns error' do + expect(subject.execute).to eq( + status: :error, + message: 'No prometheus server found', + http_status: :service_unavailable + ) + end + end + + context 'cannot proxy' do + subject { described_class.new(environment, 'POST', 'garbage', params) } + + it 'returns error' do + expect(subject.execute).to eq( + message: 'Proxy support for this API is not available currently', + status: :error + ) + end + end + + context 'with caching', :use_clean_rails_memory_store_caching do + let(:return_value) { { 'http_status' => 200, 'body' => 'body' } } + + let(:opts) do + [environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }] + end + + before do + allow(environment).to receive(:prometheus_adapter) + .and_return(prometheus_adapter) + allow(prometheus_adapter).to receive(:can_query?).and_return(true) + end + + context 'when value present in cache' do + before do + stub_reactive_cache(subject, return_value, opts) + end + + it 'returns cached value' do + result = subject.execute + + expect(result[:http_status]).to eq(return_value[:http_status]) + expect(result[:body]).to eq(return_value[:body]) + end + end + + context 'when value not present in cache' do + it 'returns nil' do + expect(ReactiveCachingWorker) + .to receive(:perform_async) + .with(subject.class, subject.id, *opts) + + result = subject.execute + + expect(result).to eq(nil) + end + end + end + + context 'call prometheus api' do + let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) } + + before do + synchronous_reactive_cache(subject) + + allow(environment).to receive(:prometheus_adapter) + .and_return(prometheus_adapter) + allow(prometheus_adapter).to receive(:can_query?).and_return(true) + allow(prometheus_adapter).to receive(:prometheus_client_wrapper) + .and_return(prometheus_client) + end + + context 'connection to prometheus server succeeds' do + let(:rest_client_response) { instance_double(RestClient::Response) } + let(:prometheus_http_status_code) { 400 } + + let(:response_body) do + '{"status":"error","errorType":"bad_data","error":"parse error at char 1: no expression found in input"}' + end + + before do + allow(prometheus_client).to receive(:proxy).and_return(rest_client_response) + + allow(rest_client_response).to receive(:code) + .and_return(prometheus_http_status_code) + allow(rest_client_response).to receive(:body).and_return(response_body) + end + + it 'returns the http status code and body from prometheus' do + expect(subject.execute).to eq( + http_status: prometheus_http_status_code, + body: response_body, + status: :success + ) + end + end + + context 'connection to prometheus server fails' do + context 'prometheus client raises Gitlab::PrometheusClient::Error' do + before do + allow(prometheus_client).to receive(:proxy) + .and_raise(Gitlab::PrometheusClient::Error, 'Network connection error') + end + + it 'returns error' do + expect(subject.execute).to eq( + status: :error, + message: 'Network connection error', + http_status: :service_unavailable + ) + end + end + end + end + end + + describe '.from_cache' do + it 'initializes an instance of ProxyService class' do + result = described_class.from_cache( + environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' } + ) + + expect(result).to be_an_instance_of(described_class) + expect(result.proxyable).to eq(environment) + expect(result.method).to eq('GET') + expect(result.path).to eq('query') + expect(result.params).to eq('query' => '1') + end + end +end diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb index b1260cf740a..9adaee6481b 100644 --- a/spec/services/task_list_toggle_service_spec.rb +++ b/spec/services/task_list_toggle_service_spec.rb @@ -113,4 +113,25 @@ describe TaskListToggleService do expect(toggler.execute).to be_falsey end + + it 'properly handles a GitLab blockquote' do + markdown = + <<-EOT.strip_heredoc + >>> + gitlab blockquote + >>> + + * [ ] Task 1 + * [x] Task 2 + EOT + + markdown_html = Banzai::Pipeline::FullPipeline.call(markdown, project: nil)[:output].to_html + toggler = described_class.new(markdown, markdown_html, + toggle_as_checked: true, + line_source: '* [ ] Task 1', line_number: 5) + + expect(toggler.execute).to be_truthy + expect(toggler.updated_markdown.lines[4]).to eq "* [x] Task 1\n" + expect(toggler.updated_markdown_html).to include('disabled checked> Task 1') + end end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 42a086d58d2..5b79c40f27b 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -224,7 +224,7 @@ shared_examples 'discussion comments' do |resource_name| find(toggle_selector).click end - it 'should have "Start discussion" selected' do + it 'has "Start discussion" selected' do find("#{menu_selector} li", match: :first) items = all("#{menu_selector} li") @@ -267,7 +267,7 @@ shared_examples 'discussion comments' do |resource_name| end end - it 'should have "Comment" selected when opening the menu' do + it 'has "Comment" selected when opening the menu' do find(toggle_selector).click find("#{menu_selector} li", match: :first) diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index 08d1d7a6059..87f825152cf 100644 --- a/spec/support/helpers/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb @@ -7,6 +7,10 @@ module PrometheusHelpers %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100} end + def prometheus_istio_query(function_name, kube_namespace) + %{floor(sum(rate(istio_revision_request_count{destination_configuration=\"#{function_name}\", destination_namespace=\"#{kube_namespace}\"}[1m])*30))} + end + def prometheus_ping_url(prometheus_query) query = { query: prometheus_query }.to_query diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index a8b00004fe7..6aa59960092 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -90,7 +90,7 @@ RSpec.shared_examples "redis_shared_examples" do subject { described_class._raw_config } let(:config_file_name) { '/var/empty/doesnotexist' } - it 'should be frozen' do + it 'is frozen' do expect(subject).to be_frozen end diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb index 3ad6e067674..ee5cfcd850d 100644 --- a/spec/support/shared_context/policies/project_policy_shared_context.rb +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -25,6 +25,7 @@ RSpec.shared_context 'ProjectPolicy context' do admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment read_merge_request download_wiki_code read_sentry_issue read_release + read_prometheus ] end diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb index 98ab04c5636..eb051166a69 100644 --- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb +++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb @@ -4,7 +4,7 @@ shared_examples 'set sort order from user preference' do # however any other field present in user_preferences table can be used for testing. context 'when database is in read-only mode' do - it 'it does not update user preference' do + it 'does not update user preference' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param }) diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb index ba9b7d3bdcf..01bee603274 100644 --- a/spec/support/shared_examples/helm_generated_script.rb +++ b/spec/support/shared_examples/helm_generated_script.rb @@ -6,7 +6,7 @@ shared_examples 'helm commands' do EOS end - it 'should return appropriate command' do + it 'returns appropriate command' do expect(subject.generate_script.strip).to eq((helm_setup + commands).strip) end end diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb index 90d67fd00fc..244f4766a84 100644 --- a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb +++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb @@ -1,11 +1,11 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| include ProjectForksHelper - def get_action(action, project) + def get_action(action, project, extra_params = {}) if action - get action, params: { author_id: project.creator.id } + get action, params: { author_id: project.creator.id }.merge(extra_params) else - get :index, params: { namespace_id: project.namespace, project_id: project } + get :index, params: { namespace_id: project.namespace, project_id: project }.merge(extra_params) end end @@ -17,23 +17,44 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| end end - before do - @issuable_ids = %w[fix improve/awesome].map do |source_branch| - create_issuable(issuable_type, project, source_branch: source_branch).id + let!(:issuables) do + %w[fix improve/awesome].map do |source_branch| + create_issuable(issuable_type, project, source_branch: source_branch) end end + let(:issuable_ids) { issuables.map(&:id) } + it "creates indexed meta-data object for issuable notes and votes count" do get_action(action, project) meta_data = assigns(:issuable_meta_data) aggregate_failures do - expect(meta_data.keys).to match_array(@issuable_ids) + expect(meta_data.keys).to match_array(issuables.map(&:id)) expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta)) end end + context 'searching' do + let(:result_issuable) { issuables.first } + let(:search) { result_issuable.title } + + before do + stub_feature_flags(attempt_project_search_optimizations: true) + end + + # .simple_sorts is the same across all Sortable classes + sorts = ::Issue.simple_sorts.keys + %w[popularity priority label_priority] + sorts.each do |sort| + it "works when sorting by #{sort}" do + get_action(action, project, search: search, sort: sort) + + expect(assigns(:issuable_meta_data).keys).to include(result_issuable.id) + end + end + end + it "avoids N+1 queries" do control = ActiveRecord::QueryRecorder.new { get_action(action, project) } issuable = create_issuable(issuable_type, project, source_branch: 'csv') diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb index d87b3181e80..033b65bdc84 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb @@ -9,12 +9,12 @@ shared_examples 'cluster application helm specs' do |application_name| application.cluster.application_helm.ca_cert = nil end - it 'should not include cert files when there is no ca_cert entry' do + it 'does not include cert files when there is no ca_cert entry' do expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') end end - it 'should include cert files when there is a ca_cert entry' do + it 'includes cert files when there is a ca_cert entry' do expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem') expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) diff --git a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb index 24576fe0021..633c7135fbc 100644 --- a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb @@ -1,4 +1,38 @@ # frozen_string_literal: true shared_examples 'duplicate quick action' do + context 'mark issue as duplicate' do + let(:original_issue) { create(:issue, project: project) } + + context 'when the current user can update issues' do + it 'does not create a note, and marks the issue as a duplicate' do + add_note("/duplicate ##{original_issue.to_reference}") + + expect(page).not_to have_content "/duplicate #{original_issue.to_reference}" + expect(page).to have_content 'Commands applied' + expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" + + expect(issue.reload).to be_closed + end + end + + context 'when the current user cannot update the issue' do + let(:guest) { create(:user) } + before do + project.add_guest(guest) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) + end + + it 'does not create a note, and does not mark the issue as a duplicate' do + add_note("/duplicate ##{original_issue.to_reference}") + + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" + + expect(issue.reload).to be_open + end + end + end end diff --git a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb index 5904164fcfc..dd1676a08e2 100644 --- a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb @@ -1,25 +1,35 @@ # frozen_string_literal: true -shared_examples 'remove_due_date action not available' do - it 'does not remove the due date' do - add_note("/remove_due_date") +shared_examples 'remove_due_date quick action' do + context 'remove_due_date action available and due date can be removed' do + it 'removes the due date accordingly' do + add_note('/remove_due_date') - expect(page).not_to have_content 'Commands applied' - expect(page).not_to have_content '/remove_due_date' - end -end + expect(page).not_to have_content '/remove_due_date' + expect(page).to have_content 'Commands applied' + + visit project_issue_path(project, issue) -shared_examples 'remove_due_date action available and due date can be removed' do - it 'removes the due date accordingly' do - add_note('/remove_due_date') + page.within '.due_date' do + expect(page).to have_content 'No due date' + end + end + end - expect(page).not_to have_content '/remove_due_date' - expect(page).to have_content 'Commands applied' + context 'remove_due_date action not available' do + let(:guest) { create(:user) } + before do + project.add_guest(guest) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) + end - visit project_issue_path(project, issue) + it 'does not remove the due date' do + add_note("/remove_due_date") - page.within '.due_date' do - expect(page).to have_content 'No due date' + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content '/remove_due_date' end end end diff --git a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb index ccb4a85325b..cf2bdb1dd68 100644 --- a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb @@ -1,4 +1,81 @@ # frozen_string_literal: true shared_examples 'target_branch quick action' do + describe '/target_branch command in merge request' do + let(:another_project) { create(:project, :public, :repository) } + let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + + before do + another_project.add_maintainer(user) + sign_in(user) + end + + it 'changes target_branch in new merge_request' do + visit project_new_merge_request_path(another_project, new_url_opts) + + fill_in "merge_request_title", with: 'My brand new feature' + fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" + click_button "Submit merge request" + + merge_request = another_project.merge_requests.first + expect(merge_request.description).to eq "le feature \nFeature description:" + expect(merge_request.target_branch).to eq 'fix' + end + + it 'does not change target branch when merge request is edited' do + new_merge_request = create(:merge_request, source_project: another_project) + + visit edit_project_merge_request_path(another_project, new_merge_request) + fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n" + click_button "Save changes" + + new_merge_request = another_project.merge_requests.first + expect(new_merge_request.description).to include('/target_branch') + expect(new_merge_request.target_branch).not_to eq('fix') + end + end + + describe '/target_branch command from note' do + context 'when the current user can change target branch' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'changes target branch from a note' do + add_note("message start \n/target_branch merge-test\n message end.") + + wait_for_requests + expect(page).not_to have_content('/target_branch') + expect(page).to have_content('message start') + expect(page).to have_content('message end.') + + expect(merge_request.reload.target_branch).to eq 'merge-test' + end + + it 'does not fail when target branch does not exists' do + add_note('/target_branch totally_not_existing_branch') + + expect(page).not_to have_content('/target_branch') + + expect(merge_request.target_branch).to eq 'feature' + end + end + + context 'when current user can not change target branch' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not change target branch' do + add_note('/target_branch merge-test') + + expect(page).not_to have_content '/target_branch merge-test' + + expect(merge_request.target_branch).to eq 'feature' + end + end + end end diff --git a/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb index 6abb12b41b2..60d6e53e74f 100644 --- a/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb @@ -1,4 +1,47 @@ # frozen_string_literal: true shared_examples 'wip quick action' do + context 'when the current user can toggle the WIP prefix' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + wait_for_requests + end + + it 'adds the WIP: prefix to the title' do + add_note('/wip') + + expect(page).not_to have_content '/wip' + expect(page).to have_content 'Commands applied' + + expect(merge_request.reload.work_in_progress?).to eq true + end + + it 'removes the WIP: prefix from the title' do + merge_request.update!(title: merge_request.wip_title) + add_note('/wip') + + expect(page).not_to have_content '/wip' + expect(page).to have_content 'Commands applied' + + expect(merge_request.reload.work_in_progress?).to eq false + end + end + + context 'when the current user cannot toggle the WIP prefix' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not change the WIP prefix' do + add_note('/wip') + + expect(page).not_to have_content '/wip' + expect(page).not_to have_content 'Commands applied' + + expect(merge_request.reload.work_in_progress?).to eq false + end + end end diff --git a/spec/support/shared_examples/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/snippet_visibility_shared_examples.rb index 4f662db2120..833c31a57cb 100644 --- a/spec/support/shared_examples/snippet_visibility_shared_examples.rb +++ b/spec/support/shared_examples/snippet_visibility_shared_examples.rb @@ -220,11 +220,11 @@ RSpec.shared_examples 'snippet visibility' do end context "For #{params[:project_type]} project and #{params[:user_type]} users" do - it 'should agree with the read_project_snippet policy' do + it 'agrees with the read_project_snippet policy' do expect(can?(user, :read_project_snippet, snippet)).to eq(outcome) end - it 'should return proper outcome' do + it 'returns proper outcome' do results = described_class.new(user, project: project).execute expect(results.include?(snippet)).to eq(outcome) @@ -232,7 +232,7 @@ RSpec.shared_examples 'snippet visibility' do end context "Without a given project and #{params[:user_type]} users" do - it 'should return proper outcome' do + it 'returns proper outcome' do results = described_class.new(user).execute expect(results.include?(snippet)).to eq(outcome) end @@ -283,16 +283,16 @@ RSpec.shared_examples 'snippet visibility' do let!(:snippet) { create(:personal_snippet, visibility_level: snippet_visibility, author: author) } context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do - it 'should agree with read_personal_snippet policy' do + it 'agrees with read_personal_snippet policy' do expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome) end - it 'should return proper outcome' do + it 'returns proper outcome' do results = described_class.new(user).execute expect(results.include?(snippet)).to eq(outcome) end - it 'should return personal snippets when the user cannot read cross project' do + it 'returns personal snippets when the user cannot read cross project' do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb index ab98976ec27..42352f9b9f8 100644 --- a/spec/uploaders/records_uploads_spec.rb +++ b/spec/uploaders/records_uploads_spec.rb @@ -71,7 +71,7 @@ describe RecordsUploads do expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count } end - it 'it destroys Upload records at the same path before recording' do + it 'destroys Upload records at the same path before recording' do existing = Upload.create!( path: File.join('uploads', 'rails_sample.jpg'), size: 512.kilobytes, @@ -88,7 +88,7 @@ describe RecordsUploads do end describe '#destroy_upload callback' do - it 'it destroys Upload records at the same path after removal' do + it 'destroys Upload records at the same path after removal' do uploader.store!(upload_fixture('rails_sample.jpg')) expect { uploader.remove! }.to change { Upload.count }.from(1).to(0) diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb index 38cfb84f0d5..29e15960fb8 100644 --- a/spec/views/groups/edit.html.haml_spec.rb +++ b/spec/views/groups/edit.html.haml_spec.rb @@ -12,7 +12,7 @@ describe 'groups/edit.html.haml' do end shared_examples_for '"Share with group lock" setting' do |checkbox_options| - it 'should have the correct label, help text, and checkbox options' do + it 'has the correct label, help text, and checkbox options' do assign(:group, test_group) allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true) allow(view).to receive(:can_change_group_visibility_level?).and_return(false) diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index 908ecb898e4..12925a5ab07 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -45,7 +45,7 @@ describe 'projects/_home_panel' do context 'badges' do shared_examples 'show badges' do - it 'should render the all badges' do + it 'renders the all badges' do render expect(rendered).to have_selector('.project-badges a') @@ -70,7 +70,7 @@ describe 'projects/_home_panel' do context 'has no badges' do let(:project) { create(:project) } - it 'should not render any badge' do + it 'does not render any badge' do render expect(rendered).not_to have_selector('.project-badges') diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb index b52fc719a64..ff2d491539b 100644 --- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb +++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb @@ -13,4 +13,14 @@ describe 'projects/settings/ci_cd/_autodevops_form' do expect(rendered).to have_text('You must add a Kubernetes cluster integration to this project with a domain in order for your deployment strategy to work correctly.') end + + context 'when the project has an available kubernetes cluster' do + let!(:cluster) { create(:cluster, cluster_type: :project_type, projects: [project]) } + + it 'does not show a warning message' do + render + + expect(rendered).not_to have_text('You must add a Kubernetes cluster') + end + end end diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb index 4769d569548..cbbb984935f 100644 --- a/spec/views/shared/milestones/_issuables.html.haml.rb +++ b/spec/views/shared/milestones/_issuables.html.haml.rb @@ -11,12 +11,12 @@ describe 'shared/milestones/_issuables.html.haml' do stub_template 'shared/milestones/_issuable.html.haml' => '' end - it 'should show the issuables count if show_counter is true' do + it 'shows the issuables count if show_counter is true' do render 'shared/milestones/issuables', show_counter: true expect(rendered).to have_content('100') end - it 'should not show the issuables count if show_counter is false' do + it 'does not show the issuables count if show_counter is false' do render 'shared/milestones/issuables', show_counter: false expect(rendered).not_to have_content('100') end @@ -24,7 +24,7 @@ describe 'shared/milestones/_issuables.html.haml' do describe 'a high issuables count' do let(:issuables_size) { 1000 } - it 'should show a delimited number if show_counter is true' do + it 'shows a delimited number if show_counter is true' do render 'shared/milestones/issuables', show_counter: true expect(rendered).to have_content('1,000') end diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb index 3b14045e61f..dc223861037 100644 --- a/spec/views/shared/projects/_project.html.haml_spec.rb +++ b/spec/views/shared/projects/_project.html.haml_spec.rb @@ -8,13 +8,13 @@ describe 'shared/projects/_project.html.haml' do allow(view).to receive(:can?) { true } end - it 'should render creator avatar if project has a creator' do + it 'renders creator avatar if project has a creator' do render 'shared/projects/project', use_creator_avatar: true, project: project expect(rendered).to have_selector('img.avatar') end - it 'should render a generic avatar if project does not have a creator' do + it 'renders a generic avatar if project does not have a creator' do project.creator = nil render 'shared/projects/project', use_creator_avatar: true, project: project diff --git a/yarn.lock b/yarn.lock index 4de579fb290..571ac952e90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,17 +645,17 @@ dependencies: bootstrap "^4.1.3" -"@gitlab/eslint-config@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.4.0.tgz#2e59e55a7cd024e3a450d2a896060ec4d763a5dc" - integrity sha512-nkecTWRNS/KD9q5lHFSc3J6zO/g1/OV9DaKiay+0nLjnGO9jQVRArRIYpnzgbUz2p15jOMVToVafW0YbbHZkwg== +"@gitlab/eslint-config@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.5.0.tgz#0c8c3ae74f276eb6671bd7c60f331bc0f2d2e5cf" + integrity sha512-KgJgoIZNpGauFpCV1iCptesYN7I8abtYRBLU9xcH0oocC/xp3JmbLfsZ+lEtrk8pl99Q2mKiAuaPpzxjXr6hBw== dependencies: babel-eslint "^10.0.1" eslint-config-airbnb-base "^13.1.0" eslint-config-prettier "^3.3.0" eslint-plugin-filenames "^1.3.2" - eslint-plugin-import "^2.14.0" - eslint-plugin-promise "^4.0.1" + eslint-plugin-import "^2.16.0" + eslint-plugin-promise "^4.1.1" eslint-plugin-vue "^5.0.0" "@gitlab/svgs@^1.58.0": @@ -663,10 +663,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f" integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg== -"@gitlab/ui@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.0.0.tgz#33ca2808dbd4395e69a366a219d1edc1f3dbccd5" - integrity sha512-pDEa2k6ln5GE/N2z0V7dNEeFtSTW0p9ipO2/N9q6QMxO7fhhOhpMC0QVbdIljKTbglspDWI5v6BcqUjzYri5Pg== +"@gitlab/ui@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.2.0.tgz#3a44ac806a22b87fe45e6edfa410cb9355164f04" + integrity sha512-If2ngMIw0jWAdQ1q3PfB8sDhCXz1r3DsRm1X5Vy767kZ2TeFd7SGBp5KP5ceMGGpQ4TTYU/V8IqYnQbTXfPKRw== dependencies: "@babel/standalone" "^7.0.0" bootstrap-vue "^2.0.0-rc.11" @@ -3665,7 +3665,7 @@ eslint-import-resolver-jest@^2.1.1: micromatch "^3.1.6" resolve "^1.5.0" -eslint-import-resolver-node@^0.3.1: +eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== @@ -3689,13 +3689,13 @@ eslint-import-resolver-webpack@^0.10.1: resolve "^1.4.0" semver "^5.3.0" -eslint-module-utils@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" - integrity sha1-snA2LNiLGkitMIl2zn+lTphBF0Y= +eslint-module-utils@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49" + integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w== dependencies: debug "^2.6.8" - pkg-dir "^1.0.0" + pkg-dir "^2.0.0" eslint-plugin-filenames@^1.3.2: version "1.3.2" @@ -3714,21 +3714,21 @@ eslint-plugin-html@5.0.0: dependencies: htmlparser2 "^3.10.0" -eslint-plugin-import@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8" - integrity sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g== +eslint-plugin-import@^2.14.0, eslint-plugin-import@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f" + integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A== dependencies: contains-path "^0.1.0" - debug "^2.6.8" + debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.1" - eslint-module-utils "^2.2.0" - has "^1.0.1" - lodash "^4.17.4" - minimatch "^3.0.3" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.3.0" + has "^1.0.3" + lodash "^4.17.11" + minimatch "^3.0.4" read-pkg-up "^2.0.0" - resolve "^1.6.0" + resolve "^1.9.0" eslint-plugin-jasmine@^2.10.1: version "2.10.1" @@ -3740,10 +3740,10 @@ eslint-plugin-jest@^22.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2" integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA== -eslint-plugin-promise@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2" - integrity sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg== +eslint-plugin-promise@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db" + integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ== eslint-plugin-vue@^5.0.0: version "5.0.0" @@ -4464,7 +4464,7 @@ fstream@^1.0.0, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: +function-bind@^1.1.0, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== @@ -4902,12 +4902,12 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" - integrity sha1-hGFzP1OLCDfJNh45qauelwTcLyg= +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: - function-bind "^1.0.2" + function-bind "^1.1.1" hash-base@^2.0.0: version "2.0.2" @@ -8080,12 +8080,12 @@ pixelmatch@^4.0.2: dependencies: pngjs "^3.0.0" -pkg-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" - integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= dependencies: - find-up "^1.0.0" + find-up "^2.1.0" pkg-dir@^3.0.0: version "3.0.0" @@ -9094,7 +9094,7 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: +resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== @@ -9983,10 +9983,10 @@ stylelint-config-recommended@^2.1.0: resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-2.1.0.tgz#f526d5c771c6811186d9eaedbed02195fee30858" integrity sha512-ajMbivOD7JxdsnlS5945KYhvt7L/HwN6YeYF2BH6kE4UCLJR0YvXMf+2j7nQpJyYLZx9uZzU5G1ZOSBiWAc6yA== -stylelint-error-string-formatter@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stylelint-error-string-formatter/-/stylelint-error-string-formatter-1.0.1.tgz#366387825d6fb59569e8c5c3f5682398733756f9" - integrity sha512-8zy0UsdnQZKVDwjWMQX36b30TaNMGcM2FzBcK9cshpXerpJ3AvF2/zw7FJ3Efm6DFviTBVsxR14F3FnDFhCxJw== +stylelint-error-string-formatter@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stylelint-error-string-formatter/-/stylelint-error-string-formatter-1.0.2.tgz#3076c6703d3e0170daeb55fe85030d63c834745e" + integrity sha512-xN69xRB0eTgYcGKVHWIiH2L+Xx8fRPliTiLjaS4YlbfBqkdTuZh2wjtfvNCkCzBTNeINWa5GpSa9RFYXdtwV6w== stylelint-scss@^3.5.4: version "3.5.4" |