diff options
498 files changed, 4223 insertions, 4685 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index a954bb4ff37..d04a10a9127 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,9 +1,9 @@ ---- env: browser: true es6: true extends: - airbnb-base + - prettier - plugin:vue/recommended globals: __webpack_public_path__: true @@ -19,34 +19,31 @@ plugins: - promise settings: html/html-extensions: - - ".html" - - ".html.raw" + - '.html' + - '.html.raw' import/resolver: webpack: - config: "./config/webpack.config.js" + config: './config/webpack.config.js' rules: filenames/match-regex: - error - - "^[a-z0-9_]+$" + - '^[a-z0-9_]+$' import/no-commonjs: error - no-multiple-empty-lines: - - error - - max: 1 promise/catch-or-return: error no-param-reassign: - error - props: true ignorePropertyModificationsFor: - - "acc" # for reduce accumulators - - "accumulator" # for reduce accumulators - - "el" # for DOM elements - - "element" # for DOM elements - - "state" # for Vuex mutations + - 'acc' # for reduce accumulators + - 'accumulator' # for reduce accumulators + - 'el' # for DOM elements + - 'element' # for DOM elements + - 'state' # for Vuex mutations no-underscore-dangle: - error - allow: - - __ - - _links + - __ + - _links no-mixed-operators: off vue/html-self-closing: - error @@ -60,31 +57,7 @@ rules: - error - properties: never ignoreDestructuring: true - ## Conflicting rules with prettier: - space-before-function-paren: off - curly: off - arrow-parens: off - function-paren-newline: off - object-curly-newline: off - padded-blocks: off - # Disabled for now, to make the eslint 3 -> eslint 5 update smoother - ## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite - indent: off - indent-legacy: - - error - - 2 - - SwitchCase: 1 - VariableDeclarator: 1 - outerIIFEBody: 1 - FunctionDeclaration: - parameters: 1 - body: 1 - FunctionExpression: - parameters: 1 - body: 1 # Disabled for now, to make the airbnb-base 12.1.0 -> 13.1.0 update smoother - operator-linebreak: off - implicit-arrow-linebreak: off no-else-return: - error - allowElseIf: true diff --git a/.rubocop.yml b/.rubocop.yml index b7aec5b8b14..0f4018326a1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,7 +3,9 @@ inherit_gem: - rubocop-default.yml inherit_from: .rubocop_todo.yml -require: ./rubocop/rubocop +require: + - ./rubocop/rubocop + - rubocop-rspec AllCops: TargetRailsVersion: 4.2 @@ -48,12 +50,20 @@ Style/FrozenStringLiteralComment: - 'danger/**/*' - 'db/**/*' - 'ee/**/*' - - 'lib/**/*' + - 'lib/gitlab/**/*' + - 'lib/tasks/**/*' - 'qa/**/*' - 'rubocop/**/*' - 'scripts/**/*' - 'spec/**/*' +RSpec/FilePath: + Exclude: + - 'qa/**/*' + - 'spec/javascripts/fixtures/*' + - 'ee/spec/javascripts/fixtures/*' + - 'spec/requests/api/v3/*' + Naming/FileName: ExpectMatchingDefinition: true Exclude: @@ -75,6 +85,7 @@ Naming/FileName: - EE - JSON - LDAP + - SAML - IO - HMAC - QA diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 26aaba0e866..6085e946503 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.2.0 +1.2.1 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index d127a0ff9f1..9da0a092a0d 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.3.3 +8.4.0
\ No newline at end of file diff --git a/PROCESS.md b/PROCESS.md index 38ec01f9de0..5fc2c4cf1df 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -208,6 +208,7 @@ the stable branch are: * Fixes or improvements to automated QA scenarios * [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release * New or updated translations (as long as they do not touch application code) +* Changes that are behind a feature flag and have the ~"feature flag" label During the feature freeze all merge requests that are meant to go into the upcoming release should have the correct milestone assigned _and_ the diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 5d7a3bed301..0d7e8a5a3cb 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -1,4 +1,4 @@ -/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, max-len, no-restricted-syntax, guard-for-in, no-continue */ +/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 9ad451fa375..75477ebb3b3 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,25 +1,20 @@ -/* eslint-disable comma-dangle */ - import Sortable from 'sortablejs'; import Vue from 'vue'; import { n__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import Tooltip from '~/vue_shared/directives/tooltip'; import AccessorUtilities from '../../lib/utils/accessor'; -import boardList from './board_list.vue'; import BoardBlankState from './board_blank_state.vue'; -import './board_delete'; - -const Store = gl.issueBoards.BoardsStore; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; +import BoardDelete from './board_delete'; +import BoardList from './board_list.vue'; +import boardsStore from '../stores/boards_store'; +import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; -gl.issueBoards.Board = Vue.extend({ +export default Vue.extend({ components: { - boardList, - 'board-delete': gl.issueBoards.BoardDelete, BoardBlankState, + BoardDelete, + BoardList, Icon, }, directives: { @@ -49,8 +44,8 @@ gl.issueBoards.Board = Vue.extend({ }, data () { return { - detailIssue: Store.detail, - filter: Store.filter, + detailIssue: boardsStore.detail, + filter: boardsStore.filter, }; }, computed: { @@ -72,20 +67,20 @@ gl.issueBoards.Board = Vue.extend({ } }, mounted () { - this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ + this.sortableOptions = getBoardSortableDefaultOptions({ disabled: this.disabled, group: 'boards', draggable: '.is-draggable', handle: '.js-board-handle', onEnd: (e) => { - gl.issueBoards.onEnd(); + sortableEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { const order = this.sortable.toArray(); - const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); + const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10)); this.$nextTick(() => { - Store.moveList(list, order); + boardsStore.moveList(list, order); }); } } diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index cde22725a89..38aaec73d7d 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -2,8 +2,7 @@ /* global ListLabel */ import _ from 'underscore'; import Cookies from 'js-cookie'; - -const Store = gl.issueBoards.BoardsStore; +import boardsStore from '../stores/boards_store'; export default { data() { @@ -19,7 +18,7 @@ export default { this.clearBlankState(); this.predefinedLabels.forEach((label, i) => { - Store.addList({ + boardsStore.addList({ title: label.title, position: i, list_type: 'label', @@ -30,14 +29,14 @@ export default { }); }); - Store.state.lists = _.sortBy(Store.state.lists, 'position'); + boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); // Save the labels gl.boardService.generateDefaultLists() .then(res => res.data) .then((data) => { data.forEach((listObj) => { - const list = Store.findList('title', listObj.title); + const list = boardsStore.findList('title', listObj.title); list.id = listObj.id; list.label.id = listObj.label.id; @@ -48,14 +47,14 @@ export default { }); }) .catch(() => { - Store.removeList(undefined, 'label'); + boardsStore.removeList(undefined, 'label'); Cookies.remove('issue_board_welcome_hidden', { path: '', }); - Store.addBlankState(); + boardsStore.addBlankState(); }); }, - clearBlankState: Store.removeBlankState.bind(Store), + clearBlankState: boardsStore.removeBlankState.bind(boardsStore), }, }; diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 0398102ad02..843498f0d06 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -2,8 +2,7 @@ /* eslint-disable vue/require-default-prop */ import IssueCardInner from './issue_card_inner.vue'; import eventHub from '../eventhub'; - - const Store = gl.issueBoards.BoardsStore; + import boardsStore from '../stores/boards_store'; export default { name: 'BoardsIssueCard', @@ -42,7 +41,7 @@ data() { return { showDetail: false, - detailIssue: Store.detail, + detailIssue: boardsStore.detail, }; }, computed: { @@ -63,11 +62,11 @@ if (this.showDetail) { this.showDetail = false; - if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { + if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { eventHub.$emit('clearDetailIssue'); } else { eventHub.$emit('newDetailIssue', this.issue); - Store.detail.list = this.list; + boardsStore.detail.list = this.list; } } }, diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index c5945e8098d..a5f9d65e4d5 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -1,12 +1,7 @@ -/* eslint-disable comma-dangle, no-alert */ - import $ from 'jquery'; import Vue from 'vue'; -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.BoardDelete = Vue.extend({ +export default Vue.extend({ props: { list: { type: Object, @@ -14,12 +9,13 @@ gl.issueBoards.BoardDelete = Vue.extend({ }, }, methods: { - deleteBoard () { + deleteBoard() { $(this.$el).tooltip('hide'); + // eslint-disable-next-line no-alert if (window.confirm('Are you sure you want to delete this list?')) { this.list.destroy(); } - } - } + }, + }, }); diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 7ddb22ad824..4dc56c670f0 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -3,8 +3,8 @@ import Sortable from 'sortablejs'; import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; - -const Store = gl.issueBoards.BoardsStore; +import boardsStore from '../stores/boards_store'; +import { getBoardSortableDefaultOptions, sortableStart } from '../mixins/sortable_default_options'; export default { name: 'BoardList', @@ -46,7 +46,7 @@ export default { data() { return { scrollOffset: 250, - filters: Store.state.filters, + filters: boardsStore.state.filters, showCount: false, showIssueForm: false, }; @@ -61,13 +61,14 @@ export default { }, issues() { this.$nextTick(() => { - if (this.scrollHeight() <= this.listHeight() && - this.list.issuesSize > this.list.issues.length) { + if ( + this.scrollHeight() <= this.listHeight() && + this.list.issuesSize > this.list.issues.length + ) { this.list.page += 1; - this.list.getIssues(false) - .catch(() => { - // TODO: handle request error - }); + this.list.getIssues(false).catch(() => { + // TODO: handle request error + }); } if (this.scrollHeight() > Math.ceil(this.listHeight())) { @@ -83,7 +84,7 @@ export default { eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); }, mounted() { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ + const options = getBoardSortableDefaultOptions({ scroll: true, disabled: this.disabled, filter: '.board-list-count, .is-disabled', @@ -108,7 +109,8 @@ export default { // So from there, we can get reference to actual container // and thus the container type to enable Copy or Move if (e.target) { - const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); + const containerEl = + e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); const toBoardType = containerEl.dataset.boardType; const cloneActions = { label: ['milestone', 'assignee'], @@ -120,8 +122,9 @@ export default { const fromBoardType = this.list.type; // For each list we check if the destination list is // a the list were we should clone the issue - const shouldClone = Object.entries(cloneActions).some(entry => ( - fromBoardType === entry[0] && entry[1].includes(toBoardType))); + const shouldClone = Object.entries(cloneActions).some( + entry => fromBoardType === entry[0] && entry[1].includes(toBoardType), + ); if (shouldClone) { return 'clone'; @@ -133,28 +136,36 @@ export default { }, revertClone: true, }, - onStart: (e) => { + onStart: e => { const card = this.$refs.issue[e.oldIndex]; card.showDetail = false; - Store.moving.list = card.list; - Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); + boardsStore.moving.list = card.list; + boardsStore.moving.issue = boardsStore.moving.list.findIssue(+e.item.dataset.issueId); - gl.issueBoards.onStart(); + sortableStart(); }, - onAdd: (e) => { - gl.issueBoards.BoardsStore - .moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); + onAdd: e => { + boardsStore.moveIssueToList( + boardsStore.moving.list, + this.list, + boardsStore.moving.issue, + e.newIndex, + ); this.$nextTick(() => { e.item.remove(); }); }, - onUpdate: (e) => { - const sortedArray = this.sortable.toArray() - .filter(id => id !== '-1'); - gl.issueBoards.BoardsStore - .moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); + onUpdate: e => { + const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); + boardsStore.moveIssueInList( + this.list, + boardsStore.moving.issue, + e.oldIndex, + e.newIndex, + sortedArray, + ); }, onMove(e) { return !e.related.classList.contains('board-list-count'); @@ -192,16 +203,14 @@ export default { if (getIssues) { this.list.loadingMore = true; - getIssues - .then(loadingDone) - .catch(loadingDone); + getIssues.then(loadingDone).catch(loadingDone); } }, toggleForm() { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { + if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { this.loadNextPage(); } }, diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index f7ce5128964..030288a1c9d 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -4,8 +4,7 @@ import { Button } from '@gitlab-org/gitlab-ui'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; - -const Store = gl.issueBoards.BoardsStore; +import boardsStore from '../stores/boards_store'; export default { name: 'BoardNewIssue', @@ -68,8 +67,8 @@ export default { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); - Store.detail.issue = issue; - Store.detail.list = this.list; + boardsStore.detail.issue = issue; + boardsStore.detail.list = this.list; }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 109e60cbde2..62666954de0 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, no-new */ +/* eslint-disable no-new */ import $ from 'jquery'; import Vue from 'vue'; @@ -14,13 +14,9 @@ import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; import MilestoneSelect from '../../milestone_select'; +import boardsStore from '../stores/boards_store'; -const Store = gl.issueBoards.BoardsStore; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.BoardSidebar = Vue.extend({ +export default Vue.extend({ components: { AssigneeTitle, Assignees, @@ -35,7 +31,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }, data() { return { - detail: Store.detail, + detail: boardsStore.detail, issue: {}, list: {}, loadingAssignees: false, @@ -117,18 +113,18 @@ gl.issueBoards.BoardSidebar = Vue.extend({ this.saveAssignees(); }, removeAssignee (a) { - gl.issueBoards.BoardsStore.detail.issue.removeAssignee(a); + boardsStore.detail.issue.removeAssignee(a); }, addAssignee (a) { - gl.issueBoards.BoardsStore.detail.issue.addAssignee(a); + boardsStore.detail.issue.addAssignee(a); }, removeAllAssignees () { - gl.issueBoards.BoardsStore.detail.issue.removeAllAssignees(); + boardsStore.detail.issue.removeAllAssignees(); }, saveAssignees () { this.loadingAssignees = true; - gl.issueBoards.BoardsStore.detail.issue.update() + boardsStore.detail.issue.update() .then(() => { this.loadingAssignees = false; }) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 8b5536200e1..28956c2f3c5 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -3,8 +3,7 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../eventhub'; import tooltip from '../../vue_shared/directives/tooltip'; - - const Store = gl.issueBoards.BoardsStore; + import boardsStore from '../stores/boards_store'; export default { components: { @@ -110,7 +109,7 @@ filterByLabel(label, e) { if (!this.updateFilters) return; - const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); + const filterPath = boardsStore.filter.path.split('&'); const labelTitle = encodeURIComponent(label.title); const param = `label_name[]=${labelTitle}`; const labelIndex = filterPath.indexOf(param); @@ -122,9 +121,9 @@ filterPath.splice(labelIndex, 1); } - gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); + boardsStore.filter.path = filterPath.join('&'); - Store.updateFiltersUrl(); + boardsStore.updateFiltersUrl(); eventHub.$emit('updateTokens'); }, diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index d4affc8c3de..268ca6bca13 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -5,6 +5,7 @@ import ListsDropdown from './lists_dropdown.vue'; import { pluralize } from '../../../lib/utils/text_utility'; import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; +import boardsStore from '../../stores/boards_store'; export default { components: { @@ -14,7 +15,7 @@ export default { data() { return { modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, + state: boardsStore.state, }; }, computed: { diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 4f23e5db35c..4622fd28220 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,6 +1,7 @@ <script> import { Link } from '@gitlab-org/gitlab-ui'; import ModalStore from '../../stores/modal_store'; +import boardsStore from '../../stores/boards_store'; export default { components: { @@ -9,7 +10,7 @@ export default { data() { return { modal: ModalStore.store, - state: gl.issueBoards.BoardsStore.state, + state: boardsStore.state, }; }, computed: { diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 448ab9ed135..2c2045f8901 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -4,16 +4,12 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; import CreateLabelDropdown from '../../create_label'; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -const Store = gl.issueBoards.BoardsStore; +import boardsStore from '../stores/boards_store'; $(document).off('created.label').on('created.label', (e, label) => { - Store.new({ + boardsStore.new({ title: label.title, - position: Store.state.lists.length - 2, + position: boardsStore.state.lists.length - 2, list_type: 'label', label: { id: label.id, @@ -23,7 +19,7 @@ $(document).off('created.label').on('created.label', (e, label) => { }); }); -gl.issueBoards.newListDropdownInit = () => { +export default function initNewListDropdown() { $('.js-new-board-list').each(function () { const $this = $(this); new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); @@ -36,7 +32,7 @@ gl.issueBoards.newListDropdownInit = () => { }); }, renderRow (label) { - const active = Store.findList('title', label.title); + const active = boardsStore.findList('title', label.title); const $li = $('<li />'); const $a = $('<a />', { class: (active ? `is-active js-board-list-${active.id}` : ''), @@ -62,10 +58,10 @@ gl.issueBoards.newListDropdownInit = () => { const label = options.selectedObj; e.preventDefault(); - if (!Store.findList('title', label.title)) { - Store.new({ + if (!boardsStore.findList('title', label.title)) { + boardsStore.new({ title: label.title, - position: Store.state.lists.length - 2, + position: boardsStore.state.lists.length - 2, list_type: 'label', label: { id: label.id, @@ -74,9 +70,9 @@ gl.issueBoards.newListDropdownInit = () => { }, }); - Store.state.lists = _.sortBy(Store.state.lists, 'position'); + boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); } }, }); }); -}; +} diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index 90d4c710daf..b8f2e324d43 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -2,8 +2,7 @@ import Vue from 'vue'; import Flash from '../../../flash'; import { __ } from '../../../locale'; - - const Store = gl.issueBoards.BoardsStore; + import boardsStore from '../../stores/boards_store'; export default Vue.extend({ props: { @@ -49,7 +48,7 @@ list.removeIssue(issue); }); - Store.detail.issue = {}; + boardsStore.detail.issue = {}; }, /** * Build the default patch request. diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 46d61ebbf24..acf41e5689e 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,5 +1,6 @@ import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchManager from '../filtered_search/filtered_search_manager'; +import boardsStore from './stores/boards_store'; export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { @@ -23,7 +24,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { this.store.path = path.substr(1); if (this.updateUrl) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); + boardsStore.updateFiltersUrl(); } } diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index caa6ce84335..91861f2f9ee 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -14,24 +14,22 @@ import './models/issue'; import './models/list'; import './models/milestone'; import './models/project'; -import './stores/boards_store'; +import boardsStore from './stores/boards_store'; import ModalStore from './stores/modal_store'; import BoardService from './services/board_service'; import modalMixin from './mixins/modal_mixins'; -import './mixins/sortable_default_options'; import './filters/due_date_filters'; -import './components/board'; -import './components/board_sidebar'; -import './components/new_list_dropdown'; +import Board from './components/board'; +import BoardSidebar from './components/board_sidebar'; +import initNewListDropdown from './components/new_list_dropdown'; import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; import { NavigationType } from '~/lib/utils/common_utils'; +let issueBoardsApp; + export default () => { const $boardApp = document.getElementById('board-app'); - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; // check for browser back and trigger a hard reload to circumvent browser caching. window.addEventListener('pageshow', (event) => { @@ -43,25 +41,21 @@ export default () => { } }); - if (gl.IssueBoardsApp) { - gl.IssueBoardsApp.$destroy(true); + if (issueBoardsApp) { + issueBoardsApp.$destroy(true); } - Store.create(); - - // hack to allow sidebar scripts like milestone_select manipulate the BoardsStore - gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args); - gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args); + boardsStore.create(); - gl.IssueBoardsApp = new Vue({ + issueBoardsApp = new Vue({ el: $boardApp, components: { - board: gl.issueBoards.Board, - 'board-sidebar': gl.issueBoards.BoardSidebar, + Board, + BoardSidebar, BoardAddIssuesModal, }, data: { - state: Store.state, + state: boardsStore.state, loading: true, boardsEndpoint: $boardApp.dataset.boardsEndpoint, listsEndpoint: $boardApp.dataset.listsEndpoint, @@ -70,7 +64,7 @@ export default () => { issueLinkBase: $boardApp.dataset.issueLinkBase, rootPath: $boardApp.dataset.rootPath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, - detailIssue: Store.detail, + detailIssue: boardsStore.detail, defaultAvatar: $boardApp.dataset.defaultAvatar, }, computed: { @@ -85,7 +79,7 @@ export default () => { bulkUpdatePath: this.bulkUpdatePath, boardId: this.boardId, }); - Store.rootPath = this.boardsEndpoint; + boardsStore.rootPath = this.boardsEndpoint; eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('newDetailIssue', this.updateDetailIssue); @@ -99,16 +93,16 @@ export default () => { sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); }, mounted() { - this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); + this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit); this.filterManager.setup(); - Store.disabled = this.disabled; + boardsStore.disabled = this.disabled; gl.boardService .all() .then(res => res.data) .then(data => { data.forEach(board => { - const list = Store.addList(board, this.defaultAvatar); + const list = boardsStore.addList(board, this.defaultAvatar); if (list.type === 'closed') { list.position = Infinity; @@ -119,7 +113,7 @@ export default () => { this.state.lists = _.sortBy(this.state.lists, 'position'); - Store.addBlankState(); + boardsStore.addBlankState(); this.loading = false; }) .catch(() => { @@ -148,13 +142,13 @@ export default () => { }); } - Store.detail.issue = newIssue; + boardsStore.detail.issue = newIssue; }, clearDetailIssue() { - Store.detail.issue = {}; + boardsStore.detail.issue = {}; }, toggleSubscription(id) { - const { issue } = Store.detail; + const { issue } = boardsStore.detail; if (issue.id === id && issue.toggleSubscriptionEndpoint) { issue.setFetchingState('subscriptions', true); BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint) @@ -173,26 +167,28 @@ export default () => { }, }); - gl.IssueBoardsSearch = new Vue({ + // eslint-disable-next-line no-new + new Vue({ el: document.getElementById('js-add-list'), data: { - filters: Store.state.filters, + filters: boardsStore.state.filters, }, mounted() { - gl.issueBoards.newListDropdownInit(); + initNewListDropdown(); }, }); const issueBoardsModal = document.getElementById('js-add-issues-btn'); if (issueBoardsModal) { - gl.IssueBoardsModalAddBtn = new Vue({ + // eslint-disable-next-line no-new + new Vue({ el: issueBoardsModal, mixins: [modalMixin], data() { return { modal: ModalStore.store, - store: Store.state, + store: boardsStore.state, canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), }; }, diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index a8df45fc473..c9cde4effb9 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -3,32 +3,29 @@ import $ from 'jquery'; import sortableConfig from '../../sortable/sortable_config'; -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.onStart = () => { +export function sortableStart() { $('.has-tooltip').tooltip('hide') .tooltip('disable'); document.body.classList.add('is-dragging'); -}; +} -gl.issueBoards.onEnd = () => { +export function sortableEnd() { $('.has-tooltip').tooltip('enable'); document.body.classList.remove('is-dragging'); -}; +} -gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; +export function getBoardSortableDefaultOptions(obj) { + const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; -gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { const defaultSortOptions = Object.assign({}, sortableConfig, { filter: '.board-delete, .btn', - delay: gl.issueBoards.touchEnabled ? 100 : 0, - scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, + delay: touchEnabled ? 100 : 0, + scrollSensitivity: touchEnabled ? 60 : 100, scrollSpeed: 20, - onStart: gl.issueBoards.onStart, - onEnd: gl.issueBoards.onEnd, + onStart: sortableStart, + onEnd: sortableEnd, }); Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); return defaultSortOptions; -}; +} diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index c7cfb72067c..52d04389b88 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars, comma-dangle */ +/* eslint-disable no-unused-vars */ /* global ListLabel */ /* global ListMilestone */ /* global ListAssignee */ @@ -6,6 +6,7 @@ import Vue from 'vue'; import '~/vue_shared/models/label'; import IssueProject from './project'; +import boardsStore from '../stores/boards_store'; class ListIssue { constructor (obj, defaultAvatar) { @@ -86,7 +87,7 @@ class ListIssue { } getLists () { - return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); + return boardsStore.state.lists.filter(list => list.findIssue(this.id)); } updateData(newData) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index d416b76f0f4..3161f1da8c9 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,10 +1,11 @@ -/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */ +/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign */ /* global ListIssue */ import { __ } from '~/locale'; import ListLabel from '~/vue_shared/models/label'; import ListAssignee from '~/vue_shared/models/assignee'; import { urlParamsToObject } from '~/lib/utils/common_utils'; +import boardsStore from '../stores/boards_store'; const PER_PAGE = 20; @@ -89,9 +90,9 @@ class List { } destroy() { - const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); - gl.issueBoards.BoardsStore.state.lists.splice(index, 1); - gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); + const index = boardsStore.state.lists.indexOf(this); + boardsStore.state.lists.splice(index, 1); + boardsStore.updateNewListDropdown(this.id); gl.boardService.destroyList(this.id).catch(() => { // TODO: handle request error @@ -116,7 +117,7 @@ class List { getIssues(emptyIssues = true) { const data = { - ...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path), + ...urlParamsToObject(boardsStore.filter.path), page: this.page, }; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 957114cf420..471955747fd 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,15 +1,13 @@ -/* eslint-disable comma-dangle, no-shadow */ +/* eslint-disable no-shadow */ /* global List */ import $ from 'jquery'; import _ from 'underscore'; +import Vue from 'vue'; import Cookies from 'js-cookie'; import { getUrlParamsArray } from '~/lib/utils/common_utils'; -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.BoardsStore = { +const boardsStore = { disabled: false, filter: { path: '', @@ -167,3 +165,16 @@ gl.issueBoards.BoardsStore = { window.history.pushState(null, null, `?${this.filter.path}`); } }; + +// hacks added in order to allow milestone_select to function properly +// TODO: remove these + +export function boardStoreIssueSet(...args) { + Vue.set(boardsStore.detail.issue, ...args); +} + +export function boardStoreIssueDelete(...args) { + Vue.delete(boardsStore.detail.issue, ...args); +} + +export default boardsStore; diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index a1069985178..6e7b5eb5526 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg'; +import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg'; import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; import helmLogo from 'images/cluster_app_logos/helm.png'; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 410580b4c25..30d9b656fec 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, wrap-iife, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, max-len */ +/* eslint-disable func-names, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, one-var, no-unused-vars, no-return-assign, no-unused-expressions, no-sequences */ import $ from 'jquery'; diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index a252036d657..852d71f4e84 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, one-var, no-var, one-var-declaration-per-line, object-shorthand, no-else-return, max-len */ +/* eslint-disable func-names, one-var, no-var, object-shorthand, no-else-return */ import $ from 'jquery'; import { __ } from './locale'; diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index ed24d1775f4..87621761500 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ +/* eslint-disable object-shorthand, func-names, no-else-return, no-lonely-if */ /* global CommentsStore */ import $ from 'jquery'; diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 2b893e35b6d..2b78bb58735 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue, brace-style, max-len, quotes */ +/* eslint-disable object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue */ /* global CommentsStore */ import $ from 'jquery'; diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js index e2683e09f40..eb539c6b348 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names */ +/* eslint-disable object-shorthand, func-names */ /* global CommentsStore */ import Vue from 'vue'; diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js index ef35b589e58..7589f9dd6e0 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js @@ -1,4 +1,4 @@ -/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, */ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, */ const DiscussionMixins = { computed: { diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js index d7da7d974f3..d012cd02d10 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -1,4 +1,4 @@ -/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len */ +/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in */ /* global DiscussionModel */ import Vue from 'vue'; diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 15b37243030..dcf1057eb84 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -20,6 +20,11 @@ export default { Tooltip, }, props: { + discussionPath: { + type: String, + required: false, + default: '', + }, diffFile: { type: Object, required: true, @@ -65,8 +70,7 @@ export default { if (this.diffFile.submodule) { return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink; } - - return `#${this.diffFile.fileHash}`; + return this.discussionPath; }, filePath() { if (this.diffFile.submodule) { @@ -152,7 +156,7 @@ export default { v-once ref="titleWrapper" :href="titleLink" - class="append-right-4" + class="append-right-4 js-title-wrapper" > <file-icon :file-name="filePath" diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index ae8930c8968..1c5c35071de 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,5 +1,6 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; +import bp from '~/breakpoints'; import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants'; const viewTypeFromQueryString = getParameterValues('view')[0]; @@ -20,6 +21,7 @@ export default () => ({ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, tree: [], treeEntries: {}, - showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true', + showTreeList: + storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true', currentDiffFileId: '', }); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 8abd8bc581a..c7b5a35cc14 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -5,6 +5,7 @@ import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import boardsStore from './boards/stores/boards_store'; class DueDateSelect { constructor({ $dropdown, $loading } = {}) { @@ -58,7 +59,7 @@ class DueDateSelect { $dueDateInput.val(calendar.toString(dateText)); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); + boardsStore.detail.issue.dueDate = $dueDateInput.val(); this.updateIssueBoardIssue(); } else { this.saveDueDate(true); @@ -79,7 +80,7 @@ class DueDateSelect { calendar.setDate(null); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + boardsStore.detail.issue.dueDate = ''; this.updateIssueBoardIssue(); } else { $(`input[name='${this.fieldName}']`).val(''); @@ -123,7 +124,7 @@ class DueDateSelect { this.$loading.fadeOut(); }; - gl.issueBoards.BoardsStore.detail.issue + boardsStore.detail.issue .update(this.$dropdown.attr('data-issue-update')) .then(fadeOutLoader) .catch(fadeOutLoader); diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index a1d8e531940..ad5d16874f3 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -482,6 +482,7 @@ export default { v-if="!model.isFolder" class="environment-name table-mobile-content"> <a + class="qa-environment-link" :href="environmentPath" > {{ model.name }} diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index c3959ef3e9e..a8ac2f510a4 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */ +/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */ /* global fuzzaldrinPlus */ import $ from 'jquery'; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index ee8eb206980..802827fce76 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -117,7 +117,7 @@ export default { <button :disabled="!hasChanges" type="button" - class="btn btn-primary btn-sm btn-block" + class="btn btn-primary btn-sm btn-block qa-begin-commit-button" @click="toggleIsSmall" > {{ __('Commit…') }} @@ -147,7 +147,7 @@ export default { <loading-button :loading="submitCommitLoading" :label="commitButtonText" - container-class="btn btn-success btn-sm float-left" + container-class="btn btn-success btn-sm float-left qa-commit-button" @click="commitChanges" /> <button diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue index d376a004e84..699fa7dc937 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -38,14 +38,18 @@ export default { return this.modifiedFilesLength ? 'multi-file-modified' : ''; }, additionsTooltip() { - return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), { - type: this.title.toLowerCase(), - count: this.addedFilesLength, - }); + return sprintf( + n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), + { + type: this.title.toLowerCase(), + count: this.addedFilesLength, + }, + ); }, modifiedTooltip() { return sprintf( - n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), { + n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), + { type: this.title.toLowerCase(), count: this.modifiedFilesLength, }, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 8a1836a5c92..adf4b479c97 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -25,10 +25,7 @@ export default { return `discard-file-${this.path}`; }, modalTitle() { - return sprintf( - __('Discard changes to %{path}?'), - { path: this.path }, - ); + return sprintf(__('Discard changes to %{path}?'), { path: this.path }); }, }, methods: { diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue index 23be5f45f16..3587626c580 100644 --- a/app/assets/javascripts/ide/components/file_templates/bar.vue +++ b/app/assets/javascripts/ide/components/file_templates/bar.vue @@ -47,7 +47,7 @@ export default { </script> <template> - <div class="d-flex align-items-center ide-file-templates"> + <div class="d-flex align-items-center ide-file-templates qa-file-templates-bar"> <strong class="append-right-default"> {{ __('File templates') }} </strong> @@ -63,7 +63,7 @@ export default { :is-async-data="true" :searchable="true" :title="__('File templates')" - class="mr-2" + class="mr-2 qa-file-template-dropdown" @click="selectTemplate" /> <transition name="fade"> diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index ef1f6de3a86..94222c08e91 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -92,7 +92,7 @@ export default { v-model="search" :placeholder="__('Filter...')" type="search" - class="dropdown-input-field" + class="dropdown-input-field qa-dropdown-filter-input" /> <i aria-hidden="true" diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index f99ff6d6da8..dc84ee12f1e 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -24,13 +24,7 @@ export default { IdeProjectHeader, }, computed: { - ...mapState([ - 'loading', - 'currentActivityView', - 'changedFiles', - 'stagedFiles', - 'lastCommitMsg', - ]), + ...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']), ...mapGetters(['currentProject', 'someUncommitedChanges']), showSuccessMessage() { return ( diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 39d46a91731..9f9e638f1aa 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -45,7 +45,7 @@ export default { <new-entry-button :label="__('New file')" :show-label="false" - class="d-flex border-0 p-0 mr-3" + class="d-flex border-0 p-0 mr-3 qa-new-file" icon="doc-new" @click="openNewEntryModal({ type: 'blob' })" /> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index cfe25084b42..e88f01fb4f4 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -43,7 +43,7 @@ export default { <template> <div - class="ide-file-list" + class="ide-file-list qa-file-list" > <template v-if="showLoading"> <div diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index c8343e77860..f5e42e87f1b 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -37,14 +37,10 @@ export default { return this.hasSearchFocus && !this.search && !this.currentSearchType; }, type() { - return this.currentSearchType - ? this.currentSearchType.type - : ''; + return this.currentSearchType ? this.currentSearchType.type : ''; }, searchTokens() { - return this.currentSearchType - ? [this.currentSearchType] - : []; + return this.currentSearchType ? [this.currentSearchType] : []; }, }, watch: { diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue index 7f98769d484..6cee4e9a8f0 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue @@ -13,9 +13,7 @@ export default { computed: { ...mapState(['currentBranchId', 'currentMergeRequestId']), mergeRequestLabel() { - return this.currentMergeRequestId - ? `!${this.currentMergeRequestId}` - : EMPTY_LABEL; + return this.currentMergeRequestId ? `!${this.currentMergeRequestId}` : EMPTY_LABEL; }, branchLabel() { return this.currentBranchId || EMPTY_LABEL; diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index bcd53ac1ba2..f0a04011a3e 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -110,12 +110,12 @@ export default { ref="fieldName" v-model="entryName" type="text" - class="form-control" + class="form-control qa-full-file-path" placeholder="/dir/file_name" /> <ul v-if="isCreatingNew" - class="prepend-top-default list-inline" + class="prepend-top-default list-inline qa-template-list" > <li v-for="(template, index) in templateTypes" diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index bd07f372177..10aa96dffaf 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -43,34 +43,25 @@ export default { { show: this.currentMergeRequestId, title: __('Merge Request'), - views: [ - rightSidebarViews.mergeRequestInfo, - ], + views: [rightSidebarViews.mergeRequestInfo], icon: 'text-description', }, { show: true, title: __('Pipelines'), - views: [ - rightSidebarViews.pipelines, - rightSidebarViews.jobsDetail, - ], + views: [rightSidebarViews.pipelines, rightSidebarViews.jobsDetail], icon: 'rocket', }, { show: this.showLivePreview, title: __('Live preview'), - views: [ - rightSidebarViews.clientSidePreview, - ], + views: [rightSidebarViews.clientSidePreview], icon: 'live-preview', }, ]; }, tabs() { - return this.defaultTabs - .concat(this.extensionTabs) - .filter(tab => tab.show); + return this.defaultTabs.concat(this.extensionTabs).filter(tab => tab.show); }, tabViews() { return _.flatten(this.tabs.map(tab => tab.views)); diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b2599128213..7b0f717962e 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -25,12 +25,7 @@ export default { ...mapState('rightPane', { rightPaneIsOpen: 'isOpen', }), - ...mapState([ - 'rightPanelCollapsed', - 'viewer', - 'panelResizing', - 'currentActivityView', - ]), + ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']), ...mapGetters([ 'currentMergeRequest', 'getStagedFile', diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue index a7a12f6785d..30010957a16 100644 --- a/app/assets/javascripts/ide/components/shared/tokened_input.vue +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -30,9 +30,7 @@ export default { }, computed: { placeholderText() { - return this.tokens.length - ? '' - : this.placeholder; + return this.tokens.length ? '' : this.placeholder; }, }, watch: { diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index c0550116633..7a5a227db30 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -21,10 +21,7 @@ Vue.use(Translate); export function initIde(el, options = {}) { if (!el) return null; - const { - extraInitialData = () => ({}), - rootComponent = ide, - } = options; + const { extraInitialData = () => ({}), rootComponent = ide } = options; return new Vue({ el, diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js index 0e37f5c4704..9b7ed68b893 100644 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -11,14 +11,16 @@ export const computeDiff = (originalContent, newContent) => { if (findOnLine) { Object.assign(findOnLine, change, { modified: true, - endLineNumber: (lineNumber + change.count) - 1, + endLineNumber: lineNumber + change.count - 1, }); } else if ('added' in change || 'removed' in change) { - acc.push(Object.assign({}, change, { - lineNumber, - modified: undefined, - endLineNumber: (lineNumber + change.count) - 1, - })); + acc.push( + Object.assign({}, change, { + lineNumber, + modified: undefined, + endLineNumber: lineNumber + change.count - 1, + }), + ); } if (!change.removed) { diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js index 78b2eab6399..77416a8de9d 100644 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js @@ -1,7 +1,7 @@ import { computeDiff } from './diff'; // eslint-disable-next-line no-restricted-globals -self.addEventListener('message', (e) => { +self.addEventListener('message', e => { const { data } = e; // eslint-disable-next-line no-restricted-globals diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 187f8c75d07..3ac2f8b3698 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -116,57 +116,57 @@ export const openMergeRequest = ( targetProjectId, mergeRequestId, }) - .then(mr => { - dispatch('setCurrentBranchId', mr.source_branch); + .then(mr => { + dispatch('setCurrentBranchId', mr.source_branch); - dispatch('getBranchData', { - projectId, - branchId: mr.source_branch, - }); + dispatch('getBranchData', { + projectId, + branchId: mr.source_branch, + }); - return dispatch('getFiles', { - projectId, - branchId: mr.source_branch, - }); - }) - .then(() => - dispatch('getMergeRequestVersions', { - projectId, - targetProjectId, - mergeRequestId, - }), - ) - .then(() => - dispatch('getMergeRequestChanges', { - projectId, - targetProjectId, - mergeRequestId, - }), - ) - .then(mrChanges => { - if (mrChanges.changes.length) { - dispatch('updateActivityBarView', activityBarViews.review); - } + return dispatch('getFiles', { + projectId, + branchId: mr.source_branch, + }); + }) + .then(() => + dispatch('getMergeRequestVersions', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(() => + dispatch('getMergeRequestChanges', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(mrChanges => { + if (mrChanges.changes.length) { + dispatch('updateActivityBarView', activityBarViews.review); + } - mrChanges.changes.forEach((change, ind) => { - const changeTreeEntry = state.entries[change.new_path]; + mrChanges.changes.forEach((change, ind) => { + const changeTreeEntry = state.entries[change.new_path]; - if (changeTreeEntry) { - dispatch('setFileMrChange', { - file: changeTreeEntry, - mrChange: change, - }); - - if (ind < 10) { - dispatch('getFileData', { - path: change.new_path, - makeFileActive: ind === 0, + if (changeTreeEntry) { + dispatch('setFileMrChange', { + file: changeTreeEntry, + mrChange: change, }); + + if (ind < 10) { + dispatch('getFileData', { + path: change.new_path, + makeFileActive: ind === 0, + }); + } } - } + }); + }) + .catch(e => { + flash(__('Error while loading the merge request. Please try again.')); + throw e; }); - }) - .catch(e => { - flash(__('Error while loading the merge request. Please try again.')); - throw e; - }); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 543dc6c0461..2cb08ab2945 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -125,10 +125,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { }); }; -export const openBranch = ( - { dispatch, state }, - { projectId, branchId, basePath }, -) => { +export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => { dispatch('setCurrentBranchId', branchId); dispatch('getBranchData', { @@ -136,23 +133,20 @@ export const openBranch = ( branchId, }); - return ( - dispatch('getFiles', { - projectId, - branchId, - }) - .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(state.entries).find( - key => key === path && !state.entries[key].pending, - ); - const treeEntry = state.entries[treeEntryKey]; + return dispatch('getFiles', { + projectId, + branchId, + }).then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; - if (treeEntry) { - dispatch('handleTreeEntryAction', treeEntry); - } + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); } - }) - ); + } + }); }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 462ca45db9b..d97a950a8b2 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -30,9 +30,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { const currentProject = rootState.projects[rootState.currentProjectId]; const commitStats = data.stats ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { - additions: data.stats.additions, // eslint-disable-line indent-legacy - deletions: data.stats.deletions, // eslint-disable-line indent-legacy - }) // eslint-disable-line indent-legacy + additions: data.stats.additions, + deletions: data.stats.deletions, + }) : ''; const commitMsg = sprintf( __('Your changes have been committed. Commit %{commitId} %{commitStats}'), diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index baa2497ec5b..4565c11a83f 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -3,8 +3,7 @@ import Api from '../../../../api'; import { scopes } from './constants'; import * as types from './mutation_types'; -export const requestMergeRequests = ({ commit }) => - commit(types.REQUEST_MERGE_REQUESTS); +export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => { dispatch( 'setErrorMessage', diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index 9e848699163..9848bcc2e64 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, prefer-arrow-callback, max-len, no-unused-vars */ +/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback, no-unused-vars */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 8c225cd7d91..4b4e9aa48ab 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-vars, consistent-return, quotes, max-len */ +/* eslint-disable no-var, one-var, no-unused-vars, consistent-return */ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index c6ad3aa3e0d..04c1cf021d9 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,281 +1,279 @@ <script> - import Visibility from 'visibilityjs'; - import { visitUrl } from '../../lib/utils/url_utility'; - import Poll from '../../lib/utils/poll'; - import eventHub from '../event_hub'; - import Service from '../services/index'; - import Store from '../stores'; - import titleComponent from './title.vue'; - import descriptionComponent from './description.vue'; - import editedComponent from './edited.vue'; - import formComponent from './form.vue'; - import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; +import Visibility from 'visibilityjs'; +import { visitUrl } from '../../lib/utils/url_utility'; +import Poll from '../../lib/utils/poll'; +import eventHub from '../event_hub'; +import Service from '../services/index'; +import Store from '../stores'; +import titleComponent from './title.vue'; +import descriptionComponent from './description.vue'; +import editedComponent from './edited.vue'; +import formComponent from './form.vue'; +import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; - export default { - components: { - descriptionComponent, - titleComponent, - editedComponent, - formComponent, - }, - mixins: [ - recaptchaModalImplementor, - ], - props: { - endpoint: { - required: true, - type: String, - }, - updateEndpoint: { - required: true, - type: String, - }, - canUpdate: { - required: true, - type: Boolean, - }, - canDestroy: { - required: true, - type: Boolean, - }, - showInlineEditButton: { - type: Boolean, - required: false, - default: true, - }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, - issuableRef: { - type: String, - required: true, - }, - initialTitleHtml: { - type: String, - required: true, - }, - initialTitleText: { - type: String, - required: true, - }, - initialDescriptionHtml: { - type: String, - required: false, - default: '', - }, - initialDescriptionText: { - type: String, - required: false, - default: '', - }, - initialTaskStatus: { - type: String, - required: false, - default: '', - }, - updatedAt: { - type: String, - required: false, - default: '', - }, - updatedByName: { - type: String, - required: false, - default: '', - }, - updatedByPath: { - type: String, - required: false, - default: '', - }, - issuableTemplates: { - type: Array, - required: false, - default: () => [], - }, - markdownPreviewPath: { - type: String, - required: true, - }, - markdownDocsPath: { - type: String, - required: true, - }, - markdownVersion: { - type: Number, - required: false, - default: 0, - }, - projectPath: { - type: String, - required: true, - }, - projectNamespace: { - type: String, - required: true, - }, - issuableType: { - type: String, - required: false, - default: 'issue', - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, - }, +export default { + components: { + descriptionComponent, + titleComponent, + editedComponent, + formComponent, + }, + mixins: [recaptchaModalImplementor], + props: { + endpoint: { + required: true, + type: String, }, - data() { - const store = new Store({ - titleHtml: this.initialTitleHtml, - titleText: this.initialTitleText, - descriptionHtml: this.initialDescriptionHtml, - descriptionText: this.initialDescriptionText, - updatedAt: this.updatedAt, - updatedByName: this.updatedByName, - updatedByPath: this.updatedByPath, - taskStatus: this.initialTaskStatus, - }); + updateEndpoint: { + required: true, + type: String, + }, + canUpdate: { + required: true, + type: Boolean, + }, + canDestroy: { + required: true, + type: Boolean, + }, + showInlineEditButton: { + type: Boolean, + required: false, + default: true, + }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + issuableRef: { + type: String, + required: true, + }, + initialTitleHtml: { + type: String, + required: true, + }, + initialTitleText: { + type: String, + required: true, + }, + initialDescriptionHtml: { + type: String, + required: false, + default: '', + }, + initialDescriptionText: { + type: String, + required: false, + default: '', + }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + markdownPreviewPath: { + type: String, + required: true, + }, + markdownDocsPath: { + type: String, + required: true, + }, + markdownVersion: { + type: Number, + required: false, + default: 0, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + const store = new Store({ + titleHtml: this.initialTitleHtml, + titleText: this.initialTitleText, + descriptionHtml: this.initialDescriptionHtml, + descriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, + }); - return { - store, - state: store.state, - showForm: false, - }; - }, - computed: { - formState() { - return this.store.formState; - }, - hasUpdated() { - return !!this.state.updatedAt; - }, - issueChanged() { - const descriptionChanged = - this.initialDescriptionText !== this.store.formState.description; - const titleChanged = - this.initialTitleText !== this.store.formState.title; - return descriptionChanged || titleChanged; - }, + return { + store, + state: store.state, + showForm: false, + }; + }, + computed: { + formState() { + return this.store.formState; }, - created() { - this.service = new Service(this.endpoint); - this.poll = new Poll({ - resource: this.service, - method: 'getData', - successCallback: res => this.store.updateState(res.data), - errorCallback(err) { - throw new Error(err); - }, - }); + hasUpdated() { + return !!this.state.updatedAt; + }, + issueChanged() { + const descriptionChanged = this.initialDescriptionText !== this.store.formState.description; + const titleChanged = this.initialTitleText !== this.store.formState.title; + return descriptionChanged || titleChanged; + }, + }, + created() { + this.service = new Service(this.endpoint); + this.poll = new Poll({ + resource: this.service, + method: 'getData', + successCallback: res => this.store.updateState(res.data), + errorCallback(err) { + throw new Error(err); + }, + }); + + if (!Visibility.hidden()) { + this.poll.makeRequest(); + } + Visibility.change(() => { if (!Visibility.hidden()) { - this.poll.makeRequest(); + this.poll.restart(); + } else { + this.poll.stop(); } + }); - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - - window.addEventListener('beforeunload', this.handleBeforeUnloadEvent); + window.addEventListener('beforeunload', this.handleBeforeUnloadEvent); - eventHub.$on('delete.issuable', this.deleteIssuable); - eventHub.$on('update.issuable', this.updateIssuable); - eventHub.$on('close.form', this.closeForm); - eventHub.$on('open.form', this.openForm); - }, - beforeDestroy() { - eventHub.$off('delete.issuable', this.deleteIssuable); - eventHub.$off('update.issuable', this.updateIssuable); - eventHub.$off('close.form', this.closeForm); - eventHub.$off('open.form', this.openForm); - window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent); - }, - methods: { - handleBeforeUnloadEvent(e) { - const event = e; - if (this.showForm && this.issueChanged) { - event.returnValue = 'Are you sure you want to lose your issue information?'; - } - return undefined; - }, + eventHub.$on('delete.issuable', this.deleteIssuable); + eventHub.$on('update.issuable', this.updateIssuable); + eventHub.$on('close.form', this.closeForm); + eventHub.$on('open.form', this.openForm); + }, + beforeDestroy() { + eventHub.$off('delete.issuable', this.deleteIssuable); + eventHub.$off('update.issuable', this.updateIssuable); + eventHub.$off('close.form', this.closeForm); + eventHub.$off('open.form', this.openForm); + window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent); + }, + methods: { + handleBeforeUnloadEvent(e) { + const event = e; + if (this.showForm && this.issueChanged) { + event.returnValue = 'Are you sure you want to lose your issue information?'; + } + return undefined; + }, - openForm() { - if (!this.showForm) { - this.showForm = true; - this.store.setFormState({ - title: this.state.titleText, - description: this.state.descriptionText, - lockedWarningVisible: false, - updateLoading: false, - }); - } - }, - closeForm() { - this.showForm = false; - }, + openForm() { + if (!this.showForm) { + this.showForm = true; + this.store.setFormState({ + title: this.state.titleText, + description: this.state.descriptionText, + lockedWarningVisible: false, + updateLoading: false, + }); + } + }, + closeForm() { + this.showForm = false; + }, - updateIssuable() { - return this.service.updateIssuable(this.store.formState) - .then(res => res.data) - .then(data => this.checkForSpam(data)) - .then((data) => { - if (window.location.pathname !== data.web_url) { - visitUrl(data.web_url); - } + updateIssuable() { + return this.service + .updateIssuable(this.store.formState) + .then(res => res.data) + .then(data => this.checkForSpam(data)) + .then(data => { + if (window.location.pathname !== data.web_url) { + visitUrl(data.web_url); + } - return this.service.getData(); - }) - .then(res => res.data) - .then((data) => { - this.store.updateState(data); + return this.service.getData(); + }) + .then(res => res.data) + .then(data => { + this.store.updateState(data); + eventHub.$emit('close.form'); + }) + .catch(error => { + if (error && error.name === 'SpamError') { + this.openRecaptcha(); + } else { eventHub.$emit('close.form'); - }) - .catch((error) => { - if (error && error.name === 'SpamError') { - this.openRecaptcha(); - } else { - eventHub.$emit('close.form'); - window.Flash(`Error updating ${this.issuableType}`); - } - }); - }, - - closeRecaptchaModal() { - this.store.setFormState({ - updateLoading: false, + window.Flash(`Error updating ${this.issuableType}`); + } }); + }, - this.closeRecaptcha(); - }, + closeRecaptchaModal() { + this.store.setFormState({ + updateLoading: false, + }); - deleteIssuable() { - this.service.deleteIssuable() - .then(res => res.data) - .then((data) => { - // Stop the poll so we don't get 404's with the issuable not existing - this.poll.stop(); + this.closeRecaptcha(); + }, - visitUrl(data.web_url); - }) - .catch(() => { - eventHub.$emit('close.form'); - window.Flash(`Error deleting ${this.issuableType}`); - }); - }, + deleteIssuable() { + this.service + .deleteIssuable() + .then(res => res.data) + .then(data => { + // Stop the poll so we don't get 404's with the issuable not existing + this.poll.stop(); + + visitUrl(data.web_url); + }) + .catch(() => { + eventHub.$emit('close.form'); + window.Flash(`Error deleting ${this.issuableType}`); + }); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 1174177f561..461cb3271b7 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,110 +1,105 @@ <script> - import $ from 'jquery'; - import animateMixin from '../mixins/animate'; - import TaskList from '../../task_list'; - import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; +import $ from 'jquery'; +import animateMixin from '../mixins/animate'; +import TaskList from '../../task_list'; +import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; - export default { - mixins: [ - animateMixin, - recaptchaModalImplementor, - ], +export default { + mixins: [animateMixin, recaptchaModalImplementor], - props: { - canUpdate: { - type: Boolean, - required: true, - }, - descriptionHtml: { - type: String, - required: true, - }, - descriptionText: { - type: String, - required: true, - }, - taskStatus: { - type: String, - required: false, - default: '', - }, - issuableType: { - type: String, - required: false, - default: 'issue', - }, - updateUrl: { - type: String, - required: false, - default: null, - }, + props: { + canUpdate: { + type: Boolean, + required: true, }, - data() { - return { - preAnimation: false, - pulseAnimation: false, - }; + descriptionHtml: { + type: String, + required: true, }, - watch: { - descriptionHtml() { - this.animateChange(); + descriptionText: { + type: String, + required: true, + }, + taskStatus: { + type: String, + required: false, + default: '', + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + updateUrl: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + preAnimation: false, + pulseAnimation: false, + }; + }, + watch: { + descriptionHtml() { + this.animateChange(); - this.$nextTick(() => { - this.renderGFM(); - }); - }, - taskStatus() { - this.updateTaskStatusText(); - }, + this.$nextTick(() => { + this.renderGFM(); + }); }, - mounted() { - this.renderGFM(); + taskStatus() { this.updateTaskStatusText(); }, - methods: { - renderGFM() { - $(this.$refs['gfm-content']).renderGFM(); + }, + mounted() { + this.renderGFM(); + this.updateTaskStatusText(); + }, + methods: { + renderGFM() { + $(this.$refs['gfm-content']).renderGFM(); - if (this.canUpdate) { - // eslint-disable-next-line no-new - new TaskList({ - dataType: this.issuableType, - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: this.taskListUpdateSuccess.bind(this), - }); - } - }, + if (this.canUpdate) { + // eslint-disable-next-line no-new + new TaskList({ + dataType: this.issuableType, + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: this.taskListUpdateSuccess.bind(this), + }); + } + }, - taskListUpdateSuccess(data) { - try { - this.checkForSpam(data); - this.closeRecaptcha(); - } catch (error) { - if (error && error.name === 'SpamError') this.openRecaptcha(); - } - }, + taskListUpdateSuccess(data) { + try { + this.checkForSpam(data); + this.closeRecaptcha(); + } catch (error) { + if (error && error.name === 'SpamError') this.openRecaptcha(); + } + }, - updateTaskStatusText() { - const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); - const $issuableHeader = $('.issuable-meta'); - const $tasks = $('#task_status', $issuableHeader); - const $tasksShort = $('#task_status_short', $issuableHeader); + updateTaskStatusText() { + const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); + const $issuableHeader = $('.issuable-meta'); + const $tasks = $('#task_status', $issuableHeader); + const $tasksShort = $('#task_status_short', $issuableHeader); - if (taskRegexMatches) { - $tasks.text(this.taskStatus); - $tasksShort.text( - `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? - 's' : - ''}`, - ); - } else { - $tasks.text(''); - $tasksShort.text(''); - } - }, + if (taskRegexMatches) { + $tasks.text(this.taskStatus); + $tasksShort.text( + `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`, + ); + } else { + $tasks.text(''); + $tasksShort.text(''); + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index c3d39082714..5dda35d64bb 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -1,64 +1,64 @@ <script> - import { __, sprintf } from '~/locale'; - import updateMixin from '../mixins/update'; - import eventHub from '../event_hub'; +import { __, sprintf } from '~/locale'; +import updateMixin from '../mixins/update'; +import eventHub from '../event_hub'; - const issuableTypes = { - issue: __('Issue'), - epic: __('Epic'), - }; +const issuableTypes = { + issue: __('Issue'), + epic: __('Epic'), +}; - export default { - mixins: [updateMixin], - props: { - canDestroy: { - type: Boolean, - required: true, - }, - formState: { - type: Object, - required: true, - }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, - issuableType: { - type: String, - required: true, - }, +export default { + mixins: [updateMixin], + props: { + canDestroy: { + type: Boolean, + required: true, }, - data() { - return { - deleteLoading: false, - }; + formState: { + type: Object, + required: true, }, - computed: { - isSubmitEnabled() { - return this.formState.title.trim() !== ''; - }, - shouldShowDeleteButton() { - return this.canDestroy && this.showDeleteButton; - }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, }, - methods: { - closeForm() { - eventHub.$emit('close.form'); - }, - deleteIssuable() { - const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), { - issuableType: issuableTypes[this.issuableType], - }); - // eslint-disable-next-line no-alert - if (window.confirm(confirmMessage)) { - this.deleteLoading = true; + issuableType: { + type: String, + required: true, + }, + }, + data() { + return { + deleteLoading: false, + }; + }, + computed: { + isSubmitEnabled() { + return this.formState.title.trim() !== ''; + }, + shouldShowDeleteButton() { + return this.canDestroy && this.showDeleteButton; + }, + }, + methods: { + closeForm() { + eventHub.$emit('close.form'); + }, + deleteIssuable() { + const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), { + issuableType: issuableTypes[this.issuableType], + }); + // eslint-disable-next-line no-alert + if (window.confirm(confirmMessage)) { + this.deleteLoading = true; - eventHub.$emit('delete.issuable'); - } - }, + eventHub.$emit('delete.issuable'); + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index 05cd976f196..73ecb26c28d 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -1,33 +1,33 @@ <script> - import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; - export default { - components: { - timeAgoTooltip, +export default { + components: { + timeAgoTooltip, + }, + props: { + updatedAt: { + type: String, + required: false, + default: '', }, - props: { - updatedAt: { - type: String, - required: false, - default: '', - }, - updatedByName: { - type: String, - required: false, - default: '', - }, - updatedByPath: { - type: String, - required: false, - default: '', - }, + updatedByName: { + type: String, + required: false, + default: '', }, - computed: { - hasUpdatedBy() { - return this.updatedByName && this.updatedByPath; - }, + updatedByPath: { + type: String, + required: false, + default: '', }, - }; + }, + computed: { + hasUpdatedBy() { + return this.updatedByName && this.updatedByPath; + }, + }, +}; </script> <template> @@ -53,4 +53,3 @@ </span> </small> </template> - diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 1a78c59d715..e9e96a985a7 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -1,45 +1,45 @@ <script> - import updateMixin from '../../mixins/update'; - import markdownField from '../../../vue_shared/components/markdown/field.vue'; +import updateMixin from '../../mixins/update'; +import markdownField from '../../../vue_shared/components/markdown/field.vue'; - export default { - components: { - markdownField, +export default { + components: { + markdownField, + }, + mixins: [updateMixin], + props: { + formState: { + type: Object, + required: true, }, - mixins: [updateMixin], - props: { - formState: { - type: Object, - required: true, - }, - markdownPreviewPath: { - type: String, - required: true, - }, - markdownDocsPath: { - type: String, - required: true, - }, - markdownVersion: { - type: Number, - required: false, - default: 0, - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, + markdownPreviewPath: { + type: String, + required: true, }, - mounted() { - this.$refs.textarea.focus(); + markdownDocsPath: { + type: String, + required: true, }, - }; + markdownVersion: { + type: Number, + required: false, + default: 0, + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + }, + mounted() { + this.$refs.textarea.focus(); + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue index e90d9fad94e..e433bf66cfc 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -1,46 +1,46 @@ <script> - import $ from 'jquery'; - import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors'; +import $ from 'jquery'; +import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors'; - export default { - props: { - formState: { - type: Object, - required: true, - }, - issuableTemplates: { - type: Array, - required: false, - default: () => [], - }, - projectPath: { - type: String, - required: true, - }, - projectNamespace: { - type: String, - required: true, - }, +export default { + props: { + formState: { + type: Object, + required: true, }, - computed: { - issuableTemplatesJson() { - return JSON.stringify(this.issuableTemplates); - }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], }, - mounted() { - // Create the editor for the template - const editor = document.querySelector('.detail-page-description .note-textarea') || {}; - editor.setValue = (val) => { - this.formState.description = val; - }; - editor.getValue = () => this.formState.description; - - this.issuableTemplate = new IssuableTemplateSelectors({ - $dropdowns: $(this.$refs.toggle), - editor, - }); + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, }, - }; + }, + computed: { + issuableTemplatesJson() { + return JSON.stringify(this.issuableTemplates); + }, + }, + mounted() { + // Create the editor for the template + const editor = document.querySelector('.detail-page-description .note-textarea') || {}; + editor.setValue = val => { + this.formState.description = val; + }; + editor.getValue = () => this.formState.description; + + this.issuableTemplate = new IssuableTemplateSelectors({ + $dropdowns: $(this.$refs.toggle), + editor, + }); + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index b7f2b1a6050..11f4153b8d5 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -1,15 +1,15 @@ <script> - import updateMixin from '../../mixins/update'; +import updateMixin from '../../mixins/update'; - export default { - mixins: [updateMixin], - props: { - formState: { - type: Object, - required: true, - }, +export default { + mixins: [updateMixin], + props: { + formState: { + type: Object, + required: true, }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 03d8d0ec67c..3b430d92912 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -1,79 +1,79 @@ <script> - import lockedWarning from './locked_warning.vue'; - import titleField from './fields/title.vue'; - import descriptionField from './fields/description.vue'; - import editActions from './edit_actions.vue'; - import descriptionTemplate from './fields/description_template.vue'; +import lockedWarning from './locked_warning.vue'; +import titleField from './fields/title.vue'; +import descriptionField from './fields/description.vue'; +import editActions from './edit_actions.vue'; +import descriptionTemplate from './fields/description_template.vue'; - export default { - components: { - lockedWarning, - titleField, - descriptionField, - descriptionTemplate, - editActions, +export default { + components: { + lockedWarning, + titleField, + descriptionField, + descriptionTemplate, + editActions, + }, + props: { + canDestroy: { + type: Boolean, + required: true, }, - props: { - canDestroy: { - type: Boolean, - required: true, - }, - formState: { - type: Object, - required: true, - }, - issuableTemplates: { - type: Array, - required: false, - default: () => [], - }, - issuableType: { - type: String, - required: true, - }, - markdownPreviewPath: { - type: String, - required: true, - }, - markdownDocsPath: { - type: String, - required: true, - }, - markdownVersion: { - type: Number, - required: false, - default: 0, - }, - projectPath: { - type: String, - required: true, - }, - projectNamespace: { - type: String, - required: true, - }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, + formState: { + type: Object, + required: true, }, - computed: { - hasIssuableTemplates() { - return this.issuableTemplates.length; - }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], }, - }; + issuableType: { + type: String, + required: true, + }, + markdownPreviewPath: { + type: String, + required: true, + }, + markdownDocsPath: { + type: String, + required: true, + }, + markdownVersion: { + type: Number, + required: false, + default: 0, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + hasIssuableTemplates() { + return this.issuableTemplates.length; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue index ad0d40faf32..0682c6f2a35 100644 --- a/app/assets/javascripts/issue_show/components/locked_warning.vue +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -1,11 +1,11 @@ <script> - export default { - computed: { - currentPath() { - return window.location.pathname; - }, +export default { + computed: { + currentPath() { + return window.location.pathname; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index af8b0414266..32044d6da25 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -25,8 +25,10 @@ export default class Store { } stateShouldUpdate(data) { - return this.state.titleText !== data.title_text || - this.state.descriptionText !== data.description_text; + return ( + this.state.titleText !== data.title_text || + this.state.descriptionText !== data.description_text + ); } setFormState(state) { diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue index d5866f9b9f1..17fd5321642 100644 --- a/app/assets/javascripts/jobs/components/artifacts_block.vue +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -1,30 +1,28 @@ <script> - import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - import timeagoMixin from '~/vue_shared/mixins/timeago'; +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; - export default { - components: { - TimeagoTooltip, +export default { + components: { + TimeagoTooltip, + }, + mixins: [timeagoMixin], + props: { + artifact: { + type: Object, + required: true, }, - mixins: [ - timeagoMixin, - ], - props: { - artifact: { - type: Object, - required: true, - }, + }, + computed: { + isExpired() { + return this.artifact.expired; }, - computed: { - isExpired() { - return this.artifact.expired; - }, - // Only when the key is `false` we can render this block - willExpire() { - return this.artifact.expired === false; - }, + // Only when the key is `false` we can render this block + willExpire() { + return this.artifact.expired === false; }, - }; + }, +}; </script> <template> <div class="block"> diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 4b1788a1c16..7d51f6afd10 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -1,26 +1,26 @@ <script> - import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - export default { - components: { - ClipboardButton, +export default { + components: { + ClipboardButton, + }, + props: { + commit: { + type: Object, + required: true, }, - props: { - commit: { - type: Object, - required: true, - }, - mergeRequest: { - type: Object, - required: false, - default: null, - }, - isLastBlock: { - type: Boolean, - required: true, - }, + mergeRequest: { + type: Object, + required: false, + default: null, }, - }; + isLastBlock: { + type: Boolean, + required: true, + }, + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index ff500eddddb..ee5ceb99b0a 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -1,38 +1,38 @@ <script> - export default { - props: { - illustrationPath: { - type: String, - required: true, - }, - illustrationSizeClass: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - content: { - type: String, - required: false, - default: null, - }, - action: { - type: Object, - required: false, - default: null, - validator(value) { - return ( - value === null || - (Object.prototype.hasOwnProperty.call(value, 'path') && - Object.prototype.hasOwnProperty.call(value, 'method') && - Object.prototype.hasOwnProperty.call(value, 'button_title')) - ); - }, +export default { + props: { + illustrationPath: { + type: String, + required: true, + }, + illustrationSizeClass: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + content: { + type: String, + required: false, + default: null, + }, + action: { + type: Object, + required: false, + default: null, + validator(value) { + return ( + value === null || + (Object.prototype.hasOwnProperty.call(value, 'path') && + Object.prototype.hasOwnProperty.call(value, 'method') && + Object.prototype.hasOwnProperty.call(value, 'button_title')) + ); }, }, - }; + }, +}; </script> <template> <div class="row empty-state"> diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index e6e1d418194..6d1eb713886 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -1,129 +1,131 @@ <script> - import _ from 'underscore'; - import CiIcon from '~/vue_shared/components/ci_icon.vue'; - import { sprintf, __ } from '../../locale'; +import _ from 'underscore'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { sprintf, __ } from '../../locale'; - export default { - components: { - CiIcon, +export default { + components: { + CiIcon, + }, + props: { + deploymentStatus: { + type: Object, + required: true, }, - props: { - deploymentStatus: { - type: Object, - required: true, - }, - iconStatus: { - type: Object, - required: true, - }, + iconStatus: { + type: Object, + required: true, }, - computed: { - environment() { - let environmentText; - switch (this.deploymentStatus.status) { - case 'last': + }, + computed: { + environment() { + let environmentText; + switch (this.deploymentStatus.status) { + case 'last': + environmentText = sprintf( + __('This job is the most recent deployment to %{link}.'), + { link: this.environmentLink }, + false, + ); + break; + case 'out_of_date': + if (this.hasLastDeployment) { environmentText = sprintf( - __('This job is the most recent deployment to %{link}.'), - { link: this.environmentLink }, + __( + 'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.', + ), + { + environmentLink: this.environmentLink, + deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`), + }, false, ); - break; - case 'out_of_date': - if (this.hasLastDeployment) { - environmentText = sprintf( - __( - 'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.', - ), - { - environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`), - }, - false, - ); - } else { - environmentText = sprintf( - __('This job is an out-of-date deployment to %{environmentLink}.'), - { environmentLink: this.environmentLink }, - false, - ); - } - - break; - case 'failed': + } else { environmentText = sprintf( - __('The deployment of this job to %{environmentLink} did not succeed.'), + __('This job is an out-of-date deployment to %{environmentLink}.'), { environmentLink: this.environmentLink }, false, ); - break; - case 'creating': - if (this.hasLastDeployment) { - environmentText = sprintf( - __( - 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.', - ), - { - environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink(__('latest deployment')), - }, - false, - ); - } else { - environmentText = sprintf( - __('This job is creating a deployment to %{environmentLink}.'), - { environmentLink: this.environmentLink }, - false, - ); - } - break; - default: - break; - } - return environmentText; - }, - environmentLink() { - if (this.hasEnvironment) { - return sprintf( - '%{startLink}%{name}%{endLink}', - { - startLink: `<a href="${ - this.deploymentStatus.environment.environment_path - }" class="js-environment-link">`, - name: _.escape(this.deploymentStatus.environment.name), - endLink: '</a>', - }, + } + + break; + case 'failed': + environmentText = sprintf( + __('The deployment of this job to %{environmentLink} did not succeed.'), + { environmentLink: this.environmentLink }, false, ); - } - return ''; - }, - hasLastDeployment() { - return this.hasEnvironment && this.deploymentStatus.environment.last_deployment; - }, - lastDeployment() { - return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; - }, - hasEnvironment() { - return !_.isEmpty(this.deploymentStatus.environment); - }, - lastDeploymentPath() { - return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : ''; - }, + break; + case 'creating': + if (this.hasLastDeployment) { + environmentText = sprintf( + __( + 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.', + ), + { + environmentLink: this.environmentLink, + deploymentLink: this.deploymentLink(__('latest deployment')), + }, + false, + ); + } else { + environmentText = sprintf( + __('This job is creating a deployment to %{environmentLink}.'), + { environmentLink: this.environmentLink }, + false, + ); + } + break; + default: + break; + } + return environmentText; }, - methods: { - deploymentLink(name) { + environmentLink() { + if (this.hasEnvironment) { return sprintf( '%{startLink}%{name}%{endLink}', { - startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`, - name, + startLink: `<a href="${ + this.deploymentStatus.environment.environment_path + }" class="js-environment-link">`, + name: _.escape(this.deploymentStatus.environment.name), endLink: '</a>', }, false, ); - }, + } + return ''; + }, + hasLastDeployment() { + return this.hasEnvironment && this.deploymentStatus.environment.last_deployment; + }, + lastDeployment() { + return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; + }, + hasEnvironment() { + return !_.isEmpty(this.deploymentStatus.environment); + }, + lastDeploymentPath() { + return !_.isEmpty(this.lastDeployment.deployable) + ? this.lastDeployment.deployable.build_path + : ''; + }, + }, + methods: { + deploymentLink(name) { + return sprintf( + '%{startLink}%{name}%{endLink}', + { + startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`, + name, + endLink: '</a>', + }, + false, + ); }, - }; + }, +}; </script> <template> <div class="prepend-top-default js-environment-container"> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue index 3d6d9ba4387..5ffbfb6e19a 100644 --- a/app/assets/javascripts/jobs/components/erased_block.vue +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -1,28 +1,28 @@ <script> - import _ from 'underscore'; - import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import _ from 'underscore'; +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - export default { - components: { - TimeagoTooltip, +export default { + components: { + TimeagoTooltip, + }, + props: { + user: { + type: Object, + required: false, + default: () => ({}), }, - props: { - user: { - type: Object, - required: false, - default: () => ({}), - }, - erasedAt: { - type: String, - required: true, - }, + erasedAt: { + type: String, + required: true, }, - computed: { - isErasedByUser() { - return !_.isEmpty(this.user); - }, + }, + computed: { + isErasedByUser() { + return !_.isEmpty(this.user); }, - }; + }, +}; </script> <template> <div class="prepend-top-default js-build-erased"> diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 047e55866ce..4e8d3ad24cc 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -18,7 +18,7 @@ StuckBlock, }, props: { - runnerHelpUrl: { + runnerSettingsUrl: { type: String, required: false, default: null, @@ -30,7 +30,7 @@ 'headerActions', 'headerTime', 'shouldRenderCalloutMessage', - 'jobHasStarted', + 'shouldRenderTriggeredLabel', 'hasEnvironment', 'isJobStuck', 'hasTrace', @@ -58,7 +58,7 @@ :user="job.user" :actions="headerActions" :has-sidebar-button="true" - :should-render-triggered-label="jobHasStarted" + :should-render-triggered-label="shouldRenderTriggeredLabel" :item-name="__('Job')" /> </div> @@ -76,7 +76,7 @@ class="js-job-stuck" :has-no-runners-for-project="job.runners.available" :tags="job.tags" - :runners-path="runnerHelpUrl" + :runners-path="runnerSettingsUrl" /> <environments-block @@ -87,8 +87,8 @@ /> <erased-block - v-if="job.erased" - class="js-job-erased" + v-if="job.erased_at" + class="js-job-erased-block" :user="job.erased_by" :erased-at="job.erased_at" /> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index b12e963b60c..9d78d89239a 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -1,17 +1,17 @@ <script> - export default { - name: 'JobLog', - props: { - trace: { - type: String, - required: true, - }, - isComplete: { - type: Boolean, - required: true, - }, +export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, }, - }; + isComplete: { + type: Boolean, + required: true, + }, + }, +}; </script> <template> <pre class="build-trace"> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 3e62ababea3..cc885ea8e1b 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,72 +1,71 @@ <script> - import { polyfillSticky } from '~/lib/utils/sticky'; - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; - import { numberToHumanSize } from '~/lib/utils/number_utils'; - import { sprintf } from '~/locale'; +import { polyfillSticky } from '~/lib/utils/sticky'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { sprintf } from '~/locale'; - export default { - components: { - Icon, +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + erasePath: { + type: String, + required: false, + default: null, }, - directives: { - tooltip, + size: { + type: Number, + required: true, }, - props: { - erasePath: { - type: String, - required: false, - default: null, - }, - size: { - type: Number, - required: true, - }, - rawPath: { - type: String, - required: false, - default: null, - }, - isScrollTopDisabled: { - type: Boolean, - required: true, - }, - isScrollBottomDisabled: { - type: Boolean, - required: true, - }, - isScrollingDown: { - type: Boolean, - required: true, - }, - isTraceSizeVisible: { - type: Boolean, - required: true, - }, + rawPath: { + type: String, + required: false, + default: null, }, - computed: { - jobLogSize() { - return sprintf('Showing last %{size} of log -', { - size: numberToHumanSize(this.size), - }); - }, + isScrollTopDisabled: { + type: Boolean, + required: true, }, - mounted() { - polyfillSticky(this.$el); + isScrollBottomDisabled: { + type: Boolean, + required: true, }, - methods: { - handleScrollToTop() { - this.$emit('scrollJobLogTop'); - }, - handleScrollToBottom() { - this.$emit('scrollJobLogBottom'); - }, + isScrollingDown: { + type: Boolean, + required: true, }, - - }; + isTraceSizeVisible: { + type: Boolean, + required: true, + }, + }, + computed: { + jobLogSize() { + return sprintf('Showing last %{size} of log -', { + size: numberToHumanSize(this.size), + }); + }, + }, + mounted() { + polyfillSticky(this.$el); + }, + methods: { + handleScrollToTop() { + this.$emit('scrollJobLogTop'); + }, + handleScrollToBottom() { + this.$emit('scrollJobLogBottom'); + }, + }, +}; </script> <template> - <div class="top-bar"> + <div class="top-bar affix js-top-bar"> <!-- truncate information --> <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> <template v-if="isTraceSizeVisible"> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index 271b7790d75..03f36ec5c8b 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -1,36 +1,36 @@ <script> - import _ from 'underscore'; - import CiIcon from '~/vue_shared/components/ci_icon.vue'; - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; +import _ from 'underscore'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - components: { - CiIcon, - Icon, +export default { + components: { + CiIcon, + Icon, + }, + directives: { + tooltip, + }, + props: { + jobs: { + type: Array, + required: true, }, - directives: { - tooltip, + jobId: { + type: Number, + required: true, }, - props: { - jobs: { - type: Array, - required: true, - }, - jobId: { - type: Number, - required: true, - }, + }, + methods: { + isJobActive(currentJobId) { + return this.jobId === currentJobId; }, - methods: { - isJobActive(currentJobId) { - return this.jobId === currentJobId; - }, - tooltipText(job) { - return `${_.escape(job.name)} - ${job.status.tooltip}`; - }, + tooltipText(job) { + return `${_.escape(job.name)} - ${job.status.tooltip}`; }, - }; + }, +}; </script> <template> <div class="js-jobs-container builds-container"> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 22bcd402e72..8f3c6aced23 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -1,116 +1,116 @@ <script> - import _ from 'underscore'; - import { mapActions, mapState } from 'vuex'; - import timeagoMixin from '~/vue_shared/mixins/timeago'; - import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; - import Icon from '~/vue_shared/components/icon.vue'; - import DetailRow from './sidebar_detail_row.vue'; - import ArtifactsBlock from './artifacts_block.vue'; - import TriggerBlock from './trigger_block.vue'; - import CommitBlock from './commit_block.vue'; - import StagesDropdown from './stages_dropdown.vue'; - import JobsContainer from './jobs_container.vue'; +import _ from 'underscore'; +import { mapActions, mapState } from 'vuex'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import Icon from '~/vue_shared/components/icon.vue'; +import DetailRow from './sidebar_detail_row.vue'; +import ArtifactsBlock from './artifacts_block.vue'; +import TriggerBlock from './trigger_block.vue'; +import CommitBlock from './commit_block.vue'; +import StagesDropdown from './stages_dropdown.vue'; +import JobsContainer from './jobs_container.vue'; - export default { - name: 'JobSidebar', - components: { - ArtifactsBlock, - CommitBlock, - DetailRow, - Icon, - TriggerBlock, - StagesDropdown, - JobsContainer, +export default { + name: 'JobSidebar', + components: { + ArtifactsBlock, + CommitBlock, + DetailRow, + Icon, + TriggerBlock, + StagesDropdown, + JobsContainer, + }, + mixins: [timeagoMixin], + props: { + runnerHelpUrl: { + type: String, + required: false, + default: '', }, - mixins: [timeagoMixin], - props: { - runnerHelpUrl: { - type: String, - required: false, - default: '', - }, - terminalPath: { - type: String, - required: false, - default: null, - }, + terminalPath: { + type: String, + required: false, + default: null, }, - computed: { - ...mapState(['job', 'isLoading', 'stages', 'jobs']), - coverage() { - return `${this.job.coverage}%`; - }, - duration() { - return timeIntervalInWords(this.job.duration); - }, - queued() { - return timeIntervalInWords(this.job.queued); - }, - runnerId() { - return `${this.job.runner.description} (#${this.job.runner.id})`; - }, - retryButtonClass() { - let className = - 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; - className += - this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; - return className; - }, - hasTimeout() { - return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; - }, - timeout() { - if (this.job.metadata == null) { - return ''; - } + }, + computed: { + ...mapState(['job', 'isLoading', 'stages', 'jobs', 'selectedStage']), + coverage() { + return `${this.job.coverage}%`; + }, + duration() { + return timeIntervalInWords(this.job.duration); + }, + queued() { + return timeIntervalInWords(this.job.queued); + }, + runnerId() { + return `${this.job.runner.description} (#${this.job.runner.id})`; + }, + retryButtonClass() { + let className = + 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + className += + this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; + return className; + }, + hasTimeout() { + return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; + }, + timeout() { + if (this.job.metadata == null) { + return ''; + } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } - return t; - }, - renderBlock() { - return ( - this.job.merge_request || - this.job.duration || - this.job.finished_data || - this.job.erased_at || - this.job.queued || - this.job.runner || - this.job.coverage || - this.job.tags.length || - this.job.cancel_path - ); - }, - hasArtifact() { - return !_.isEmpty(this.job.artifact); - }, - hasTriggers() { - return !_.isEmpty(this.job.trigger); - }, - hasStages() { - return ( - (this.job && - this.job.pipeline && - this.job.pipeline.stages && - this.job.pipeline.stages.length > 0) || - false - ); - }, - commit() { - return this.job.pipeline.commit || {}; - }, + return t; + }, + renderBlock() { + return ( + this.job.merge_request || + this.job.duration || + this.job.finished_data || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage || + this.job.tags.length || + this.job.cancel_path + ); + }, + hasArtifact() { + return !_.isEmpty(this.job.artifact); + }, + hasTriggers() { + return !_.isEmpty(this.job.trigger); + }, + hasStages() { + return ( + (this.job && + this.job.pipeline && + this.job.pipeline.stages && + this.job.pipeline.stages.length > 0) || + false + ); }, - methods: { - ...mapActions(['fetchJobsForStage']), + commit() { + return this.job.pipeline.commit || {}; }, - }; + }, + methods: { + ...mapActions(['fetchJobsForStage']), + }, +}; </script> <template> <aside - class="right-sidebar right-sidebar-expanded build-sidebar" + class="js-build-sidebar right-sidebar right-sidebar-expanded build-sidebar" data-offset-top="101" data-spy="affix" > @@ -276,6 +276,7 @@ <stages-dropdown :stages="stages" :pipeline="job.pipeline" + :selected-stage="selectedStage" @requestSidebarStageDropdown="fetchJobsForStage" /> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index 83560a8ff0e..aeafe98a70b 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -1,31 +1,31 @@ <script> - export default { - name: 'SidebarDetailRow', - props: { - title: { - type: String, - required: false, - default: '', - }, - value: { - type: String, - required: true, - }, - helpUrl: { - type: String, - required: false, - default: '', - }, +export default { + name: 'SidebarDetailRow', + props: { + title: { + type: String, + required: false, + default: '', }, - computed: { - hasTitle() { - return this.title.length > 0; - }, - hasHelpURL() { - return this.helpUrl.length > 0; - }, + value: { + type: String, + required: true, }, - }; + helpUrl: { + type: String, + required: false, + default: '', + }, + }, + computed: { + hasTitle() { + return this.title.length > 0; + }, + hasHelpURL() { + return this.helpUrl.length > 0; + }, + }, +}; </script> <template> <p class="build-detail-row"> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 1c15af55a8b..e5e1d56e287 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,50 +1,39 @@ <script> - import _ from 'underscore'; - import CiIcon from '~/vue_shared/components/ci_icon.vue'; - import Icon from '~/vue_shared/components/icon.vue'; - import { __ } from '~/locale'; +import _ from 'underscore'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; - export default { - components: { - CiIcon, - Icon, +export default { + components: { + CiIcon, + Icon, + }, + props: { + pipeline: { + type: Object, + required: true, }, - props: { - pipeline: { - type: Object, - required: true, - }, - stages: { - type: Array, - required: true, - }, + stages: { + type: Array, + required: true, }, - data() { - return { - selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'), - }; + selectedStage: { + type: String, + required: true, }, - computed: { - hasRef() { - return !_.isEmpty(this.pipeline.ref); - }, - }, - watch: { - // When the component is initially mounted it may start with an empty stages array. - // Once the prop is updated, we set the first stage as the selected one - stages(newVal) { - if (newVal.length) { - this.selectedStage = newVal[0].name; - } - }, + }, + + computed: { + hasRef() { + return !_.isEmpty(this.pipeline.ref); }, - methods: { - onStageClick(stage) { - this.$emit('requestSidebarStageDropdown', stage); - this.selectedStage = stage.name; - }, + }, + methods: { + onStageClick(stage) { + this.$emit('requestSidebarStageDropdown', stage); }, - }; + }, +}; </script> <template> <div class="block-last dropdown"> diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue index d7b3c4fcb5b..41de4a6e85a 100644 --- a/app/assets/javascripts/jobs/components/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -1,27 +1,27 @@ <script> - export default { - props: { - trigger: { - type: Object, - required: true, - }, +export default { + props: { + trigger: { + type: Object, + required: true, }, - data() { - return { - areVariablesVisible: false, - }; + }, + data() { + return { + areVariablesVisible: false, + }; + }, + computed: { + hasVariables() { + return this.trigger.variables && this.trigger.variables.length > 0; }, - computed: { - hasVariables() { - return this.trigger.variables && this.trigger.variables.length > 0; - }, + }, + methods: { + revealVariables() { + this.areVariablesVisible = true; }, - methods: { - revealVariables() { - this.areVariablesVisible = true; - }, - }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 3eb75e72506..15cd79b1c50 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -9,8 +9,7 @@ import createStore from './store'; export default () => { const { dataset } = document.getElementById('js-job-details-vue'); - // eslint-disable-next-line no-new - new Job(); + const store = createStore(); store.dispatch('setJobEndpoint', dataset.endpoint); @@ -33,7 +32,7 @@ export default () => { props: { isLoading: this.isLoading, job: this.job, - runnerHelpUrl: dataset.runnerHelpUrl, + runnerSettingsUrl: dataset.runnerSettingsUrl, }, }); }, @@ -71,4 +70,7 @@ export default () => { }); }, }); + + // eslint-disable-next-line no-new + new Job(); }; diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index 298367c9342..d0040161dc3 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -139,10 +139,12 @@ export const fetchStages = ({ state, dispatch }) => { dispatch('requestStages'); axios - .get(state.job.pipeline.path) + .get(`${state.job.pipeline.path}.json`) .then(({ data }) => { + // Set selected stage dispatch('receiveStagesSuccess', data.details.stages); - dispatch('fetchJobsForStage', data.details.stages[0]); + const selectedStage = data.details.stages.find(stage => stage.name === state.selectedStage); + dispatch('fetchJobsForStage', selectedStage); }) .catch(() => dispatch('receiveStagesError')); }; @@ -156,11 +158,12 @@ export const receiveStagesError = ({ commit }) => { /** * Jobs list on sidebar - depend on stages dropdown */ -export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); +export const requestJobsForStage = ({ commit }, stage) => + commit(types.REQUEST_JOBS_FOR_STAGE, stage); // On stage click, set selected stage + fetch job export const fetchJobsForStage = ({ dispatch }, stage) => { - dispatch('requestJobsForStage'); + dispatch('requestJobsForStage', stage); axios .get(stage.dropdown_path, { diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index afe5f88b292..9f4f372e3d2 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -22,10 +22,10 @@ export const shouldRenderCalloutMessage = state => !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); /** - * When job has not started the key will be `false` + * When job has not started the key will be null * When job started the key will be a string with a date. */ -export const jobHasStarted = state => !(state.job.started === false); +export const shouldRenderTriggeredLabel = state => _.isString(state.job.started); export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js index 96e38f9a2fa..bba01426af7 100644 --- a/app/assets/javascripts/jobs/store/index.js +++ b/app/assets/javascripts/jobs/store/index.js @@ -7,9 +7,10 @@ import mutations from './mutations'; Vue.use(Vuex); -export default () => new Vuex.Store({ - actions, - mutations, - getters, - state: state(), -}); +export default () => + new Vuex.Store({ + actions, + mutations, + getters, + state: state(), + }); diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index c3f2359fa4d..f00e06e1a6c 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -53,6 +53,16 @@ export default { state.isLoading = false; state.hasError = false; state.job = job; + + /** + * We only update it on the first request + * The dropdown can be changed by the user + * after the first request, + * and we do not want to hijack that + */ + if (state.selectedStage === 'More' && job.stage) { + state.selectedStage = job.stage; + } }, [types.RECEIVE_JOB_ERROR](state) { state.isLoading = false; @@ -81,8 +91,9 @@ export default { state.stages = []; }, - [types.REQUEST_JOBS_FOR_STAGE](state) { + [types.REQUEST_JOBS_FOR_STAGE](state, stage) { state.isLoadingJobs = true; + state.selectedStage = stage.name; }, [types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) { state.isLoadingJobs = false; diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 509cb69a5d3..afbc959bb71 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export default () => ({ jobEndpoint: null, traceEndpoint: null, @@ -34,7 +36,7 @@ export default () => ({ // sidebar dropdown isLoadingStages: false, isLoadingJobs: false, - selectedStage: null, + selectedStage: __('More'), stages: [], jobs: [], }); diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index c10b1a2b233..2c995c5902f 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names, max-len */ +/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names */ import $ from 'jquery'; import Sortable from 'sortablejs'; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 1c7bca78df3..3c38d998b6c 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,4 +1,4 @@ -/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty */ +/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, one-var, no-unused-vars, prefer-template, no-new, consistent-return, object-shorthand, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */ /* global Issuable */ /* global ListLabel */ @@ -11,6 +11,7 @@ import DropdownUtils from './filtered_search/dropdown_utils'; import CreateLabelDropdown from './create_label'; import flash from './flash'; import ModalStore from './boards/stores/modal_store'; +import boardsStore from './boards/stores/boards_store'; export default class LabelsSelect { constructor(els, options = {}) { @@ -378,7 +379,7 @@ export default class LabelsSelect { } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if ($el.hasClass('is-active')) { - gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({ + boardsStore.detail.issue.labels.push(new ListLabel({ id: label.id, title: label.title, color: label.color[0], @@ -386,16 +387,16 @@ export default class LabelsSelect { })); } else { - var { labels } = gl.issueBoards.BoardsStore.detail.issue; + var { labels } = boardsStore.detail.issue; labels = labels.filter(function (selectedLabel) { return selectedLabel.id !== label.id; }); - gl.issueBoards.BoardsStore.detail.issue.labels = labels; + boardsStore.detail.issue.labels = labels; } $loading.fadeIn(); - gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + boardsStore.detail.issue.update($dropdown.attr('data-issue-update')) .then(fadeOutLoader) .catch(fadeOutLoader); } diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index 616d8952ada..2d976dbdbbe 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -4,7 +4,7 @@ import Cache from './cache'; class AjaxCache extends Cache { constructor() { super(); - this.pendingRequests = { }; + this.pendingRequests = {}; } override(endpoint, data) { @@ -19,12 +19,13 @@ class AjaxCache extends Cache { let pendingRequest = this.pendingRequests[endpoint]; if (!pendingRequest) { - pendingRequest = axios.get(endpoint) + pendingRequest = axios + .get(endpoint) .then(({ data }) => { this.internalStorage[endpoint] = data; delete this.pendingRequests[endpoint]; }) - .catch((e) => { + .catch(e => { const error = new Error(`${endpoint}: ${e.message}`); error.textStatus = e.message; diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 792871e2ecf..69159e2d741 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -7,7 +7,7 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // Maintain a global counter for active requests // see: spec/support/wait_for_requests.rb -axios.interceptors.request.use((config) => { +axios.interceptors.request.use(config => { window.activeVueResources = window.activeVueResources || 0; window.activeVueResources += 1; @@ -15,15 +15,18 @@ axios.interceptors.request.use((config) => { }); // Remove the global counter -axios.interceptors.response.use((config) => { - window.activeVueResources -= 1; - - return config; -}, (e) => { - window.activeVueResources -= 1; - - return Promise.reject(e); -}); +axios.interceptors.response.use( + config => { + window.activeVueResources -= 1; + + return config; + }, + e => { + window.activeVueResources -= 1; + + return Promise.reject(e); + }, +); export default axios; diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index c28ed04f94f..a24c71aeab1 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -93,9 +93,13 @@ export default class LinkedTabs { const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; - window.history.replaceState({ - url: newState, - }, document.title, newState); + window.history.replaceState( + { + url: newState, + }, + document.title, + newState, + ); return newState; } diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js index 596bd1e388a..45048145139 100644 --- a/app/assets/javascripts/lib/utils/cache.js +++ b/app/assets/javascripts/lib/utils/cache.js @@ -1,6 +1,6 @@ export default class Cache { constructor() { - this.internalStorage = { }; + this.internalStorage = {}; } get(key) { diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js index e98c9068367..19e4085dbbb 100644 --- a/app/assets/javascripts/lib/utils/datefix.js +++ b/app/assets/javascripts/lib/utils/datefix.js @@ -1,12 +1,11 @@ - -export const pad = (val, len = 2) => (`0${val}`).slice(-len); +export const pad = (val, len = 2) => `0${val}`.slice(-len); /** * Formats dates in Pickaday * @param {String} dateString Date in yyyy-mm-dd format * @return {Date} UTC format */ -export const parsePikadayDate = (dateString) => { +export const parsePikadayDate = dateString => { const parts = dateString.split('-'); const year = parseInt(parts[0], 10); const month = parseInt(parts[1] - 1, 10); @@ -20,7 +19,7 @@ export const parsePikadayDate = (dateString) => { * @param {Date} date UTC format * @return {String} Date formated in yyyy-mm-dd */ -export const pikadayToString = (date) => { +export const pikadayToString = date => { const day = pad(date.getDate()); const month = pad(date.getMonth() + 1); const year = date.getFullYear(); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 305ad3e5e26..d93873e0214 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, max-len */ +/* eslint-disable func-names, no-var, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand */ function notificationGranted(message, opts, onclick) { var notification; @@ -8,7 +8,7 @@ function notificationGranted(message, opts, onclick) { return notification.close(); }, 8000); - return notification.onclick = onclick || notification.close; + return (notification.onclick = onclick || notification.close); } function notifyPermissions() { @@ -21,7 +21,7 @@ function notifyMe(message, body, icon, onclick) { var opts; opts = { body: body, - icon: icon + icon: icon, }; // Let's check if the browser supports notifications if (!('Notification' in window)) { diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js index b1ffd797f7e..d92b8a7179f 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ b/app/assets/javascripts/lib/utils/pretty_time.js @@ -27,10 +27,10 @@ export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); - return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { + return _.mapObject(timePeriodConstraints, minutesPerPeriod => { const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); - unorderedMinutes -= (periodCount * minutesPerPeriod); + unorderedMinutes -= periodCount * minutesPerPeriod; return periodCount; }); @@ -42,10 +42,14 @@ export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) */ export function stringifyTime(timeObject) { - const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; - return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; - }, '').trim(); + const reducedTime = _.reduce( + timeObject, + (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, + '', + ).trim(); return reducedTime.length ? reducedTime : '0m'; } @@ -55,7 +59,5 @@ export function stringifyTime(timeObject) { */ export function abbreviateTime(timeStr) { - return timeStr.split(' ') - .filter(unitStr => unitStr.charAt(0) !== '0')[0]; + return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; } - diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js index baa0b51d59b..25b60dcd14a 100644 --- a/app/assets/javascripts/lib/utils/regexp.js +++ b/app/assets/javascripts/lib/utils/regexp.js @@ -5,6 +5,7 @@ // Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203 // Unicode 6.1 -const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC'; +const unicodeLetters = + '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC'; export default { unicodeLetters }; diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js index 25ca98afbe7..473f179ad86 100644 --- a/app/assets/javascripts/lib/utils/simple_poll.js +++ b/app/assets/javascripts/lib/utils/simple_poll.js @@ -2,7 +2,7 @@ export default (fn, interval = 2000, timeout = 60000) => { const startTime = Date.now(); return new Promise((resolve, reject) => { - const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg)); const next = () => { if (Date.now() - startTime < timeout) { setTimeout(fn.bind(null, next, stop), interval); diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 15a4dd62012..f3244301350 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -24,7 +24,11 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { } else if (top > stickyTop && el.classList.contains('is-stuck')) { el.classList.remove('is-stuck'); - if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) { + if ( + insertPlaceholder && + el.nextElementSibling && + el.nextElementSibling.classList.contains('sticky-placeholder') + ) { el.nextElementSibling.remove(); } } @@ -42,11 +46,19 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { if (!el) return; - if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; + if ( + typeof CSS === 'undefined' || + !CSS.supports('(position: -webkit-sticky) or (position: sticky)') + ) + return; - document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { - passive: true, - }); + document.addEventListener( + 'scroll', + () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), + { + passive: true, + }, + ); }; /** @@ -55,6 +67,6 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { * - If the current environment supports `position: sticky`, do nothing. * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. */ -export const polyfillSticky = (el) => { +export const polyfillSticky = el => { StickyFill.add(el); }; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index f7429601afa..e26a6b986be 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, no-param-reassign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, max-len, consistent-return, no-unused-vars, max-len */ +/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, consistent-return, no-unused-vars */ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; @@ -8,12 +8,18 @@ function selectedText(text, textarea) { function lineBefore(text, textarea) { var split; - split = text.substring(0, textarea.selectionStart).trim().split('\n'); + split = text + .substring(0, textarea.selectionStart) + .trim() + .split('\n'); return split[split.length - 1]; } function lineAfter(text, textarea) { - return text.substring(textarea.selectionEnd).trim().split('\n')[0]; + return text + .substring(textarea.selectionEnd) + .trim() + .split('\n')[0]; } function blockTagText(text, textArea, blockTag, selected) { @@ -27,7 +33,7 @@ function blockTagText(text, textArea, blockTag, selected) { } return selected; } else { - return blockTag + "\n" + selected + "\n" + blockTag; + return blockTag + '\n' + selected + '\n' + blockTag; } } @@ -58,7 +64,14 @@ function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) { } export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) { - var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; + var textToInsert, + inserted, + selectedSplit, + startChar, + removedLastNewLine, + removedFirstNewLine, + currentLineEmpty, + lastNewLine; removedLastNewLine = false; removedFirstNewLine = false; currentLineEmpty = false; @@ -94,21 +107,23 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr if (blockTag != null && blockTag !== '') { textToInsert = blockTagText(text, textArea, blockTag, selected); } else { - textToInsert = selectedSplit.map(function(val) { - if (tag.indexOf(textPlaceholder) > -1) { - return tag.replace(textPlaceholder, val); - } - if (val.indexOf(tag) === 0) { - return "" + (val.replace(tag, '')); - } else { - return "" + tag + val; - } - }).join('\n'); + textToInsert = selectedSplit + .map(function(val) { + if (tag.indexOf(textPlaceholder) > -1) { + return tag.replace(textPlaceholder, val); + } + if (val.indexOf(tag) === 0) { + return '' + val.replace(tag, ''); + } else { + return '' + tag + val; + } + }) + .join('\n'); } } else if (tag.indexOf(textPlaceholder) > -1) { textToInsert = tag.replace(textPlaceholder, selected); } else { - textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' '); + textToInsert = '' + startChar + tag + selected + (wrap ? tag : ' '); } if (removedFirstNewLine) { @@ -120,7 +135,13 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr } insertText(textArea, textToInsert); - return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), wrap, removedLastNewLine, select }); + return moveCursor({ + textArea, + tag: tag.replace(textPlaceholder, selected), + wrap, + removedLastNewLine, + select, + }); } function updateText({ textArea, tag, blockTag, wrap, select }) { @@ -138,15 +159,18 @@ function replaceRange(s, start, end, substitute) { } export function addMarkdownListeners(form) { - return $('.js-md', form).off('click').on('click', function() { - const $this = $(this); - return updateText({ - textArea: $this.closest('.md-area').find('textarea'), - tag: $this.data('mdTag'), - blockTag: $this.data('mdBlock'), - wrap: !$this.data('mdPrepend'), - select: $this.data('mdSelect') }); - }); + return $('.js-md', form) + .off('click') + .on('click', function() { + const $this = $(this); + return updateText({ + textArea: $this.closest('.md-area').find('textarea'), + tag: $this.data('mdTag'), + blockTag: $this.data('mdBlock'), + wrap: !$this.data('mdPrepend'), + select: $this.data('mdSelect'), + }); + }); } export function removeMarkdownListeners(form) { diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 879f94a26ec..250980919dd 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -8,7 +8,7 @@ * @returns {String} */ export const addDelimiter = text => - (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text); + text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text; /** * Returns '99+' for numbers bigger than 99. @@ -94,9 +94,7 @@ export function capitalizeFirstCharacter(text) { * @return {String} */ export function getFirstCharacterCapitalized(text) { - return text - ? text.charAt(0).toUpperCase() - : ''; + return text ? text.charAt(0).toUpperCase() : ''; } /** @@ -136,10 +134,9 @@ export const convertToSentenceCase = string => { * e.g. HelloWorld => Hello World * * @param {*} string -*/ -export const splitCamelCase = string => ( + */ +export const splitCamelCase = string => string - .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') - .replace(/([a-z\d])([A-Z])/g, '$1 $2') - .trim() -); + .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') + .replace(/([a-z\d])([A-Z])/g, '$1 $2') + .trim(); diff --git a/app/assets/javascripts/lib/utils/tick_formats.js b/app/assets/javascripts/lib/utils/tick_formats.js index 0c10a85e336..af3ca714400 100644 --- a/app/assets/javascripts/lib/utils/tick_formats.js +++ b/app/assets/javascripts/lib/utils/tick_formats.js @@ -26,7 +26,7 @@ initDateFormats(); see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat */ -export const dateTickFormat = (date) => { +export const dateTickFormat = date => { if (date.getDate() !== 1) { return dateTimeFormats.dayFormat.format(date); } diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index b01ec6b81a3..c0d45e017b4 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -7,21 +7,20 @@ class UsersCache extends Cache { return Promise.resolve(this.get(username)); } - return Api.users('', { username }) - .then(({ data }) => { - if (!data.length) { - throw new Error(`User "${username}" could not be found!`); - } + return Api.users('', { username }).then(({ data }) => { + if (!data.length) { + throw new Error(`User "${username}" could not be found!`); + } - if (data.length > 1) { - throw new Error(`Expected username "${username}" to be unique!`); - } + if (data.length > 1) { + throw new Error(`Expected username "${username}" to be unique!`); + } - const user = data[0]; - this.internalStorage[username] = user; - return user; - }); - // missing catch is intentional, error handling depends on use case + const user = data[0]; + this.internalStorage[username] = user; + return user; + }); + // missing catch is intentional, error handling depends on use case } } diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 291655235d5..d58fd63bb33 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */ +/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, prefer-template, consistent-return, one-var, no-else-return */ import $ from 'jquery'; diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 81950515ab4..425b806e9d6 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-param-reassign, max-len */ +/* eslint-disable no-useless-computed-key, object-shorthand, no-param-reassign */ /* global ace */ import Vue from 'vue'; @@ -6,7 +6,7 @@ import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; -((global) => { +(global => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.diffFileEditor = Vue.extend({ @@ -35,10 +35,10 @@ import { __ } from '~/locale'; computed: { classObject() { return { - 'saved': this.saved, - 'is-loading': this.loading + saved: this.saved, + 'is-loading': this.loading, }; - } + }, }, watch: { ['file.showEditor'](val) { @@ -49,7 +49,7 @@ import { __ } from '~/locale'; } this.loadEditor(); - } + }, }, mounted() { if (this.file.loadEditor) { @@ -60,7 +60,8 @@ import { __ } from '~/locale'; loadEditor() { this.loading = true; - axios.get(this.file.content_path) + axios + .get(this.file.content_path) .then(({ data }) => { const content = this.$el.querySelector('pre'); const fileContent = document.createTextNode(data.content); @@ -101,7 +102,7 @@ import { __ } from '~/locale'; }, acceptDiscardConfirmation(file) { this.onAcceptDiscardConfirmation(file); - } - } + }, + }, }); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index 69208ac2d36..c2de0379d23 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import actionsMixin from '../mixins/line_conflict_actions'; import utilsMixin from '../mixins/line_conflict_utils'; -((global) => { +(global => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.parallelConflictLines = Vue.extend({ diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index 1501296ac4f..0333335de06 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -1,10 +1,10 @@ -/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */ +/* eslint-disable object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue */ import $ from 'jquery'; import Vue from 'vue'; import Cookies from 'js-cookie'; -((global) => { +(global => { global.mergeConflicts = global.mergeConflicts || {}; const diffViewType = Cookies.get('diff_view'); @@ -17,11 +17,11 @@ import Cookies from 'js-cookie'; const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE; const VIEW_TYPES = { INLINE: 'inline', - PARALLEL: 'parallel' + PARALLEL: 'parallel', }; const CONFLICT_TYPES = { TEXT: 'text', - TEXT_EDITOR: 'text-editor' + TEXT_EDITOR: 'text-editor', }; global.mergeConflicts.mergeConflictsStore = { @@ -31,7 +31,7 @@ import Cookies from 'js-cookie'; isSubmitting: false, isParallel: diffViewType === VIEW_TYPES.PARALLEL, diffViewType: diffViewType, - conflictsData: {} + conflictsData: {}, }, setConflictsData(data) { @@ -47,7 +47,7 @@ import Cookies from 'js-cookie'; }, decorateFiles(files) { - files.forEach((file) => { + files.forEach(file => { file.content = ''; file.resolutionData = {}; file.promptDiscardConfirmation = false; @@ -72,7 +72,7 @@ import Cookies from 'js-cookie'; setInlineLine(file) { file.inlineLines = []; - file.sections.forEach((section) => { + file.sections.forEach(section => { let currentLineType = 'new'; const { conflict, lines, id } = section; @@ -80,7 +80,7 @@ import Cookies from 'js-cookie'; file.inlineLines.push(this.getHeadHeaderLine(id)); } - lines.forEach((line) => { + lines.forEach(line => { const { type } = line; if ((type === 'new' || type === 'old') && currentLineType !== type) { @@ -102,7 +102,7 @@ import Cookies from 'js-cookie'; file.parallelLines = []; const linesObj = { left: [], right: [] }; - file.sections.forEach((section) => { + file.sections.forEach(section => { const { conflict, lines, id } = section; if (conflict) { @@ -110,7 +110,7 @@ import Cookies from 'js-cookie'; linesObj.right.push(this.getHeadHeaderLine(id)); } - lines.forEach((line) => { + lines.forEach(line => { const { type } = line; if (conflict) { @@ -131,10 +131,7 @@ import Cookies from 'js-cookie'; }); for (let i = 0, len = linesObj.left.length; i < len; i += 1) { - file.parallelLines.push([ - linesObj.right[i], - linesObj.left[i] - ]); + file.parallelLines.push([linesObj.right[i], linesObj.left[i]]); } }, @@ -159,9 +156,9 @@ import Cookies from 'js-cookie'; const { files } = this.state.conflictsData; let count = 0; - files.forEach((file) => { + files.forEach(file => { if (file.type === CONFLICT_TYPES.TEXT) { - file.sections.forEach((section) => { + file.sections.forEach(section => { if (section.conflict) { count += 1; } @@ -198,7 +195,7 @@ import Cookies from 'js-cookie'; isHeader: true, isHead: true, isSelected: false, - isUnselected: false + isUnselected: false, }; }, @@ -229,7 +226,7 @@ import Cookies from 'js-cookie'; section: isHead ? 'head' : 'origin', richText: rich_text, isSelected: false, - isUnselected: false + isUnselected: false, }; }, @@ -243,7 +240,7 @@ import Cookies from 'js-cookie'; isHeader: true, isOrigin: true, isSelected: false, - isUnselected: false + isUnselected: false, }; }, @@ -290,14 +287,14 @@ import Cookies from 'js-cookie'; }, restoreFileLinesState(file) { - file.inlineLines.forEach((line) => { + file.inlineLines.forEach(line => { if (line.hasConflict || line.isHeader) { line.isSelected = false; line.isUnselected = false; } }); - file.parallelLines.forEach((lines) => { + file.parallelLines.forEach(lines => { const left = lines[0]; const right = lines[1]; const isLeftMatch = left.hasConflict || left.isHeader; @@ -354,7 +351,7 @@ import Cookies from 'js-cookie'; const initial = 'Commit to source branch'; const inProgress = 'Committing...'; - return this.state ? this.state.isSubmitting ? inProgress : initial : initial; + return this.state ? (this.state.isSubmitting ? inProgress : initial) : initial; }, getCommitData() { @@ -362,13 +359,13 @@ import Cookies from 'js-cookie'; commitData = { commit_message: this.state.conflictsData.commitMessage, - files: [] + files: [], }; - this.state.conflictsData.files.forEach((file) => { + this.state.conflictsData.files.forEach(file => { const addFile = { old_path: file.old_path, - new_path: file.new_path + new_path: file.new_path, }; if (file.type === CONFLICT_TYPES.TEXT) { @@ -391,13 +388,13 @@ import Cookies from 'js-cookie'; handleSelected(file, sectionId, selection) { Vue.set(file.resolutionData, sectionId, selection); - file.inlineLines.forEach((line) => { + file.inlineLines.forEach(line => { if (line.id === sectionId && (line.hasConflict || line.isHeader)) { this.markLine(line, selection); } }); - file.parallelLines.forEach((lines) => { + file.parallelLines.forEach(lines => { const left = lines[0]; const right = lines[1]; const hasSameId = right.id === sectionId || left.id === sectionId; @@ -430,6 +427,6 @@ import Cookies from 'js-cookie'; fileTextTypePresent() { return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT); - } + }, }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 7bf2c56dd5d..9b6d7d1772f 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, comma-dangle, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, no-var, no-underscore-dangle, one-var, consistent-return, prefer-arrow-callback */ import $ from 'jquery'; import { __ } from '~/locale'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 78f56ab57ff..03f3bb42193 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -18,7 +18,6 @@ import syntaxHighlight from './syntax_highlight'; import Notes from './notes'; import { polyfillSticky } from './lib/utils/sticky'; -/* eslint-disable max-len */ // MergeRequestTabs // // Handles persisting and restoring the current tab selection and lazily-loading @@ -62,7 +61,6 @@ import { polyfillSticky } from './lib/utils/sticky'; // </div> // </div> // -/* eslint-enable max-len */ // Store the `location` object, allowing for easier stubbing in tests let { location } = window; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 640a4c8260f..42fb5c7177a 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ +/* eslint-disable one-var, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ @@ -9,6 +9,7 @@ import '~/gl_dropdown'; import axios from './lib/utils/axios_utils'; import { timeFor } from './lib/utils/datetime_utility'; import ModalStore from './boards/stores/modal_store'; +import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store'; export default class MilestoneSelect { constructor(currentProject, els, options = {}) { @@ -187,7 +188,7 @@ export default class MilestoneSelect { return $dropdown.closest('form').submit(); } else if ($dropdown.hasClass('js-issue-board-sidebar')) { if (selected.id !== -1 && isSelecting) { - gl.issueBoards.boardStoreIssueSet( + boardStoreIssueSet( 'milestone', new ListMilestone({ id: selected.id, @@ -195,13 +196,13 @@ export default class MilestoneSelect { }), ); } else { - gl.issueBoards.boardStoreIssueDelete('milestone'); + boardStoreIssueDelete('milestone'); } $dropdown.trigger('loading.gl.dropdown'); $loading.removeClass('hidden').fadeIn(); - gl.issueBoards.BoardsStore.detail.issue + boardsStore.detail.issue .update($dropdown.attr('data-issue-update')) .then(() => { $dropdown.trigger('loaded.gl.dropdown'); diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 3cccaf72ed7..ed5c8b15945 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -148,7 +148,7 @@ export default { point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point.x += 7; - this.seriesUnderMouse = this.timeSeries.filter((series) => { + this.seriesUnderMouse = this.timeSeries.filter(series => { const mouseX = series.timeSeriesScaleX.invert(point.x); let minDistance = Infinity; @@ -221,21 +221,18 @@ export default { .scale(axisYScale) .ticks(measurements.yTicks); - d3 - .select(this.$refs.baseSvg) + d3.select(this.$refs.baseSvg) .select('.x-axis') .call(xAxis); const width = this.graphWidth; - d3 - .select(this.$refs.baseSvg) + d3.select(this.$refs.baseSvg) .select('.y-axis') .call(yAxis) .selectAll('.tick') .each(function createTickLines(d, i) { if (i > 0) { - d3 - .select(this) + d3.select(this) .select('line') .attr('x2', width) .attr('class', 'axis-tick'); diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue index 8a604a51eb2..616410ec34f 100644 --- a/app/assets/javascripts/monitoring/components/graph/axis.vue +++ b/app/assets/javascripts/monitoring/components/graph/axis.vue @@ -38,38 +38,25 @@ export default { computed: { textTransform() { const yCoordinate = - (this.graphHeight - - this.margin.top + - this.measurements.axisLabelLineOffset) / - 2 || 0; + (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0; return `translate(15, ${yCoordinate}) rotate(-90)`; }, rectTransform() { const yCoordinate = - (this.graphHeight - - this.margin.top + - this.measurements.axisLabelLineOffset) / - 2 + + (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 + this.yLabelWidth / 2 || 0; return `translate(0, ${yCoordinate}) rotate(-90)`; }, xPosition() { - return ( - (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - - this.margin.right || 0 - ); + return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0; }, yPosition() { - return ( - this.graphHeight - - this.margin.top + - this.measurements.axisLabelLineOffset || 0 - ); + return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0; }, yAxisLabelSentenceCase() { diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 5f00d20ca3f..1720476480e 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -92,7 +92,8 @@ export default { methods: { seriesMetricValue(seriesIndex, series) { const indexFromCoordinates = this.currentCoordinates[series.metricTag] - ? this.currentCoordinates[series.metricTag].currentDataIndex : 0; + ? this.currentCoordinates[series.metricTag].currentDataIndex + : 0; const index = this.deploymentFlagData ? this.deploymentFlagData.seriesIndex : indexFromCoordinates; diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue index ec1c2222af9..3464067834f 100644 --- a/app/assets/javascripts/monitoring/components/graph/track_info.vue +++ b/app/assets/javascripts/monitoring/components/graph/track_info.vue @@ -26,4 +26,3 @@ export default { {{ summaryMetrics }} </span> </template> - diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue index ba3f93b39ff..e04fd9c1f35 100644 --- a/app/assets/javascripts/monitoring/components/graph/track_line.vue +++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue @@ -33,4 +33,3 @@ export default { </svg> </td> </template> - diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 007451d5c7a..87c3d969de4 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -6,7 +6,7 @@ const mixins = { if (!this.reducedDeploymentData) return false; let dataFound = false; - this.reducedDeploymentData = this.reducedDeploymentData.map((d) => { + this.reducedDeploymentData = this.reducedDeploymentData.map(d => { const deployment = d; if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { dataFound = d.xPos + 1; @@ -61,7 +61,7 @@ const mixins = { this.currentCoordinates = {}; - this.seriesUnderMouse.forEach((series) => { + this.seriesUnderMouse.forEach(series => { const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate); const currentData = series.values[currentDataIndex]; const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 260d424378e..24b4acaf6da 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -8,18 +8,20 @@ const MAX_REQUESTS = 3; function backOffRequest(makeRequestCallback) { let requestCounter = 0; return backOff((next, stop) => { - makeRequestCallback().then((resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - requestCounter += 1; - if (requestCounter < MAX_REQUESTS) { - next(); + makeRequestCallback() + .then(resp => { + if (resp.status === statusCodes.NO_CONTENT) { + requestCounter += 1; + if (requestCounter < MAX_REQUESTS) { + next(); + } else { + stop(new Error('Failed to connect to the prometheus server')); + } } else { - stop(new Error('Failed to connect to the prometheus server')); + stop(resp); } - } else { - stop(resp); - } - }).catch(stop); + }) + .catch(stop); }); } @@ -33,7 +35,7 @@ export default class MonitoringService { getGraphsData() { return backOffRequest(() => axios.get(this.metricsEndpoint)) .then(resp => resp.data) - .then((response) => { + .then(response => { if (!response || !response.data) { throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); } @@ -47,22 +49,27 @@ export default class MonitoringService { } return backOffRequest(() => axios.get(this.deploymentEndpoint)) .then(resp => resp.data) - .then((response) => { + .then(response => { if (!response || !response.deployments) { - throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint')); + throw new Error( + s__('Metrics|Unexpected deployment data response from prometheus endpoint'), + ); } return response.deployments; }); } getEnvironmentsData() { - return axios.get(this.environmentsEndpoint) - .then(resp => resp.data) - .then((response) => { - if (!response || !response.environments) { - throw new Error(s__('Metrics|There was an error fetching the environments data, please try again')); - } - return response.environments; - }); + return axios + .get(this.environmentsEndpoint) + .then(resp => resp.data) + .then(response => { + if (!response || !response.environments) { + throw new Error( + s__('Metrics|There was an error fetching the environments data, please try again'), + ); + } + return response.environments; + }); } } diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js index ee866850e13..7c771f43eee 100644 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -1,5 +1,6 @@ export default { - small: { // Covers both xs and sm screen sizes + small: { + // Covers both xs and sm screen sizes margin: { top: 40, right: 40, @@ -18,7 +19,8 @@ export default { }, axisLabelLineOffset: -20, }, - large: { // This covers both md and lg screen sizes + large: { + // This covers both md and lg screen sizes margin: { top: 80, right: 80, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index d5971730e31..bb24a1acdb3 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -66,7 +66,8 @@ function queryTimeSeries(query, graphDrawData, lineStyle) { // offset the same amount as the original data const [minX, maxX] = graphDrawData.xDom; const offset = d3.timeMinute(minX) - Number(minX); - const datesWithoutGaps = d3.timeSecond.every(60) + const datesWithoutGaps = d3.timeSecond + .every(60) .range(d3.timeMinute.offset(minX, -1), maxX) .map(d => d - offset); @@ -208,9 +209,7 @@ export default function createTimeSeries(queries, graphWidth, graphHeight, graph const timeSeries = queries.reduce((series, query, index) => { const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length]; - return series.concat( - queryTimeSeries(query, graphDrawData, lineStyle), - ); + return series.concat(queryTimeSeries(query, graphDrawData, lineStyle)); }, []); return { diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 17370edeb0c..ec4c0910e92 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, object-shorthand, no-else-return, prefer-template, prefer-arrow-callback */ import $ from 'jquery'; import Api from './api'; diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 94da1be4066..ad7136adb8c 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ +/* eslint-disable func-names, no-var, one-var, no-loop-func, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase */ import $ from 'jquery'; import { __ } from '../locale'; diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 205d9766656..8f6ea9e61c1 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, one-var, max-len, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ +/* eslint-disable func-names, no-var, one-var, consistent-return, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return */ import $ from 'jquery'; import RefSelectDropdown from './ref_select_dropdown'; diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index 7d2a1a33b98..0691ba64f8e 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -1,45 +1,45 @@ <script> - import Prism from '../../lib/highlight'; - import Prompt from '../prompt.vue'; +import Prism from '../../lib/highlight'; +import Prompt from '../prompt.vue'; - export default { - components: { - prompt: Prompt, +export default { + components: { + prompt: Prompt, + }, + props: { + count: { + type: Number, + required: false, + default: 0, }, - props: { - count: { - type: Number, - required: false, - default: 0, - }, - codeCssClass: { - type: String, - required: false, - default: '', - }, - type: { - type: String, - required: true, - }, - rawCode: { - type: String, - required: true, - }, + codeCssClass: { + type: String, + required: false, + default: '', }, - computed: { - code() { - return this.rawCode; - }, - promptType() { - const type = this.type.split('put')[0]; - - return type.charAt(0).toUpperCase() + type.slice(1); - }, + type: { + type: String, + required: true, + }, + rawCode: { + type: String, + required: true, }, - mounted() { - Prism.highlightElement(this.$refs.code); + }, + computed: { + code() { + return this.rawCode; + }, + promptType() { + const type = this.type.split('put')[0]; + + return type.charAt(0).toUpperCase() + type.slice(1); }, - }; + }, + mounted() { + Prism.highlightElement(this.$refs.code); + }, +}; </script> <template> diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 3d09d24b6ab..5aa83db0986 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,12 +1,12 @@ <script> - /* global katex */ - import marked from 'marked'; - import sanitize from 'sanitize-html'; - import Prompt from './prompt.vue'; +/* global katex */ +import marked from 'marked'; +import sanitize from 'sanitize-html'; +import Prompt from './prompt.vue'; - const renderer = new marked.Renderer(); +const renderer = new marked.Renderer(); - /* +/* Regex to match KaTex blocks. Supports the following: @@ -17,7 +17,7 @@ The matched text then goes through the KaTex renderer & then outputs the HTML */ - const katexRegexString = `( +const katexRegexString = `( ^\\\\begin{[a-zA-Z]+}\\s | ^\\$\\$ @@ -32,66 +32,69 @@ | \\$ ) - `.replace(/\s/g, '').trim(); + ` + .replace(/\s/g, '') + .trim(); - renderer.paragraph = (t) => { - let text = t; - let inline = false; +renderer.paragraph = t => { + let text = t; + let inline = false; - if (typeof katex !== 'undefined') { - const katexString = text.replace(/&/g, '&') - .replace(/&=&/g, '\\space=\\space') - .replace(/<(\/?)em>/g, '_'); - const regex = new RegExp(katexRegexString, 'gi'); - const matchLocation = katexString.search(regex); - const numberOfMatches = katexString.match(regex); + if (typeof katex !== 'undefined') { + const katexString = text + .replace(/&/g, '&') + .replace(/&=&/g, '\\space=\\space') + .replace(/<(\/?)em>/g, '_'); + const regex = new RegExp(katexRegexString, 'gi'); + const matchLocation = katexString.search(regex); + const numberOfMatches = katexString.match(regex); - if (numberOfMatches && numberOfMatches.length !== 0) { - if (matchLocation > 0) { - let matches = regex.exec(katexString); - inline = true; + if (numberOfMatches && numberOfMatches.length !== 0) { + if (matchLocation > 0) { + let matches = regex.exec(katexString); + inline = true; - while (matches !== null) { - const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, '')); - text = `${text.replace(matches[0], ` ${renderedKatex}`)}`; - matches = regex.exec(katexString); - } - } else { - const matches = regex.exec(katexString); - text = katex.renderToString(matches[2]); + while (matches !== null) { + const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, '')); + text = `${text.replace(matches[0], ` ${renderedKatex}`)}`; + matches = regex.exec(katexString); } + } else { + const matches = regex.exec(katexString); + text = katex.renderToString(matches[2]); } } + } - return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`; - }; + return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`; +}; - marked.setOptions({ - sanitize: true, - renderer, - }); +marked.setOptions({ + sanitize: true, + renderer, +}); - export default { - components: { - prompt: Prompt, - }, - props: { - cell: { - type: Object, - required: true, - }, +export default { + components: { + prompt: Prompt, + }, + props: { + cell: { + type: Object, + required: true, }, - computed: { - markdown() { - return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { - allowedTags: false, - allowedAttributes: { - '*': ['class'], - }, - }); - }, + }, + computed: { + markdown() { + return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { + allowedTags: false, + allowedAttributes: { + '*': ['class'], + }, + }); }, - }; + }, +}; </script> <template> @@ -105,13 +108,13 @@ </template> <style> - .markdown .katex { - display: block; - text-align: center; - } +.markdown .katex { + display: block; + text-align: center; +} - .markdown .inline-katex .katex { - display: inline; - text-align: initial; - } +.markdown .inline-katex .katex { + display: inline; + text-align: initial; +} </style> diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 0535ee7afa8..c6fc786fa76 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,30 +1,28 @@ <script> - import sanitize from 'sanitize-html'; - import Prompt from '../prompt.vue'; +import sanitize from 'sanitize-html'; +import Prompt from '../prompt.vue'; - export default { - components: { - prompt: Prompt, +export default { + components: { + prompt: Prompt, + }, + props: { + rawCode: { + type: String, + required: true, }, - props: { - rawCode: { - type: String, - required: true, - }, + }, + computed: { + sanitizedOutput() { + return sanitize(this.rawCode, { + allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']), + allowedAttributes: { + img: ['src'], + }, + }); }, - computed: { - sanitizedOutput() { - return sanitize(this.rawCode, { - allowedTags: sanitize.defaults.allowedTags.concat([ - 'img', 'svg', - ]), - allowedAttributes: { - img: ['src'], - }, - }); - }, - }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index 67d6c5ad12b..a17868963ce 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -1,21 +1,21 @@ <script> - import Prompt from '../prompt.vue'; +import Prompt from '../prompt.vue'; - export default { - components: { - prompt: Prompt, +export default { + components: { + prompt: Prompt, + }, + props: { + outputType: { + type: String, + required: true, }, - props: { - outputType: { - type: String, - required: true, - }, - rawCode: { - type: String, - required: true, - }, + rawCode: { + type: String, + required: true, }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 4183b976814..d9f8604ed10 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,78 +1,78 @@ <script> - import CodeCell from '../code/index.vue'; - import Html from './html.vue'; - import Image from './image.vue'; +import CodeCell from '../code/index.vue'; +import Html from './html.vue'; +import Image from './image.vue'; - export default { - components: { - 'code-cell': CodeCell, - 'html-output': Html, - 'image-output': Image, +export default { + components: { + 'code-cell': CodeCell, + 'html-output': Html, + 'image-output': Image, + }, + props: { + codeCssClass: { + type: String, + required: false, + default: '', }, - props: { - codeCssClass: { - type: String, - required: false, - default: '', - }, - count: { - type: Number, - required: false, - default: 0, - }, - output: { - type: Object, - requred: true, - default: () => ({}), - }, + count: { + type: Number, + required: false, + default: 0, }, - computed: { - componentName() { - if (this.output.text) { - return 'code-cell'; - } else if (this.output.data['image/png']) { - return 'image-output'; - } else if (this.output.data['text/html']) { - return 'html-output'; - } else if (this.output.data['image/svg+xml']) { - return 'html-output'; - } - + output: { + type: Object, + requred: true, + default: () => ({}), + }, + }, + computed: { + componentName() { + if (this.output.text) { return 'code-cell'; - }, - rawCode() { - if (this.output.text) { - return this.output.text.join(''); - } + } else if (this.output.data['image/png']) { + return 'image-output'; + } else if (this.output.data['text/html']) { + return 'html-output'; + } else if (this.output.data['image/svg+xml']) { + return 'html-output'; + } - return this.dataForType(this.outputType); - }, - outputType() { - if (this.output.text) { - return ''; - } else if (this.output.data['image/png']) { - return 'image/png'; - } else if (this.output.data['text/html']) { - return 'text/html'; - } else if (this.output.data['image/svg+xml']) { - return 'image/svg+xml'; - } + return 'code-cell'; + }, + rawCode() { + if (this.output.text) { + return this.output.text.join(''); + } + + return this.dataForType(this.outputType); + }, + outputType() { + if (this.output.text) { + return ''; + } else if (this.output.data['image/png']) { + return 'image/png'; + } else if (this.output.data['text/html']) { + return 'text/html'; + } else if (this.output.data['image/svg+xml']) { + return 'image/svg+xml'; + } - return 'text/plain'; - }, + return 'text/plain'; }, - methods: { - dataForType(type) { - let data = this.output.data[type]; + }, + methods: { + dataForType(type) { + let data = this.output.data[type]; - if (typeof data === 'object') { - data = data.join(''); - } + if (typeof data === 'object') { + data = data.join(''); + } - return data; - }, + return data; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index fe1fc37e1dc..d96f701ee3e 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -1,23 +1,23 @@ <script> - export default { - props: { - type: { - type: String, - required: false, - default: '', - }, - count: { - type: Number, - required: false, - default: 0, - }, +export default { + props: { + type: { + type: String, + required: false, + default: '', }, - computed: { - hasKeys() { - return this.type !== '' && this.count; - }, + count: { + type: Number, + required: false, + default: 0, }, - }; + }, + computed: { + hasKeys() { + return this.type !== '' && this.count; + }, + }, +}; </script> <template> @@ -29,9 +29,9 @@ </template> <style scoped> - .prompt { - padding: 0 10px; - min-width: 7em; - font-family: monospace; - } +.prompt { + padding: 0 10px; + min-width: 7em; + font-family: monospace; +} </style> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index f241df9620d..c5cc8c97dda 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -1,51 +1,48 @@ <script> - import { - MarkdownCell, - CodeCell, - } from './cells'; +import { MarkdownCell, CodeCell } from './cells'; - export default { - components: { - 'code-cell': CodeCell, - 'markdown-cell': MarkdownCell, +export default { + components: { + 'code-cell': CodeCell, + 'markdown-cell': MarkdownCell, + }, + props: { + notebook: { + type: Object, + required: true, }, - props: { - notebook: { - type: Object, - required: true, - }, - codeCssClass: { - type: String, - required: false, - default: '', - }, + codeCssClass: { + type: String, + required: false, + default: '', }, - computed: { - cells() { - if (this.notebook.worksheets) { - const data = { - cells: [], - }; + }, + computed: { + cells() { + if (this.notebook.worksheets) { + const data = { + cells: [], + }; - return this.notebook.worksheets.reduce((cellData, sheet) => { - const cellDataCopy = cellData; - cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells); - return cellDataCopy; - }, data).cells; - } + return this.notebook.worksheets.reduce((cellData, sheet) => { + const cellDataCopy = cellData; + cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells); + return cellDataCopy; + }, data).cells; + } - return this.notebook.cells; - }, - hasNotebook() { - return Object.keys(this.notebook).length; - }, + return this.notebook.cells; }, - methods: { - cellType(type) { - return `${type}-cell`; - }, + hasNotebook() { + return Object.keys(this.notebook).length; }, - }; + }, + methods: { + cellType(type) { + return `${type}-cell`; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index f301f093ef4..1369b5820d5 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,5 +1,5 @@ -/* eslint-disable no-restricted-properties, func-names, no-var, wrap-iife, camelcase, -no-unused-expressions, max-len, one-var, one-var-declaration-per-line, default-case, +/* eslint-disable no-restricted-properties, func-names, no-var, camelcase, +no-unused-expressions, one-var, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, vars-on-top, no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */ diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 353aa790743..d9e99603238 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -94,6 +94,7 @@ export default { class="diff-file file-holder" > <diff-file-header + :discussion-path="discussion.discussionPath" :diff-file="diffFile" :can-current-user-fork="false" :discussions-expanded="isDiscussionsExpanded" diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 6c1788dc160..58bb8c5b0c8 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ +/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, prefer-template, no-return-assign */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index a02ec9e5f00..5f91686347a 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, max-len, no-restricted-syntax, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ +/* eslint-disable func-names, no-restricted-syntax, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, no-return-assign, prefer-arrow-callback, prefer-template, no-else-return, no-shadow */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index d12249bf612..cd0e2bc023c 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ +/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, no-param-reassign, no-return-assign, prefer-arrow-callback, consistent-return, no-unused-vars, no-cond-assign, no-else-return */ import _ from 'underscore'; export default { diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index 77368c47451..70fbb3f301c 100644 --- a/app/assets/javascripts/pages/projects/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ +/* eslint-disable func-names, no-var, prefer-template */ import $ from 'jquery'; import BranchGraph from '../../../network/branch_graph'; diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 34a13eb3251..52d66beefc9 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -1,5 +1,4 @@ -/* eslint-disable func-names, no-var, no-return-assign, one-var, - one-var-declaration-per-line, object-shorthand, vars-on-top */ +/* eslint-disable func-names, no-var, no-return-assign, one-var, object-shorthand, vars-on-top */ import $ from 'jquery'; import Cookies from 'js-cookie'; diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 97cf1aeaadc..d621f988d86 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, consistent-return, class-methods-use-this */ +/* eslint-disable consistent-return, class-methods-use-this */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 0d7324f3fb5..cb14d4400d1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -35,7 +35,7 @@ export default { }, data() { return { - pipelineId: '', + pipelineId: 0, endpoint: '', cancelingPipeline: null, }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 09ee190b8ca..b03438ddba1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -47,7 +47,7 @@ export default { required: true, }, cancelingPipeline: { - type: String, + type: Number, required: false, default: null, }, diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index f641b23e519..af134881f31 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -1,4 +1,4 @@ -/* eslint-disable no-useless-escape, max-len, no-var, no-underscore-dangle, func-names, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ +/* eslint-disable no-useless-escape, no-var, no-underscore-dangle, func-names, no-unused-vars, no-return-assign, object-shorthand, one-var, consistent-return, class-methods-use-this */ import $ from 'jquery'; import 'cropper'; diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 05485e352dc..12cfa7de316 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, prefer-template, no-unused-vars, no-return-assign */ +/* eslint-disable func-names, no-var, consistent-return, one-var, no-cond-assign, prefer-template, no-unused-vars, no-return-assign */ import $ from 'jquery'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 6f3b32f8eea..eaaeda8b339 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, wrap-iife, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ +/* eslint-disable func-names, no-var, object-shorthand, one-var, no-else-return */ import $ from 'jquery'; import Api from './api'; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b27d635c6ac..64f3dde5be7 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, no-param-reassign, max-len */ +/* eslint-disable func-names, no-var, no-unused-vars, consistent-return, one-var, prefer-template, no-else-return, no-param-reassign */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 50dd3c12382..7bde4860973 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,4 +1,4 @@ -/* eslint-disable no-return-assign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top, max-len */ +/* eslint-disable no-return-assign, one-var, no-var, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top */ import $ from 'jquery'; import { escape, throttle } from 'underscore'; diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 6fea03af46a..74166313940 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,4 +1,4 @@ -/* eslint-disable no-useless-return, max-len */ +/* eslint-disable no-useless-return */ import $ from 'jquery'; import Api from '../api'; diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 85123a63a45..066fd6278a7 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */ +/* eslint-disable func-names, consistent-return, no-var, one-var, no-else-return, prefer-arrow-callback, class-methods-use-this */ import $ from 'jquery'; import { visitUrl } from './lib/utils/url_utility'; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index b9dfa22dd49..e2259a8d07b 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, one-var, no-var, prefer-rest-params, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ +/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ /* global Issuable */ /* global emitSidebarEvent */ diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 8184ef33022..c19b67f00fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -162,18 +162,20 @@ <span class="label-branch"> <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> </span> - with - <a - :href="mr.mergeCommitPath" - class="commit-sha js-mr-merged-commit-sha" - v-text="mr.shortMergeCommitSha" - > - </a> - <clipboard-button - :title="__('Copy commit SHA to clipboard')" - :text="mr.mergeCommitSha" - css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" - /> + <template v-if="mr.mergeCommitSha"> + with + <a + :href="mr.mergeCommitPath" + class="commit-sha js-mr-merged-commit-sha" + v-text="mr.shortMergeCommitSha" + > + </a> + <clipboard-button + :title="__('Copy commit SHA to clipboard')" + :text="mr.mergeCommitSha" + css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" + /> + </template> </p> <p v-if="mr.sourceBranchRemoved"> {{ s__("mrWidget|The source branch has been removed") }} diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index b023c5cfeb1..b5444d43ded 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -41,10 +41,14 @@ export default { }, }, mounted() { - $(this.$el).on('shown.bs.modal', this.opened).on('hidden.bs.modal', this.closed); + $(this.$el) + .on('shown.bs.modal', this.opened) + .on('hidden.bs.modal', this.closed); }, beforeDestroy() { - $(this.$el).off('shown.bs.modal', this.opened).off('hidden.bs.modal', this.closed); + $(this.$el) + .off('shown.bs.modal', this.opened) + .off('hidden.bs.modal', this.closed); }, methods: { emitCancel(event) { @@ -103,7 +107,7 @@ export default { <slot name="footer"> <button type="button" - class="btn js-modal-cancel-action" + class="btn js-modal-cancel-action qa-modal-cancel-button" data-dismiss="modal" @click="emitCancel($event)" > @@ -112,7 +116,7 @@ export default { <button :class="`btn-${footerPrimaryButtonVariant}`" type="button" - class="btn js-modal-primary-action" + class="btn js-modal-primary-action qa-modal-primary-button" data-dismiss="modal" @click="emitSubmit($event)" > diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 0138c9be803..bdb2351c344 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, class-methods-use-this */ // Zen Mode (full screen) textarea // diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index ffe65ce780e..bd1cca69c03 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -17,7 +17,7 @@ */ @import "../../../node_modules/pikaday/scss/pikaday"; -@import "../../../node_modules/dropzone/dist/basic.css"; +@import "../../../node_modules/dropzone/dist/basic"; /* * GitLab UI framework diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index be41dbfc61f..1c84baf68ed 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -12,6 +12,15 @@ max-width: $max-width; } +/** + * Mixin for fixed width container + */ +@mixin fixed-width-container { + max-width: $limited-layout-width - ($gl-padding * 2); + margin-left: auto; + margin-right: auto; +} + /* * Mixin for markdown tables */ diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 17b02c6e31e..cba5324ce53 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1046,3 +1046,19 @@ left: auto; line-height: 0; } + +@media (max-width: map-get($grid-breakpoints, md)-1) { + .diffs .files { + @include fixed-width-container; + flex-direction: column; + + .diff-tree-list { + width: 100%; + } + + .tree-list-holder { + max-height: calc(50px + 50vh); + padding-right: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 62a9f97caa9..00b06aea898 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,8 +1,6 @@ // Limit MR description for side-by-side diff view .fixed-width-container { - max-width: $limited-layout-width - ($gl-padding * 2); - margin-left: auto; - margin-right: auto; + @include fixed-width-container; } .issuable-warning-icon { diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 44864f9c7d0..25cc241e5b0 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -3,12 +3,5 @@ class Admin::HealthCheckController < Admin::ApplicationController def show @errors = HealthCheck::Utils.process_checks(['standard']) - @failing_storage_statuses = Gitlab::Git::Storage::Health.for_failing_storages - end - - def reset_storage_health - Gitlab::Git::Storage::FailureInfo.reset_all! - redirect_to admin_health_check_path, - notice: _('Git storage health information has been reset') end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ec45e2813c5..bbeaeb7694e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -66,7 +66,7 @@ class ApplicationController < ActionController::Base head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window end - rescue_from Gitlab::Git::Storage::Inaccessible, GRPC::Unavailable, Gitlab::Git::CommandError do |exception| + rescue_from GRPC::Unavailable, Gitlab::Git::CommandError do |exception| log_exception(exception) headers['Retry-After'] = exception.retry_after if exception.respond_to?(:retry_after) diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index ab4bc911e17..dc9a52f8da5 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class HealthController < ActionController::Base - protect_from_forgery with: :exception, except: :storage_check, prepend: true + protect_from_forgery with: :exception, prepend: true include RequiresWhitelistedMonitoringClient CHECKS = [ @@ -25,15 +25,6 @@ class HealthController < ActionController::Base render_check_results(results) end - def storage_check - results = Gitlab::Git::Storage::Checker.check_all - - render json: { - check_interval: Gitlab::CurrentSettings.current_application_settings.circuitbreaker_check_interval, - results: results - } - end - private def render_check_results(results) diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 970efa79dfb..45d5591e81b 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -7,8 +7,9 @@ class BranchesFinder end def execute - branches = @repository.branches_sorted_by(sort) - filter_by_name(branches) + branches = repository.branches_sorted_by(sort) + branches = by_search(branches) + branches end private @@ -23,11 +24,39 @@ class BranchesFinder @params[:sort].presence || 'name' end - def filter_by_name(branches) - if search - branches.select { |branch| branch.name.upcase.include?(search.upcase) } + def by_search(branches) + return branches unless search + + case search + when ->(v) { v.starts_with?('^') } + filter_branches_with_prefix(branches, search.slice(1..-1).upcase) + when ->(v) { v.ends_with?('$') } + filter_branches_with_suffix(branches, search.chop.upcase) else - branches + matches = filter_branches_by_name(branches, search.upcase) + set_exact_match_as_first_result(matches, search) end end + + def filter_branches_with_prefix(branches, prefix) + branches.select { |branch| branch.name.upcase.starts_with?(prefix) } + end + + def filter_branches_with_suffix(branches, suffix) + branches.select { |branch| branch.name.upcase.ends_with?(suffix) } + end + + def filter_branches_by_name(branches, term) + branches.select { |branch| branch.name.upcase.include?(term) } + end + + def set_exact_match_as_first_result(matches, term) + exact_match_index = find_exact_match_index(matches, term) + matches.insert(0, matches.delete_at(exact_match_index)) if exact_match_index + matches + end + + def find_exact_match_index(matches, term) + matches.index { |branch| branch.name.casecmp(term) == 0 } + end end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index c2404412006..6ececcd4152 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -42,17 +42,7 @@ class ProjectsFinder < UnionFinder init_collection end - collection = by_ids(collection) - collection = by_personal(collection) - collection = by_starred(collection) - collection = by_trending(collection) - collection = by_visibilty_level(collection) - collection = by_tags(collection) - collection = by_search(collection) - collection = by_archived(collection) - collection = by_custom_attributes(collection) - collection = by_deleted_status(collection) - + collection = filter_projects(collection) sort(collection) end @@ -66,6 +56,21 @@ class ProjectsFinder < UnionFinder end end + # EE would override this to add more filters + def filter_projects(collection) + collection = by_ids(collection) + collection = by_personal(collection) + collection = by_starred(collection) + collection = by_trending(collection) + collection = by_visibilty_level(collection) + collection = by_tags(collection) + collection = by_search(collection) + collection = by_archived(collection) + collection = by_custom_attributes(collection) + collection = by_deleted_status(collection) + collection + end + # rubocop: disable CodeReuse/ActiveRecord def collection_with_user if owned_projects? diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 15cbfeea609..d6753e46165 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -108,37 +108,6 @@ module ApplicationSettingsHelper options_for_select(options, selected) end - def circuitbreaker_failure_count_help_text - health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path) - api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health")) - message = _("The number of failures of after which GitLab will completely "\ - "prevent access to the storage. The number of failures can be "\ - "reset in the admin interface: %{link_to_health_page} or using "\ - "the %{api_documentation_link}.") - message = message % { link_to_health_page: health_link, api_documentation_link: api_link } - - message.html_safe - end - - def circuitbreaker_access_retries_help_text - _('The number of attempts GitLab will make to access a storage.') - end - - def circuitbreaker_failure_reset_time_help_text - _("The time in seconds GitLab will keep failure information. When no "\ - "failures occur during this time, information about the mount is reset.") - end - - def circuitbreaker_storage_timeout_help_text - _("The time in seconds GitLab will try to access storage. After this time a "\ - "timeout error will be raised.") - end - - def circuitbreaker_check_interval_help_text - _("The time in seconds between storage checks. When a previous check did "\ - "complete yet, GitLab will skip a check.") - end - def visible_attributes [ :admin_notification_email, @@ -150,11 +119,6 @@ module ApplicationSettingsHelper :authorized_keys_enabled, :auto_devops_enabled, :auto_devops_domain, - :circuitbreaker_access_retries, - :circuitbreaker_check_interval, - :circuitbreaker_failure_count_threshold, - :circuitbreaker_failure_reset_time, - :circuitbreaker_storage_timeout, :clientside_sentry_dsn, :clientside_sentry_enabled, :container_registry_token_expire_delay, diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index ff9842d4cd9..f4f46b0fe96 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -18,22 +18,20 @@ module PreferencesHelper groups: _("Your Groups"), todos: _("Your Todos"), issues: _("Assigned Issues"), - merge_requests: _("Assigned Merge Requests") + merge_requests: _("Assigned Merge Requests"), + operations: _("Operations Dashboard") }.with_indifferent_access.freeze # Returns an Array usable by a select field for more user-friendly option text def dashboard_choices - defined = User.dashboards + dashboards = User.dashboards.keys - if defined.size != DASHBOARD_CHOICES.size - # Ensure that anyone adding new options updates this method too - raise "`User` defines #{defined.size} dashboard choices," \ - " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." - else - defined.map do |key, _| - # Use `fetch` so `KeyError` gets raised when a key is missing - [DASHBOARD_CHOICES.fetch(key), key] - end + validate_dashboard_choices!(dashboards) + dashboards -= excluded_dashboard_choices + + dashboards.map do |key| + # Use `fetch` so `KeyError` gets raised when a key is missing + [DASHBOARD_CHOICES.fetch(key), key] end end @@ -52,4 +50,20 @@ module PreferencesHelper def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end + + private + + # Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too + def validate_dashboard_choices!(user_dashboards) + if user_dashboards.size != DASHBOARD_CHOICES.size + raise "`User` defines #{user_dashboards.size} dashboard choices," \ + " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." + end + end + + # List of dashboard choice to be excluded from CE. + # EE would override this. + def excluded_dashboard_choices + ['operations'] + end end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 53bd43d4861..8ed2a2ec9f4 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -48,15 +48,21 @@ module SortingHelper def groups_sort_options_hash { - sort_value_name => sort_title_name, - sort_value_name_desc => sort_title_name_desc, + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_created => sort_title_oldest_created, sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated + sort_value_oldest_updated => sort_title_oldest_updated } end + def subgroups_sort_options_hash + groups_sort_options_hash.merge( + sort_value_most_stars => sort_title_most_stars + ) + end + def admin_groups_sort_options_hash groups_sort_options_hash.merge( sort_value_largest_group => sort_title_largest_group diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb deleted file mode 100644 index 182e8e6641b..00000000000 --- a/app/helpers/storage_health_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module StorageHealthHelper - def failing_storage_health_message(storage_health) - storage_name = content_tag(:strong, h(storage_health.storage_name)) - host_names = h(storage_health.failing_on_hosts.to_sentence) - translation_params = { storage_name: storage_name, - host_names: host_names, - failed_attempts: storage_health.total_failures } - - translation = n_('%{storage_name}: failed storage access attempt on host:', - '%{storage_name}: %{failed_attempts} failed storage access attempts:', - storage_health.total_failures) % translation_params - - translation.html_safe - end - - def message_for_circuit_breaker(circuit_breaker) - maximum_failures = circuit_breaker.failure_count_threshold - current_failures = circuit_breaker.failure_count - - translation_params = { number_of_failures: current_failures, - maximum_failures: maximum_failures } - - if circuit_breaker.circuit_broken? - s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ - "retry automatically. Reset storage information when the problem is "\ - "resolved.") % translation_params - else - _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ - "allow access on the next attempt.") % translation_params - end - end -end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 65a2f760f93..23131af1b7d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -4,6 +4,7 @@ class ApplicationSetting < ActiveRecord::Base include CacheableAttributes include CacheMarkdownField include TokenAuthenticatable + include IgnorableColumn add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token @@ -27,6 +28,12 @@ class ApplicationSetting < ActiveRecord::Base serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize + ignore_column :circuitbreaker_failure_count_threshold + ignore_column :circuitbreaker_failure_reset_time + ignore_column :circuitbreaker_storage_timeout + ignore_column :circuitbreaker_access_retries + ignore_column :circuitbreaker_check_interval + cache_markdown_field :sign_in_text cache_markdown_field :help_page_text cache_markdown_field :shared_runners_text, pipeline: :plain_markdown @@ -150,17 +157,6 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { greater_than_or_equal_to: 0 } - validates :circuitbreaker_failure_count_threshold, - :circuitbreaker_failure_reset_time, - :circuitbreaker_storage_timeout, - :circuitbreaker_check_interval, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } - - validates :circuitbreaker_access_retries, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 1 } - validates :gitaly_timeout_default, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 6962b54441b..62dc0f2cbeb 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -19,6 +19,17 @@ class Deployment < ActiveRecord::Base after_create :create_ref after_create :invalidate_cache + scope :for_environment, -> (environment) { where(environment_id: environment) } + + def self.last_for_environment(environment) + ids = self + .for_environment(environment) + .select('MAX(id) AS id') + .group(:environment_id) + .map(&:id) + find(ids) + end + def commit project.commit(sha) end diff --git a/app/models/environment.rb b/app/models/environment.rb index 309bd4f37c9..0816c395185 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -48,6 +48,8 @@ class Environment < ActiveRecord::Base order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC')) end scope :in_review_folder, -> { where(environment_type: "review") } + scope :for_name, -> (name) { where(name: name) } + scope :for_project, -> (project) { where(project_id: project) } state_machine :state, initial: :available do event :start do diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 68ba4b213b2..b2fb79bc7ed 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -37,38 +37,4 @@ class WebHook < ActiveRecord::Base def allow_local_requests? false end - - # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields. - # Ensure that the encrypted version always takes precedence if present. - alias_method :attr_encrypted_token, :token - def token - attr_encrypted_token.presence || read_attribute(:token) - end - - # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields. - # Pending a background migration to encrypt all fields, we should just clear - # the unencrypted value whenever the new value is set. - alias_method :'attr_encrypted_token=', :'token=' - def token=(value) - self.attr_encrypted_token = value - - write_attribute(:token, nil) - end - - # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields. - # Ensure that the encrypted version always takes precedence if present. - alias_method :attr_encrypted_url, :url - def url - attr_encrypted_url.presence || read_attribute(:url) - end - - # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields. - # Pending a background migration to encrypt all fields, we should just clear - # the unencrypted value whenever the new value is set. - alias_method :'attr_encrypted_url=', :'url=' - def url=(value) - self.attr_encrypted_url = value - - write_attribute(:url, nil) - end end diff --git a/app/models/project.rb b/app/models/project.rb index 05e14c578b5..c7ca322853f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1789,7 +1789,7 @@ class Project < ActiveRecord::Base return unless export_file_exists? import_export_upload.remove_export_file! - import_export_upload.save + import_export_upload.save unless import_export_upload.destroyed? end def export_file_exists? diff --git a/app/models/user.rb b/app/models/user.rb index 8a7acfb73b1..a0665518cf5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -217,7 +217,7 @@ class User < ActiveRecord::Base # User's Dashboard preference # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests] + enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests, :operations] # User's Project preference # Note: When adding an option, it MUST go on the end of the array. diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 3d508a9a407..7bdcfcc38f7 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -4,6 +4,7 @@ class BuildDetailsEntity < JobEntity expose :coverage, :erased_at, :duration expose :tag_list, as: :tags expose :has_trace?, as: :has_trace + expose :stage expose :user, using: UserEntity expose :runner, using: RunnerEntity expose :pipeline, using: PipelineEntity diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 908b30cc3ce..c6c29ed1f21 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -20,32 +20,5 @@ Manage repository storage paths. Learn more in the = succeed "." do = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") - .sub-section - %h4 Circuit breaker - .form-group - = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'label-bold' - = f.number_field :circuitbreaker_check_interval, class: 'form-control' - .form-text.text-muted - = circuitbreaker_check_interval_help_text - .form-group - = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'label-bold' - = f.number_field :circuitbreaker_access_retries, class: 'form-control' - .form-text.text-muted - = circuitbreaker_access_retries_help_text - .form-group - = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'label-bold' - = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' - .form-text.text-muted - = circuitbreaker_storage_timeout_help_text - .form-group - = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'label-bold' - = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' - .form-text.text-muted - = circuitbreaker_failure_count_help_text - .form-group - = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'label-bold' - = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' - .form-text.text-muted - = circuitbreaker_failure_reset_time_help_text = f.submit 'Save changes', class: "btn btn-success qa-save-changes-button" diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml index be13138a764..b50a0dd5a18 100644 --- a/app/views/admin/application_settings/repository.html.haml +++ b/app/views/admin/application_settings/repository.html.haml @@ -20,7 +20,7 @@ %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded_by_default? ? _('Collapse') : _('Expand') %p - = _('Configure storage path and circuit breaker settings.') + = _('Configure storage path settings.') .settings-content = render 'repository_storage' diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index e69143abe45..df3eeba907c 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -22,7 +22,7 @@ .input-group %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } .input-group-append - = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default") + = clipboard_button(target: '#secret', title: _("Copy secret to clipboard"), class: "btn btn btn-default") %tr %td = _('Callback URL') diff --git a/app/views/admin/health_check/_failing_storages.html.haml b/app/views/admin/health_check/_failing_storages.html.haml deleted file mode 100644 index 6830201538d..00000000000 --- a/app/views/admin/health_check/_failing_storages.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -- if failing_storages.any? - = _('There are problems accessing Git storage: ') - %ul - - failing_storages.each do |storage_health| - %li - = failing_storage_health_message(storage_health) - %ul - - storage_health.failing_circuit_breakers.each do |circuit_breaker| - %li - #{circuit_breaker.hostname}: #{message_for_circuit_breaker(circuit_breaker)} - - = _("Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again.") - .prepend-top-10 - = button_to _("Reset git storage health information"), reset_storage_health_admin_health_check_path, - method: :post, class: 'btn btn-default' diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index d51ac854b04..0f5e97e288a 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title _('Health Check') -- no_errors = @errors.blank? && @failing_storage_statuses.blank? +- no_errors = @errors.blank? %div{ class: container_class } %h3.page-title= page_title @@ -39,4 +39,3 @@ #{ s_('HealthCheck|No Health Problems Detected') } - else = @errors - = render partial: 'failing_storages', object: @failing_storage_statuses diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 269a3721e06..12271ee5adb 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -5,7 +5,7 @@ .d-flex.justify-content-between.flex-wrap - providers.each do |provider| - has_icon = provider_has_icon?(provider) - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login qa-saml-login-button', id: "oauth-login-#{provider}" do - if has_icon = provider_image_tag(provider) %span diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 90ed20404c5..9a827523ed4 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -5,22 +5,22 @@ = devise_error_messages! .form-group = f.label :name, 'Full name', class: 'label-bold' - = f.text_field :name, class: "form-control top", required: true, title: "This field is required." + = f.text_field :name, class: "form-control top qa-new-user-name", required: true, title: "This field is required." .username.form-group = f.label :username, class: 'label-bold' - = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle qa-new-user-username", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... .form-group = f.label :email, class: 'label-bold' - = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." + = f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: "Please provide a valid email address." .form-group = f.label :email_confirmation, class: 'label-bold' - = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." + = f.email_field :email_confirmation, class: "form-control middle qa-new-user-email-confirmation", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength = f.label :password, class: 'label-bold' - = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." + = f.password_field :password, class: "form-control bottom qa-new-user-password", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? .form-group @@ -33,4 +33,4 @@ - if Gitlab::Recaptcha.enabled? = recaptcha_tags .submit-container - = f.submit "Register", class: "btn-register btn" + = f.submit "Register", class: "btn-register btn qa-new-user-register-button" diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 776bbc36ec2..cac00f9c854 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -25,7 +25,7 @@ .input-group %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } .input-group-append - = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default") + = clipboard_button(target: '#secret', title: _("Copy secret to clipboard"), class: "btn btn btn-default") %tr %td = _('Callback URL') diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 6a293daaf95..cc294f6a931 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -53,7 +53,7 @@ = _("Archived projects") .nav-controls - = render "shared/groups/dropdown" + = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash .tab-content #subgroups_and_projects.tab-pane diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5e467c862ab..8f8b6b454d9 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -66,6 +66,7 @@ - if Gitlab::Sherlock.enabled? || can?(current_user, :read_instance_statistics) %li.line-separator.d-none.d-sm-block + = render_if_exists 'dashboard/operations/nav_link' - if can?(current_user, :read_instance_statistics) = nav_link(controller: [:conversational_development_index, :cohorts]) do = link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 25cd53b378a..3625224fbcd 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -216,7 +216,7 @@ = _('Metrics') = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do - = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments' do + = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do %span = _('Environments') @@ -309,7 +309,7 @@ %span = _('General') = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: _('Members') do + = link_to project_project_members_path(@project), title: _('Members'), class: 'qa-link-members-settings' do %span = _('Members') - if can_edit diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index 5b092427496..2c8dd45670f 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -3,15 +3,15 @@ Template .template-selector-dropdowns-wrap .template-type-selector.js-template-type-selector-wrap.hidden - = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector', title: "Choose a template type" } ) + = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', title: "Choose a template type" } ) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector qa-license-dropdown', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) + = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) .template-selectors-undo-menu.hidden %span.text-info Template applied %button.btn.btn-sm.btn-info Undo diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 8b9c52f0802..45515fb492f 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -8,7 +8,7 @@ - if show_menu .project-action-button.dropdown.inline - %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' } + %a.btn.dropdown-toggle.has-tooltip.qa-create-new-dropdown{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown @@ -28,7 +28,7 @@ %li.dropdown-header= _('This repository') - if can_push_code - %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') + %li.qa-new-file-option= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - unless @project.empty_repo? %li= link_to _('New branch'), new_project_branch_path(@project) %li= link_to _('New tag'), new_project_tag_path(@project) diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index eaf3a93bd15..171ceeceb68 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -68,7 +68,7 @@ .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-experimental-support'), target: '_blank' + = 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 diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 56551ed4d65..54a6e685bb0 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -32,7 +32,7 @@ .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-experimental-support'), target: '_blank' + = 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/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index 4694bc39d54..b3a82d1ef41 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,4 +1,4 @@ - if environment.external_url && can?(current_user, :read_environment, environment) - = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip', title: s_('Environments|Open live environment') do + = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip qa-view-deployment', title: s_('Environments|Open live environment') do = sprite_icon('external-link') View deployment diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index a5f814b722d..02a088d338b 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -47,4 +47,6 @@ .js-build-options{ data: javascript_build_options } -#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner') } } +#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), + 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') } } diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index ccb83148ded..dbb563f51ea 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -30,5 +30,3 @@ %span.js-details-content.hide = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full" = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard") - - = render_if_exists "projects/pipelines/info_extension", pipeline: @pipeline diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 517fd249f6e..5e21442bb60 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -3,7 +3,7 @@ = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group = label_tag :user_ids, "Select members to invite", class: "label-bold" - = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") + = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, "Choose a role permission", class: "label-bold" .select-wrapper @@ -17,5 +17,5 @@ = label_tag :expires_at, 'Access expiration date', class: 'label-bold' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - = f.submit "Add to project", class: "btn btn-success" + = f.submit "Add to project", class: "btn btn-success qa-add-member-button" = link_to "Import", import_project_project_members_path(@project), class: "btn btn-default", title: "Import members from another project" diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 0c5a187f208..9682f8ac922 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -14,5 +14,5 @@ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } = icon("search") = render 'shared/members/sort_dropdown' - %ul.content-list.members-list + %ul.content-list.members-list.qa-members-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 9d196075bf1..601e3f25852 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -82,7 +82,7 @@ - if can_collaborate = succeed " " do - = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default' do + = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do = _('Web IDE') = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index 5351c9ce6a4..df3308abe0d 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -5,7 +5,7 @@ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page') = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do - %h4 + %h4.text-left = s_('WikiEmpty|The wiki lets you write documentation for your project') %p.text-left = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.") diff --git a/bin/storage_check b/bin/storage_check deleted file mode 100755 index 5a818732bd1..00000000000 --- a/bin/storage_check +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env ruby - -require 'optparse' -require 'net/http' -require 'json' -require 'socket' -require 'logger' - -require_relative '../lib/gitlab/storage_check' - -Gitlab::StorageCheck::CLI.start!(ARGV) diff --git a/changelogs/unreleased/22311-fix-duplicated-key-in-license-management-job.yml b/changelogs/unreleased/22311-fix-duplicated-key-in-license-management-job.yml new file mode 100644 index 00000000000..ab64a1387d9 --- /dev/null +++ b/changelogs/unreleased/22311-fix-duplicated-key-in-license-management-job.yml @@ -0,0 +1,5 @@ +--- +title: "fix duplicated key in license management job auto devops gitlab ci template" +merge_request: 22311 +author: Adam Lemanski +type: fixed diff --git a/changelogs/unreleased/48684-sort-projects-by-stars-in-groups.yml b/changelogs/unreleased/48684-sort-projects-by-stars-in-groups.yml new file mode 100644 index 00000000000..01681adab24 --- /dev/null +++ b/changelogs/unreleased/48684-sort-projects-by-stars-in-groups.yml @@ -0,0 +1,4 @@ +--- +title: Add new sort option "most_stars" to "Group > Children" pages +merge_request: 22121 +author: Rene Hennig diff --git a/changelogs/unreleased/48889-message-for-were-merged-into.yml b/changelogs/unreleased/48889-message-for-were-merged-into.yml new file mode 100644 index 00000000000..552b8826829 --- /dev/null +++ b/changelogs/unreleased/48889-message-for-were-merged-into.yml @@ -0,0 +1,5 @@ +--- +title: Fix 'merged with' UI being displayed when merge request has no merge commit +merge_request: 22022 +author: +type: fixed diff --git a/changelogs/unreleased/50185-fix-broken-file-name-navigation.yml b/changelogs/unreleased/50185-fix-broken-file-name-navigation.yml new file mode 100644 index 00000000000..d1b341af457 --- /dev/null +++ b/changelogs/unreleased/50185-fix-broken-file-name-navigation.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken file name navigation on MRs +merge_request: 22109 +author: +type: fixed diff --git a/changelogs/unreleased/52361-fix-file-tree-mobile.yml b/changelogs/unreleased/52361-fix-file-tree-mobile.yml new file mode 100644 index 00000000000..fe978eeca2d --- /dev/null +++ b/changelogs/unreleased/52361-fix-file-tree-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Improve MR file tree in smaller screens +merge_request: 22273 +author: +type: fixed diff --git a/changelogs/unreleased/52367-cleanup-web-hooks-columns.yml b/changelogs/unreleased/52367-cleanup-web-hooks-columns.yml new file mode 100644 index 00000000000..d1f3ca83613 --- /dev/null +++ b/changelogs/unreleased/52367-cleanup-web-hooks-columns.yml @@ -0,0 +1,5 @@ +--- +title: Remove legacy unencrypted webhook columns from the database +merge_request: 22199 +author: +type: changed diff --git a/changelogs/unreleased/52408-pip-cache-dir-to-cache-python-dependencies.yml b/changelogs/unreleased/52408-pip-cache-dir-to-cache-python-dependencies.yml new file mode 100644 index 00000000000..19d3e35c15c --- /dev/null +++ b/changelogs/unreleased/52408-pip-cache-dir-to-cache-python-dependencies.yml @@ -0,0 +1,5 @@ +--- +title: Use the standard PIP_CACHE_DIR for Python dependency caching template +merge_request: 22211 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/52472-pipeline-endpoint-json.yml b/changelogs/unreleased/52472-pipeline-endpoint-json.yml new file mode 100644 index 00000000000..feff195beb8 --- /dev/null +++ b/changelogs/unreleased/52472-pipeline-endpoint-json.yml @@ -0,0 +1,5 @@ +--- +title: Fix caching issue with pipelines URL +merge_request: 22293 +author: +type: fixed diff --git a/changelogs/unreleased/52519-runners-link.yml b/changelogs/unreleased/52519-runners-link.yml new file mode 100644 index 00000000000..5d904a8b340 --- /dev/null +++ b/changelogs/unreleased/52519-runners-link.yml @@ -0,0 +1,5 @@ +--- +title: Fixes stuck block URL linking to documentation instead of settings page +merge_request: 22286 +author: +type: fixed diff --git a/changelogs/unreleased/52570-erased-block.yml b/changelogs/unreleased/52570-erased-block.yml new file mode 100644 index 00000000000..6ec295bf81b --- /dev/null +++ b/changelogs/unreleased/52570-erased-block.yml @@ -0,0 +1,5 @@ +--- +title: Fix erased block not being rendered when job was erased +merge_request: 22294 +author: +type: fixed diff --git a/changelogs/unreleased/52608-sidebar.yml b/changelogs/unreleased/52608-sidebar.yml new file mode 100644 index 00000000000..9eca30f7b95 --- /dev/null +++ b/changelogs/unreleased/52608-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Hides sidebar for job page in mobile +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/52614-update-job-started-check.yml b/changelogs/unreleased/52614-update-job-started-check.yml new file mode 100644 index 00000000000..60ea237dbf3 --- /dev/null +++ b/changelogs/unreleased/52614-update-job-started-check.yml @@ -0,0 +1,5 @@ +--- +title: Fixes triggered/created labeled in job header +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/52618-incorrect-stage-being-shown-in-side-bar-of-job-view-api.yml b/changelogs/unreleased/52618-incorrect-stage-being-shown-in-side-bar-of-job-view-api.yml new file mode 100644 index 00000000000..fdbde709e77 --- /dev/null +++ b/changelogs/unreleased/52618-incorrect-stage-being-shown-in-side-bar-of-job-view-api.yml @@ -0,0 +1,5 @@ +--- +title: Load correct stage in the stages dropdown +merge_request: 22317 +author: +type: fixed diff --git a/changelogs/unreleased/even-more-frozen-string-lib.yml b/changelogs/unreleased/even-more-frozen-string-lib.yml new file mode 100644 index 00000000000..3f5fd7710aa --- /dev/null +++ b/changelogs/unreleased/even-more-frozen-string-lib.yml @@ -0,0 +1,5 @@ +--- +title: Enable even more frozen string in lib/**/*.rb +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/feature-improved-branch-filter-sorting.yml b/changelogs/unreleased/feature-improved-branch-filter-sorting.yml new file mode 100644 index 00000000000..539c297e0dd --- /dev/null +++ b/changelogs/unreleased/feature-improved-branch-filter-sorting.yml @@ -0,0 +1,6 @@ +--- +title: Improving branch filter sorting by listing exact matches first and added support + for begins_with (^) and ends_with ($) matching. +merge_request: 22166 +author: Jason Rutherford +type: changed diff --git a/changelogs/unreleased/fl-update-svgs.yml b/changelogs/unreleased/fl-update-svgs.yml new file mode 100644 index 00000000000..e6e76617df1 --- /dev/null +++ b/changelogs/unreleased/fl-update-svgs.yml @@ -0,0 +1,5 @@ +--- +title: Updates svg dependency +merge_request: +author: +type: other diff --git a/changelogs/unreleased/gt-update-application-copy-secret-to-clipboard-data.yml b/changelogs/unreleased/gt-update-application-copy-secret-to-clipboard-data.yml new file mode 100644 index 00000000000..7205c138777 --- /dev/null +++ b/changelogs/unreleased/gt-update-application-copy-secret-to-clipboard-data.yml @@ -0,0 +1,5 @@ +--- +title: Update copy to clipboard button data for application secret +merge_request: 22268 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/gt-update-wiki-empty-state.yml b/changelogs/unreleased/gt-update-wiki-empty-state.yml new file mode 100644 index 00000000000..76f923ae814 --- /dev/null +++ b/changelogs/unreleased/gt-update-wiki-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Update wiki empty state +merge_requrst: 22218 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/sh-fix-project-deletion-with-export.yml b/changelogs/unreleased/sh-fix-project-deletion-with-export.yml new file mode 100644 index 00000000000..b9437f8ad6a --- /dev/null +++ b/changelogs/unreleased/sh-fix-project-deletion-with-export.yml @@ -0,0 +1,5 @@ +--- +title: Fix project deletion when there is a export available +merge_request: 22276 +author: +type: fixed diff --git a/changelogs/unreleased/zj-circuit-breaker-removal.yml b/changelogs/unreleased/zj-circuit-breaker-removal.yml new file mode 100644 index 00000000000..f753cec993f --- /dev/null +++ b/changelogs/unreleased/zj-circuit-breaker-removal.yml @@ -0,0 +1,5 @@ +--- +title: Remove Git circuit breaker +merge_request: 22212 +author: +type: removed diff --git a/config/routes.rb b/config/routes.rb index 1242bbbf932..5c093aa5626 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,7 +56,6 @@ Rails.application.routes.draw do # '/-/health' implemented by BasicHealthMiddleware get 'liveness' => 'health#liveness' get 'readiness' => 'health#readiness' - post 'storage_check' => 'health#storage_check' resources :metrics, only: [:index] mount Peek::Railtie => '/peek', as: 'peek_routes' diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 7cdaa2daa14..fb29c4748c1 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -69,9 +69,7 @@ namespace :admin do end resource :logs, only: [:show] - resource :health_check, controller: 'health_check', only: [:show] do - post :reset_storage_health - end + resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index 020e9a00d87..e06cce3e97a 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -95,7 +95,7 @@ end before_fork do |server, worker| # the following is highly recommended for Rails + "preload_app true" # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) and + defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect! # The following is only recommended for memory/DB-constrained @@ -133,7 +133,7 @@ after_fork do |server, worker| # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) # the following is *required* for Rails + "preload_app true", - defined?(ActiveRecord::Base) and + defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection # reset prometheus client, this will cause any opened metrics files to be closed diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development index 5712549a66d..f31df66015a 100644 --- a/config/unicorn.rb.example.development +++ b/config/unicorn.rb.example.development @@ -7,7 +7,7 @@ check_client_connection false before_fork do |server, worker| # the following is highly recommended for Rails + "preload_app true" # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) and + defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect! if /darwin/ =~ RUBY_PLATFORM @@ -27,6 +27,6 @@ after_fork do |server, worker| require 'rbtrace' if ENV['ENABLE_RBTRACE'] # the following is *required* for Rails + "preload_app true", - defined?(ActiveRecord::Base) and + defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection end diff --git a/db/post_migrate/20181008145341_steal_encrypt_columns.rb b/db/post_migrate/20181008145341_steal_encrypt_columns.rb new file mode 100644 index 00000000000..c107ac72913 --- /dev/null +++ b/db/post_migrate/20181008145341_steal_encrypt_columns.rb @@ -0,0 +1,15 @@ +class StealEncryptColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + Gitlab::BackgroundMigration.steal('EncryptColumns') + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20181008145359_remove_web_hooks_token_and_url.rb b/db/post_migrate/20181008145359_remove_web_hooks_token_and_url.rb new file mode 100644 index 00000000000..0c44bca5f1a --- /dev/null +++ b/db/post_migrate/20181008145359_remove_web_hooks_token_and_url.rb @@ -0,0 +1,10 @@ +class RemoveWebHooksTokenAndUrl < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + remove_column :web_hooks, :token, :string + remove_column :web_hooks, :url, :string, limit: 2000 + end +end diff --git a/db/post_migrate/20181008200441_remove_circuit_breaker.rb b/db/post_migrate/20181008200441_remove_circuit_breaker.rb new file mode 100644 index 00000000000..838addb7286 --- /dev/null +++ b/db/post_migrate/20181008200441_remove_circuit_breaker.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class RemoveCircuitBreaker < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + CIRCUIT_BREAKER_COLUMS_WITH_DEFAULT = { + circuitbreaker_failure_count_threshold: 3, + circuitbreaker_failure_reset_time: 1800, + circuitbreaker_storage_timeout: 15, + circuitbreaker_access_retries: 3, + circuitbreaker_check_interval: 1 + }.freeze + + def up + CIRCUIT_BREAKER_COLUMS_WITH_DEFAULT.keys.each do |column| + remove_column(:application_settings, column) if column_exists?(:application_settings, column) + end + end + + def down + CIRCUIT_BREAKER_COLUMS_WITH_DEFAULT.each do |column, default| + add_column_with_default(:application_settings, column, :integer, default: default) unless column_exists?(:application_settings, column) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4ff0272428a..5b44bbb2756 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181002172433) do +ActiveRecord::Schema.define(version: 20181008200441) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -139,10 +139,6 @@ ActiveRecord::Schema.define(version: 20181002172433) do t.boolean "hashed_storage_enabled", default: false, null: false t.boolean "project_export_enabled", default: true, null: false t.boolean "auto_devops_enabled", default: true, null: false - t.integer "circuitbreaker_failure_count_threshold", default: 3 - t.integer "circuitbreaker_failure_reset_time", default: 1800 - t.integer "circuitbreaker_storage_timeout", default: 15 - t.integer "circuitbreaker_access_retries", default: 3 t.boolean "throttle_unauthenticated_enabled", default: false, null: false t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false @@ -152,7 +148,6 @@ ActiveRecord::Schema.define(version: 20181002172433) do t.boolean "throttle_authenticated_web_enabled", default: false, null: false t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false - t.integer "circuitbreaker_check_interval", default: 1, null: false t.boolean "password_authentication_enabled_for_web" t.boolean "password_authentication_enabled_for_git", default: true t.integer "gitaly_timeout_default", default: 55, null: false @@ -2256,7 +2251,6 @@ ActiveRecord::Schema.define(version: 20181002172433) do add_index "web_hook_logs", ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree create_table "web_hooks", force: :cascade do |t| - t.string "url", limit: 2000 t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" @@ -2269,7 +2263,6 @@ ActiveRecord::Schema.define(version: 20181002172433) do t.boolean "note_events", default: false, null: false t.boolean "enable_ssl_verification", default: true t.boolean "wiki_page_events", default: false, null: false - t.string "token" t.boolean "pipeline_events", default: false, null: false t.boolean "confidential_issues_events", default: false, null: false t.boolean "repository_update_events", default: false, null: false diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index b5e2b5448f7..e1b2a0a24eb 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -25,15 +25,13 @@ gitaly['prometheus_listen_addr'] = 'localhost:9236' ``` To change a Gitaly setting in installations from source you can edit -`/home/git/gitaly/config.toml`. +`/home/git/gitaly/config.toml`. Changes will be applied when you run +`service gitlab restart`. ```toml prometheus_listen_addr = "localhost:9236" ``` -Changes to `/home/git/gitaly/config.toml` are applied when you run `service -gitlab restart`. - ## Client-side GRPC logs Gitaly uses the [gRPC](https://grpc.io/) RPC framework. The Ruby gRPC diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 95e2caf0cad..040c9ecae55 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -3,6 +3,11 @@ You can view information and options set for each of the mounted NFS file systems by running `nfsstat -m` and `cat /etc/fstab`. +NOTE: **Note:** Filesystem performance has a big impact on overall GitLab +performance, especially for actions that read or write to Git repositories. See +[Filesystem Performance Benchmarking](../operations/filesystem_benchmarking.md) +for steps to test filesystem performance. + ## NFS Server features ### Required features @@ -87,7 +92,7 @@ Mount `/gitlab-nfs` then use the following Omnibus configuration to move each data location to a subdirectory: ```ruby -git_data_dirs({"default" => "/gitlab-nfs/gitlab-data/git-data"}) +git_data_dirs({"default" => { "path" => "/gitlab-nfs/gitlab-data/git-data"} }) user['home'] = '/gitlab-nfs/gitlab-data/home' gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads' gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared' @@ -133,7 +138,7 @@ following are the 5 locations need to be shared: | Location | Description | Default configuration | | -------- | ----------- | --------------------- | -| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})` +| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => { "path" => "/var/opt/gitlab/git-data"} })` | `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'` | `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'` | `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'` diff --git a/doc/administration/img/circuitbreaker_config.png b/doc/administration/img/circuitbreaker_config.png Binary files differdeleted file mode 100644 index 20233276055..00000000000 --- a/doc/administration/img/circuitbreaker_config.png +++ /dev/null diff --git a/doc/administration/img/failing_storage.png b/doc/administration/img/failing_storage.png Binary files differdeleted file mode 100644 index 652d7dcb5d7..00000000000 --- a/doc/administration/img/failing_storage.png +++ /dev/null diff --git a/doc/administration/index.md b/doc/administration/index.md index d713247983b..8e6fa9563c7 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -118,10 +118,9 @@ created in snippets, wikis, and repos. ## Continuous Integration settings - [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance. -- [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time. +- [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Enable or disable Auto DevOps site-wide and define the artifacts' max size and expiration time. - [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully). - [Job traces](job_traces.md): Information about the job traces (logs). -- [Artifacts size and expiration](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size): Define maximum artifacts limits and expiration date. - [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance. - [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for Shared Runners. - [Enable/disable Auto DevOps](../topics/autodevops/index.md#enabling-auto-devops): Enable or disable Auto DevOps for your instance. diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index 293036f2f4b..b61c5409a56 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -80,10 +80,10 @@ our AsciiDoc snippets, wikis and repos using delimited blocks: ``` [plantuml, format="png", id="myDiagram", width="200px"] - -- + ---- Bob->Alice : hello Alice -> Bob : Go Away - -- + ---- ``` - **reStructuredText** diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 757865ea2c5..2feac1fd3b0 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -98,7 +98,7 @@ _The artifacts are stored by default in If you don't want to use the local disk where GitLab is installed to store the artifacts, you can use an object storage like AWS S3 instead. This configuration relies on valid AWS credentials to be configured already. -Use an [Object storage option][os] like AWS S3 to store job artifacts. +Use an object storage option like AWS S3 to store job artifacts. ### Object Storage Settings @@ -315,4 +315,3 @@ memory and disk I/O. [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" [restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" [gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository" -[os]: https://docs.gitlab.com/administration/job_artifacts.html#using-object-storage diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 7bc92ae77ee..c6fd7ef7360 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -45,9 +45,6 @@ The following metrics are available: | redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded | | redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping | | user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in | -| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible | -| filesystem_circuitbreaker | Gauge | 9.5 | Whether or not the circuit for a certain shard is broken or not | -| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took | | failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login | | successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login | diff --git a/doc/administration/operations/filesystem_benchmarking.md b/doc/administration/operations/filesystem_benchmarking.md new file mode 100644 index 00000000000..44018e966e0 --- /dev/null +++ b/doc/administration/operations/filesystem_benchmarking.md @@ -0,0 +1,55 @@ +# Filesystem Performance Benchmarking + +Filesystem performance has a big impact on overall GitLab performance, +especially for actions that read or write to Git repositories. This information +will help benchmark filesystem performance against known good and bad real-world +systems. + +Normally when talking about filesystem performance the biggest concern is +with Network Filesystems (NFS). However, even some local disks can have slow +IO. The information on this page can be used for either scenario. + +## Write Performance + +The following one-line command is a quick benchmark for filesystem write +performance. This will write 1,000 small files to the directory in which it is +executed. + +1. Change into the root of the appropriate + [repository storage path](../repository_storage_paths.md). +1. Create a temporary directory for the test so it's easy to remove the files later: + + ```sh + mkdir test; cd test + ``` +1. Run the command: + + ```sh + time for i in {0..1000}; do echo 'test' > "test${i}.txt"; done + ``` +1. Remove the test files: + + ```sh + cd ../; rm -rf test + ``` + +The output of the `time for ...` command will look similar to the following. The +important metric is the `real` time. + +```sh +$ time for i in {0..1000}; do echo 'test' > "test${i}.txt"; done + +real 0m0.116s +user 0m0.025s +sys 0m0.091s +``` + +From experience with multiple customers, the following are ranges that indicate +whether your filesystem performance is satisfactory or less than ideal: + +| Rating | Benchmark result | +|:----------|:------------------------| +| Best | Less than 10 seconds | +| OK | 10-18 seconds | +| Poor | 18-25 seconds | +| Very poor | Greater than 25 seconds | diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md index dea98cb8197..a16fc7ae74f 100644 --- a/doc/administration/operations/index.md +++ b/doc/administration/operations/index.md @@ -16,3 +16,7 @@ to restart Sidekiq. indexed lookup to the GitLab database](fast_ssh_key_lookup.md), and/or by [doing away with user SSH keys stored on GitLab entirely in favor of SSH certificates](ssh_certificates.md). +- [Filesystem Performance Benchmarking](filesystem_benchmarking.md): Filesystem +performance can have a big impact on GitLab performance, especially for actions +that read or write Git repositories. This information will help benchmark +filesystem performance against known good and bad real-world systems. diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md index 6b8ad1b039b..ccd9c0afb1d 100644 --- a/doc/administration/raketasks/github_import.md +++ b/doc/administration/raketasks/github_import.md @@ -1,37 +1,41 @@ # GitHub import ->**Note:** -> -> - [Introduced][ce-10308] in GitLab 9.1. -> - You need a personal access token in order to retrieve and import GitHub -> projects. You can get it from: https://github.com/settings/tokens -> - You also need to pass an username as the second argument to the rake task -> which will become the owner of the project. -> - You can also resume an import with the same command. +> [Introduced]( https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10308) in GitLab 9.1. + +In order to retrieve and import GitHub repositories, you will need a +[GitHub personal access token](https://github.com/settings/tokens). +A username should be passed as the second argument to the rake task +which will become the owner of the project. You can resume an import +with the same command. + +Bear in mind that the syntax is very specific. Remove any spaces within the argument block and +before/after the brackets. Also, Some shells (e.g., zsh) can interpret the open/close brackets +(`[]`) separately. You may need to either escape the brackets or use double quotes. + +## Importing multiple projects To import a project from the list of your GitHub projects available: ```bash # Omnibus installations -sudo gitlab-rake import:github[access_token,root,foo/bar] +sudo gitlab-rake "import:github[access_token,root,foo/bar]" # Installations from source -bundle exec rake import:github[access_token,root,foo/bar] RAILS_ENV=production +bundle exec rake "import:github[access_token,root,foo/bar]" RAILS_ENV=production ``` In this case, `access_token` is your GitHub personal access token, `root` is your GitLab username, and `foo/bar` is the new GitLab namespace/project that will get created from your GitHub project. Subgroups are also possible: `foo/foo/bar`. +## Importing a single project To import a specific GitHub project (named `foo/github_repo` here): ```bash # Omnibus installations -sudo gitlab-rake import:github[access_token,root,foo/bar,foo/github_repo] +sudo gitlab-rake "import:github[access_token,root,foo/bar,foo/github_repo]" # Installations from source -bundle exec rake import:github[access_token,root,foo/bar,foo/github_repo] RAILS_ENV=production +bundle exec rake "import:github[access_token,root,foo/bar,foo/github_repo]" RAILS_ENV=production ``` - -[ce-10308]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10308 diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md index 7ea7ed48850..c03f23a8931 100644 --- a/doc/administration/repository_storage_paths.md +++ b/doc/administration/repository_storage_paths.md @@ -97,55 +97,6 @@ be stored via the **Application Settings** in the Admin area. Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be randomly placed on one of the selected paths. -## Handling failing repository storage - -> [Introduced][ce-11449] in GitLab 9.5. - -When GitLab detects access to the repositories storage fails repeatedly, it can -gracefully prevent attempts to access the storage. This might be useful when -the repositories are stored somewhere on the network. - -This can be configured from the admin interface: - -![circuitbreaker configuration](img/circuitbreaker_config.png) - -**Number of access attempts**: The number of attempts GitLab will make to access a -storage when probing a shard. - -**Number of failures before backing off**: The number of failures after which -GitLab will start temporarily disabling access to a storage shard on a host. - -**Maximum git storage failures:** The number of failures of after which GitLab will -completely prevent access to the storage. The number of failures can be reset in -the admin interface: `https://gitlab.example.com/admin/health_check` or using the -[api](../api/repository_storage_health.md) to allow access to the storage again. - -**Seconds to wait after a storage failure:** When access to a storage fails. GitLab -will prevent access to the storage for the time specified here. This allows the -filesystem to recover. - -**Seconds before reseting failure information:** The time in seconds GitLab will -keep failure information. When no failures occur during this time, information about the -mount is reset. - -**Seconds to wait for a storage access attempt:** The time in seconds GitLab will -try to access storage. After this time a timeout error will be raised. - -To enable the circuitbreaker for repository storage you can flip the feature flag from a rails console: - -``` -Feature.enable('git_storage_circuit_breaker') -``` - -Alternatively it can be enabled by setting `true` in the `GIT_STORAGE_CIRCUIT_BREAKER` environment variable. -This approach would be used when enabling the circuit breaker on a single host. - -When storage failures occur, this will be visible in the admin interface like this: - -![failing storage](img/failing_storage.png) - -To allow access to all storages, click the `Reset git storage health information` button. - [ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578 [restart-gitlab]: restart_gitlab.md#installations-from-source [reconfigure-gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure diff --git a/doc/api/events.md b/doc/api/events.md index cd84b32029e..ccac5b8bb60 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -71,7 +71,7 @@ Parameters: Example request: ``` -curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01 ``` Example response: @@ -276,7 +276,7 @@ Parameters: Example request: ``` -curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01 +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01 ``` Example response: diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 0623a6b02ae..c3624f1a535 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -97,7 +97,10 @@ POST /projects/:id/repository/files/:file_path ``` ```bash -curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' --header "Content-Type: application/json" \ + --data '{"branch": "master", "author_email": "author@example.com", "author_name": "Firstname Lastname", \ + "content": "some content", "commit_message": "create a new file"}' \ + 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb' ``` Example response: @@ -129,7 +132,10 @@ PUT /projects/:id/repository/files/:file_path ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' --header "Content-Type: application/json" \ + --data '{"branch": "master", "author_email": "author@example.com", "author_name": "Firstname Lastname", \ + "content": "some content", "commit_message": "update file"}' \ + 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb' ``` Example response: @@ -171,7 +177,10 @@ DELETE /projects/:id/repository/files/:file_path ``` ```bash -curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' --header "Content-Type: application/json" \ + --data '{"branch": "master", "author_email": "author@example.com", "author_name": "Firstname Lastname", \ + "commit_message": "delete file"}' \ + 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb' ``` Parameters: diff --git a/doc/api/repository_storage_health.md b/doc/api/repository_storage_health.md index e0c0315c2d7..edf4b04acea 100644 --- a/doc/api/repository_storage_health.md +++ b/doc/api/repository_storage_health.md @@ -1,74 +1,5 @@ # Circuitbreaker API -> [Introduced][ce-11449] in GitLab 9.5. - -The Circuitbreaker API is only accessible to administrators. All requests by -guests will respond with `401 Unauthorized`, and all requests by normal users -will respond with `403 Forbidden`. - -## Repository Storages - -### Get all storage information - -Returns of all currently configured storages and their health information. - -``` -GET /circuit_breakers/repository_storage -``` - -```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage -``` - -```json -[ - { - "storage_name": "default", - "failing_on_hosts": [], - "total_failures": 0 - }, - { - "storage_name": "broken", - "failing_on_hosts": [ - "web01", "worker01" - ], - "total_failures": 1 - } -] -``` - -### Get failing storages - -This returns a list of all currently failing storages. - -``` -GET /circuit_breakers/repository_storage/failing -``` - -```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage/failing -``` - -```json -[ - { - "storage_name":"broken", - "failing_on_hosts":["web01", "worker01"], - "total_failures":2 - } -] -``` - -## Reset failing storage information - -Use this remove all failing storage information and allow access to the storage again. - -``` -DELETE /circuit_breakers/repository_storage -``` - -```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage -``` - -[ce-11449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11449 +NOTE: **Deprecated:** +Support of the circuit breaker is removed, as Gitaly can be configured to +to work without NFS and [communicate solely over HTTP](../administration/gitaly/index.md). diff --git a/doc/api/settings.md b/doc/api/settings.md index 1c41b3345ad..4482030888d 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -138,11 +138,6 @@ are listed in the descriptions of the relevant settings. | `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. | | `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. | | `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. | -| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. | -| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. | -| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures after which GitLab will completely prevent access to the storage. | -| `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | -| `circuitbreaker_storage_timeout` | integer | no | Seconds to wait for a storage access attempt. | | `clientside_sentry_dsn` | string | required by: `clientside_sentry_enabled` | Clientside Sentry Data Source Name. | | `clientside_sentry_enabled` | boolean | no | (**If enabled, requires:** `clientside_sentry_dsn`) Enable Sentry error reporting for the client side. | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. | diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md index f479dc74d1f..758ab37861b 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -253,7 +253,7 @@ image: python:latest # Change pip's cache directory to be inside the project directory since we can # only cache local items. variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache" + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" # Pip's cache doesn't store the python packages # https://pip.pypa.io/en/stable/reference/pip_install/#caching @@ -262,7 +262,7 @@ variables: # them in a virtualenv and cache it as well. cache: paths: - - .cache/ + - .cache/pip - venv/ before_script: diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md index bd60d641493..f53f7c50281 100644 --- a/doc/ci/examples/deployment/README.md +++ b/doc/ci/examples/deployment/README.md @@ -5,7 +5,7 @@ continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI. >**Note:** -We recommend to use Dpl if you're deploying to any of these of these services: +We recommend to use Dpl if you're deploying to any of these services: https://github.com/travis-ci/dpl#supported-providers. ## Requirements diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index ab429e0ded3..70020d461d9 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -125,7 +125,7 @@ They can be added per project by navigating to the project's **Settings** > **CI To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier. We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password. -We also need to add the public key to **Project** > **Settings** > **Repository** as [Deploy Keys](../../../ssh/README.md/#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../../gitlab-basics/command-line-commands.md/#start-working-on-your-project). +We also need to add the public key to **Project** > **Settings** > **Repository** as [Deploy Keys](../../../ssh/README.md#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../../gitlab-basics/command-line-commands.md#start-working-on-your-project). ```bash @@ -378,7 +378,7 @@ These are persistent data and will be shared to every new release. Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments.md), which will be described [later](#setting-up-gitlab-ci-cd) in this tutorial. Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch. -To keep things simple, we commit directly to `master`, without using [feature-branches](../../../workflow/gitlab_flow.md/#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial. +To keep things simple, we commit directly to `master`, without using [feature-branches](../../../workflow/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial. In a real world project, teams may use [Issue Tracker](../../../user/project/issues/index.md) and [Merge Requests](../../../user/project/merge_requests/index.md) to move their code across branches: ```bash @@ -398,7 +398,7 @@ In the case you're not familiar with Docker, refer to [How to Automate Docker De To be able to build, test, and deploy our app with GitLab CI/CD, we need to prepare our work environment. To do that, we'll use a Docker image which has the minimum requirements that a Laravel app needs to run. -[There are other ways](../php.md/#test-php-projects-using-the-docker-executor) to do that as well, but they may lead our builds run slowly, which is not what we want when there are faster options to use. +[There are other ways](../php.md#test-php-projects-using-the-docker-executor) to do that as well, but they may lead our builds run slowly, which is not what we want when there are faster options to use. With Docker images our builds run incredibly faster! @@ -536,7 +536,7 @@ That's a lot to take in, isn't it? Let's run through it step by step. [GitLab Runners](../../runners/README.md) run the script defined by `.gitlab-ci.yml`. The `image` keyword tells the Runners which image to use. -The `services` keyword defines additional images [that are linked to the main image](../../docker/using_docker_images.md/#what-is-a-service). +The `services` keyword defines additional images [that are linked to the main image](../../docker/using_docker_images.md#what-is-a-service). Here we use the container image we created before as our main image and also use MySQL 5.7 as a service. ```yaml @@ -560,7 +560,7 @@ So we should adjust the configuration of MySQL instance by defining `MYSQL_DATAB Find out more about MySQL variables at the [official MySQL Docker Image](https://hub.docker.com/r/_/mysql/). Also set the variables `DB_HOST` to `mysql` and `DB_USERNAME` to `root`, which are Laravel specific variables. -We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../docker/using_docker_images.md/#how-services-are-linked-to-the-build). +We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../docker/using_docker_images.md#how-services-are-linked-to-the-build). ```yaml ... @@ -602,7 +602,7 @@ unit_test: #### Deploy to production The job `deploy_production` will deploy the app to the production server. -To deploy our app with Envoy, we had to set up the `$SSH_PRIVATE_KEY` variable as an [SSH private key](../../ssh_keys/README.md/#ssh-keys-when-using-the-docker-executor). +To deploy our app with Envoy, we had to set up the `$SSH_PRIVATE_KEY` variable as an [SSH private key](../../ssh_keys/README.md#ssh-keys-when-using-the-docker-executor). If the SSH keys have added successfully, we can run Envoy. As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well. diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index df4805ea7ac..c1048f3d2e3 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -20,7 +20,7 @@ build environment. Let's first specify the PHP image that will be used for the job process (you can read more about what an image means in the Runner's lingo reading -about [Using Docker images](../docker/using_docker_images.md#what-is-image)). +about [Using Docker images](../docker/using_docker_images.md#what-is-an-image)). Start by adding the image to your `.gitlab-ci.yml`: diff --git a/doc/ci/img/pipeline_incremental_rollout.png b/doc/ci/img/pipeline_incremental_rollout.png Binary files differnew file mode 100644 index 00000000000..b3498e9a5a5 --- /dev/null +++ b/doc/ci/img/pipeline_incremental_rollout.png diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index ea47d676edb..44589500eb0 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -193,6 +193,18 @@ stage has a job with a manual action. ![Pipelines example](img/pipelines.png) +### Delay a particular job in the pipeline graph + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4. + +When you do not want to run a job immediately, you can [delay the job to run after a certain period](yaml/README.md#delayed). +This is especially useful for timed incremental rollout that new code is rolled out gradually. +For example, if you start rolling out new code and users do not experience trouble, GitLab automatically completes the deployment from 0% to 100%. +Alternatively, if you start rolling out and you noticed that a few users experience trouble with the version, +you can stop the timed incremental rollout by canceling the pipeline, and [rolling](environments.md#rolling-back-changes) it back to the stable version. + +![Pipelines example](img/pipeline_incremental_rollout.png) + ### Ordering of jobs in pipeline graphs **Regular pipeline graph** @@ -211,6 +223,7 @@ by name. The order of severity is: - pending - running - manual +- scheduled - canceled - success - skipped diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index cf92d90ba30..bffb0121603 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -2,7 +2,7 @@ > **Notes**: > -> - [Introduced][ci-229] in GitLab CE 7.14. +> - [Introduced](https://about.gitlab.com/2015/08/22/gitlab-7-14-released/) in GitLab 7.14. > - GitLab 8.12 has a completely redesigned job permissions system. Read all > about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers). @@ -154,10 +154,10 @@ This information is also exposed in the UI. Using trigger variables can be proven useful for a variety of reasons: -* Identifiable jobs. Since the variable is exposed in the UI you can know +- Identifiable jobs. Since the variable is exposed in the UI you can know why the rebuild was triggered if you pass a variable that explains the purpose. -* Conditional job processing. You can have conditional jobs that run whenever +- Conditional job processing. You can have conditional jobs that run whenever a certain variable is present. Consider the following `.gitlab-ci.yml` where we set three @@ -221,7 +221,6 @@ removed with one of the future versions of GitLab. You are advised to [take ownership](#taking-ownership) of any legacy triggers. [ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017 -[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 [ee]: https://about.gitlab.com/pricing/ [variables]: ../variables/README.md [predef]: ../variables/README.md#predefined-variables-environment-variables diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 8b770495853..24d60a0cdcc 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -415,7 +415,7 @@ Four keys are now available: `refs`, `kubernetes` and `variables` and `changes`. Refs strategy equals to simplified only/except configuration, whereas kubernetes strategy accepts only `active` keyword. -### `variables` +### `only:variables` `variables` keyword is used to define variables expressions. In other words you can use predefined variables / project / group or @@ -460,7 +460,7 @@ end-to-end: Learn more about variables expressions on [a separate page][variables-expressions]. -### `changes` +### `only:changes` Using `changes` keyword with `only` or `except` makes it possible to define if a job should be created based on files modified by a git push event. @@ -673,6 +673,42 @@ user wants to trigger an action. In other words, in order to trigger a manual action assigned to a branch that the pipeline is running for, user needs to have ability to merge to this branch. +### `when:delayed` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4. + +Delayed job are for executing scripts after a certain period. +This is useful if you want to avoid jobs entering `pending` state immediately. + +You can set the period with `start_in` key. The value of `start_in` key is an elapsed time in seconds, unless a unit is +provided. `start_key` must be less than or equal to one hour. Examples of valid values include: + +- `10 seconds` +- `30 minutes` +- `1 hour` + +When there is a delayed job in a stage, the pipeline will not progress until the delayed job has finished. +This means this keyword can also be used for inserting delays between different stages. + +The timer of a delayed job starts immediately after the previous stage has completed. +Similar to other types of jobs, a delayed job's timer will not start unless the previous stage passed. + +The following example creates a job named `timed rollout 10%` that is executed 30 minutes after the previous stage has completed: + +```yaml +timed rollout 10%: + stage: deploy + script: echo 'Rolling out 10% ...' + when: delayed + start_in: 30 minutes +``` + +You can stop the active timer of a delayed job by clicking the **Unschedule** button. +This job will never be executed in the future unless you execute the job manually. + +You can start a delayed job immediately by clicking the **Play** button. +GitLab runner will pick your job soon and start the job. + ## `environment` > **Notes:** diff --git a/doc/development/README.md b/doc/development/README.md index 43d3865da0e..d8604a4f3e5 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -50,6 +50,7 @@ description: 'Learn how to contribute to GitLab.' - [Permissions](permissions.md) - [Prometheus metrics](prometheus_metrics.md) - [Guidelines for reusing abstractions](reusing_abstractions.md) +- [DeclarativePolicy framework](policies.md) ## Performance guides diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 2f06677bfec..1b25a5a2fb7 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -108,12 +108,12 @@ Priority labels help us define the time a ~bug fix should be completed. Priority If there are multiple defects, the priority decides which defect has to be fixed immediately versus later. This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes. -| Label | Meaning | Estimate time to fix | -|-------|-----------------|------------------------------------------------------------------| -| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | -| ~P2 | High Priority | The next release | -| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | -| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | +| Label | Meaning | Defect SLA (applies only to ~bug and ~security defects) | +|-------|-----------------|----------------------------------------------------------------------------| +| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com (30 days) | +| ~P2 | High Priority | The next release (60 days) | +| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter or 90 days) | +| ~P4 | Low Priority | Anything outside the next 3 releases (more than one quarter or 120 days) | ## Severity labels diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index a286e74908c..0d20e1a02dd 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -156,7 +156,7 @@ the feature you contribute through all of these steps. 1. Performance/scalability implications have been considered, addressed, and tested 1. [Documented][doc-guidelines] in the `/doc` directory 1. [Changelog entry added][changelog], if necessary -1. Reviewed and any concerns are addressed +1. Reviewed by UX/FE/BE and any concerns are addressed 1. Merged by a project maintainer 1. Added to the release blog article, if relevant 1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index 2db78e4a365..ce2424eca3b 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -321,7 +321,7 @@ The following sample `markdownlint` configuration modifies the available default } ``` -For [`markdownlint`](https://gitahub.com/DavidAnson/markdownlint/), this configuration must be +For [`markdownlint`](https://github.com/DavidAnson/markdownlint/), this configuration must be placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For example, `~/.markdownlintrc`. diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md index 1002836096a..01068e23082 100644 --- a/doc/development/documentation/structure.md +++ b/doc/development/documentation/structure.md @@ -21,21 +21,21 @@ Before getting started, read through the following docs: Every document should include the following content in the following sequence: - **Feature name**: defines an intuitive name for the feature that clearly -states what it is and is consistent with any relevant UI text. + states what it is and is consistent with any relevant UI text. - **Feature overview** and description: describe what it is, what it does, and in what context it should be used. - **Use cases**: describes real use case scenarios for that feature. - **Requirements**: describes what software and/or configuration is required to be able to -use the feature and, if applicable, prerequisite knowledge for being able to follow/implement the tutorial. -For example, familiarity with GitLab CI/CD, an account on a third-party service, dependencies installed, etc. -Link each one to its most relevant resource; i.e., where the reader can go to begin to fullfil that requirement. -(Another doc page, a third party application's site, etc.) + use the feature and, if applicable, prerequisite knowledge for being able to follow/implement the tutorial. + For example, familiarity with GitLab CI/CD, an account on a third-party service, dependencies installed, etc. + Link each one to its most relevant resource; i.e., where the reader can go to begin to fullfil that requirement. + (Another doc page, a third party application's site, etc.) - **Instructions**: clearly describes the steps to use the feature, leaving no gaps. - **Troubleshooting** guide (recommended but not required): if you know beforehand what issues -one might have when setting it up, or when something is changed, or on upgrading, it's -important to describe those too. Think of things that may go wrong and include them in the -docs. This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. Answering them beforehand only makes your -document better and more approachable. + one might have when setting it up, or when something is changed, or on upgrading, it's + important to describe those too. Think of things that may go wrong and include them in the + docs. This is important to minimize requests for support, and to avoid doc comments with + questions that you know someone might ask. Answering them beforehand only makes your + document better and more approachable. For additional details, see the subsections below, as well as the [Documentation template for new docs](#Documentation-template-for-new-docs). @@ -55,10 +55,11 @@ You should answer this question: what can you do with this feature/change? Use c are examples of how this feature or change can be used in real life. Examples: -- CE and EE: [Issues](../user/project/issues/index.md#use-cases) -- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview) -- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview) -- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview) + +- CE and EE: [Issues](../../user/project/issues/index.md#use-cases) +- CE and EE: [Merge Requests](../../user/project/merge_requests/index.md) +- EE-only: [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) +- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.html) Note that if you don't have anything to add between the doc title (`<h1>`) and the header `## Overview`, you can omit the header, but keep the content of the @@ -72,14 +73,14 @@ and for every **major** feature present in Community Edition. Your new document will be discoverable by the user only if: - Crosslinked from the higher-level index (e.g., Issue Boards docs -should be linked from Issues; Prometheus docs should be linked from -Monitoring; CI/CD tutorials should be linked from CI/CD examples). + should be linked from Issues; Prometheus docs should be linked from + Monitoring; CI/CD tutorials should be linked from CI/CD examples). - When referencing other GitLab products and features, link to their -respective docs; when referencing third-party products or technologies, -link out to their external sites, documentation, and resources. + respective docs; when referencing third-party products or technologies, + link out to their external sites, documentation, and resources. - The headings are clear. E.g., "App testing" is a bad heading, "Testing -an application with GitLab CI/CD" is much better. Think of something -someone will search for and use these keywords in the headings. + an application with GitLab CI/CD" is much better. Think of something + someone will search for and use these keywords in the headings. ## Documentation template for new docs @@ -133,7 +134,7 @@ is simple and the document is short. - Be clear, concise, and stick to the goal of the doc: explain how to use that feature. - Use inclusive language and avoid jargons, as well as uncommon and -fancy words. The docs should be clear and very easy to understand. +fancy words. The docs should be clear and easy to understand. - Write in the 3rd person (use "we", "you", "us", "one", instead of "I" or "me"). - Always provide internal and external reference links. - Always link the doc from its higher-level index. diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 656ddd868cd..1e0529262ad 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -1,11 +1,13 @@ # Style guides and linting + See the relevant style guides for our guidelines and for information on linting: ## JavaScript + We defer to [Airbnb][airbnb-js-style-guide] on most style-related conventions and enforce them with eslint. -See [our current .eslintrc][eslintrc] for specific rules and patterns. +See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc.yml) for specific rules and patterns. ### Common @@ -21,10 +23,10 @@ refactor an existing one, you should abide by the eslint rules. ```javascript // bad /* eslint-disable */ - + // better /* eslint-disable some-rule, some-other-rule */ - + // best // nothing :) ``` @@ -34,14 +36,14 @@ refactor an existing one, you should abide by the eslint rules. ```javascript // bad /* eslint-disable no-new */ - + import Foo from 'foo'; - + new Foo(); - + // better import Foo from 'foo'; - + // eslint-disable-next-line no-new new Foo(); ``` @@ -58,11 +60,11 @@ followed by any global declarations, then a blank newline prior to any imports o /* global Foo */ /* eslint-disable no-new */ import Bar from './bar'; - + // good /* eslint-disable no-new */ /* global Foo */ - + import Bar from './bar'; ``` @@ -73,7 +75,7 @@ followed by any global declarations, then a blank newline prior to any imports o ```javascript // bad /* globals Flash, Cookies, jQuery */ - + // good /* global Flash */ /* global Cookies */ @@ -85,7 +87,7 @@ followed by any global declarations, then a blank newline prior to any imports o ```javascript // bad fn(p1, p2, p3, p4) {} - + // good fn(options) {} ``` @@ -191,28 +193,28 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod ```javascript // bad const values = {foo: 1}; - + function impureFunction(items) { const bar = 1; - + items.foo = items.a * bar + 2; - + return items.a; } - + const c = impureFunction(values); - + // good var values = {foo: 1}; - + function pureFunction (foo) { var bar = 1; - + foo = foo * bar + 2; - + return foo; } - + var c = pureFunction(values.foo); ``` @@ -231,10 +233,10 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod document.addEventListener('click', this.handleCallback) }, handleCallback() { - + } } - + // Good export class Foo { constructor() { @@ -253,12 +255,12 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod ```javascript const users = [ { name: 'Foo' }, { name: 'Bar' } ]; - + // bad users.forEach((user, index) => { user.id = index; }); - + // good const usersWithId = users.map((user, index) => { return Object.assign({}, user, { id: index }); @@ -272,10 +274,10 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod ```javascript // bad +'10' // 10 - + // good Number('10') // 10 - + // better parseInt('10', 10); ``` @@ -289,7 +291,7 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod <button class="add-user"> Add User </button> - + // good <button class="js-add-user"> Add User @@ -299,10 +301,12 @@ Do not use them anymore and feel free to remove them when refactoring legacy cod ### Vue.js #### `eslint-vue-plugin` + We default to [eslint-vue-plugin][eslint-plugin-vue], with the `plugin:vue/recommended`. Please check this [rules][eslint-plugin-vue-rules] for more documentation. #### Basic Rules + 1. The service has it's own file 1. The store has it's own file 1. Use a function in the bundle file to instantiate the Vue component: @@ -314,7 +318,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. new Component({}) } } - + // good document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#element', @@ -336,7 +340,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. } } } - + // good class Store { constructor() { @@ -354,14 +358,14 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ```javascript // bad import cardBoard from 'cardBoard.vue' - + components: { cardBoard, }; - + // good import CardBoard from 'cardBoard.vue' - + components: { CardBoard, }; @@ -373,13 +377,13 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ```javascript // bad <component class="btn"> - + // good <component css-class="btn"> - + // bad <component myProp="prop" /> - + // good <component my-prop="prop" /> ``` @@ -387,6 +391,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. [#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371 #### Alignment + 1. Follow these alignment styles for the template method: 1. With more than one attribute, all attributes should be on a new line: @@ -395,31 +400,31 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. // bad <component v-if="bar" param="baz" /> - + <button class="btn">Click me</button> - + // good <component v-if="bar" param="baz" /> - + <button class="btn"> Click me </button> ``` - + 1. The tag can be inline if there is only one attribute: ```javascript // good <component bar="bar" /> - + // good <component bar="bar" /> - + // bad <component bar="bar" /> @@ -434,7 +439,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. template: ` <button :class='style'>Button</button> ` - + // good template: ` <button :class="style">Button</button> @@ -447,7 +452,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. ```javascript // bad props: ['foo'] - + // good props: { foo: { @@ -467,7 +472,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. type: String, } } - + // good props: { foo: { @@ -490,7 +495,7 @@ On those a default key should not be provided. required: false, } } - + // good props: { foo: { @@ -499,7 +504,7 @@ On those a default key should not be provided. default: 'bar' } } - + // good props: { foo: { @@ -534,7 +539,7 @@ On those a default key should not be provided. ```javascript // bad <component v-on:click="eventHandler"/> - + // good <component @click="eventHandler"/> ``` @@ -544,7 +549,7 @@ On those a default key should not be provided. ```javascript // bad <component v-bind:class="btn"/> - + // good <component :class="btsn"/> ``` @@ -556,7 +561,7 @@ On those a default key should not be provided. ```javascript // bad <component></component> - + // good <component /> ``` @@ -650,7 +655,7 @@ Useful links: title="Some tooltip text"> Text </span> - + // good <span v-tooltip @@ -666,10 +671,10 @@ Useful links: ```javascript // bad <span data-original-title="tooltip text">Foo</span> - + // good <span title="tooltip text">Foo</span> - + $('span').tooltip('_fixTitle'); ``` diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 417298205f5..0f1f079bdb4 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -69,6 +69,37 @@ For more information about rolling out changes using feature flags, refer to the [Rolling out changes using feature flags](rolling_out_changes_using_feature_flags.md) guide. +### Frontend + +For frontend code you can use the method `push_frontend_feature_flag`, which is +available to all controllers that inherit from `ApplicationController`. Using +this method you can expose the state of a feature flag as follows: + +```ruby +before_action do + push_frontend_feature_flag(:vim_bindings) +end + +def index + # ... +end + +def edit + # ... +end +``` + +You can then check for the state of the feature flag in JavaScript as follows: + +```javascript +if ( gon.features.vimBindings ) { + // ... +} +``` + +The name of the feature flag in JavaScript will always be camelCased, meaning +that checking for `gon.features.vim_bindings` would not work. + ### Specs In the test environment `Feature.enabled?` is stubbed to always respond to `true`, diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md index 905aa26a40b..dada59ce242 100644 --- a/doc/development/rolling_out_changes_using_feature_flags.md +++ b/doc/development/rolling_out_changes_using_feature_flags.md @@ -151,3 +151,27 @@ most cases this will translate to a feature (with a feature flag) being shipped in RC1, followed by the feature flag being removed in RC2. This in turn means the feature will be stable by the time we publish a stable package around the 22nd of the month. + +## Undefined feature flags default to "on" + +By default, the [`Project#feature_available?`][project-fa], +[`Namespace#feature_available?`][namespace-fa] (EE), and +[`License.feature_available?`][license-fa] (EE) methods will check if the +specified feature is behind a feature flag. Unless the feature is explicitly +disabled or limited to a percentage of users, the feature flag check will +default to `true`. + +As an example, if you were to ship the backend half of a feature behind a flag, +you'd want to explicitly disable that flag until the frontend half is also ready +to be shipped. You can do this via ChatOps: + +``` +/chatops run feature set some_feature 0 +``` + +Note that you can do this at any time, even before the merge request using the +flag has been merged! + +[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68 +[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85 +[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300 diff --git a/doc/install/installation.md b/doc/install/installation.md index 25aa5d3369d..9e2e58657f1 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -103,7 +103,7 @@ Is the system packaged Git too old? Remove it and compile from source. # When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git -**Note:** In order to receive mail notifications, make sure to install a mail server. By default, Debian is shipped with exim4 but this [has problems](https://github.com/gitlabhq/gitlabhq/issues/4866#issuecomment-32726573) while Ubuntu does not ship with one. The recommended mail server is postfix and you can install it with: +**Note:** In order to receive mail notifications, make sure to install a mail server. By default, Debian is shipped with exim4 but this [has problems](https://gitlab.com/gitlab-org/gitlab-ce/issues/12754) while Ubuntu does not ship with one. The recommended mail server is postfix and you can install it with: sudo apt-get install -y postfix diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 0d1ba3e8f9a..c60d25eda1b 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -7,6 +7,14 @@ applications. ## Overview +NOTE: **Enabled by default:** +Starting with GitLab 11.3, the Auto DevOps pipeline will be enabled by default for all +projects. If it's not explicitly enabled for the project, Auto DevOps will be automatically +disabled on the first pipeline failure. Your project will continue to use an alternative +[CI/CD configuration file](../../ci/yaml/README.md) if one is found. A GitLab +administrator can [change this setting](../../user/admin_area/settings/continuous_integration.html#auto-devops) +in the admin area. + With Auto DevOps, the software development process becomes easier to set up as every project can have a complete workflow from verification to monitoring without needing to configure anything. Just push your code and GitLab takes @@ -214,22 +222,16 @@ manually triggered either by pushing a new commit to the repository or by visiti a new pipeline for your default branch, generally `master`. NOTE: **Note:** -If you are a GitLab Administrator, you can enable Auto DevOps instance wide -in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that, -all the projects that haven't explicitly set an option will have Auto DevOps -enabled by default. +If you are a GitLab Administrator, you can +[enable/disable Auto DevOps instance-wide](../../user/admin_area/settings/continuous_integration.md#auto-devops), +and all projects that haven't explicitly set an option will have Auto DevOps +enabled/disabled by default. NOTE: **Note:** There is also a feature flag to enable Auto DevOps to a percentage of projects which can be enabled from the console with `Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. -NOTE: **Enabled by default:** -Starting with GitLab 11.3, the Auto DevOps pipeline will be enabled by default for all -projects. If it's not explicitly enabled for the project, Auto DevOps will be automatically -disabled on the first pipeline failure. Your project will continue to use an alternative -[CI/CD configuration file](../../ci/yaml/README.md) if one is found. - ### Deployment strategy > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. diff --git a/doc/update/11.3-to-11.4.md b/doc/update/11.3-to-11.4.md index 985239369d7..b50e21f27dd 100644 --- a/doc/update/11.3-to-11.4.md +++ b/doc/update/11.3-to-11.4.md @@ -80,8 +80,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/ ### 5. Update Go -NOTE: GitLab 11.0 and higher only supports Go 1.9.x and newer, and dropped support for Go -1.5.x through 1.8.x. Be sure to upgrade your installation if necessary. +NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go +1.9.x. Be sure to upgrade your installation if necessary. You can check which version you are running with `go version`. diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 1b3fb9db4ec..097b18ad496 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -281,7 +281,7 @@ Additionally locked issues can not be reopened. [ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053 [ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061 [ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531 -[ce-31847]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31847 +[ce-31847]: https://gitlab.com/gitlab-org/gitlab-ce/issues/31847 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png [discussion-view]: img/discussion_view.png diff --git a/doc/user/img/color_inline_colorchip_render_gfm.png b/doc/user/img/color_inline_colorchip_render_gfm.png Binary files differnew file mode 100644 index 00000000000..6a8a674d6e0 --- /dev/null +++ b/doc/user/img/color_inline_colorchip_render_gfm.png diff --git a/doc/user/img/math_inline_sup_render_gfm.png b/doc/user/img/math_inline_sup_render_gfm.png Binary files differnew file mode 100644 index 00000000000..bf1464457bc --- /dev/null +++ b/doc/user/img/math_inline_sup_render_gfm.png diff --git a/doc/user/img/mermaid_diagram_render_gfm.png b/doc/user/img/mermaid_diagram_render_gfm.png Binary files differnew file mode 100644 index 00000000000..3b3eb3a738a --- /dev/null +++ b/doc/user/img/mermaid_diagram_render_gfm.png diff --git a/doc/user/img/task_list_ordered_render_gfm.png b/doc/user/img/task_list_ordered_render_gfm.png Binary files differnew file mode 100644 index 00000000000..fdff8a9886c --- /dev/null +++ b/doc/user/img/task_list_ordered_render_gfm.png diff --git a/doc/user/img/unordered_check_list_render_gfm.png b/doc/user/img/unordered_check_list_render_gfm.png Binary files differnew file mode 100644 index 00000000000..2e3fb7cbb79 --- /dev/null +++ b/doc/user/img/unordered_check_list_render_gfm.png diff --git a/doc/user/markdown.md b/doc/user/markdown.md index fb132f0613b..f9bdaea185b 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -112,8 +112,8 @@ GFM will autolink almost any URL you copy and paste into your text: * https://www.google.com * https://google.com/ * ftp://ftp.us.debian.org/debian/ -* smb://foo/bar/baz -* irc://irc.freenode.net/gitlab +* <a href="smb://foo/bar/baz">smb://foo/bar/baz</a> +* <a href="irc://irc.freenode.net/gitlab">irc://irc.freenode.net/gitlab</a> * http://localhost:3000 ### Multiline Blockquote @@ -138,17 +138,13 @@ you can quote that without having to manually prepend `>` to every line! >>> ``` ->>> -If you paste a message from somewhere else - -that - -spans - -multiple lines, - -you can quote that without having to manually prepend `>` to every line! ->>> +<blockquote dir="auto"> +<p>If you paste a message from somewhere else</p> +<p>that</p> +<p>spans</p> +<p>multiple lines,</p> +<p>you can quote that without having to manually prepend <code>></code> to every line!</p> +</blockquote> ### Code and Syntax Highlighting @@ -269,15 +265,15 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. -Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: +Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px">. Well we have a gift for you: -:zap: You can use emoji anywhere GFM is supported. :v: +<img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/zap.png" width="20px" height="20px">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/v.png" width="20px" height="20px"> -You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. +You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/bug.png" width="20px" height="20px"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speak_no_evil.png" width="20px" height="20px"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/snail.png" width="20px" height="20px"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/birthday.png" width="20px" height="20px">. People will <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/heart.png" width="20px" height="20px"> you for that. -If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. +If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px">. All you need to do is to look up one of the supported codes. -Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: +Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px"> Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. @@ -286,7 +282,6 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. - ### Special GitLab References GFM recognizes special references. @@ -356,11 +351,7 @@ You can add task lists to issues, merge requests and comments. To create a task - [ ] Sub-task 3 ``` -- [x] Completed task -- [ ] Incomplete task - - [ ] Sub-task 1 - - [x] Sub-task 2 - - [ ] Sub-task 3 +![alt unordered-check-list-render-gfm](img/unordered_check_list_render_gfm.png) Tasks formatted as ordered lists are supported as well: @@ -371,10 +362,7 @@ Tasks formatted as ordered lists are supported as well: 1. [x] Sub-task 2 ``` -1. [x] Completed task -1. [ ] Incomplete task - 1. [ ] Sub-task 1 - 1. [x] Sub-task 2 +![alt task-list-ordered-render-gfm](img/task_list_ordered_render_gfm.png) Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes. @@ -393,7 +381,10 @@ The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`. Here's a sample video: -![Sample Video](img/markdown_video.mp4) +<div class="video-container"> + <video src="img/markdown_video.mp4" width="400" controls="true" data-setup="{}" data-title="Sample Video"></video> + <p><a href="img/markdown_video.mp4" target="_blank" rel="noopener noreferrer" title="Download 'Sample Video'">Sample Video</a></p> +</div> ### Math @@ -417,12 +408,11 @@ Example: Becomes: -This math is inline $`a^2+b^2=c^2`$. +This math is inline ![alt text](img/math_inline_sup_render_gfm.png). This is on a separate line -```math -a^2+b^2=c^2 -``` + +<div align="center"><img src="./img/math_inline_sup_render_gfm.png" ></div> _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ @@ -452,15 +442,7 @@ Examples: Become: -`#F00` -`#F00A` -`#FF0000` -`#FF0000AA` -`RGB(0,255,0)` -`RGB(0%,100%,0%)` -`RGBA(0,255,0,0.7)` -`HSL(540,70%,50%)` -`HSLA(540,70%,50%,0.7)` +![alt color-inline-colorchip-render-gfm](img/color_inline_colorchip_render_gfm.png) #### Supported formats: @@ -492,13 +474,7 @@ Example: Becomes: -```mermaid -graph TD; - A-->B; - A-->C; - B-->D; - C-->D; -``` +<img src="./img/mermaid_diagram_render_gfm.png" width="200px" height="400px"> For details see the [Mermaid official page][mermaid]. diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md index 5b04d7d88b8..a0bf31c526f 100644 --- a/doc/user/project/integrations/services_templates.md +++ b/doc/user/project/integrations/services_templates.md @@ -1,8 +1,10 @@ # Services templates A GitLab administrator can add a service template that sets a default for each -project. After a service template is enabled, it will be applied to new -projects only and its details will be pre-filled on the project's Service page. +project. After a service template is enabled, it will be applied to **all** +projects that don't have it already enabled and its details will be pre-filled +on the project's Service page. By disabling the template, it will be disabled +for new projects only. ## Enable a service template diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 22ef11e4049..06b3779668b 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -12,9 +12,11 @@ to cherry-pick the changes introduced by that merge request. ![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png) -After you click that button, a modal will appear where you can choose to -cherry-pick the changes directly into the selected branch or you can opt to -create a new merge request with the cherry-pick changes +After you click that button, a modal will appear showing a [branch filter search box](../repository/branches/index.md#branch-filter-search-box) +where you can choose to either: + +- Cherry-pick the changes directly into the selected branch. +- Create a new merge request with the cherry-picked changes. ## Cherry-picking a Commit diff --git a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png Binary files differindex ac766c99935..3b3bf88df31 100644 --- a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png +++ b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 43ca498d006..f9ebf277125 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -205,9 +205,9 @@ have been marked as a **Work In Progress**. ## Merge request diff file navigation -The diff view has a persistent dropdown for file navigation. As you scroll through -diffs with a large number of files and/or many changes in those files, you can -easily jump to any changed file through the dropdown navigation. +The diff view has a file tree for file navigation. As you scroll through +diffs with a large number of files, you can easily jump to any changed file +using the file tree. ![Merge request diff file navigation](img/merge_request_diff_file_navigation.png) diff --git a/doc/user/project/repository/branches/img/branch_filter_search_box.png b/doc/user/project/repository/branches/img/branch_filter_search_box.png Binary files differnew file mode 100644 index 00000000000..c4364ef39f4 --- /dev/null +++ b/doc/user/project/repository/branches/img/branch_filter_search_box.png diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index 19417d91fec..e1d8345f415 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -6,6 +6,7 @@ Read through GiLab's branching documentation: - [Default branch](#default-branch) - [Protected branches](../../protected_branches.md#protected-branches) - [Delete merged branches](#delete-merged-branches) +- [Branch filter search box](#branch-filter-search-box) See also: @@ -40,5 +41,22 @@ this operation. It's particularly useful to clean up old branches that were not deleted automatically when a merge request was merged. + +## Branch filter search box + +> [Introduced][https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166] in GitLab 11.5. + +![Branch filter search box](img/branch_filter_search_box.png) + +This feature allows you to search and select branches quickly. Search results appear in the following order: + +- Branches with names that matched search terms exactly. +- Other branches with names that include search terms, sorted alphabetically. + +Sometimes when you have hundreds of branches you may want a more flexible matching pattern. In such cases you can use the following: + +- `^feature` will only match branch names that begin with 'feature'. +- `feature$` will only match branch names that end with 'feature'. + [ce-6449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449 "Add button to delete all merged branches" [protected]: ../../protected_branches.md diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index 4d016277824..ce79bfc0a03 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -85,12 +85,13 @@ You can live preview changes submitted to a new branch with With [GitLab Starter](https://about.gitlab.com/pricing/), you can also request [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers. -To create, delete, and [branches](branches/index.md) via GitLab's UI: +To create, delete, and view [branches](branches/index.md) via GitLab's UI: - [Default branches](branches/index.md#default-branch) - [Create a branch](web_editor.md#create-a-new-branch) - [Protected branches](../protected_branches.md#protected-branches) - [Delete merged branches](branches/index.md#delete-merged-branches) +- [Branch filter search box](branches/index.md#branch-filter-search-box) Alternatively, you can use the [command line](../../../gitlab-basics/start-using-git.md#create-a-branch). @@ -169,7 +170,7 @@ vendored code, and most markup languages are excluded. ## Compare -Select branches to compare and view the changes inline: +Select branches to compare using the [branch filter search box](branches/index.md#branch-filter-search-box), then click the **Compare** button to view the changes inline: ![compare branches](img/compare_branches.png) diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb index 6eddc5e5b61..da756daadcc 100644 --- a/lib/api/circuit_breakers.rb +++ b/lib/api/circuit_breakers.rb @@ -13,37 +13,24 @@ module API end resource ':type' do namespace '', requirements: { type: 'repository_storage' } do - helpers do - def failing_storage_health - @failing_storage_health ||= Gitlab::Git::Storage::Health.for_failing_storages - end - - def storage_health - @storage_health ||= Gitlab::Git::Storage::Health.for_all_storages - end - end - desc 'Get all git storages' do detail 'This feature was introduced in GitLab 9.5' - success Entities::RepositoryStorageHealth end get do - present storage_health, with: Entities::RepositoryStorageHealth + present [] end desc 'Get all failing git storages' do detail 'This feature was introduced in GitLab 9.5' - success Entities::RepositoryStorageHealth end get 'failing' do - present failing_storage_health, with: Entities::RepositoryStorageHealth + present [] end desc 'Reset all storage failures and open circuitbreaker' do detail 'This feature was introduced in GitLab 9.5' end delete do - Gitlab::Git::Storage::FailureInfo.reset_all! end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 120545792f2..5a4b85f98cf 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1364,12 +1364,6 @@ module API expose :submitted, as: :akismet_submitted end - class RepositoryStorageHealth < Grape::Entity - expose :storage_name - expose :failing_on_hosts - expose :total_failures - end - class CustomAttribute < Grape::Entity expose :key expose :value diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb index 91175b49c79..15cdd25e711 100644 --- a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb +++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators' module Rails diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb index f95e423ef22..7b238623418 100644 --- a/lib/gitaly/server.rb +++ b/lib/gitaly/server.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitaly class Server def self.all diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml index 5f9d54ff574..bf7831b937c 100644 --- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml @@ -2,33 +2,33 @@ image: openjdk:8-jdk variables: - ANDROID_COMPILE_SDK: "25" - ANDROID_BUILD_TOOLS: "24.0.0" - ANDROID_SDK_TOOLS: "24.4.1" + ANDROID_COMPILE_SDK: "28" + ANDROID_BUILD_TOOLS: "28.0.3" + ANDROID_SDK_TOOLS: "26.1.1" before_script: - - apt-get --quiet update --yes - - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 - - wget --quiet --output-document=android-sdk.tgz https://dl.google.com/android/android-sdk_r${ANDROID_SDK_TOOLS}-linux.tgz - - tar --extract --gzip --file=android-sdk.tgz - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_COMPILE_SDK} - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter platform-tools - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS} - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-android-m2repository - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-google_play_services - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-m2repository - - export ANDROID_HOME=$PWD/android-sdk-linux - - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ - - chmod +x ./gradlew +- apt-get --quiet update --yes +- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 +- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip +- unzip android-sdk.zip -d android-sdk-linux +- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null +- echo y | android-sdk-linux/tools/bin/sdkmanager platform-tools > /dev/null +- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" > /dev/null +- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;google_play_services" > /dev/null +- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;m2repository" > /dev/null +- export ANDROID_HOME=$PWD/android-sdk-linux +- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ +- yes | android-sdk-linux/tools/bin/sdkmanager --licenses & +- chmod +x ./gradlew stages: - - build - - test +- build +- test build: stage: build script: - - ./gradlew assembleDebug + - ./gradlew assembleDebug artifacts: paths: - app/build/outputs/ @@ -36,7 +36,7 @@ build: unitTests: stage: test script: - - ./gradlew test + - ./gradlew test functionalTests: stage: test diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 72547c1b407..6fa59e41d20 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -126,8 +126,8 @@ license_management: artifacts: paths: [gl-license-management-report.json] only: - - branches - only: + refs: + - branches variables: - $GITLAB_FEATURES =~ /\blicense_management\b/ except: diff --git a/lib/gitlab/ci/templates/Python.gitlab-ci.yml b/lib/gitlab/ci/templates/Python.gitlab-ci.yml index 2e0589de652..098abe4daf5 100644 --- a/lib/gitlab/ci/templates/Python.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Python.gitlab-ci.yml @@ -5,7 +5,7 @@ image: python:latest # Change pip's cache directory to be inside the project directory since we can # only cache local items. variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache" + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" # Pip's cache doesn't store the python packages # https://pip.pypa.io/en/stable/reference/pip_install/#caching diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 7732049b69b..4218e812146 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -96,10 +96,6 @@ module Gitlab raise Gitlab::Git::CommandError.new(e.message) end - def circuit_breaker - @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage) - end - def exists? gitaly_repository_client.exists? end diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb deleted file mode 100644 index 5933312b0b5..00000000000 --- a/lib/gitlab/git/storage.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Gitlab - module Git - module Storage - class Inaccessible < StandardError - attr_reader :retry_after - - def initialize(message = nil, retry_after = nil) - super(message) - @retry_after = retry_after - end - end - - CircuitOpen = Class.new(Inaccessible) - Misconfiguration = Class.new(Inaccessible) - Failing = Class.new(Inaccessible) - - REDIS_KEY_PREFIX = 'storage_accessible:'.freeze - REDIS_KNOWN_KEYS = "#{REDIS_KEY_PREFIX}known_keys_set".freeze - - def self.redis - Gitlab::Redis::SharedState - end - end - end -end diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb deleted file mode 100644 index 391f0d70583..00000000000 --- a/lib/gitlab/git/storage/checker.rb +++ /dev/null @@ -1,120 +0,0 @@ -module Gitlab - module Git - module Storage - class Checker - include CircuitBreakerSettings - - attr_reader :storage_path, :storage, :hostname, :logger - METRICS_MUTEX = Mutex.new - STORAGE_TIMING_BUCKETS = [0.1, 0.15, 0.25, 0.33, 0.5, 1, 1.5, 2.5, 5, 10, 15].freeze - - def self.check_all(logger = Rails.logger) - threads = Gitlab.config.repositories.storages.keys.map do |storage_name| - Thread.new do - Thread.current[:result] = new(storage_name, logger).check_with_lease - end - end - - threads.map do |thread| - thread.join - thread[:result] - end - end - - def self.check_histogram - @check_histogram ||= - METRICS_MUTEX.synchronize do - @check_histogram || Gitlab::Metrics.histogram(:circuitbreaker_storage_check_duration_seconds, - 'Storage check time in seconds', - {}, - STORAGE_TIMING_BUCKETS - ) - end - end - - def initialize(storage, logger = Rails.logger) - @storage = storage - config = Gitlab.config.repositories.storages[@storage] - @storage_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { config.legacy_disk_path } - @logger = logger - - @hostname = Gitlab::Environment.hostname - end - - def check_with_lease - lease_key = "storage_check:#{cache_key}" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: storage_timeout) - result = { storage: storage, success: nil } - - if uuid = lease.try_obtain - result[:success] = check - - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - else - logger.warn("#{hostname}: #{storage}: Skipping check, previous check still running") - end - - result - end - - def check - if perform_access_check - track_storage_accessible - true - else - track_storage_inaccessible - logger.error("#{hostname}: #{storage}: Not accessible.") - false - end - end - - private - - def perform_access_check - start_time = Gitlab::Metrics::System.monotonic_time - - Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries) - ensure - execution_time = Gitlab::Metrics::System.monotonic_time - start_time - self.class.check_histogram.observe({ storage: storage }, execution_time) - end - - def track_storage_inaccessible - first_failure = current_failure_info.first_failure || Time.now - last_failure = Time.now - - Gitlab::Git::Storage.redis.with do |redis| - redis.pipelined do - redis.hset(cache_key, :first_failure, first_failure.to_i) - redis.hset(cache_key, :last_failure, last_failure.to_i) - redis.hincrby(cache_key, :failure_count, 1) - redis.expire(cache_key, failure_reset_time) - maintain_known_keys(redis) - end - end - end - - def track_storage_accessible - Gitlab::Git::Storage.redis.with do |redis| - redis.pipelined do - redis.hset(cache_key, :first_failure, nil) - redis.hset(cache_key, :last_failure, nil) - redis.hset(cache_key, :failure_count, 0) - maintain_known_keys(redis) - end - end - end - - def maintain_known_keys(redis) - expire_time = Time.now.to_i + failure_reset_time - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, expire_time, cache_key) - redis.zremrangebyscore(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, '-inf', Time.now.to_i) - end - - def current_failure_info - FailureInfo.load(cache_key) - end - end - end - end -end diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb deleted file mode 100644 index fcee9ae566c..00000000000 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ /dev/null @@ -1,78 +0,0 @@ -module Gitlab - module Git - module Storage - class CircuitBreaker - include CircuitBreakerSettings - - attr_reader :storage, - :hostname - - delegate :last_failure, :failure_count, :no_failures?, - to: :failure_info - - def self.for_storage(storage) - cached_circuitbreakers = Gitlab::SafeRequestStore.fetch(:circuitbreaker_cache) do - Hash.new do |hash, storage_name| - hash[storage_name] = build(storage_name) - end - end - - cached_circuitbreakers[storage] - end - - def self.build(storage, hostname = Gitlab::Environment.hostname) - config = Gitlab.config.repositories.storages[storage] - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - if !config.present? - NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured")) - elsif !config.legacy_disk_path.present? - NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured")) - else - new(storage, hostname) - end - end - end - - def initialize(storage, hostname) - @storage = storage - @hostname = hostname - end - - def perform - return yield unless enabled? - - check_storage_accessible! - - yield - end - - def circuit_broken? - return false if no_failures? - - failure_count > failure_count_threshold - end - - private - - # The circuitbreaker can be enabled for the entire fleet using a Feature - # flag. - # - # Enabling it for a single host can be done setting the - # `GIT_STORAGE_CIRCUIT_BREAKER` environment variable. - def enabled? - ENV['GIT_STORAGE_CIRCUIT_BREAKER'].present? || Feature.enabled?('git_storage_circuit_breaker') - end - - def failure_info - @failure_info ||= FailureInfo.load(cache_key) - end - - def check_storage_accessible! - if circuit_broken? - raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_reset_time) - end - end - end - end - end -end diff --git a/lib/gitlab/git/storage/circuit_breaker_settings.rb b/lib/gitlab/git/storage/circuit_breaker_settings.rb deleted file mode 100644 index c9e225f187d..00000000000 --- a/lib/gitlab/git/storage/circuit_breaker_settings.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Gitlab - module Git - module Storage - module CircuitBreakerSettings - def failure_count_threshold - application_settings.circuitbreaker_failure_count_threshold - end - - def failure_reset_time - application_settings.circuitbreaker_failure_reset_time - end - - def storage_timeout - application_settings.circuitbreaker_storage_timeout - end - - def access_retries - application_settings.circuitbreaker_access_retries - end - - def check_interval - application_settings.circuitbreaker_check_interval - end - - def cache_key - @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" - end - - private - - def application_settings - Gitlab::CurrentSettings.current_application_settings - end - end - end - end -end diff --git a/lib/gitlab/git/storage/failure_info.rb b/lib/gitlab/git/storage/failure_info.rb deleted file mode 100644 index 1d28a850049..00000000000 --- a/lib/gitlab/git/storage/failure_info.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Gitlab - module Git - module Storage - class FailureInfo - attr_accessor :first_failure, :last_failure, :failure_count - - def self.reset_all! - Gitlab::Git::Storage.redis.with do |redis| - all_storage_keys = redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) - redis.del(*all_storage_keys) unless all_storage_keys.empty? - end - - Gitlab::SafeRequestStore.delete(:circuitbreaker_cache) - end - - def self.load(cache_key) - first_failure, last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis| - redis.hmget(cache_key, :first_failure, :last_failure, :failure_count) - end - - last_failure = Time.at(last_failure.to_i) if last_failure.present? - first_failure = Time.at(first_failure.to_i) if first_failure.present? - - new(first_failure, last_failure, failure_count.to_i) - end - - def initialize(first_failure, last_failure, failure_count) - @first_failure = first_failure - @last_failure = last_failure - @failure_count = failure_count - end - - def no_failures? - first_failure.blank? && last_failure.blank? && failure_count == 0 - end - end - end - end -end diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb deleted file mode 100644 index 0a4e557b59b..00000000000 --- a/lib/gitlab/git/storage/forked_storage_check.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Gitlab - module Git - module Storage - module ForkedStorageCheck - extend self - - def storage_available?(path, timeout_seconds = 5, retries = 1) - partial_timeout = timeout_seconds / retries - status = timeout_check(path, partial_timeout) - - # If the status check did not succeed the first time, we retry a few - # more times to avoid one-off failures - current_attempts = 1 - while current_attempts < retries && !status.success? - status = timeout_check(path, partial_timeout) - current_attempts += 1 - end - - status.success? - end - - def timeout_check(path, timeout_seconds) - filesystem_check_pid = check_filesystem_in_process(path) - - deadline = timeout_seconds.seconds.from_now.utc - wait_time = 0.01 - status = nil - - while status.nil? - - if deadline > Time.now.utc - sleep(wait_time) - _pid, status = Process.wait2(filesystem_check_pid, Process::WNOHANG) - else - Process.kill('KILL', filesystem_check_pid) - # Blocking wait, so we are sure the process is gone before continuing - _pid, status = Process.wait2(filesystem_check_pid) - end - end - - status - end - - # This will spawn a new 2 processes to do the check: - # The outer child (waiter) will spawn another child process (stater). - # - # The stater is the process is performing the actual filesystem check - # the check might hang if the filesystem is acting up. - # In this case we will send a `KILL` to the waiter, which will still - # be responsive while the stater is hanging. - def check_filesystem_in_process(path) - spawn('ruby', '-e', ruby_check, path, [:out, :err] => '/dev/null') - end - - def ruby_check - <<~RUBY_FILESYSTEM_CHECK - inner_pid = fork { File.stat(ARGV.first) } - Process.waitpid(inner_pid) - exit $?.exitstatus - RUBY_FILESYSTEM_CHECK - end - end - end - end -end diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb deleted file mode 100644 index 8e14acb4ccb..00000000000 --- a/lib/gitlab/git/storage/health.rb +++ /dev/null @@ -1,92 +0,0 @@ -module Gitlab - module Git - module Storage - class Health - attr_reader :storage_name, :info - - def self.prefix_for_storage(storage_name) - "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage_name}:" - end - - def self.for_all_storages - storage_names = Gitlab.config.repositories.storages.keys - results_per_storage = nil - - Gitlab::Git::Storage.redis.with do |redis| - keys_per_storage = all_keys_for_storages(storage_names, redis) - results_per_storage = load_for_keys(keys_per_storage, redis) - end - - results_per_storage.map do |name, info| - info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } - new(name, info) - end - end - - private_class_method def self.all_keys_for_storages(storage_names, redis) - keys_per_storage = {} - all_keys = redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) - - storage_names.each do |storage_name| - prefix = prefix_for_storage(storage_name) - - keys_per_storage[storage_name] = all_keys.select { |key| key.starts_with?(prefix) } - end - - keys_per_storage - end - - private_class_method def self.load_for_keys(keys_per_storage, redis) - info_for_keys = {} - - redis.pipelined do - keys_per_storage.each do |storage_name, keys_future| - info_for_storage = keys_future.map do |key| - { name: key, failure_count: redis.hget(key, :failure_count) } - end - - info_for_keys[storage_name] = info_for_storage - end - end - - info_for_keys - end - - def self.for_failing_storages - for_all_storages.select(&:failing?) - end - - def initialize(storage_name, info) - @storage_name = storage_name - @info = info - end - - def failing_info - @failing_info ||= info.select { |info_for_host| info_for_host[:failure_count] > 0 } - end - - def failing? - failing_info.any? - end - - def failing_on_hosts - @failing_on_hosts ||= failing_info.map do |info_for_host| - info_for_host[:name].split(':').last - end - end - - def failing_circuit_breakers - @failing_circuit_breakers ||= failing_on_hosts.map do |hostname| - CircuitBreaker.build(storage_name, hostname) - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def total_failures - @total_failures ||= failing_info.sum { |info_for_host| info_for_host[:failure_count] } - end - # rubocop: enable CodeReuse/ActiveRecord - end - end - end -end diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb deleted file mode 100644 index 261c936c689..00000000000 --- a/lib/gitlab/git/storage/null_circuit_breaker.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Gitlab - module Git - module Storage - class NullCircuitBreaker - include CircuitBreakerSettings - - # These will have actual values - attr_reader :storage, - :hostname - - # These will always have nil values - attr_reader :storage_path - - delegate :last_failure, :failure_count, :no_failures?, - to: :failure_info - - def initialize(storage, hostname, error: nil) - @storage = storage - @hostname = hostname - @error = error - end - - def perform - @error ? raise(@error) : yield - end - - def circuit_broken? - !!@error - end - - def backing_off? - false - end - - def failure_info - @failure_info ||= - if circuit_broken? - Gitlab::Git::Storage::FailureInfo.new(Time.now, - Time.now, - failure_count_threshold) - else - Gitlab::Git::Storage::FailureInfo.new(nil, - nil, - 0) - end - end - end - end - end -end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index deaa14c8434..c1726659a90 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -30,5 +30,20 @@ module Gitlab gon.current_user_avatar_url = current_user.avatar_url end end + + # Exposes the state of a feature flag to the frontend code. + # + # name - The name of the feature flag, e.g. `my_feature`. + # args - Any additional arguments to pass to `Feature.enabled?`. This allows + # you to check if a flag is enabled for a particular user. + def push_frontend_feature_flag(name, *args) + var_name = name.to_s.camelize(:lower) + enabled = Feature.enabled?(name, *args) + + # Here the `true` argument signals gon that the value should be merged + # into any existing ones, instead of overwriting them. This allows you to + # use this method to push multiple feature flags. + gon.push({ features: { var_name => enabled } }, true) + end end end diff --git a/lib/gitlab/storage_check.rb b/lib/gitlab/storage_check.rb deleted file mode 100644 index fe81513c9ec..00000000000 --- a/lib/gitlab/storage_check.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'storage_check/cli' -require_relative 'storage_check/gitlab_caller' -require_relative 'storage_check/option_parser' -require_relative 'storage_check/response' - -module Gitlab - module StorageCheck - ENDPOINT = '/-/storage_check'.freeze - Options = Struct.new(:target, :token, :interval, :dryrun) - end -end diff --git a/lib/gitlab/storage_check/cli.rb b/lib/gitlab/storage_check/cli.rb deleted file mode 100644 index 9b64c8e033a..00000000000 --- a/lib/gitlab/storage_check/cli.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Gitlab - module StorageCheck - class CLI - def self.start!(args) - runner = new(Gitlab::StorageCheck::OptionParser.parse!(args)) - runner.start_loop - end - - attr_reader :logger, :options - - def initialize(options) - @options = options - @logger = Logger.new(STDOUT) - end - - def start_loop - logger.info "Checking #{options.target} every #{options.interval} seconds" - - if options.dryrun - logger.info "Dryrun, exiting..." - return - end - - begin - loop do - response = GitlabCaller.new(options).call! - log_response(response) - update_settings(response) - - sleep options.interval - end - rescue Interrupt - logger.info "Ending storage-check" - end - end - - def update_settings(response) - previous_interval = options.interval - - if response.valid? - options.interval = response.check_interval || previous_interval - end - - if previous_interval != options.interval - logger.info "Interval changed: #{options.interval} seconds" - end - end - - def log_response(response) - unless response.valid? - return logger.error("Invalid response checking nfs storage: #{response.http_response.inspect}") - end - - if response.responsive_shards.any? - logger.debug("Responsive shards: #{response.responsive_shards.join(', ')}") - end - - warnings = [] - if response.skipped_shards.any? - warnings << "Skipped shards: #{response.skipped_shards.join(', ')}" - end - - if response.failing_shards.any? - warnings << "Failing shards: #{response.failing_shards.join(', ')}" - end - - logger.warn(warnings.join(' - ')) if warnings.any? - end - end - end -end diff --git a/lib/gitlab/storage_check/gitlab_caller.rb b/lib/gitlab/storage_check/gitlab_caller.rb deleted file mode 100644 index 44952b68844..00000000000 --- a/lib/gitlab/storage_check/gitlab_caller.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'excon' - -module Gitlab - module StorageCheck - class GitlabCaller - def initialize(options) - @options = options - end - - def call! - Gitlab::StorageCheck::Response.new(get_response) - rescue Errno::ECONNREFUSED, Excon::Error - # Server not ready, treated as invalid response. - Gitlab::StorageCheck::Response.new(nil) - end - - def get_response - scheme, *other_parts = URI.split(@options.target) - socket_path = if scheme == 'unix' - other_parts.compact.join - end - - connection = Excon.new(@options.target, socket: socket_path) - connection.post(path: Gitlab::StorageCheck::ENDPOINT, - headers: headers) - end - - def headers - @headers ||= begin - headers = {} - headers['Content-Type'] = headers['Accept'] = 'application/json' - headers['TOKEN'] = @options.token if @options.token - - headers - end - end - end - end -end diff --git a/lib/gitlab/storage_check/option_parser.rb b/lib/gitlab/storage_check/option_parser.rb deleted file mode 100644 index 66ed7906f97..00000000000 --- a/lib/gitlab/storage_check/option_parser.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Gitlab - module StorageCheck - class OptionParser - def self.parse!(args) - # Start out with some defaults - options = Gitlab::StorageCheck::Options.new(nil, nil, 1, false) - - parser = ::OptionParser.new do |opts| - opts.banner = "Usage: bin/storage_check [options]" - - opts.on('-t=string', '--target string', 'URL or socket to trigger storage check') do |value| - options.target = value - end - - opts.on('-T=string', '--token string', 'Health token to use') { |value| options.token = value } - - opts.on('-i=n', '--interval n', ::OptionParser::DecimalInteger, 'Seconds between checks') do |value| - options.interval = value - end - - opts.on('-d', '--dryrun', "Output what will be performed, but don't start the process") do |value| - options.dryrun = value - end - end - parser.parse!(args) - - unless options.target - raise ::OptionParser::InvalidArgument.new('Provide a URI to provide checks') - end - - if URI.parse(options.target).scheme.nil? - raise ::OptionParser::InvalidArgument.new('Add the scheme to the target, `unix://`, `https://` or `http://` are supported') - end - - options - end - end - end -end diff --git a/lib/gitlab/storage_check/response.rb b/lib/gitlab/storage_check/response.rb deleted file mode 100644 index 326ab236e3e..00000000000 --- a/lib/gitlab/storage_check/response.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'json' - -module Gitlab - module StorageCheck - class Response - attr_reader :http_response - - def initialize(http_response) - @http_response = http_response - end - - def valid? - @http_response && (200...299).cover?(@http_response.status) && - @http_response.headers['Content-Type'].include?('application/json') && - parsed_response - end - - def check_interval - return nil unless parsed_response - - parsed_response['check_interval'] - end - - def responsive_shards - divided_results[:responsive_shards] - end - - def skipped_shards - divided_results[:skipped_shards] - end - - def failing_shards - divided_results[:failing_shards] - end - - private - - def results - return [] unless parsed_response - - parsed_response['results'] - end - - def divided_results - return @divided_results if @divided_results - - @divided_results = {} - @divided_results[:responsive_shards] = [] - @divided_results[:skipped_shards] = [] - @divided_results[:failing_shards] = [] - - results.each do |info| - name = info['storage'] - - case info['success'] - when true - @divided_results[:responsive_shards] << name - when false - @divided_results[:failing_shards] << name - else - @divided_results[:skipped_shards] << name - end - end - - @divided_results - end - - def parsed_response - return @parsed_response if defined?(@parsed_response) - - @parsed_response = JSON.parse(@http_response.body) - rescue JSON::JSONError - @parsed_response = nil - end - end - end -end diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb index 1aeaa387a49..e724e58e9ca 100644 --- a/lib/google_api/auth.rb +++ b/lib/google_api/auth.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module GoogleApi class Auth attr_reader :access_token, :redirect_uri, :state diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb index 77b6610286f..e74ff6a9129 100644 --- a/lib/google_api/cloud_platform/client.rb +++ b/lib/google_api/cloud_platform/client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'google/apis/compute_v1' require 'google/apis/container_v1' require 'google/apis/cloudbilling_v1' diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb index adbed20f152..5ecd6169ecf 100644 --- a/lib/haml_lint/inline_javascript.rb +++ b/lib/haml_lint/inline_javascript.rb @@ -1,4 +1,7 @@ -unless Rails.env.production? # rubocop:disable Naming/FileName +# rubocop:disable Naming/FileName +# frozen_string_literal: true + +unless Rails.env.production? require 'haml_lint/haml_visitor' require 'haml_lint/linter' require 'haml_lint/linter_registry' diff --git a/lib/json_web_token/rsa_token.rb b/lib/json_web_token/rsa_token.rb index d6d6af7089c..160e1e506f1 100644 --- a/lib/json_web_token/rsa_token.rb +++ b/lib/json_web_token/rsa_token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module JSONWebToken class RSAToken < Token attr_reader :key_file diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb index 5b67715b0b2..ce5d6f248d0 100644 --- a/lib/json_web_token/token.rb +++ b/lib/json_web_token/token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module JSONWebToken class Token attr_accessor :issuer, :subject, :audience, :id diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb index d80cd7d2a4e..293d0c563c5 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mattermost ClientError = Class.new(Mattermost::Error) diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 704813dfdf0..a02745486d6 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mattermost class Command < Client def create(params) diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb index dee6deb7974..054bd5457bd 100644 --- a/lib/mattermost/error.rb +++ b/lib/mattermost/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mattermost Error = Class.new(StandardError) end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 2aa7a2f64d8..e2083848a8d 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mattermost class NoSessionError < Mattermost::Error def message diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index 95c2f6f9d6b..58120178f50 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mattermost class Team < Client # Returns all teams that the current user is a member of diff --git a/lib/microsoft_teams/activity.rb b/lib/microsoft_teams/activity.rb index d2c420efdaf..207e90d2638 100644 --- a/lib/microsoft_teams/activity.rb +++ b/lib/microsoft_teams/activity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MicrosoftTeams class Activity def initialize(title:, subtitle:, text:, image:) diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb index 226ee1373db..c7dec09ba6b 100644 --- a/lib/microsoft_teams/notifier.rb +++ b/lib/microsoft_teams/notifier.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MicrosoftTeams class Notifier def initialize(webhook) diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index 97f56e10ccf..fd26663fef0 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ObjectStorage # # The DirectUpload c;ass generates a set of presigned URLs diff --git a/lib/omni_auth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb index ce1bdfe6ee4..6c914b4222a 100644 --- a/lib/omni_auth/strategies/bitbucket.rb +++ b/lib/omni_auth/strategies/bitbucket.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'omniauth-oauth2' module OmniAuth diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb index ebdb5c7faf0..a792903fde7 100644 --- a/lib/omni_auth/strategies/jwt.rb +++ b/lib/omni_auth/strategies/jwt.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'omniauth' require 'jwt' diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb index 9beb442bfa3..581cc6a37b4 100644 --- a/lib/peek/rblineprof/custom_controller_helpers.rb +++ b/lib/peek/rblineprof/custom_controller_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Peek module Rblineprof module CustomControllerHelpers @@ -41,7 +43,7 @@ module Peek ] end.sort_by{ |a,b,c,d,e,f| -f } - output = "<div class='modal-dialog modal-xl'><div class='modal-content'>" + output = ["<div class='modal-dialog modal-xl'><div class='modal-content'>"] output << "<div class='modal-header'>" output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>" output << "<button class='close' type='button' data-dismiss='modal' aria-label='close'><span aria-hidden='true'>×</span></button>" @@ -93,7 +95,7 @@ module Peek output << "</div></div></div>" - response.body += "<div class='modal' id='modal-peek-line-profile' tabindex=-1>#{output}</div>".html_safe + response.body += "<div class='modal' id='modal-peek-line-profile' tabindex=-1>#{output.join}</div>".html_safe end ret diff --git a/lib/peek/views/gitaly.rb b/lib/peek/views/gitaly.rb index ab35f7a2258..860963ef94f 100644 --- a/lib/peek/views/gitaly.rb +++ b/lib/peek/views/gitaly.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Peek module Views class Gitaly < View diff --git a/lib/peek/views/host.rb b/lib/peek/views/host.rb index 4fc70fbcebc..b77355ea11b 100644 --- a/lib/peek/views/host.rb +++ b/lib/peek/views/host.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Peek module Views class Host < View diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index e877ab10248..e2a7d3ef5ba 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rouge module Formatters class HTMLGitlab < Rouge::Formatters::HTML diff --git a/lib/rouge/plugins/common_mark.rb b/lib/rouge/plugins/common_mark.rb index 8f9de061124..d240df5a0e0 100644 --- a/lib/rouge/plugins/common_mark.rb +++ b/lib/rouge/plugins/common_mark.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # A rouge plugin for CommonMark markdown engine. # Used to highlight code generated by CommonMark. diff --git a/lib/rspec_flaky/config.rb b/lib/rspec_flaky/config.rb index 06e96f969f1..55c1d4747b4 100644 --- a/lib/rspec_flaky/config.rb +++ b/lib/rspec_flaky/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RspecFlaky class Config def self.generate_report? diff --git a/lib/rspec_flaky/example.rb b/lib/rspec_flaky/example.rb index b6e790cbbab..3c1b05257a0 100644 --- a/lib/rspec_flaky/example.rb +++ b/lib/rspec_flaky/example.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RspecFlaky # This is a wrapper class for RSpec::Core::Example class Example diff --git a/lib/rspec_flaky/flaky_example.rb b/lib/rspec_flaky/flaky_example.rb index 6be24014d89..da5dbf06bc9 100644 --- a/lib/rspec_flaky/flaky_example.rb +++ b/lib/rspec_flaky/flaky_example.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RspecFlaky # This represents a flaky RSpec example and is mainly meant to be saved in a JSON file class FlakyExample < OpenStruct diff --git a/lib/rspec_flaky/flaky_examples_collection.rb b/lib/rspec_flaky/flaky_examples_collection.rb index dea23c325be..290a51766e9 100644 --- a/lib/rspec_flaky/flaky_examples_collection.rb +++ b/lib/rspec_flaky/flaky_examples_collection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/hash_with_indifferent_access' require_relative 'flaky_example' diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb index 9cd0c38cb55..19cc0baa2d3 100644 --- a/lib/rspec_flaky/listener.rb +++ b/lib/rspec_flaky/listener.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' require_dependency 'rspec_flaky/config' diff --git a/lib/rspec_flaky/report.rb b/lib/rspec_flaky/report.rb index 1c362fdd20d..9a0fb88c424 100644 --- a/lib/rspec_flaky/report.rb +++ b/lib/rspec_flaky/report.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' require 'time' diff --git a/lib/system_check/app/active_users_check.rb b/lib/system_check/app/active_users_check.rb index 1d72c8d6903..8446c2fc2c8 100644 --- a/lib/system_check/app/active_users_check.rb +++ b/lib/system_check/app/active_users_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class ActiveUsersCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/database_config_exists_check.rb b/lib/system_check/app/database_config_exists_check.rb index d1fae192350..1769145ed63 100644 --- a/lib/system_check/app/database_config_exists_check.rb +++ b/lib/system_check/app/database_config_exists_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class DatabaseConfigExistsCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/git_config_check.rb b/lib/system_check/app/git_config_check.rb index d08a81639e3..4e8d607096c 100644 --- a/lib/system_check/app/git_config_check.rb +++ b/lib/system_check/app/git_config_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class GitConfigCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb index ad41760dff2..6cd53779bfd 100644 --- a/lib/system_check/app/git_user_default_ssh_config_check.rb +++ b/lib/system_check/app/git_user_default_ssh_config_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class GitUserDefaultSSHConfigCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb index 44ec888c197..994af3ab53e 100644 --- a/lib/system_check/app/git_version_check.rb +++ b/lib/system_check/app/git_version_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class GitVersionCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/gitlab_config_exists_check.rb b/lib/system_check/app/gitlab_config_exists_check.rb index 247aa0994e4..1cc5ead0d89 100644 --- a/lib/system_check/app/gitlab_config_exists_check.rb +++ b/lib/system_check/app/gitlab_config_exists_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class GitlabConfigExistsCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/gitlab_config_up_to_date_check.rb b/lib/system_check/app/gitlab_config_up_to_date_check.rb index c609e48e133..58c7e3039c8 100644 --- a/lib/system_check/app/gitlab_config_up_to_date_check.rb +++ b/lib/system_check/app/gitlab_config_up_to_date_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class GitlabConfigUpToDateCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/init_script_exists_check.rb b/lib/system_check/app/init_script_exists_check.rb index d246e058e86..d36dbe7d67d 100644 --- a/lib/system_check/app/init_script_exists_check.rb +++ b/lib/system_check/app/init_script_exists_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class InitScriptExistsCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb index 53a47eb0f42..569c41df6e4 100644 --- a/lib/system_check/app/init_script_up_to_date_check.rb +++ b/lib/system_check/app/init_script_up_to_date_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class InitScriptUpToDateCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/log_writable_check.rb b/lib/system_check/app/log_writable_check.rb index 3e0c436d6ee..e26ad143eb8 100644 --- a/lib/system_check/app/log_writable_check.rb +++ b/lib/system_check/app/log_writable_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class LogWritableCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/migrations_are_up_check.rb b/lib/system_check/app/migrations_are_up_check.rb index 5eedbacce77..b12e9ac6bba 100644 --- a/lib/system_check/app/migrations_are_up_check.rb +++ b/lib/system_check/app/migrations_are_up_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class MigrationsAreUpCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/orphaned_group_members_check.rb b/lib/system_check/app/orphaned_group_members_check.rb index 2b46d36fe51..3e6ffb8190b 100644 --- a/lib/system_check/app/orphaned_group_members_check.rb +++ b/lib/system_check/app/orphaned_group_members_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class OrphanedGroupMembersCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/projects_have_namespace_check.rb b/lib/system_check/app/projects_have_namespace_check.rb index a6ec9f7665c..2bf2529acf1 100644 --- a/lib/system_check/app/projects_have_namespace_check.rb +++ b/lib/system_check/app/projects_have_namespace_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class ProjectsHaveNamespaceCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb index a0610e73576..890f8b44d13 100644 --- a/lib/system_check/app/redis_version_check.rb +++ b/lib/system_check/app/redis_version_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class RedisVersionCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb index 57bbabece1f..d73c39f2c3f 100644 --- a/lib/system_check/app/ruby_version_check.rb +++ b/lib/system_check/app/ruby_version_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class RubyVersionCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/tmp_writable_check.rb b/lib/system_check/app/tmp_writable_check.rb index 99a75e57abf..6687df091d3 100644 --- a/lib/system_check/app/tmp_writable_check.rb +++ b/lib/system_check/app/tmp_writable_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class TmpWritableCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/uploads_directory_exists_check.rb b/lib/system_check/app/uploads_directory_exists_check.rb index 7026d0ba075..940eff9d4cf 100644 --- a/lib/system_check/app/uploads_directory_exists_check.rb +++ b/lib/system_check/app/uploads_directory_exists_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class UploadsDirectoryExistsCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/uploads_path_permission_check.rb b/lib/system_check/app/uploads_path_permission_check.rb index 7df6c060254..4a49f3bc2bb 100644 --- a/lib/system_check/app/uploads_path_permission_check.rb +++ b/lib/system_check/app/uploads_path_permission_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class UploadsPathPermissionCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/app/uploads_path_tmp_permission_check.rb b/lib/system_check/app/uploads_path_tmp_permission_check.rb index b276a81eac1..ae374f4707c 100644 --- a/lib/system_check/app/uploads_path_tmp_permission_check.rb +++ b/lib/system_check/app/uploads_path_tmp_permission_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module App class UploadsPathTmpPermissionCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb index 0f5742dd67f..e06245294c4 100644 --- a/lib/system_check/base_check.rb +++ b/lib/system_check/base_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck # Base class for Checks. You must inherit from here # and implement the methods below when necessary diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb index 6227e461d24..07d479848fe 100644 --- a/lib/system_check/helpers.rb +++ b/lib/system_check/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module Helpers include ::Gitlab::TaskHelpers diff --git a/lib/system_check/incoming_email/foreman_configured_check.rb b/lib/system_check/incoming_email/foreman_configured_check.rb index 1db7bf2b782..944913087da 100644 --- a/lib/system_check/incoming_email/foreman_configured_check.rb +++ b/lib/system_check/incoming_email/foreman_configured_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module IncomingEmail class ForemanConfiguredCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb index 3550c5796b0..613c2296375 100644 --- a/lib/system_check/incoming_email/imap_authentication_check.rb +++ b/lib/system_check/incoming_email/imap_authentication_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module IncomingEmail class ImapAuthenticationCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/incoming_email/initd_configured_check.rb b/lib/system_check/incoming_email/initd_configured_check.rb index ea23b8ef49c..acb4b5a9e74 100644 --- a/lib/system_check/incoming_email/initd_configured_check.rb +++ b/lib/system_check/incoming_email/initd_configured_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module IncomingEmail class InitdConfiguredCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/incoming_email/mail_room_running_check.rb b/lib/system_check/incoming_email/mail_room_running_check.rb index c1807501829..b7aead4624e 100644 --- a/lib/system_check/incoming_email/mail_room_running_check.rb +++ b/lib/system_check/incoming_email/mail_room_running_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module IncomingEmail class MailRoomRunningCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb index 09b57c7b408..53b2d8fd5b3 100644 --- a/lib/system_check/orphans/namespace_check.rb +++ b/lib/system_check/orphans/namespace_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module Orphans class NamespaceCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb index 2695c658874..ef8fe945f61 100644 --- a/lib/system_check/orphans/repository_check.rb +++ b/lib/system_check/orphans/repository_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck module Orphans class RepositoryCheck < SystemCheck::BaseCheck diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index 99c9e984107..11818ae54f8 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemCheck # Simple Executor is current default executor for GitLab # It is a simple port from display logic in the old check.rake diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8b7d4b0f17e..c1fdad64229 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -132,23 +132,12 @@ msgstr "" msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgstr "" -msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." -msgstr "" - -msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." -msgstr "" - msgid "%{openOrClose} %{noteable}" msgstr "" msgid "%{percent}%% complete" msgstr "" -msgid "%{storage_name}: failed storage access attempt on host:" -msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" -msgstr[0] "" -msgstr[1] "" - msgid "%{text} %{files}" msgid_plural "%{text} %{files} files" msgstr[0] "" @@ -314,9 +303,6 @@ msgstr "" msgid "Access expiration date" msgstr "" -msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." -msgstr "" - msgid "Account" msgstr "" @@ -401,9 +387,6 @@ msgstr "" msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running." msgstr "" -msgid "AdminHealthPageLink|health page" -msgstr "" - msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered." msgstr "" @@ -1162,9 +1145,6 @@ msgstr "" msgid "Chat" msgstr "" -msgid "Check interval" -msgstr "" - msgid "Checking %{text} availability…" msgstr "" @@ -1300,9 +1280,6 @@ msgstr "" msgid "CiVariable|Validation failed" msgstr "" -msgid "CircuitBreakerApiLink|circuitbreaker api" -msgstr "" - msgid "Clear search" msgstr "" @@ -1844,7 +1821,7 @@ msgstr "" msgid "Configure push mirrors." msgstr "" -msgid "Configure storage path and circuit breaker settings." +msgid "Configure storage path settings." msgstr "" msgid "Configure the way a user creates a new account." @@ -2917,9 +2894,6 @@ msgstr "" msgid "Git revision" msgstr "" -msgid "Git storage health information has been reset" -msgstr "" - msgid "Git strategy for pipelines" msgstr "" @@ -3736,9 +3710,6 @@ msgstr "" msgid "Max access level" msgstr "" -msgid "Maximum git storage failures" -msgstr "" - msgid "Maximum job timeout" msgstr "" @@ -4206,9 +4177,6 @@ msgstr "" msgid "November" msgstr "" -msgid "Number of access attempts" -msgstr "" - msgid "Oct" msgstr "" @@ -4268,6 +4236,9 @@ msgstr "" msgid "Operations" msgstr "" +msgid "Operations Dashboard" +msgstr "" + msgid "Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab." msgstr "" @@ -5155,9 +5126,6 @@ msgstr "" msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab." msgstr "" -msgid "Reset git storage health information" -msgstr "" - msgid "Reset health check access token" msgstr "" @@ -5370,12 +5338,6 @@ msgstr "" msgid "SearchAutocomplete|in this project" msgstr "" -msgid "Seconds before reseting failure information" -msgstr "" - -msgid "Seconds to wait for a storage access attempt" -msgstr "" - msgid "Secret" msgstr "" @@ -5980,12 +5942,6 @@ msgstr "" msgid "The maximum file size allowed is 200KB." msgstr "" -msgid "The number of attempts GitLab will make to access a storage." -msgstr "" - -msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}." -msgstr "" - msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>" msgstr "" @@ -6031,15 +5987,6 @@ msgstr "" msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "" -msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset." -msgstr "" - -msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised." -msgstr "" - -msgid "The time in seconds between storage checks. When a previous check did complete yet, GitLab will skip a check." -msgstr "" - msgid "The time taken by each data entry gathered by that stage." msgstr "" @@ -6079,9 +6026,6 @@ msgstr "" msgid "There are no unstaged changes" msgstr "" -msgid "There are problems accessing Git storage: " -msgstr "" - msgid "There was an error loading users activity calendar." msgstr "" diff --git a/package.json b/package.json index 35984e6d81f..dafb03bf75a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/preset-env": "^7.1.0", - "@gitlab-org/gitlab-svgs": "^1.29.0", + "@gitlab-org/gitlab-svgs": "^1.32.0", "@gitlab-org/gitlab-ui": "^1.8.0", "autosize": "^4.0.0", "axios": "^0.17.1", @@ -121,6 +121,7 @@ "commander": "^2.18.0", "eslint": "~5.6.0", "eslint-config-airbnb-base": "^13.1.0", + "eslint-config-prettier": "^3.1.0", "eslint-import-resolver-webpack": "^0.10.1", "eslint-plugin-filenames": "^1.3.2", "eslint-plugin-html": "4.0.5", @@ -144,7 +145,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0-beta.0", "nodemon": "^1.18.4", - "prettier": "1.12.1", + "prettier": "1.14.3", "webpack-dev-server": "^3.1.8" } } @@ -16,6 +16,8 @@ module QA autoload :Browser, 'qa/runtime/browser' autoload :Env, 'qa/runtime/env' autoload :Address, 'qa/runtime/address' + autoload :Path, 'qa/runtime/path' + autoload :Fixtures, 'qa/runtime/fixtures' module API autoload :Client, 'qa/runtime/api/client' @@ -95,6 +97,7 @@ module QA module Integration autoload :Github, 'qa/scenario/test/integration/github' autoload :LDAP, 'qa/scenario/test/integration/ldap' + autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml' autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage' @@ -120,6 +123,7 @@ module QA module Main autoload :Login, 'qa/page/main/login' + autoload :Menu, 'qa/page/main/menu' autoload :OAuth, 'qa/page/main/oauth' autoload :SignUp, 'qa/page/main/sign_up' end @@ -128,13 +132,6 @@ module QA autoload :Common, 'qa/page/settings/common' end - module Menu - autoload :Main, 'qa/page/menu/main' - autoload :Side, 'qa/page/menu/side' - autoload :Admin, 'qa/page/menu/admin' - autoload :Profile, 'qa/page/menu/profile' - end - module Dashboard autoload :Projects, 'qa/page/dashboard/projects' autoload :Groups, 'qa/page/dashboard/groups' @@ -158,6 +155,7 @@ module QA autoload :New, 'qa/page/project/new' autoload :Show, 'qa/page/project/show' autoload :Activity, 'qa/page/project/activity' + autoload :Menu, 'qa/page/project/menu' module Import autoload :Github, 'qa/page/project/import/github' @@ -183,6 +181,7 @@ module QA autoload :SecretVariables, 'qa/page/project/settings/secret_variables' autoload :Runners, 'qa/page/project/settings/runners' autoload :MergeRequest, 'qa/page/project/settings/merge_request' + autoload :Members, 'qa/page/project/settings/members' end module Issue @@ -201,6 +200,11 @@ module QA end module Operations + module Environments + autoload :Index, 'qa/page/project/operations/environments/index' + autoload :Show, 'qa/page/project/operations/environments/show' + end + module Kubernetes autoload :Index, 'qa/page/project/operations/kubernetes/index' autoload :Add, 'qa/page/project/operations/kubernetes/add' @@ -214,9 +218,14 @@ module QA autoload :New, 'qa/page/project/wiki/new' autoload :Show, 'qa/page/project/wiki/show' end + + module WebIDE + autoload :Edit, 'qa/page/project/web_ide/edit' + end end module Profile + autoload :Menu, 'qa/page/profile/menu' autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' autoload :SSHKeys, 'qa/page/profile/ssh_keys' end @@ -235,6 +244,8 @@ module QA end module Admin + autoload :Menu, 'qa/page/admin/menu' + module Settings autoload :Repository, 'qa/page/admin/settings/repository' @@ -257,6 +268,9 @@ module QA autoload :Dropzone, 'qa/page/component/dropzone' autoload :GroupsFilter, 'qa/page/component/groups_filter' autoload :Select2, 'qa/page/component/select2' + autoload :DropdownFilter, 'qa/page/component/dropdown_filter' + autoload :UsersSelect, 'qa/page/component/users_select' + module Issuable autoload :Common, 'qa/page/component/issuable/common' end @@ -289,6 +303,18 @@ module QA autoload :Config, 'qa/specs/config' autoload :Runner, 'qa/specs/runner' end + + ## + # Classes that describe the structure of vendor/third party application pages + # + module Vendor + module SAMLIdp + module Page + autoload :Base, 'qa/vendor/saml_idp/page/base' + autoload :Login, 'qa/vendor/saml_idp/page/login' + end + end + end end QA::Runtime::Release.extend_autoloads! diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb index 60539992073..f3b52565d17 100644 --- a/qa/qa/factory/resource/branch.rb +++ b/qa/qa/factory/resource/branch.rb @@ -43,7 +43,7 @@ module QA # to `allow_to_push` variable. return branch unless @protected - Page::Menu::Side.act do + Page::Project::Menu.act do click_repository_settings end diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb index ea8a3ad687d..4c53c500c27 100644 --- a/qa/qa/factory/resource/deploy_key.rb +++ b/qa/qa/factory/resource/deploy_key.rb @@ -24,7 +24,7 @@ module QA def fabricate! project.visit! - Page::Menu::Side.act do + Page::Project::Menu.act do click_repository_settings end diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb index 2016d10ddae..f8dea06d361 100644 --- a/qa/qa/factory/resource/file.rb +++ b/qa/qa/factory/resource/file.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Factory module Resource @@ -19,7 +21,7 @@ module QA def fabricate! project.visit! - Page::Project::Show.act { go_to_new_file! } + Page::Project::Show.act { create_new_file! } Page::File::Form.perform do |page| page.add_name(@name) diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 1fa47e92983..83dd4000f0a 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -32,7 +32,7 @@ module QA puts "Visited project page" Capybara::Screenshot.screenshot_and_save_page - return if Page::Menu::Main.act { has_personal_area?(wait: 0) } + return if Page::Main::Menu.act { has_personal_area?(wait: 0) } puts "Not signed in. Attempting to sign in again." Capybara::Screenshot.screenshot_and_save_page diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb index ed9d0329081..cdee35c54e3 100644 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ b/qa/qa/factory/resource/kubernetes_cluster.rb @@ -16,7 +16,7 @@ module QA def fabricate! @project.visit! - Page::Menu::Side.act { click_operations_kubernetes } + Page::Project::Menu.act { click_operations_kubernetes } Page::Project::Operations::Kubernetes::Index.perform do |page| page.add_kubernetes_cluster diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb index 514e3615d18..166054cfcdc 100644 --- a/qa/qa/factory/resource/personal_access_token.rb +++ b/qa/qa/factory/resource/personal_access_token.rb @@ -12,8 +12,8 @@ module QA end def fabricate! - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { click_access_tokens } + Page::Main::Menu.act { go_to_profile_settings } + Page::Profile::Menu.act { click_access_tokens } Page::Profile::PersonalAccessTokens.perform do |page| page.fill_token_name(name || 'api-test-token') diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb index 47a5e74204f..1251ae03135 100644 --- a/qa/qa/factory/resource/project_milestone.rb +++ b/qa/qa/factory/resource/project_milestone.rb @@ -17,7 +17,7 @@ module QA def fabricate! project.visit! - Page::Menu::Side.act do + Page::Project::Menu.act do click_issues click_milestones end diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb index 03b69eb1bdf..7ac65fe6913 100644 --- a/qa/qa/factory/resource/runner.rb +++ b/qa/qa/factory/resource/runner.rb @@ -26,7 +26,7 @@ module QA def fabricate! project.visit! - Page::Menu::Side.act { click_ci_cd_settings } + Page::Project::Menu.act { click_ci_cd_settings } Service::Runner.new(name).tap do |runner| Page::Project::Settings::CICD.perform do |settings| diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb index 4f6039f300f..5249e1755a6 100644 --- a/qa/qa/factory/resource/sandbox.rb +++ b/qa/qa/factory/resource/sandbox.rb @@ -11,7 +11,7 @@ module QA end def fabricate! - Page::Menu::Main.act { go_to_groups } + Page::Main::Menu.act { go_to_groups } Page::Dashboard::Groups.perform do |page| if page.has_group?(@name) diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb index 12a830da116..4084a7fc2cd 100644 --- a/qa/qa/factory/resource/secret_variable.rb +++ b/qa/qa/factory/resource/secret_variable.rb @@ -12,7 +12,7 @@ module QA def fabricate! project.visit! - Page::Menu::Side.act { click_ci_cd_settings } + Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Settings::CICD.perform do |setting| setting.expand_secret_variables do |page| diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb index 6c872f32d16..45236f69de9 100644 --- a/qa/qa/factory/resource/ssh_key.rb +++ b/qa/qa/factory/resource/ssh_key.rb @@ -27,8 +27,8 @@ module QA end def fabricate! - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { click_ssh_keys } + Page::Main::Menu.act { go_to_profile_settings } + Page::Profile::Menu.act { click_ssh_keys } Page::Profile::SSHKeys.perform do |page| page.add_key(public_key, title) diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index 34b52223b2d..e8b9ea2e6b4 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -38,8 +38,8 @@ module QA def fabricate! # Don't try to log-out if we're not logged-in - if Page::Menu::Main.act { has_personal_area?(wait: 0) } - Page::Menu::Main.perform { |main| main.sign_out } + if Page::Main::Menu.act { has_personal_area?(wait: 0) } + Page::Main::Menu.perform { |main| main.sign_out } end if credentials_given? diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb index cc200a512d5..acfe143fa61 100644 --- a/qa/qa/factory/resource/wiki.rb +++ b/qa/qa/factory/resource/wiki.rb @@ -10,7 +10,7 @@ module QA end def fabricate! - Page::Menu::Side.act { click_wiki } + Page::Project::Menu.act { click_wiki } Page::Project::Wiki::New.perform do |page| page.go_to_create_first_page page.set_title(@title) diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb index c69ebed3c6b..f2e58a3ea38 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/factory/settings/hashed_storage.rb @@ -6,8 +6,8 @@ module QA raise ArgumentError unless traits.include?(:enabled) Page::Main::Login.act { sign_in_using_credentials } - Page::Menu::Main.act { go_to_admin_area } - Page::Menu::Admin.act { go_to_settings } + Page::Main::Menu.act { go_to_admin_area } + Page::Admin::Menu.act { go_to_repository_settings } Page::Admin::Settings::Main.perform do |setting| setting.expand_repository_storage do |page| @@ -16,7 +16,7 @@ module QA end end - QA::Page::Menu::Main.act { sign_out } + QA::Page::Main::Menu.act { sign_out } end end end diff --git a/qa/qa/page/menu/admin.rb b/qa/qa/page/admin/menu.rb index bf05a912bc6..e8c7d274966 100644 --- a/qa/qa/page/menu/admin.rb +++ b/qa/qa/page/admin/menu.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Admin < Page::Base + module Admin + class Menu < Page::Base view 'app/views/layouts/nav/sidebar/_admin.html.haml' do element :admin_sidebar element :admin_sidebar_submenu diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 142707521df..160ec58cf2c 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'capybara/dsl' module QA @@ -7,6 +9,8 @@ module QA include Scenario::Actable extend SingleForwardable + ElementNotFound = Class.new(RuntimeError) + def_delegators :evaluator, :view, :views def refresh @@ -28,6 +32,21 @@ module QA false end + def with_retry(max_attempts: 3, reload: false) + attempts = 0 + + while attempts < max_attempts + result = yield + return result if result + + refresh if reload + + attempts += 1 + end + + false + end + def scroll_to(selector, text: nil) page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); diff --git a/qa/qa/page/component/dropdown_filter.rb b/qa/qa/page/component/dropdown_filter.rb new file mode 100644 index 00000000000..e896c382779 --- /dev/null +++ b/qa/qa/page/component/dropdown_filter.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module DropdownFilter + def filter_and_select(item) + wait(reload: false) do + page.has_css?('.dropdown-input-field') + end + + find('.dropdown-input-field').set(item) + click_link item + end + end + end + end +end diff --git a/qa/qa/page/component/users_select.rb b/qa/qa/page/component/users_select.rb new file mode 100644 index 00000000000..f88d6450a33 --- /dev/null +++ b/qa/qa/page/component/users_select.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module UsersSelect + def select_user(element, username) + find("#{element_selector_css(element)} input").set(username) + find('.ajax-users-dropdown .user-username', text: "@#{username}").click + end + end + end + end +end diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb index f6e502f500b..3752f5066c9 100644 --- a/qa/qa/page/file/form.rb +++ b/qa/qa/page/file/form.rb @@ -3,6 +3,7 @@ module QA module File class Form < Page::Base include Shared::CommitMessage + include Page::Component::DropdownFilter view 'app/views/projects/blob/_editor.html.haml' do element :file_name, "text_field_tag 'file_name'" @@ -13,6 +14,14 @@ module QA element :commit_changes, "button_tag 'Commit changes'" end + view 'app/views/projects/blob/_template_selectors.html.haml' do + element :template_type_dropdown + element :gitignore_dropdown + element :gitlab_ci_yml_dropdown + element :dockerfile_dropdown + element :license_dropdown + end + def add_name(name) fill_in 'file_name', with: name end @@ -29,6 +38,25 @@ module QA click_on 'Commit changes' end + def select_template(template_type, template) + click_element :template_type_dropdown + click_link template_type + + case template_type + when '.gitignore' + click_element :gitignore_dropdown + when '.gitlab-ci.yml' + click_element :gitlab_ci_yml_dropdown + when 'Dockerfile' + click_element :dockerfile_dropdown + when 'LICENSE' + click_element :license_dropdown + else + raise %Q(Unsupported template_type "#{template_type}". Please confirm that it is a valid option.) + end + filter_and_select template + end + private def text_area diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 9b3183ba328..94b9486b0d5 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -31,19 +31,23 @@ module QA element :register_tab end + view 'app/views/devise/shared/_omniauth_box.html.haml' do + element :saml_login_button + end + def initialize # The login page is usually the entry point for all the scenarios so # we need to wait for the instance to start. That said, in some cases # we are already logged-in so we check both cases here. wait(max: 500) do has_css?('.login-page') || - Page::Menu::Main.act { has_personal_area?(wait: 0) } + Page::Main::Menu.act { has_personal_area?(wait: 0) } end end def sign_in_using_credentials(user = nil) # Don't try to log-in if we're already logged-in - return if Page::Menu::Main.act { has_personal_area?(wait: 0) } + return if Page::Main::Menu.act { has_personal_area?(wait: 0) } using_wait_time 0 do set_initial_password_if_present @@ -57,7 +61,7 @@ module QA end end - Page::Menu::Main.act { has_personal_area? } + Page::Main::Menu.act { has_personal_area? } end def sign_in_using_admin_credentials @@ -72,7 +76,7 @@ module QA sign_in_using_gitlab_credentials(admin) end - Page::Menu::Main.act { has_personal_area? } + Page::Main::Menu.act { has_personal_area? } end def self.path @@ -130,6 +134,11 @@ module QA click_element :sign_in_button end + def sign_in_with_saml + set_initial_password_if_present + click_element :saml_login_button + end + def sign_in_using_gitlab_credentials(user) switch_to_sign_in_tab if has_sign_in_tab? switch_to_standard_tab if has_standard_tab? diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/main/menu.rb index 2ae86bbc7dc..a2ee696e1b3 100644 --- a/qa/qa/page/menu/main.rb +++ b/qa/qa/page/main/menu.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Main < Page::Base + module Main + class Menu < Page::Base view 'app/views/layouts/header/_current_user_dropdown.html.haml' do element :user_sign_out_link, 'link_to _("Sign out")' element :settings_link, 'link_to s_("CurrentUser|Settings")' @@ -66,10 +68,6 @@ module QA end end - def assert_has_personal_area - raise "Failed to sign in" unless has_personal_area? - end - private def within_top_menu diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb index 64cd395de78..b33ea03fc55 100644 --- a/qa/qa/page/main/sign_up.rb +++ b/qa/qa/page/main/sign_up.rb @@ -1,25 +1,32 @@ +# frozen_string_literal: true + module QA module Page module Main class SignUp < Page::Base view 'app/views/devise/shared/_signup_box.html.haml' do - element :name, 'text_field :name' - element :username, 'text_field :username' - element :email_field, 'email_field :email' - element :email_confirmation, 'email_field :email_confirmation' - element :password, 'password_field :password' - element :register_button, 'submit "Register"' + element :new_user_name + element :new_user_username + element :new_user_email + element :new_user_email_confirmation + element :new_user_password + element :new_user_register_button end def sign_up!(user) - fill_in :new_user_name, with: user.name - fill_in :new_user_username, with: user.username - fill_in :new_user_email, with: user.email - fill_in :new_user_email_confirmation, with: user.email - fill_in :new_user_password, with: user.password - click_button 'Register' + fill_element :new_user_name, user.name + fill_element :new_user_username, user.username + fill_element :new_user_email, user.email + fill_element :new_user_email_confirmation, user.email + fill_element :new_user_password, user.password + + signed_in = with_retry do + click_element :new_user_register_button + + Page::Main::Menu.act { has_personal_area? } + end - Page::Menu::Main.act { assert_has_personal_area } + raise "Failed to register and sign in" unless signed_in end end end diff --git a/qa/qa/page/menu/profile.rb b/qa/qa/page/profile/menu.rb index 7e24fa85c33..f8a7d64e016 100644 --- a/qa/qa/page/menu/profile.rb +++ b/qa/qa/page/profile/menu.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Profile < Page::Base + module Profile + class Menu < Page::Base view 'app/views/layouts/nav/sidebar/_profile.html.haml' do element :access_token_link, 'link_to profile_personal_access_tokens_path' element :access_token_title, 'Access Tokens' diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/project/menu.rb index a1eedfea42e..63c719e5fe1 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/project/menu.rb @@ -1,14 +1,18 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Side < Page::Base + module Project + class Menu < Page::Base view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :settings_link, 'link_to edit_project_path' element :repository_link, "title: _('Repository')" element :link_pipelines + element :link_members_settings element :pipelines_settings_link, "title: _('CI / CD')" element :operations_kubernetes_link, "title: _('Kubernetes')" + element :operations_environments_link element :issues_link, /link_to.*shortcuts-issues/ element :issues_link_text, "Issues" element :merge_requests_link, /link_to.*shortcuts-merge_requests/ @@ -40,6 +44,22 @@ module QA end end + def click_operations_environments + hover_operations do + within_submenu do + click_element(:operations_environments_link) + end + end + end + + def click_members_settings + hover_settings do + within_submenu do + click_element :link_members_settings + end + end + end + def click_operations_kubernetes hover_operations do within_submenu do diff --git a/qa/qa/page/project/operations/environments/index.rb b/qa/qa/page/project/operations/environments/index.rb new file mode 100644 index 00000000000..63965a57edd --- /dev/null +++ b/qa/qa/page/project/operations/environments/index.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Operations + module Environments + class Index < Page::Base + view 'app/assets/javascripts/environments/components/environment_item.vue' do + element :environment_link + end + + def go_to_environment(environment_name) + wait(reload: false) do + find(element_selector_css(:environment_link), text: environment_name).click + end + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/environments/show.rb b/qa/qa/page/project/operations/environments/show.rb new file mode 100644 index 00000000000..aa88c218c89 --- /dev/null +++ b/qa/qa/page/project/operations/environments/show.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Operations + module Environments + class Show < Page::Base + view 'app/views/projects/environments/_external_url.html.haml' do + element :view_deployment + end + + def view_deployment(&block) + new_window = window_opened_by { click_element(:view_deployment) } + + within_window(new_window, &block) if block + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/members.rb b/qa/qa/page/project/settings/members.rb new file mode 100644 index 00000000000..7fed93ca83f --- /dev/null +++ b/qa/qa/page/project/settings/members.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class Members < Page::Base + include Page::Component::UsersSelect + + view 'app/views/projects/project_members/_new_project_member.html.haml' do + element :member_select_input + element :add_member_button + end + + view 'app/views/projects/project_members/_team.html.haml' do + element :members_list + end + + def add_member(username) + select_user :member_select_input, username + click_element :add_member_button + end + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 267e7bbc249..2e7be1deb27 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Page module Project @@ -31,16 +33,22 @@ module QA element :tree_holder, '.tree-holder' end - view 'app/presenters/project_presenter.rb' do - element :new_file_button, "_('New file')," + view 'app/views/projects/buttons/_dropdown.html.haml' do + element :create_new_dropdown + element :new_file_option + end + + view 'app/views/projects/tree/_tree_header.html.haml' do + element :web_ide_button end def project_name find('.qa-project-name').text end - def go_to_new_file! - click_on 'New file' + def create_new_file! + click_element :create_new_dropdown + click_element :new_file_option end def switch_to_branch(branch_name) @@ -78,6 +86,10 @@ module QA def fork_project click_on 'Fork' end + + def open_web_ide! + click_element :web_ide_button + end end end end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb new file mode 100644 index 00000000000..23e580b81b6 --- /dev/null +++ b/qa/qa/page/project/web_ide/edit.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module WebIDE + class Edit < Page::Base + include Page::Component::DropdownFilter + + view 'app/assets/javascripts/ide/components/ide_tree.vue' do + element :new_file + end + + view 'app/assets/javascripts/ide/components/ide_tree_list.vue' do + element :file_list + end + + view 'app/assets/javascripts/ide/components/new_dropdown/modal.vue' do + element :full_file_path + element :template_list + end + + view 'app/assets/javascripts/ide/components/file_templates/bar.vue' do + element :file_templates_bar + element :file_template_dropdown + end + + view 'app/assets/javascripts/ide/components/file_templates/dropdown.vue' do + element :dropdown_filter_input + end + + view 'app/assets/javascripts/ide/components/commit_sidebar/form.vue' do + element :begin_commit_button + element :commit_button + end + + def has_file?(file_name) + within_element(:file_list) do + page.has_content? file_name + end + end + + def create_new_file_from_template(file_name, template) + click_element :new_file + within_element(:template_list) do + begin + click_on file_name + rescue Capybara::ElementNotFound + raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.) + end + end + + wait(reload: false) do + within_element(:file_templates_bar) do + click_element :file_template_dropdown + fill_element :dropdown_filter_input, template + + begin + click_on template + rescue Capybara::ElementNotFound + raise ElementNotFound, %Q(Couldn't find template "#{template}" for #{file_name}. Please confirm that it exists in the list of templates.) + end + end + end + end + + def commit_changes + click_element :begin_commit_button + click_element :commit_button + + wait(reload: false) do + page.has_content?('Your changes have been committed') + end + end + end + end + end + end +end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 4c64270ce92..9aaf57e8d83 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -51,6 +51,10 @@ module QA } ) + if QA::Runtime::Env.accept_insecure_certs? + capabilities['acceptInsecureCerts'] = true + end + options = Selenium::WebDriver::Chrome::Options.new options.add_argument("window-size=1240,1680") diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 5bebb5ccec0..4a2109799fa 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -8,6 +8,10 @@ module QA enabled?(ENV['CHROME_HEADLESS']) end + def accept_insecure_certs? + enabled?(ENV['ACCEPT_INSECURE_CERTS']) + end + def running_in_ci? ENV['CI'] || ENV['CI_SERVER'] end diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb new file mode 100644 index 00000000000..72004d5b00a --- /dev/null +++ b/qa/qa/runtime/fixtures.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Fixtures + def fetch_template_from_api(api_path, key) + request = Runtime::API::Request.new(api_client, "/templates/#{api_path}/#{key}") + get request.url + json_body[:content] + end + + private + + def api_client + @api_client ||= Runtime::API::Client.new(:gitlab) + end + end + end +end diff --git a/qa/qa/runtime/path.rb b/qa/qa/runtime/path.rb new file mode 100644 index 00000000000..3169c5dd743 --- /dev/null +++ b/qa/qa/runtime/path.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Path + extend self + + def qa_root + ::File.expand_path('../../', __dir__) + end + end + end +end diff --git a/qa/qa/scenario/test/integration/instance_saml.rb b/qa/qa/scenario/test/integration/instance_saml.rb new file mode 100644 index 00000000000..0697d0c2a0e --- /dev/null +++ b/qa/qa/scenario/test/integration/instance_saml.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class InstanceSAML < Test::Instance::All + tags :instance_saml + end + end + end + end +end diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb index d868515555c..c5f12255d72 100644 --- a/qa/qa/service/kubernetes_cluster.rb +++ b/qa/qa/service/kubernetes_cluster.rb @@ -25,6 +25,7 @@ module QA gcloud container clusters create #{cluster_name} #{auth_options} + --enable-basic-auth --zone #{Runtime::Env.gcloud_zone} && gcloud container clusters get-credentials @@ -33,6 +34,15 @@ module QA CMD @api_url = `kubectl config view --minify -o jsonpath='{.clusters[].cluster.server}'` + + @admin_user = "#{cluster_name}-admin" + master_auth = JSON.parse(`gcloud container clusters describe #{cluster_name} --zone #{Runtime::Env.gcloud_zone} --format 'json(masterAuth.username, masterAuth.password)'`) + shell <<~CMD.tr("\n", ' ') + kubectl config set-credentials #{@admin_user} + --username #{master_auth['masterAuth']['username']} + --password #{master_auth['masterAuth']['password']} + CMD + if rbac create_service_account @@ -64,7 +74,7 @@ module QA def create_service_account shell('kubectl create -f -', stdin_data: service_account) - shell('kubectl create -f -', stdin_data: service_account_role_binding) + shell("kubectl --user #{@admin_user} create -f -", stdin_data: service_account_role_binding) end def service_account diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb index 1c7da930567..ae196349c6b 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb @@ -8,7 +8,7 @@ module QA # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. # - Page::Menu::Main.perform do |menu| + Page::Main::Menu.perform do |menu| expect(menu).to have_personal_area end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb index c296296def6..217870531da 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -10,7 +10,7 @@ module QA # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. # - Page::Menu::Main.perform do |menu| + Page::Main::Menu.perform do |menu| expect(menu).to have_personal_area end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb new file mode 100644 index 00000000000..8d5055aab45 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module QA + context :manage, :orchestrated, :instance_saml do + describe 'Instance wide SAML SSO' do + it 'User logs in to gitlab with SAML SSO' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + + Page::Main::Login.act { sign_in_with_saml } + + Vendor::SAMLIdp::Page::Login.act { login } + + expect(page).to have_content('Welcome to GitLab') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index bc1c2abdf56..fb6b4937554 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -10,7 +10,7 @@ module QA # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. # - Page::Menu::Main.perform do |menu| + Page::Main::Menu.perform do |menu| expect(menu).to have_personal_area end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb new file mode 100644 index 00000000000..b276c7ee579 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + context :manage do + describe 'Add project member' do + it 'user adds project member' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + + user = Factory::Resource::User.fabricate! + + Page::Main::Menu.perform { |main| main.sign_out } + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'add-member-project' + end + + Page::Project::Menu.act { click_members_settings } + Page::Project::Settings::Members.perform do |page| + page.add_member(user.username) + end + + expect(page).to have_content("#{user.name} @#{user.username} Given access") + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index 2ef8de61441..d1cd9865aef 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -27,7 +27,7 @@ module QA imported_project # import the project - Page::Menu::Main.act { go_to_projects } + Page::Main::Menu.act { go_to_projects } Page::Dashboard::Projects.perform do |dashboard| dashboard.go_to_project(imported_project.name) end @@ -48,7 +48,7 @@ module QA end def verify_issues_import - Page::Menu::Side.act { click_issues } + Page::Project::Menu.act { click_issues } expect(page).to have_content('This is a sample issue') click_link 'This is a sample issue' @@ -66,7 +66,7 @@ module QA end def verify_merge_requests_import - Page::Menu::Side.act { click_merge_requests } + Page::Project::Menu.act { click_merge_requests } expect(page).to have_content('Improve README.md') click_link 'Improve README.md' @@ -101,7 +101,7 @@ module QA end def verify_wiki_import - Page::Menu::Side.act { click_wiki } + Page::Project::Menu.act { click_wiki } expect(page).to have_content('Welcome to the test-project wiki!') end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index 34bb6f1c197..97ac35e8dba 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -13,7 +13,7 @@ module QA push.commit_message = 'Add README.md' end - Page::Menu::Side.act { go_to_activity } + Page::Project::Menu.act { go_to_activity } Page::Project::Activity.act { go_to_push_events } diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index 542f532a629..49d76f31e3a 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -17,7 +17,7 @@ module QA it 'user creates an issue' do create_issue - Page::Menu::Side.act { click_issues } + Page::Project::Menu.act { click_issues } expect(page).to have_content(issue_title) end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 407a15800ab..922feadb4e1 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -11,7 +11,7 @@ module QA merge_request.fork_branch = 'feature-branch' end - Page::Menu::Main.perform { |main| main.sign_out } + Page::Main::Menu.perform { |main| main.sign_out } Page::Main::Login.perform { |login| login.sign_in_using_credentials } merge_request.visit! diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index ddcbc94b1b1..984cea8ca10 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -11,7 +11,7 @@ module QA project.name = "only-fast-forward" end - Page::Menu::Side.act { go_to_settings } + Page::Project::Menu.act { go_to_settings } Page::Project::Settings::MergeRequest.act { enable_ff_only } merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb new file mode 100644 index 00000000000..c7edcf4c025 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'File templates' do + include Runtime::Fixtures + + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + before(:all) do + login + + @project = Factory::Resource::Project.fabricate! do |project| + project.name = 'file-template-project' + project.description = 'Add file templates via the Files view' + end + + Page::Main::Menu.act { sign_out } + end + + templates = [ + { + file_name: '.gitignore', + name: 'Android', + api_path: 'gitignores', + api_key: 'Android' + }, + { + file_name: '.gitlab-ci.yml', + name: 'Julia', + api_path: 'gitlab_ci_ymls', + api_key: 'Julia' + }, + { + file_name: 'Dockerfile', + name: 'Python', + api_path: 'dockerfiles', + api_key: 'Python' + }, + { + file_name: 'LICENSE', + name: 'Mozilla Public License 2.0', + api_path: 'licenses', + api_key: 'mpl-2.0' + } + ] + + templates.each do |template| + it "user adds #{template[:file_name]} via file template #{template[:name]}" do + content = fetch_template_from_api(template[:api_path], template[:api_key]) + + login + @project.visit! + + Page::Project::Show.act { create_new_file! } + Page::File::Form.perform do |page| + page.select_template template[:file_name], template[:name] + end + + expect(page).to have_content('Template applied') + expect(page).to have_button('Undo') + expect(page).to have_content(content[0..100]) + + Page::File::Form.perform(&:commit_changes) + + expect(page).to have_content('The file has been successfully created.') + expect(page).to have_content(template[:file_name]) + expect(page).to have_content('Add new file') + expect(page).to have_content(content[0..100]) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index 84f663c4866..b163ca896a7 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -16,8 +16,8 @@ module QA expect(page).to have_content("Title: #{key_title}") expect(page).to have_content(key.fingerprint) - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { click_ssh_keys } + Page::Main::Menu.act { go_to_profile_settings } + Page::Profile::Menu.act { click_ssh_keys } Page::Profile::SSHKeys.perform do |ssh_keys| ssh_keys.remove_key(key_title) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb index 7c989bfd8cc..563393b3d07 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb @@ -28,8 +28,8 @@ module QA expect(page).to have_content('README.md') expect(page).to have_content('Test Use SSH Key') - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { click_ssh_keys } + Page::Main::Menu.act { go_to_profile_settings } + Page::Profile::Menu.act { click_ssh_keys } Page::Profile::SSHKeys.perform do |ssh_keys| ssh_keys.remove_key(key_title) diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb new file mode 100644 index 00000000000..ab5d97d5b66 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module QA + context :create do + describe 'Web IDE file templates' do + include Runtime::Fixtures + + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + before(:all) do + login + + @project = Factory::Resource::Project.fabricate! do |project| + project.name = 'file-template-project' + project.description = 'Add file templates via the Web IDE' + end + + # Add a file via the regular Files view because the Web IDE isn't + # available unless there is a file present + Page::Project::Show.act { create_new_file! } + Page::File::Form.perform do |page| + page.add_name('dummy') + page.add_content('Enable the Web IDE') + page.commit_changes + end + + Page::Main::Menu.act { sign_out } + end + + templates = [ + { + file_name: '.gitignore', + name: 'Android', + api_path: 'gitignores', + api_key: 'Android' + }, + { + file_name: '.gitlab-ci.yml', + name: 'Julia', + api_path: 'gitlab_ci_ymls', + api_key: 'Julia' + }, + { + file_name: 'Dockerfile', + name: 'Python', + api_path: 'dockerfiles', + api_key: 'Python' + }, + { + file_name: 'LICENSE', + name: 'Mozilla Public License 2.0', + api_path: 'licenses', + api_key: 'mpl-2.0' + } + ] + + templates.each do |template| + it "user adds #{template[:file_name]} via file template #{template[:name]}" do + content = fetch_template_from_api(template[:api_path], template[:api_key]) + + login + @project.visit! + + Page::Project::Show.act { open_web_ide! } + Page::Project::WebIDE::Edit.perform do |page| + page.create_new_file_from_template template[:file_name], template[:name] + + expect(page.has_file?(template[:file_name])).to be_truthy + end + + expect(page).to have_button('Undo') + expect(page).to have_content(content[0..100]) + + Page::Project::WebIDE::Edit.perform do |page| + page.commit_changes + end + + expect(page).to have_content(template[:file_name]) + expect(page).to have_content(content[0..100]) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 8009b9e8609..44dd85c1746 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -40,7 +40,7 @@ module QA push.file_content = '# My Third Wiki Content' push.commit_message = 'Update Home.md' end - Page::Menu::Side.act { click_wiki } + Page::Project::Menu.act { click_wiki } expect(page).to have_content('My Third Wiki Content') end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index cdfe9b90e15..e901531b1bf 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -64,7 +64,7 @@ module QA expect(page).to have_content('Add .gitlab-ci.yml') - Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Menu.act { click_ci_cd_pipelines } expect(page).to have_content('All 1') expect(page).to have_content('Add .gitlab-ci.yml') diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 8352d13b06d..73af24e7f50 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -28,7 +28,7 @@ module QA resource.image = 'gitlab/gitlab-runner:ubuntu' end - Page::Menu::Main.act { sign_out } + Page::Main::Menu.act { sign_out } end after(:all) do @@ -90,7 +90,7 @@ module QA sha1sum = Digest::SHA1.hexdigest(gitlab_ci) Page::Project::Show.act { wait_for_push } - Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Menu.act { click_ci_cd_pipelines } Page::Project::Pipeline::Index.act { go_to_latest_pipeline } Page::Project::Pipeline::Show.act { go_to_first_job } diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 4604936916b..3735bc00aff 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -51,13 +51,13 @@ module QA end project.visit! - Page::Menu::Side.act { click_ci_cd_settings } + Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Settings::CICD.perform do |p| p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") end project.visit! - Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Menu.act { click_ci_cd_pipelines } Page::Project::Pipeline::Index.act { go_to_latest_pipeline } Page::Project::Pipeline::Show.perform do |pipeline| @@ -65,6 +65,16 @@ module QA expect(pipeline).to have_build('test', status: :success, wait: 600) expect(pipeline).to have_build('production', status: :success, wait: 1200) end + + Page::Project::Menu.act { click_operations_environments } + Page::Project::Operations::Environments::Index.perform do |index| + index.go_to_environment('production') + end + Page::Project::Operations::Environments::Show.perform do |show| + show.view_deployment do + expect(page).to have_content('Hello World!') + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb index 6ffdc55538a..af24b36b734 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb @@ -6,7 +6,7 @@ module QA it 'user creates a group with a mattermost team' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Page::Menu::Main.act { go_to_groups } + Page::Main::Menu.act { go_to_groups } Page::Dashboard::Groups.perform do |page| page.go_to_new_group diff --git a/qa/qa/vendor/saml_idp/page/base.rb b/qa/qa/vendor/saml_idp/page/base.rb new file mode 100644 index 00000000000..286cb0a8cd8 --- /dev/null +++ b/qa/qa/vendor/saml_idp/page/base.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module QA + module Vendor + module SAMLIdp + module Page + class Base + include Capybara::DSL + include Scenario::Actable + end + end + end + end +end diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb new file mode 100644 index 00000000000..9c1f9904a7a --- /dev/null +++ b/qa/qa/vendor/saml_idp/page/login.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'capybara/dsl' + +module QA + module Vendor + module SAMLIdp + module Page + class Login < Page::Base + def login + fill_in 'username', with: 'user1' + fill_in 'password', with: 'user1pass' + click_on 'Login' + end + end + end + end + end +end diff --git a/qa/spec/scenario/test/integration/instance_saml_spec.rb b/qa/spec/scenario/test/integration/instance_saml_spec.rb new file mode 100644 index 00000000000..cb8a6a630cc --- /dev/null +++ b/qa/spec/scenario/test/integration/instance_saml_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +describe QA::Scenario::Test::Integration::InstanceSAML do + context '#perform' do + it_behaves_like 'a QA scenario class' do + let(:tags) { [:instance_saml] } + end + end +end diff --git a/scripts/trigger-build b/scripts/trigger-build index 0b5fd5995dd..b76cd5dd6f0 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require 'gitlab' @@ -6,38 +7,27 @@ require 'gitlab' # Configure credentials to be used with gitlab gem # Gitlab.configure do |config| - config.endpoint = 'https://gitlab.com/api/v4' - config.private_token = ENV['GITLAB_QA_ACCESS_TOKEN'] # gitlab-qa bot access token + config.endpoint = 'https://gitlab.com/api/v4' end module Trigger - TOKEN = ENV['BUILD_TRIGGER_TOKEN'] - def self.ee? ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md') end class Base - def initialize(api_token) - Gitlab.private_token = api_token - end - def invoke!(post_comment: false) pipeline = Gitlab.run_trigger( downstream_project_path, - Trigger::TOKEN, + trigger_token, ref, variables) - puts "Triggered #{pipeline.web_url}" + puts "Triggered downstream pipeline: #{pipeline.web_url}\n" puts "Waiting for downstream pipeline status" - begin - Trigger::CommitComment.post!(downstream_project_path, pipeline) if post_comment - rescue Gitlab::Error::Error => error - puts "Ignoring the following error: #{error}" - end - Trigger::Pipeline.new(downstream_project_path, pipeline.id) + Trigger::CommitComment.post!(pipeline, access_token) if post_comment + Trigger::Pipeline.new(downstream_project_path, pipeline.id, access_token) end private @@ -52,6 +42,16 @@ module Trigger raise NotImplementedError end + # Must be overriden + def trigger_token + raise NotImplementedError + end + + # Must be overriden + def access_token + raise NotImplementedError + end + # Can be overriden def extra_variables {} @@ -68,8 +68,11 @@ module Trigger def base_variables { - 'TRIGGERED_USER' => ENV['GITLAB_USER_NAME'], - 'TRIGGER_SOURCE' => ENV['CI_JOB_URL'] + 'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'], + 'TRIGGER_SOURCE' => ENV['CI_JOB_URL'], + 'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'], + 'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'], + 'TOP_UPSTREAM_SOURCE_SHA' => ENV['CI_COMMIT_SHA'] } end @@ -85,13 +88,21 @@ module Trigger private def downstream_project_path - 'gitlab-org/omnibus-gitlab'.freeze + 'gitlab-org/omnibus-gitlab' end def ref ENV['OMNIBUS_BRANCH'] || 'master' end + def trigger_token + ENV['BUILD_TRIGGER_TOKEN'] + end + + def access_token + ENV['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] + end + def extra_variables { 'GITLAB_VERSION' => ENV['CI_COMMIT_SHA'], @@ -112,6 +123,14 @@ module Trigger ENV['CNG_BRANCH'] || 'master' end + def trigger_token + ENV['BUILD_TRIGGER_TOKEN'] + end + + def access_token + ENV['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] + end + def extra_variables edition = Trigger.ee? ? 'EE' : 'CE' @@ -134,11 +153,16 @@ module Trigger end class CommitComment - def self.post!(downstream_project_path, downstream_pipeline) + def self.post!(downstream_pipeline, access_token) + Gitlab.private_token = access_token + Gitlab.create_commit_comment( ENV['CI_PROJECT_PATH'], ENV['CI_COMMIT_SHA'], "The [`#{ENV['CI_JOB_NAME']}`](#{ENV['CI_JOB_URL']}) job from pipeline #{ENV['CI_PIPELINE_URL']} triggered #{downstream_pipeline.web_url} downstream.") + + rescue Gitlab::Error::Error => error + puts "Ignoring the following error: #{error}" end end @@ -146,15 +170,16 @@ module Trigger INTERVAL = 60 # seconds MAX_DURATION = 3600 * 3 # 3 hours - attr_reader :project, :id + attr_reader :project, :id, :api_token - def initialize(project, id) + def initialize(project, id, api_token) @project = project @id = id + @api_token = api_token @start = Time.now.to_i # gitlab-bot's token "GitLab multi-project pipeline polling" - Gitlab.private_token = ENV['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] + Gitlab.private_token = api_token end def wait! @@ -197,9 +222,9 @@ end case ARGV[0] when 'omnibus' - Trigger::Omnibus.new(ENV['GITLAB_QA_ACCESS_TOKEN']).invoke!(post_comment: true).wait! + Trigger::Omnibus.new.invoke!(post_comment: true).wait! when 'cng' - Trigger::CNG.new(ENV['GITLAB_QA_ACCESS_TOKEN']).invoke!.wait! + Trigger::CNG.new.invoke!.wait! else puts "Please provide a valid option: omnibus - Triggers a pipeline that builds the omnibus-gitlab package diff --git a/spec/bin/storage_check_spec.rb b/spec/bin/storage_check_spec.rb deleted file mode 100644 index 02f6fcb6e3a..00000000000 --- a/spec/bin/storage_check_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe 'bin/storage_check' do - it 'is executable' do - command = %w[bin/storage_check -t unix://the/path/to/a/unix-socket.sock -i 10 -d] - expected_output = 'Checking unix://the/path/to/a/unix-socket.sock every 10 seconds' - - output, status = Gitlab::Popen.popen(command, Rails.root.to_s) - - expect(status).to eq(0) - expect(output).to include(expected_output) - end -end diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb index d15ee0021d9..e13401fc06b 100644 --- a/spec/controllers/admin/health_check_controller_spec.rb +++ b/spec/controllers/admin/health_check_controller_spec.rb @@ -8,18 +8,10 @@ describe Admin::HealthCheckController do end describe 'GET show' do - it 'loads the git storage health information' do + it 'loads the health information' do get :show - expect(assigns[:failing_storage_statuses]).not_to be_nil - end - end - - describe 'POST reset_storage_health' do - it 'resets all storage health information' do - expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!) - - post :reset_storage_health + expect(assigns[:errors]).not_to be_nil end end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 2b28cfd16cc..a8556771edd 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -190,30 +190,6 @@ describe ApplicationController do end end - describe 'rescue from Gitlab::Git::Storage::Inaccessible' do - controller(described_class) do - def index - raise Gitlab::Git::Storage::Inaccessible.new('broken', 100) - end - end - - it 'renders a 503 when storage is not available' do - sign_in(create(:user)) - - get :index - - expect(response.status).to eq(503) - end - - it 'renders includes a Retry-After header' do - sign_in(create(:user)) - - get :index - - expect(response.headers['Retry-After']).to eq(100) - end - end - describe 'response format' do controller(described_class) do def index diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index d800ad7c187..ec73c89cb11 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -14,48 +14,6 @@ describe HealthController do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end - describe '#storage_check' do - before do - allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip) - end - - subject { post :storage_check } - - it 'checks all the configured storages' do - expect(Gitlab::Git::Storage::Checker).to receive(:check_all).and_call_original - - subject - end - - it 'returns the check interval' do - stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true') - stub_application_setting(circuitbreaker_check_interval: 10) - - subject - - expect(json_response['check_interval']).to eq(10) - end - - context 'with failing storages', :broken_storage do - before do - stub_storage_settings( - broken: { path: 'tmp/tests/non-existent-repositories' } - ) - end - - it 'includes the failure information' do - subject - - expected_results = [ - { 'storage' => 'broken', 'success' => false }, - { 'storage' => 'default', 'success' => true } - ] - - expect(json_response['results']).to eq(expected_results) - end - end - end - describe '#readiness' do shared_context 'endpoint responding with readiness data' do let(:request_params) { {} } diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 5c8180baf8a..1484676eea3 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -352,6 +352,10 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(json_response['has_trace']).to be true end end + + it 'exposes the stage the job belongs to' do + expect(json_response['stage']).to eq('test') + end end context 'when requesting JSON job is triggered' do diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index aaa3e8dc821..790051dd933 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe "Admin Health Check", :feature do include StubENV + set(:admin) { create(:admin) } before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - sign_in(create(:admin)) + sign_in(admin) end describe '#show' do @@ -56,27 +57,4 @@ describe "Admin Health Check", :feature do expect(page).to have_content('The server is on fire') end end - - context 'with repository storage failures', :broken_storage do - before do - visit admin_health_check_path - end - - it 'shows storage failure information' do - hostname = Gitlab::Environment.hostname - maximum_failures = Gitlab::CurrentSettings.current_application_settings - .circuitbreaker_failure_count_threshold - number_of_failures = maximum_failures + 1 - - expect(page).to have_content("broken: #{number_of_failures} failed storage access attempts:") - expect(page).to have_content("#{hostname}: #{number_of_failures} of #{maximum_failures} failures.") - end - - it 'allows resetting storage failures' do - click_button 'Reset git storage health information' - - expect(page).to have_content('Git storage health information has been reset') - expect(page).not_to have_content('failed storage access attempt') - end - end end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index ac961e98a61..4e6f73ef58a 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -101,4 +101,25 @@ describe 'Group show page' do expect(page).to have_emoji('smile') end end + + context 'where group has projects' do + let(:user) { create(:user) } + + before do + group.add_owner(user) + sign_in(user) + end + + it 'allows users to sorts projects by most stars', :js do + project1 = create(:project, namespace: group, star_count: 2) + project2 = create(:project, namespace: group, star_count: 3) + project3 = create(:project, namespace: group, star_count: 0) + + visit group_path(group, sort: :stars_desc) + + expect(find('.group-row:nth-child(1) .namespace-title > a')).to have_content(project2.title) + expect(find('.group-row:nth-child(2) .namespace-title > a')).to have_content(project1.title) + expect(find('.group-row:nth-child(3) .namespace-title > a')).to have_content(project3.title) + end + end end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 2076ce7b4f7..6224cbffe9d 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -663,6 +663,56 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).to have_content('This job does not have a trace.') end end + + context 'with erased job', :js do + let(:job) { create(:ci_build, :erased, pipeline: pipeline) } + + it 'renders erased job warning' do + visit project_job_path(project, job) + wait_for_requests + + page.within('.js-job-erased-block') do + expect(page).to have_content('Job has been erased') + end + end + end + + context 'without erased job', :js do + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'does not render erased job warning' do + visit project_job_path(project, job) + wait_for_requests + + expect(page).not_to have_css('.js-job-erased-block') + end + end + + context 'on mobile', :js do + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'renders collpased sidebar' do + page.current_window.resize_to(600, 800) + + visit project_job_path(project, job) + wait_for_requests + + expect(page).to have_css('.js-build-sidebar.right-sidebar-collapsed', visible: false) + expect(page).not_to have_css('.js-build-sidebar.right-sidebar-expanded', visible: false) + end + end + + context 'on desktop', :js do + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'renders expanded sidebar' do + visit project_job_path(project, job) + wait_for_requests + + expect(page).to have_css('.js-build-sidebar.right-sidebar-expanded') + expect(page).not_to have_css('.js-build-sidebar.right-sidebar-collpased') + end + end end describe "POST /:project/jobs/:id/cancel", :js do diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 9e3f2c69606..7d164539d9a 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -66,7 +66,7 @@ describe BranchesFinder do context 'filter and sort' do it 'filters branches by name and sorts by recently_updated' do - params = { sort: 'updated_desc', search: 'feature' } + params = { sort: 'updated_desc', search: 'feat' } branches_finder = described_class.new(repository, params) result = branches_finder.execute @@ -75,6 +75,17 @@ describe BranchesFinder do expect(result.count).to eq(2) end + it 'filters branches by name and sorts by recently_updated, with exact matches first' do + params = { sort: 'updated_desc', search: 'feature' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature') + expect(result.second.name).to eq('feature_conflict') + expect(result.count).to eq(2) + end + it 'filters branches by name and sorts by last_updated' do params = { sort: 'updated_asc', search: 'feature' } branches_finder = described_class.new(repository, params) @@ -84,6 +95,26 @@ describe BranchesFinder do expect(result.first.name).to eq('feature') expect(result.count).to eq(2) end + + it 'filters branches by name that begins with' do + params = { search: '^feature_' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature_conflict') + expect(result.count).to eq(1) + end + + it 'filters branches by name that ends with' do + params = { search: 'feature$' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature') + expect(result.count).to eq(1) + end end end end diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 8c8cdf8bcb2..44835386cfc 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -20,12 +20,35 @@ "name" ], "properties": { - "name": { "type": "string" } + "name": { "type": "string" }, + "ref_path": { "type": "string" } }, "additionalProperties": false }, "sha": { "type": "string" }, - "tag": { "type": "boolean" } + "tag": { "type": "boolean" }, + "user": { + "oneOf": [ + { "type": "null" }, + { "$ref": "entities/user.json" } + ] + }, + "commit": { + "oneOf": [ + { "type": "null" }, + { "$ref": "entities/commit.json" } + ] + }, + "deployable": { + "oneOf": [ + { "type": "null" }, + { "$ref": "job/job.json" } + ] + }, + "manual_actions": { + "type": "array", + "items": { "$ref": "job/job.json" } + } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/entities/commit.json b/spec/fixtures/api/schemas/entities/commit.json index 686d29c97d2..324702e3f94 100644 --- a/spec/fixtures/api/schemas/entities/commit.json +++ b/spec/fixtures/api/schemas/entities/commit.json @@ -17,11 +17,10 @@ "author": { "oneOf": [ { "type": "null" }, - { "type": "user.json" } + { "$ref": "user.json" } ] } - }, - "additionalProperties": false + } } ] } diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 07e674216fa..8218474705c 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -7,7 +7,8 @@ "artifact", "runner", "runners", - "has_trace" + "has_trace", + "stage" ], "properties": { "artifact": { "$ref": "artifact.json" }, @@ -16,6 +17,7 @@ "deployment_status": { "$ref": "deployment_status.json" }, "runner": { "$ref": "runner.json" }, "runners": { "$ref": "runners.json" }, - "has_trace": { "type": "boolean" } + "has_trace": { "type": "boolean" }, + "stage": { "type": "string" } } } diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 363ebc88afd..c112c8ed633 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -2,6 +2,13 @@ require 'spec_helper' describe PreferencesHelper do describe '#dashboard_choices' do + let(:user) { build(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?).and_return(false) + end + it 'raises an exception when defined choices may be missing' do expect(User).to receive(:dashboards).and_return(foo: 'foo') expect { helper.dashboard_choices }.to raise_error(RuntimeError) diff --git a/spec/helpers/storage_health_helper_spec.rb b/spec/helpers/storage_health_helper_spec.rb deleted file mode 100644 index 874498e6338..00000000000 --- a/spec/helpers/storage_health_helper_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe StorageHealthHelper do - describe '#failing_storage_health_message' do - let(:health) do - Gitlab::Git::Storage::Health.new( - "<script>alert('storage name');)</script>", - [] - ) - end - - it 'escapes storage names' do - escaped_storage_name = '<script>alert('storage name');)</script>' - - result = helper.failing_storage_health_message(health) - - expect(result).to include(escaped_storage_name) - end - end -end diff --git a/spec/javascripts/.eslintrc.yml b/spec/javascripts/.eslintrc.yml index 9b2c84ce9f5..1448849a0b7 100644 --- a/spec/javascripts/.eslintrc.yml +++ b/spec/javascripts/.eslintrc.yml @@ -37,7 +37,5 @@ rules: - 'fixtures/blob' # Temporarily disabled to facilitate an upgrade to eslint-plugin-jasmine jasmine/new-line-before-expect: off - jasmine/new-line-between-declarations: off jasmine/no-promise-without-done-fail: off - jasmine/prefer-jasmine-matcher: off jasmine/prefer-toHaveBeenCalledWith: off diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index ada26b37f4a..3f84a46e8d9 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-expressions, no-unused-vars, prefer-template, max-len */ +/* eslint-disable no-var, one-var, no-unused-expressions, no-unused-vars, prefer-template */ import $ from 'jquery'; import Cookies from 'js-cookie'; @@ -55,6 +55,7 @@ import '~/lib/utils/common_utils'; }); }; }); + afterEach(function() { // restore original url root value gon.relative_url_root = urlRoot; @@ -64,6 +65,7 @@ import '~/lib/utils/common_utils'; awardsHandler.destroy(); }); + describe('::showEmojiMenu', function() { it('should show emoji menu when Add emoji button clicked', function(done) { $('.js-add-award') @@ -78,6 +80,7 @@ import '~/lib/utils/common_utils'; return expect($('.js-awards-block.current').length).toBe(1); }); }); + it('should also show emoji menu for the smiley icon in notes', function(done) { $('.js-add-award.note-action-button').click(); return lazyAssert(done, function() { @@ -85,6 +88,7 @@ import '~/lib/utils/common_utils'; return expect($emojiMenu.length).toBe(1); }); }); + it('should remove emoji menu when body is clicked', function(done) { $('.js-add-award') .eq(0) @@ -98,6 +102,7 @@ import '~/lib/utils/common_utils'; return expect($('.js-awards-block.current').length).toBe(0); }); }); + it('should not remove emoji menu when search is clicked', function(done) { $('.js-add-award') .eq(0) @@ -123,6 +128,7 @@ import '~/lib/utils/common_utils'; expect($emojiButton.next('.js-counter').text()).toBe('1'); return expect($votesBlock.hasClass('hidden')).toBe(false); }); + it('should remove the emoji when we click again', function() { var $emojiButton, $votesBlock; $votesBlock = $('.js-awards-block').eq(0); @@ -142,6 +148,7 @@ import '~/lib/utils/common_utils'; return expect($emojiButton.next('.js-counter').text()).toBe('4'); }); }); + describe('::userAuthored', function() { it('should update tooltip to user authored title', function() { var $thumbsUpEmoji, $votesBlock; @@ -153,6 +160,7 @@ import '~/lib/utils/common_utils'; 'You cannot vote on your own issue, MR and note', ); }); + it('should restore tooltip back to initial vote list', function() { var $thumbsUpEmoji, $votesBlock; jasmine.clock().install(); @@ -165,6 +173,7 @@ import '~/lib/utils/common_utils'; return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); + describe('::getAwardUrl', function() { return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe( @@ -172,6 +181,7 @@ import '~/lib/utils/common_utils'; ); }); }); + describe('::addAward and ::checkMutuality', function() { return it('should handle :+1: and :-1: mutuality', function() { var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl; @@ -189,6 +199,7 @@ import '~/lib/utils/common_utils'; return expect($thumbsDownEmoji.hasClass('active')).toBe(true); }); }); + describe('::removeEmoji', function() { return it('should remove emoji', function() { var $votesBlock, awardUrl; @@ -200,6 +211,7 @@ import '~/lib/utils/common_utils'; return expect($votesBlock.find('[data-name=fire]').length).toBe(0); }); }); + describe('::addYouToUserList', function() { it('should prepend "You" to the award tooltip', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -222,6 +234,7 @@ import '~/lib/utils/common_utils'; return expect($thumbsUpEmoji.data('originalTitle')).toBe('You and sam'); }); }); + describe('::removeYouToUserList', function() { it('removes "You" from the front of the tooltip', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -246,6 +259,7 @@ import '~/lib/utils/common_utils'; return expect($thumbsUpEmoji.data('originalTitle')).toBe('sam'); }); }); + describe('::searchEmojis', () => { it('should filter the emoji', function(done) { return openAndWaitForEmojiMenu() @@ -263,6 +277,7 @@ import '~/lib/utils/common_utils'; done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); + it('should clear the search when searching for nothing', function(done) { return openAndWaitForEmojiMenu() .then(() => { @@ -305,6 +320,7 @@ import '~/lib/utils/common_utils'; done.fail(`Failed to open and build emoji menu: ${err.message}`); }); }); + it('should remove already selected emoji', function(done) { return openEmojiMenuAndAddEmoji() .then(() => { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index d8aa5c636da..b3a5eac8982 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -58,12 +58,14 @@ describe('Quick Submit behavior', function () { expect(submitButton).toBeDisabled(); }); + it('disables button of type submit', () => { const submitButton = $('.js-quick-submit input[type=submit]'); this.textarea.trigger(keydownEvent()); expect(submitButton).toBeDisabled(); }); + it('only clicks one submit', () => { const existingSubmit = $('.js-quick-submit input[type=submit]'); // Add an extra submit button diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 0e4e1697fd0..50505b41313 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import BoardBlankState from '~/boards/components/board_blank_state.vue'; import { mockBoardService } from './mock_data'; @@ -10,7 +10,7 @@ describe('Boards blank state', () => { beforeEach((done) => { const Comp = Vue.extend(BoardBlankState); - gl.issueBoards.BoardsStore.create(); + boardsStore.create(); gl.boardService = mockBoardService(); spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => { @@ -57,7 +57,7 @@ describe('Boards blank state', () => { vm.$el.querySelector('.btn-default').click(); setTimeout(() => { - expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeTruthy(); + expect(boardsStore.welcomeIsHidden()).toBeTruthy(); done(); }); @@ -67,9 +67,9 @@ describe('Boards blank state', () => { vm.$el.querySelector('.btn-success').click(); setTimeout(() => { - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - expect(gl.issueBoards.BoardsStore.state.lists[0].title).toEqual('To Do'); - expect(gl.issueBoards.BoardsStore.state.lists[1].title).toEqual('Doing'); + expect(boardsStore.state.lists.length).toBe(2); + expect(boardsStore.state.lists[0].title).toEqual('To Do'); + expect(boardsStore.state.lists[1].title).toEqual('Doing'); done(); }); @@ -81,8 +81,8 @@ describe('Boards blank state', () => { vm.$el.querySelector('.btn-success').click(); setTimeout(() => { - expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy(); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + expect(boardsStore.welcomeIsHidden()).toBeFalsy(); + expect(boardsStore.state.lists.length).toBe(1); done(); }); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index ad263791cd4..20cfe426807 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -10,7 +10,7 @@ import eventHub from '~/boards/eventhub'; import '~/vue_shared/models/label'; import '~/vue_shared/models/assignee'; import '~/boards/models/list'; -import '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card.vue'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; @@ -23,8 +23,8 @@ describe('Board card', () => { mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); - gl.issueBoards.BoardsStore.create(); - gl.issueBoards.BoardsStore.detail.issue = {}; + boardsStore.create(); + boardsStore.detail.issue = {}; const BoardCardComp = Vue.extend(boardCard); const list = new List(listObj); @@ -62,7 +62,7 @@ describe('Board card', () => { }); it('returns true when detailIssue is equal to card issue', () => { - gl.issueBoards.BoardsStore.detail.issue = vm.issue; + boardsStore.detail.issue = vm.issue; expect(vm.issueDetailVisible).toBe(true); }); @@ -119,19 +119,19 @@ describe('Board card', () => { }); it('does not set detail issue if showDetail is false', () => { - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(boardsStore.detail.issue).toEqual({}); }); it('does not set detail issue if link is clicked', () => { triggerEvent('mouseup', vm.$el.querySelector('a')); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(boardsStore.detail.issue).toEqual({}); }); it('does not set detail issue if button is clicked', () => { triggerEvent('mouseup', vm.$el.querySelector('button')); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(boardsStore.detail.issue).toEqual({}); }); it('does not set detail issue if img is clicked', (done) => { @@ -145,7 +145,7 @@ describe('Board card', () => { Vue.nextTick(() => { triggerEvent('mouseup', vm.$el.querySelector('img')); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(boardsStore.detail.issue).toEqual({}); done(); }); @@ -154,7 +154,7 @@ describe('Board card', () => { it('does not set detail issue if showDetail is false after mouseup', () => { triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(boardsStore.detail.issue).toEqual({}); }); it('sets detail issue to card issue on mouse up', () => { @@ -164,7 +164,7 @@ describe('Board card', () => { triggerEvent('mouseup'); expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue); - expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list); + expect(boardsStore.detail.list).toEqual(vm.list); }); it('adds active class if detail issue is set', (done) => { @@ -181,7 +181,7 @@ describe('Board card', () => { it('resets detail issue to empty if already set', () => { spyOn(eventHub, '$emit'); - gl.issueBoards.BoardsStore.detail.issue = vm.issue; + boardsStore.detail.issue = vm.issue; triggerEvent('mousedown'); triggerEvent('mouseup'); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 290600cf995..037e06cf3b2 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -1,15 +1,15 @@ /* global List */ /* global ListIssue */ + import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Sortable from 'sortablejs'; import BoardList from '~/boards/components/board_list.vue'; import eventHub from '~/boards/eventhub'; -import '~/boards/mixins/sortable_default_options'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; window.Sortable = Sortable; @@ -25,8 +25,7 @@ describe('Board list component', () => { mock = new MockAdapter(axios); mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); - gl.issueBoards.BoardsStore.create(); - gl.IssueBoardsApp = new Vue(); + boardsStore.create(); const BoardListComp = Vue.extend(BoardList); const list = new List(listObj); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 1245e3e099a..9fea625a4ac 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -4,6 +4,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import boardNewIssue from '~/boards/components/board_new_issue.vue'; +import boardsStore from '~/boards/stores/boards_store'; import '~/boards/models/list'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; @@ -36,8 +37,7 @@ describe('Issue boards new issue form', () => { mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); - gl.issueBoards.BoardsStore.create(); - gl.IssueBoardsApp = new Vue(); + boardsStore.create(); list = new List(listObj); @@ -148,13 +148,13 @@ describe('Issue boards new issue form', () => { }); it('sets detail issue after submit', (done) => { - expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe(undefined); + expect(boardsStore.detail.issue.title).toBe(undefined); vm.title = 'submit issue'; Vue.nextTick() .then(submitIssue) .then(() => { - expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); + expect(boardsStore.detail.issue.title).toBe('submit issue'); }) .then(done) .catch(done.fail); @@ -166,7 +166,7 @@ describe('Issue boards new issue form', () => { Vue.nextTick() .then(submitIssue) .then(() => { - expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); + expect(boardsStore.detail.list.id).toBe(list.id); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 1ee6f4cf680..dfd3ea0db66 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, no-unused-vars */ +/* eslint-disable no-unused-vars */ /* global ListIssue */ import Vue from 'vue'; @@ -11,7 +11,7 @@ import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/services/board_service'; -import '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('Store', () => { @@ -21,7 +21,7 @@ describe('Store', () => { mock = new MockAdapter(axios); mock.onAny().reply(boardsMockInterceptor); gl.boardService = mockBoardService(); - gl.issueBoards.BoardsStore.create(); + boardsStore.create(); spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => { resolve(); @@ -38,35 +38,35 @@ describe('Store', () => { }); it('starts with a blank state', () => { - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); + expect(boardsStore.state.lists.length).toBe(0); }); describe('lists', () => { it('creates new list without persisting to DB', () => { - gl.issueBoards.BoardsStore.addList(listObj); + boardsStore.addList(listObj); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + expect(boardsStore.state.lists.length).toBe(1); }); it('finds list by ID', () => { - gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); + boardsStore.addList(listObj); + const list = boardsStore.findList('id', listObj.id); expect(list.id).toBe(listObj.id); }); it('finds list by type', () => { - gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('type', 'label'); + boardsStore.addList(listObj); + const list = boardsStore.findList('type', 'label'); expect(list).toBeDefined(); }); it('gets issue when new list added', (done) => { - gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); + boardsStore.addList(listObj); + const list = boardsStore.findList('id', listObj.id); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + expect(boardsStore.state.lists.length).toBe(1); setTimeout(() => { expect(list.issues.length).toBe(1); @@ -76,7 +76,7 @@ describe('Store', () => { }); it('persists new list', (done) => { - gl.issueBoards.BoardsStore.new({ + boardsStore.new({ title: 'Test', list_type: 'label', label: { @@ -86,10 +86,10 @@ describe('Store', () => { description: 'testing;' } }); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + expect(boardsStore.state.lists.length).toBe(1); setTimeout(() => { - const list = gl.issueBoards.BoardsStore.findList('id', listObj.id); + const list = boardsStore.findList('id', listObj.id); expect(list).toBeDefined(); expect(list.id).toBe(listObj.id); expect(list.position).toBe(0); @@ -98,61 +98,61 @@ describe('Store', () => { }); it('check for blank state adding', () => { - expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); + expect(boardsStore.shouldAddBlankState()).toBe(true); }); it('check for blank state not adding', () => { - gl.issueBoards.BoardsStore.addList(listObj); - expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); + boardsStore.addList(listObj); + expect(boardsStore.shouldAddBlankState()).toBe(false); }); it('check for blank state adding when closed list exist', () => { - gl.issueBoards.BoardsStore.addList({ + boardsStore.addList({ list_type: 'closed' }); - expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); + expect(boardsStore.shouldAddBlankState()).toBe(true); }); it('adds the blank state', () => { - gl.issueBoards.BoardsStore.addBlankState(); + boardsStore.addBlankState(); - const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank'); + const list = boardsStore.findList('type', 'blank', 'blank'); expect(list).toBeDefined(); }); it('removes list from state', () => { - gl.issueBoards.BoardsStore.addList(listObj); + boardsStore.addList(listObj); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + expect(boardsStore.state.lists.length).toBe(1); - gl.issueBoards.BoardsStore.removeList(listObj.id, 'label'); + boardsStore.removeList(listObj.id, 'label'); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); + expect(boardsStore.state.lists.length).toBe(0); }); it('moves the position of lists', () => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj); - const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listOne = boardsStore.addList(listObj); + const listTwo = boardsStore.addList(listObjDuplicate); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + expect(boardsStore.state.lists.length).toBe(2); - gl.issueBoards.BoardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]); + boardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]); expect(listOne.position).toBe(1); }); it('moves an issue from one list to another', (done) => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj); - const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listOne = boardsStore.addList(listObj); + const listTwo = boardsStore.addList(listObjDuplicate); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + expect(boardsStore.state.lists.length).toBe(2); setTimeout(() => { expect(listOne.issues.length).toBe(1); expect(listTwo.issues.length).toBe(1); - gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1)); + boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1)); expect(listOne.issues.length).toBe(0); expect(listTwo.issues.length).toBe(1); @@ -162,19 +162,19 @@ describe('Store', () => { }); it('moves an issue from backlog to a list', (done) => { - const backlog = gl.issueBoards.BoardsStore.addList({ + const backlog = boardsStore.addList({ ...listObj, list_type: 'backlog', }); - const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listTwo = boardsStore.addList(listObjDuplicate); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + expect(boardsStore.state.lists.length).toBe(2); setTimeout(() => { expect(backlog.issues.length).toBe(1); expect(listTwo.issues.length).toBe(1); - gl.issueBoards.BoardsStore.moveIssueToList(backlog, listTwo, backlog.findIssue(1)); + boardsStore.moveIssueToList(backlog, listTwo, backlog.findIssue(1)); expect(backlog.issues.length).toBe(0); expect(listTwo.issues.length).toBe(1); @@ -184,10 +184,10 @@ describe('Store', () => { }); it('moves issue to top of another list', (done) => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj); - const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listOne = boardsStore.addList(listObj); + const listTwo = boardsStore.addList(listObjDuplicate); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + expect(boardsStore.state.lists.length).toBe(2); setTimeout(() => { listOne.issues[0].id = 2; @@ -195,7 +195,7 @@ describe('Store', () => { expect(listOne.issues.length).toBe(1); expect(listTwo.issues.length).toBe(1); - gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0); + boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0); expect(listOne.issues.length).toBe(0); expect(listTwo.issues.length).toBe(2); @@ -207,10 +207,10 @@ describe('Store', () => { }); it('moves issue to bottom of another list', (done) => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj); - const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listOne = boardsStore.addList(listObj); + const listTwo = boardsStore.addList(listObjDuplicate); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + expect(boardsStore.state.lists.length).toBe(2); setTimeout(() => { listOne.issues[0].id = 2; @@ -218,7 +218,7 @@ describe('Store', () => { expect(listOne.issues.length).toBe(1); expect(listTwo.issues.length).toBe(1); - gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1); + boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1); expect(listOne.issues.length).toBe(0); expect(listTwo.issues.length).toBe(2); @@ -238,14 +238,14 @@ describe('Store', () => { labels: [], assignees: [], }); - const list = gl.issueBoards.BoardsStore.addList(listObj); + const list = boardsStore.addList(listObj); setTimeout(() => { list.addIssue(issue); expect(list.issues.length).toBe(2); - gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]); + boardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]); expect(list.issues[0].id).toBe(2); expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index 19346e305cf..4ebd4cecc08 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import '~/boards/services/board_service'; -import '~/boards/components/board'; +import Board from '~/boards/components/board'; import '~/boards/models/list'; import { mockBoardService } from '../mock_data'; @@ -21,7 +21,7 @@ describe('Board component', () => { boardId: 1, }); - vm = new gl.issueBoards.Board({ + vm = new Board({ propsData: { boardId: '1', disabled: false, diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index b6c61e7bad7..58b7d45d913 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -8,7 +8,6 @@ import '~/vue_shared/models/label'; import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/stores/boards_store'; import IssueCardInner from '~/boards/components/issue_card_inner.vue'; import { listObj } from './mock_data'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index db68096e3bd..e8387068831 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -1,4 +1,3 @@ -/* eslint-disable comma-dangle */ /* global ListIssue */ import Vue from 'vue'; @@ -7,7 +6,7 @@ import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/services/board_service'; -import '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import { mockBoardService } from './mock_data'; describe('Issue model', () => { @@ -15,7 +14,7 @@ describe('Issue model', () => { beforeEach(() => { gl.boardService = mockBoardService(); - gl.issueBoards.BoardsStore.create(); + boardsStore.create(); issue = new ListIssue({ title: 'Testing', diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index ac8bbb8f2a8..ba6e81a29a9 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -1,4 +1,3 @@ -/* eslint-disable comma-dangle */ /* global List */ /* global ListIssue */ @@ -10,7 +9,7 @@ import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; import '~/boards/services/board_service'; -import '~/boards/stores/boards_store'; +import boardsStore from '~/boards/stores/boards_store'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; describe('List model', () => { @@ -23,7 +22,7 @@ describe('List model', () => { gl.boardService = mockBoardService({ bulkUpdatePath: '/test/issue-boards/board/1/lists', }); - gl.issueBoards.BoardsStore.create(); + boardsStore.create(); list = new List(listObj); }); @@ -59,13 +58,13 @@ describe('List model', () => { }); it('destroys the list', (done) => { - gl.issueBoards.BoardsStore.addList(listObj); - list = gl.issueBoards.BoardsStore.findList('id', listObj.id); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + boardsStore.addList(listObj); + list = boardsStore.findList('id', listObj.id); + expect(boardsStore.state.lists.length).toBe(1); list.destroy(); setTimeout(() => { - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); + expect(boardsStore.state.lists.length).toBe(0); done(); }, 0); }); diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index f380ef450db..c28e41ec175 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,4 +1,5 @@ -/* global BoardService */ +import BoardService from '~/boards/services/board_service'; + export const listObj = { id: 300, position: 0, diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js index 052465d8d88..cd61d920fa0 100644 --- a/spec/javascripts/bootstrap_jquery_spec.js +++ b/spec/javascripts/bootstrap_jquery_spec.js @@ -9,6 +9,7 @@ import '~/commons/bootstrap'; beforeEach(function() { return setFixtures('<input type="text" />'); }); + it('adds the disabled attribute', function() { var $input; $input = $('input').first(); @@ -26,6 +27,7 @@ import '~/commons/bootstrap'; beforeEach(function() { return setFixtures('<input type="text" disabled="disabled" class="disabled" />'); }); + it('removes the disabled attribute', function() { var $input; $input = $('input').first(); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 6c3e73f134e..24e4871ce44 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -158,9 +158,9 @@ describe('getTimeframeWindowFrom', () => { const timeframe = datetimeUtility.getTimeframeWindowFrom(startDate, 5); expect(timeframe.length).toBe(5); timeframe.forEach((timeframeItem, index) => { - expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBe(true); - expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBe(true); - expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy(); + expect(timeframeItem.getFullYear()).toBe(mockTimeframe[index].getFullYear()); + expect(timeframeItem.getMonth()).toBe(mockTimeframe[index].getMonth()); + expect(timeframeItem.getDate()).toBe(mockTimeframe[index].getDate()); }); }); }); diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js index d6fc6b56b82..c6f2e66cebd 100644 --- a/spec/javascripts/diff_comments_store_spec.js +++ b/spec/javascripts/diff_comments_store_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ +/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown */ /* global CommentsStore */ import '~/diff_notes/models/discussion'; diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index c986ea604b2..1f7d5f42322 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -6,6 +6,8 @@ import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +Vue.use(Vuex); + const discussionFixture = 'merge_requests/diff_discussion.json'; describe('diff_file_header', () => { @@ -58,19 +60,19 @@ describe('diff_file_header', () => { describe('titleLink', () => { beforeEach(() => { + props.discussionPath = 'link://to/discussion'; Object.assign(props.diffFile, { - fileHash: 'badc0ffee', submoduleLink: 'link://to/submodule', submoduleTreeUrl: 'some://tree/url', }); }); - it('returns the fileHash for files', () => { + it('returns the discussionPath for files', () => { props.diffFile.submodule = false; vm = mountComponentWithStore(Component, { props, store }); - expect(vm.titleLink).toBe(`#${props.diffFile.fileHash}`); + expect(vm.titleLink).toBe(props.discussionPath); }); it('returns the submoduleTreeUrl for submodules', () => { @@ -91,6 +93,13 @@ describe('diff_file_header', () => { expect(vm.titleLink).toBe(props.diffFile.submoduleLink); }); + + it('sets the correct path to the discussion', () => { + props.discussionPath = 'link://to/discussion'; + vm = mountComponentWithStore(Component, { props, store }); + const href = vm.$el.querySelector('.js-title-wrapper').getAttribute('href'); + expect(href).toBe(vm.discussionPath); + }); }); describe('filePath', () => { diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 13859f43e98..b8d4b31ee04 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -24,14 +24,14 @@ describe('DiffFile', () => { expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); expect(el.querySelector('.js-file-title')).toBeDefined(); - expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true); + expect(el.querySelector('.file-title-name').innerText.indexOf(filePath)).toBeGreaterThan(-1); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); expect(vm.file.renderIt).toEqual(false); vm.file.renderIt = true; vm.$nextTick(() => { - expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + expect(el.querySelectorAll('.line_content').length).toBeGreaterThan(5); }); }); @@ -98,9 +98,7 @@ describe('DiffFile', () => { 'This source diff could not be displayed because it is too large', ); expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined(); - expect(vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK) > -1).toEqual( - true, - ); + expect(vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK)).toBeGreaterThan(-1); done(); }); diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js index 663c0680845..f36454cc23e 100644 --- a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js +++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js @@ -94,7 +94,7 @@ describe('DiffLineGutterContent', () => { const component = createComponent({ lineNumber, lineCode }); const link = component.$el.querySelector('a'); - expect(link.href.indexOf(`#${lineCode}`) > -1).toEqual(true); + expect(link.href.indexOf(`#${lineCode}`)).toBeGreaterThan(-1); expect(link.dataset.linenumber).toEqual(lineNumber.toString()); }); diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js index b02328dd359..705558e860b 100644 --- a/spec/javascripts/diffs/components/inline_diff_view_spec.js +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -27,7 +27,7 @@ describe('InlineDiffView', () => { expect(el.querySelectorAll('tr.line_holder').length).toEqual(6); expect(el.querySelectorAll('tr.line_holder.new').length).toEqual(2); expect(el.querySelectorAll('tr.line_holder.match').length).toEqual(1); - expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true); + expect(el.textContent.indexOf('Bad dates')).toBeGreaterThan(-1); }); it('should render discussions', done => { @@ -37,7 +37,7 @@ describe('InlineDiffView', () => { Vue.nextTick(() => { expect(el.querySelectorAll('.notes_holder').length).toEqual(1); expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5); - expect(el.innerText.indexOf('comment 5') > -1).toEqual(true); + expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1); component.$store.dispatch('setInitialNotes', []); done(); diff --git a/spec/javascripts/droplab/plugins/input_setter_spec.js b/spec/javascripts/droplab/plugins/input_setter_spec.js index bd625f4ae80..1988811a305 100644 --- a/spec/javascripts/droplab/plugins/input_setter_spec.js +++ b/spec/javascripts/droplab/plugins/input_setter_spec.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - import InputSetter from '~/droplab/plugins/input_setter'; describe('InputSetter', function () { diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js index 124d91f4477..629422780e8 100644 --- a/spec/javascripts/emoji_spec.js +++ b/spec/javascripts/emoji_spec.js @@ -140,6 +140,7 @@ describe('gl_emoji', () => { }, ); }); + it('bomb emoji with sprite fallback', () => { const emojiKey = 'bomb'; const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, { @@ -195,24 +196,31 @@ describe('gl_emoji', () => { it('should gracefully handle empty string', () => { expect(isFlagEmoji('')).toBeFalsy(); }); + it('should detect flag_ac', () => { expect(isFlagEmoji('🇦🇨')).toBeTruthy(); }); + it('should detect flag_us', () => { expect(isFlagEmoji('🇺🇸')).toBeTruthy(); }); + it('should detect flag_zw', () => { expect(isFlagEmoji('🇿🇼')).toBeTruthy(); }); + it('should not detect flags', () => { expect(isFlagEmoji('🎏')).toBeFalsy(); }); + it('should not detect triangular_flag_on_post', () => { expect(isFlagEmoji('🚩')).toBeFalsy(); }); + it('should not detect single letter', () => { expect(isFlagEmoji('🇦')).toBeFalsy(); }); + it('should not detect >2 letters', () => { expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy(); }); @@ -222,15 +230,19 @@ describe('gl_emoji', () => { it('should gracefully handle empty string', () => { expect(isRainbowFlagEmoji('')).toBeFalsy(); }); + it('should detect rainbow_flag', () => { expect(isRainbowFlagEmoji('🏳🌈')).toBeTruthy(); }); + it('should not detect flag_white on its\' own', () => { expect(isRainbowFlagEmoji('🏳')).toBeFalsy(); }); + it('should not detect rainbow on its\' own', () => { expect(isRainbowFlagEmoji('🌈')).toBeFalsy(); }); + it('should not detect flag_white with something else', () => { expect(isRainbowFlagEmoji('🏳🔵')).toBeFalsy(); }); @@ -240,15 +252,19 @@ describe('gl_emoji', () => { it('should gracefully handle empty string', () => { expect(isKeycapEmoji('')).toBeFalsy(); }); + it('should detect one(keycap)', () => { expect(isKeycapEmoji('1️⃣')).toBeTruthy(); }); + it('should detect nine(keycap)', () => { expect(isKeycapEmoji('9️⃣')).toBeTruthy(); }); + it('should not detect ten(keycap)', () => { expect(isKeycapEmoji('🔟')).toBeFalsy(); }); + it('should not detect hash(keycap)', () => { expect(isKeycapEmoji('#⃣')).toBeFalsy(); }); @@ -258,24 +274,31 @@ describe('gl_emoji', () => { it('should gracefully handle empty string', () => { expect(isSkinToneComboEmoji('')).toBeFalsy(); }); + it('should detect hand_splayed_tone5', () => { expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); }); + it('should not detect hand_splayed', () => { expect(isSkinToneComboEmoji('🖐')).toBeFalsy(); }); + it('should detect lifter_tone1', () => { expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy(); }); + it('should not detect lifter', () => { expect(isSkinToneComboEmoji('🏋')).toBeFalsy(); }); + it('should detect rowboat_tone4', () => { expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy(); }); + it('should not detect rowboat', () => { expect(isSkinToneComboEmoji('🚣')).toBeFalsy(); }); + it('should not detect individual tone emoji', () => { expect(isSkinToneComboEmoji('🏻')).toBeFalsy(); }); @@ -285,9 +308,11 @@ describe('gl_emoji', () => { it('should gracefully handle empty string', () => { expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy(); }); + it('should detect horse_racing_tone2', () => { expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); }); + it('should not detect horse_racing', () => { expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy(); }); @@ -297,36 +322,47 @@ describe('gl_emoji', () => { it('should gracefully handle empty string', () => { expect(isPersonZwjEmoji('')).toBeFalsy(); }); + it('should detect couple_mm', () => { expect(isPersonZwjEmoji('👨❤️👨')).toBeTruthy(); }); + it('should not detect couple_with_heart', () => { expect(isPersonZwjEmoji('💑')).toBeFalsy(); }); + it('should not detect couplekiss', () => { expect(isPersonZwjEmoji('💏')).toBeFalsy(); }); + it('should detect family_mmb', () => { expect(isPersonZwjEmoji('👨👨👦')).toBeTruthy(); }); + it('should detect family_mwgb', () => { expect(isPersonZwjEmoji('👨👩👧👦')).toBeTruthy(); }); + it('should not detect family', () => { expect(isPersonZwjEmoji('👪')).toBeFalsy(); }); + it('should detect kiss_ww', () => { expect(isPersonZwjEmoji('👩❤️💋👩')).toBeTruthy(); }); + it('should not detect girl', () => { expect(isPersonZwjEmoji('👧')).toBeFalsy(); }); + it('should not detect girl_tone5', () => { expect(isPersonZwjEmoji('👧🏿')).toBeFalsy(); }); + it('should not detect man', () => { expect(isPersonZwjEmoji('👨')).toBeFalsy(); }); + it('should not detect woman', () => { expect(isPersonZwjEmoji('👩')).toBeFalsy(); }); @@ -341,6 +377,7 @@ describe('gl_emoji', () => { ); expect(isSupported).toBeTruthy(); }); + it('should gracefully handle empty string without unicode support', () => { const isSupported = isEmojiUnicodeSupported( {}, @@ -349,6 +386,7 @@ describe('gl_emoji', () => { ); expect(isSupported).toBeFalsy(); }); + it('bomb(6.0) with 6.0 support', () => { const emojiKey = 'bomb'; const unicodeSupportMap = Object.assign({}, emptySupportMap, { diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 68bbbf838da..3dc8089cd83 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -237,7 +237,7 @@ describe('Dropdown Utils', () => { it('should not linear-gradient more than 4 colors', () => { const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD', '#EEEEEE']); - expect(gradient.indexOf('#EEEEEE') === -1).toEqual(true); + expect(gradient.indexOf('#EEEEEE')).toBe(-1); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index ab0ab72720e..cf7789a1d57 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -19,7 +19,7 @@ describe('Filtered Search Token Keys', () => { describe('get', () => { it('should return tokenKeys', () => { - expect(new FilteredSearchTokenKeys().get() !== null).toBe(true); + expect(new FilteredSearchTokenKeys().get()).not.toBeNull(); }); it('should return tokenKeys as an array', () => { @@ -40,7 +40,7 @@ describe('Filtered Search Token Keys', () => { describe('getConditions', () => { it('should return conditions', () => { - expect(new FilteredSearchTokenKeys().getConditions() !== null).toBe(true); + expect(new FilteredSearchTokenKeys().getConditions()).not.toBeNull(); }); it('should return conditions as an array', () => { @@ -51,7 +51,7 @@ describe('Filtered Search Token Keys', () => { describe('searchByKey', () => { it('should return null when key not found', () => { const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchByKey('notakey'); - expect(tokenKey === null).toBe(true); + expect(tokenKey).toBeNull(); }); it('should return tokenKey when found by key', () => { @@ -63,7 +63,7 @@ describe('Filtered Search Token Keys', () => { describe('searchBySymbol', () => { it('should return null when symbol not found', () => { const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchBySymbol('notasymbol'); - expect(tokenKey === null).toBe(true); + expect(tokenKey).toBeNull(); }); it('should return tokenKey when found by symbol', () => { @@ -75,7 +75,7 @@ describe('Filtered Search Token Keys', () => { describe('searchByKeyParam', () => { it('should return null when key param not found', () => { const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam('notakeyparam'); - expect(tokenKey === null).toBe(true); + expect(tokenKey).toBeNull(); }); it('should return tokenKey when found by key param', () => { @@ -92,7 +92,7 @@ describe('Filtered Search Token Keys', () => { describe('searchByConditionUrl', () => { it('should return null when condition url not found', () => { const condition = new FilteredSearchTokenKeys([], [], conditions).searchByConditionUrl(null); - expect(condition === null).toBe(true); + expect(condition).toBeNull(); }); it('should return condition when found by url', () => { @@ -106,7 +106,7 @@ describe('Filtered Search Token Keys', () => { it('should return null when condition tokenKey and value not found', () => { const condition = new FilteredSearchTokenKeys([], [], conditions) .searchByConditionKeyValue(null, null); - expect(condition === null).toBe(true); + expect(condition).toBeNull(); }); it('should return condition when found by tokenKey and value', () => { diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index af58dff7da7..62c87642184 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, no-param-reassign */ +/* eslint-disable no-param-reassign */ import $ from 'jquery'; import GLDropdown from '~/gl_dropdown'; @@ -168,9 +168,9 @@ describe('glDropdown', function describeDropdown() { it('should show loading indicator while search results are being fetched by backend', () => { const dropdownMenu = document.querySelector('.dropdown-menu'); - expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); + expect(dropdownMenu.className.indexOf('is-loading')).not.toBe(-1); remoteCallback(); - expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); + expect(dropdownMenu.className.indexOf('is-loading')).toBe(-1); }); it('should not focus search input while remote task is not complete', () => { diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index d8a8c8cc260..4a4d6969e86 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ +/* eslint-disable jasmine/no-suite-dupes, vars-on-top, no-var */ + import { scaleLinear, scaleTime } from 'd3-scale'; import { timeParse } from 'd3-time-format'; import { ContributorsGraph, ContributorsMasterGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 22a9afe1a9d..8854d3d554a 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ +/* eslint-disable no-var, camelcase, vars-on-top */ import ContributorsStatGraphUtil from '~/pages/projects/graphs/show/stat_graph_contributors_util'; @@ -205,10 +205,12 @@ describe("ContributorsStatGraphUtil", function () { it("returns true if date_range is null", function () { expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true); }); + it("returns true if date is in range", function () { var date_range = [new Date("2013-01-01"), new Date("2013-12-12")]; expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true); }); + it("returns false if date is not in range", function () { var date_range = [new Date("1999-12-01"), new Date("2000-12-01")]; expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false); diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index d0cac5efc40..49d4f7efd72 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -45,7 +45,7 @@ describe('GroupItemComponent', () => { expect(Object.keys(rowClass).length).toBe(classes.length); Object.keys(rowClass).forEach((className) => { - expect(classes.indexOf(className) > -1).toBeTruthy(); + expect(classes.indexOf(className)).toBeGreaterThan(-1); }); }); }); diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js index 793c4909d89..5af86b55532 100644 --- a/spec/javascripts/groups/components/groups_spec.js +++ b/spec/javascripts/groups/components/groups_spec.js @@ -53,7 +53,7 @@ describe('GroupsComponent', () => { expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); - expect(vm.$el.querySelectorAll('.has-no-search-results').length === 0).toBeTruthy(); + expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0); done(); }); }); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js index ee7ee18259e..e6a57495eb1 100644 --- a/spec/javascripts/groups/components/item_stats_spec.js +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -107,7 +107,7 @@ describe('ItemStatsComponent', () => { const visibilityIconEl = vm.$el.querySelector('.item-visibility'); expect(visibilityIconEl).not.toBe(null); expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); - expect(visibilityIconEl.querySelectorAll('svg').length > 0).toBeTruthy(); + expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); vm.$destroy(); }); @@ -120,10 +120,10 @@ describe('ItemStatsComponent', () => { const vm = createComponent(item); const projectStarIconEl = vm.$el.querySelector('.project-stars'); - expect(projectStarIconEl).not.toBe(null); - expect(projectStarIconEl.querySelectorAll('svg').length > 0).toBeTruthy(); - expect(projectStarIconEl.querySelectorAll('.stat-value').length > 0).toBeTruthy(); - expect(vm.$el.querySelectorAll('.last-updated').length > 0).toBeTruthy(); + expect(projectStarIconEl).not.toBeNull(); + expect(projectStarIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); + expect(projectStarIconEl.querySelectorAll('.stat-value').length).toBeGreaterThan(0); + expect(vm.$el.querySelectorAll('.last-updated').length).toBeGreaterThan(0); vm.$destroy(); }); diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js index 5e35ae4d36c..2a995e8efe6 100644 --- a/spec/javascripts/groups/components/item_stats_value_spec.js +++ b/spec/javascripts/groups/components/item_stats_value_spec.js @@ -57,8 +57,8 @@ describe('ItemStatsValueComponent', () => { it('renders component element correctly', () => { expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy(); - expect(vm.$el.querySelectorAll('svg').length > 0).toBeTruthy(); - expect(vm.$el.querySelectorAll('.stat-value').length > 0).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length).toBeGreaterThan(0); + expect(vm.$el.querySelectorAll('.stat-value').length).toBeGreaterThan(0); }); it('renders element tooltip correctly', () => { diff --git a/spec/javascripts/groups/store/groups_store_spec.js b/spec/javascripts/groups/store/groups_store_spec.js index d74f38f476e..78caf8f80bf 100644 --- a/spec/javascripts/groups/store/groups_store_spec.js +++ b/spec/javascripts/groups/store/groups_store_spec.js @@ -29,7 +29,7 @@ describe('ProjectsStore', () => { store.setGroups(mockGroups); expect(store.state.groups.length).toBe(mockGroups.length); expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); - expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(store.state.groups[0]).indexOf('fullName')).toBeGreaterThan(-1); }); }); @@ -41,8 +41,8 @@ describe('ProjectsStore', () => { store.setSearchedGroups(mockSearchedGroups); expect(store.state.groups.length).toBe(mockSearchedGroups.length); expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); - expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy(); - expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(store.state.groups[0]).indexOf('fullName')).toBeGreaterThan(-1); + expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName')).toBeGreaterThan(-1); }); }); @@ -54,7 +54,7 @@ describe('ProjectsStore', () => { store.setGroupChildren(mockParentGroupItem, mockRawChildren); expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object)); expect(mockParentGroupItem.children.length).toBe(1); - expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName')).toBeGreaterThan(-1); expect(mockParentGroupItem.isOpen).toBeTruthy(); expect(mockParentGroupItem.isChildrenLoading).toBeFalsy(); }); @@ -81,14 +81,14 @@ describe('ProjectsStore', () => { store = new GroupsStore(); updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); - expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1); expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count); expect(updatedGroupItem.isChildrenLoading).toBe(false); expect(updatedGroupItem.isBeingRemoved).toBe(false); store = new GroupsStore(true); updatedGroupItem = store.formatGroupItem(mockRawChildren[0]); - expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy(); + expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1); expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count); }); }); diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js index a284b981d2a..9d05c6859df 100644 --- a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js +++ b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js @@ -32,7 +32,7 @@ describe('commentIndicatorHelper', () => { expect(svgEl).toBeDefined(); const svgLink = svgEl.querySelector('use').getAttribute('xlink:href'); - expect(svgLink.indexOf('image-comment-dark') !== -1).toEqual(true); + expect(svgLink.indexOf('image-comment-dark')).not.toBe(-1); }); }); }); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index e12419b835d..62c71e00334 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle */ +/* eslint-disable one-var, no-use-before-define */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index e02eb9723fe..6e0bcf801cd 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -41,7 +41,7 @@ describe('Job App ', () => { }; const props = { - runnerHelpUrl: 'help/runners', + runnerSettingsUrl: 'settings/ci-cd/runners', }; beforeEach(() => { @@ -223,7 +223,6 @@ describe('Job App ', () => { store.dispatch( 'receiveJobSuccess', Object.assign({}, job, { - erased: true, erased_by: { username: 'root', web_url: 'gitlab.com/root', @@ -237,18 +236,18 @@ describe('Job App ', () => { store, }); - expect(vm.$el.querySelector('.js-job-erased')).not.toBeNull(); + expect(vm.$el.querySelector('.js-job-erased-block')).not.toBeNull(); }); it('does not render erased block when `erased` is false', () => { - store.dispatch('receiveJobSuccess', Object.assign({}, job, { erased: false })); + store.dispatch('receiveJobSuccess', Object.assign({}, job, { erased_at: null })); vm = mountComponentWithStore(Component, { props, store, }); - expect(vm.$el.querySelector('.js-job-erased')).toBeNull(); + expect(vm.$el.querySelector('.js-job-erased-block')).toBeNull(); }); }); diff --git a/spec/javascripts/jobs/components/job_log_controllers_spec.js b/spec/javascripts/jobs/components/job_log_controllers_spec.js index 099aca602c4..3dcb57d23ce 100644 --- a/spec/javascripts/jobs/components/job_log_controllers_spec.js +++ b/spec/javascripts/jobs/components/job_log_controllers_spec.js @@ -25,6 +25,7 @@ describe('Job log controllers', () => { beforeEach(() => { vm = mountComponent(Component, props); }); + it('renders size information', () => { expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB'); }); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 2f5c4245ced..a113377b19f 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -161,9 +161,9 @@ describe('Sidebar details block', () => { vm = mountComponentWithStore(SidebarComponent, { store }); }); - it('renders first stage as selected', () => { + it('renders value provided as selectedStage as selected', () => { expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual( - stages[0].name, + vm.selectedStage, ); }); }); diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js index aa6cc0f1b1a..fcff78b943e 100644 --- a/spec/javascripts/jobs/components/stages_dropdown_spec.js +++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import component from '~/jobs/components/stages_dropdown.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; -describe('Artifacts block', () => { +describe('Stages Dropdown', () => { const Component = Vue.extend(component); let vm; @@ -23,10 +23,6 @@ describe('Artifacts block', () => { }, path: 'pipeline/28029444', }, - ref: { - path: 'commits/50101-truncated-job-information', - name: '50101-truncated-job-information', - }, stages: [ { name: 'build', @@ -35,6 +31,7 @@ describe('Artifacts block', () => { name: 'test', }, ], + selectedStage: 'deploy' }); }); @@ -53,17 +50,10 @@ describe('Artifacts block', () => { }); it('renders dropdown with stages', () => { - expect(vm.$el.querySelector('.dropdown button').textContent).toContain('build'); + expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build'); }); - it('updates selected stage on click', done => { - vm.$el.querySelectorAll('.stage-item')[1].click(); - vm - .$nextTick() - .then(() => { - expect(vm.$el.querySelector('.dropdown button').textContent).toContain('test'); - }) - .then(done) - .catch(done.fail); + it('rendes selected stage', () => { + expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy'); }); }); diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js index 5ab1f75d0d6..bc410ae614c 100644 --- a/spec/javascripts/jobs/store/actions_spec.js +++ b/spec/javascripts/jobs/store/actions_spec.js @@ -422,8 +422,9 @@ describe('Job State actions', () => { beforeEach(() => { mockedState.job.pipeline = { - path: `${TEST_HOST}/endpoint.json/stages`, + path: `${TEST_HOST}/endpoint`, }; + mockedState.selectedStage = 'deploy' mock = new MockAdapter(axios); }); @@ -434,8 +435,8 @@ describe('Job State actions', () => { describe('success', () => { it('dispatches requestStages and receiveStagesSuccess, fetchJobsForStage ', done => { mock - .onGet(`${TEST_HOST}/endpoint.json/stages`) - .replyOnce(200, { details: { stages: [{ id: 121212, name: 'build' }] } }); + .onGet(`${TEST_HOST}/endpoint.json`) + .replyOnce(200, { details: { stages: [{ name: 'build' }, { name: 'deploy' }] } }); testAction( fetchStages, @@ -447,11 +448,11 @@ describe('Job State actions', () => { type: 'requestStages', }, { - payload: [{ id: 121212, name: 'build' }], + payload: [{ name: 'build' }, { name: 'deploy' }], type: 'receiveStagesSuccess', }, { - payload: { id: 121212, name: 'build' }, + payload: { name: 'deploy' }, type: 'fetchJobsForStage', }, ], @@ -515,9 +516,9 @@ describe('Job State actions', () => { it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => { testAction( requestJobsForStage, - null, + { name: 'deploy' }, mockedState, - [{ type: types.REQUEST_JOBS_FOR_STAGE }], + [{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }], [], done, ); @@ -549,6 +550,7 @@ describe('Job State actions', () => { [ { type: 'requestJobsForStage', + payload: { dropdown_path: `${TEST_HOST}/jobs.json` }, }, { payload: [{ id: 121212, name: 'build' }], @@ -574,6 +576,7 @@ describe('Job State actions', () => { [ { type: 'requestJobsForStage', + payload: { dropdown_path: `${TEST_HOST}/jobs.json` }, }, { type: 'receiveJobsForStageError', diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 160b2f4b34a..e262a47b837 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -77,18 +77,18 @@ describe('Job Store Getters', () => { }); }); - describe('jobHasStarted', () => { - describe('when started equals false', () => { + describe('shouldRenderTriggeredLabel', () => { + describe('when started equals null', () => { it('returns false', () => { - localState.job.started = false; - expect(getters.jobHasStarted(localState)).toEqual(false); + localState.job.started = null; + expect(getters.shouldRenderTriggeredLabel(localState)).toEqual(false); }); }); describe('when started equals string', () => { it('returns true', () => { localState.job.started = '2018-08-31T16:20:49.023Z'; - expect(getters.jobHasStarted(localState)).toEqual(true); + expect(getters.shouldRenderTriggeredLabel(localState)).toEqual(true); }); }); }); diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js index 9ba543d32a8..701fcc7f4c8 100644 --- a/spec/javascripts/jobs/store/mutations_spec.js +++ b/spec/javascripts/jobs/store/mutations_spec.js @@ -108,21 +108,33 @@ describe('Jobs Store Mutations', () => { }); describe('RECEIVE_JOB_SUCCESS', () => { - beforeEach(() => { - mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); - }); - it('sets is loading to false', () => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); expect(stateCopy.isLoading).toEqual(false); }); it('sets hasError to false', () => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); expect(stateCopy.hasError).toEqual(false); }); it('sets job data', () => { + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321 }); expect(stateCopy.job).toEqual({ id: 1312321 }); }); + + it('sets selectedStage when the selectedStage is More', () => { + expect(stateCopy.selectedStage).toEqual('More'); + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); + expect(stateCopy.selectedStage).toEqual('deploy'); + }); + + it('does not set selectedStage when the selectedStage is not More', () => { + stateCopy.selectedStage = 'notify' + expect(stateCopy.selectedStage).toEqual('notify'); + mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); + expect(stateCopy.selectedStage).toEqual('notify'); + }); }); describe('RECEIVE_JOB_ERROR', () => { @@ -200,9 +212,14 @@ describe('Jobs Store Mutations', () => { describe('REQUEST_JOBS_FOR_STAGE', () => { it('sets isLoadingStages to true', () => { - mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy); + mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy, { name: 'deploy' }); expect(stateCopy.isLoadingJobs).toEqual(true); }); + + it('sets selectedStage', () => { + mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy, { name: 'deploy' }); + expect(stateCopy.selectedStage).toEqual('deploy'); + }) }); describe('RECEIVE_JOBS_FOR_STAGE_SUCCESS', () => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 28151c7e658..c34622203f7 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -9,6 +9,7 @@ describe('common_utils', () => { it('returns an anchor tag with url', () => { expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); }); + it('url is escaped', () => { // IE11 will return a relative pathname while other browsers will return a full pathname. // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor @@ -42,7 +43,7 @@ describe('common_utils', () => { it('should remove the question mark from the search params', () => { const paramsArray = commonUtils.urlParamsToArray('?test=thing'); - expect(paramsArray[0][0] !== '?').toBe(true); + expect(paramsArray[0][0]).not.toBe('?'); }); }); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 8cf0017f4d8..15bb78032b2 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, quotes, prefer-template, no-else-return, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ +/* eslint-disable no-var, prefer-template, no-else-return, dot-notation, no-return-assign, no-new, one-var, no-underscore-dangle */ import $ from 'jquery'; import LineHighlighter from '~/line_highlighter'; @@ -23,17 +23,20 @@ import LineHighlighter from '~/line_highlighter'; __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {}) }; }); + describe('behavior', function() { it('highlights one line given in the URL hash', function() { new LineHighlighter({ hash: '#L13' }); return expect($('#LC13')).toHaveClass(this.css); }); + it('highlights one line given in the URL hash with given CSS class name', function() { const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' }); expect(hiliter.highlightLineClass).toBe('hilite'); expect($('#LC13')).toHaveClass('hilite'); expect($('#LC13')).not.toHaveClass('hll'); }); + it('highlights a range of lines given in the URL hash', function() { var line, results; new LineHighlighter({ hash: '#L5-25' }); @@ -44,18 +47,21 @@ import LineHighlighter from '~/line_highlighter'; } return results; }); + it('scrolls to the first highlighted line on initial load', function() { var spy; spy = spyOn($, 'scrollTo'); new LineHighlighter({ hash: '#L5-25' }); return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); }); + it('discards click events', function() { var spy; spy = spyOnEvent('a[data-line-number]', 'click'); clickLine(13); return expect(spy).toHaveBeenPrevented(); }); + it('handles garbage input from the hash', function() { var func; func = function() { @@ -64,6 +70,7 @@ import LineHighlighter from '~/line_highlighter'; return expect(func).not.toThrow(); }); }); + describe('clickHandler', function() { it('handles clicking on a child icon element', function() { var spy; @@ -72,11 +79,13 @@ import LineHighlighter from '~/line_highlighter'; expect(spy).toHaveBeenCalledWith(13); return expect($('#LC13')).toHaveClass(this.css); }); + describe('without shiftKey', function() { it('highlights one line when clicked', function() { clickLine(13); return expect($('#LC13')).toHaveClass(this.css); }); + it('unhighlights previously highlighted lines', function() { clickLine(13); clickLine(20); @@ -101,6 +110,7 @@ import LineHighlighter from '~/line_highlighter'; expect(spy).toHaveBeenCalledWith(13); return expect(spy).toHaveBeenCalledWith(13, 20); }); + describe('without existing highlight', function() { it('highlights the clicked line', function() { clickLine(13, { @@ -118,6 +128,7 @@ import LineHighlighter from '~/line_highlighter'; return expect(spy).toHaveBeenCalledWith(13); }); }); + describe('with existing single-line highlight', function() { it('uses existing line as last line when target is lesser', function() { var line, results; @@ -155,6 +166,7 @@ import LineHighlighter from '~/line_highlighter'; shiftKey: true }); }); + it('uses target as first line when it is less than existing first line', function() { var line, results; clickLine(5, { @@ -182,13 +194,16 @@ import LineHighlighter from '~/line_highlighter'; }); }); }); + describe('hashToRange', function() { beforeEach(function() { return this.subject = this["class"].hashToRange; }); + it('extracts a single line number from the hash', function() { return expect(this.subject('#L5')).toEqual([5, null]); }); + it('extracts a range of line numbers from the hash', function() { return expect(this.subject('#L5-15')).toEqual([5, 15]); }); @@ -196,10 +211,12 @@ import LineHighlighter from '~/line_highlighter'; return expect(this.subject('#foo')).toEqual([null, null]); }); }); + describe('highlightLine', function() { beforeEach(function() { return this.subject = this["class"].highlightLine; }); + it('highlights the specified line', function() { this.subject(13); return expect($('#LC13')).toHaveClass(this.css); @@ -213,6 +230,7 @@ import LineHighlighter from '~/line_highlighter'; beforeEach(function() { return this.subject = this["class"].setHash; }); + it('sets the location hash for a single line', function() { this.subject(5); return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5'); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 122e5bc58b2..85419e640d8 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ +/* eslint-disable one-var, no-var, no-return-assign */ import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; @@ -21,18 +21,22 @@ import NewBranchForm from '~/new_branch_form'; }); return this.form = new NewBranchForm($('.js-create-branch-form'), []); }); + it("can't start with a dot", function() { fillNameWith('.foo'); return expectToHaveError("can't start with '.'"); }); + it("can't start with a slash", function() { fillNameWith('/foo'); return expectToHaveError("can't start with '/'"); }); + it("can't have two consecutive dots", function() { fillNameWith('foo..bar'); return expectToHaveError("can't contain '..'"); }); + it("can't have spaces anywhere", function() { fillNameWith(' foo'); expectToHaveError("can't contain spaces"); @@ -41,6 +45,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo '); return expectToHaveError("can't contain spaces"); }); + it("can't have ~ anywhere", function() { fillNameWith('~foo'); expectToHaveError("can't contain '~'"); @@ -49,6 +54,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo~'); return expectToHaveError("can't contain '~'"); }); + it("can't have tilde anwhere", function() { fillNameWith('~foo'); expectToHaveError("can't contain '~'"); @@ -57,6 +63,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo~'); return expectToHaveError("can't contain '~'"); }); + it("can't have caret anywhere", function() { fillNameWith('^foo'); expectToHaveError("can't contain '^'"); @@ -65,6 +72,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo^'); return expectToHaveError("can't contain '^'"); }); + it("can't have : anywhere", function() { fillNameWith(':foo'); expectToHaveError("can't contain ':'"); @@ -73,6 +81,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith(':foo'); return expectToHaveError("can't contain ':'"); }); + it("can't have question mark anywhere", function() { fillNameWith('?foo'); expectToHaveError("can't contain '?'"); @@ -81,6 +90,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo?'); return expectToHaveError("can't contain '?'"); }); + it("can't have asterisk anywhere", function() { fillNameWith('*foo'); expectToHaveError("can't contain '*'"); @@ -89,6 +99,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo*'); return expectToHaveError("can't contain '*'"); }); + it("can't have open bracket anywhere", function() { fillNameWith('[foo'); expectToHaveError("can't contain '['"); @@ -97,6 +108,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo['); return expectToHaveError("can't contain '['"); }); + it("can't have a backslash anywhere", function() { fillNameWith('\\foo'); expectToHaveError("can't contain '\\'"); @@ -105,6 +117,7 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo\\'); return expectToHaveError("can't contain '\\'"); }); + it("can't contain a sequence @{ anywhere", function() { fillNameWith('@{foo'); expectToHaveError("can't contain '@{'"); @@ -113,48 +126,59 @@ import NewBranchForm from '~/new_branch_form'; fillNameWith('foo@{'); return expectToHaveError("can't contain '@{'"); }); + it("can't have consecutive slashes", function() { fillNameWith('foo//bar'); return expectToHaveError("can't contain consecutive slashes"); }); + it("can't end with a slash", function() { fillNameWith('foo/'); return expectToHaveError("can't end in '/'"); }); + it("can't end with a dot", function() { fillNameWith('foo.'); return expectToHaveError("can't end in '.'"); }); + it("can't end with .lock", function() { fillNameWith('foo.lock'); return expectToHaveError("can't end in '.lock'"); }); + it("can't be the single character @", function() { fillNameWith('@'); return expectToHaveError("can't be '@'"); }); + it("concatenates all error messages", function() { fillNameWith('/foo bar?~.'); return expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'"); }); + it("doesn't duplicate error messages", function() { fillNameWith('?foo?bar?zoo?'); return expectToHaveError("can't contain '?'"); }); + it("removes the error message when is a valid name", function() { fillNameWith('foo?bar'); expect($('.js-branch-name-error span').length).toEqual(1); fillNameWith('foobar'); return expect($('.js-branch-name-error span').length).toEqual(0); }); + it("can have dashes anywhere", function() { fillNameWith('-foo-bar-zoo-'); return expect($('.js-branch-name-error span').length).toEqual(0); }); + it("can have underscores anywhere", function() { fillNameWith('_foo_bar_zoo_'); return expect($('.js-branch-name-error span').length).toEqual(0); }); + it("can have numbers anywhere", function() { fillNameWith('1foo2bar3zoo4'); return expect($('.js-branch-name-error span').length).toEqual(0); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 147ffcf1b81..eefd9ddd63c 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -100,6 +100,7 @@ describe('issue_note_form component', () => { expect(vm.handleUpdate).toHaveBeenCalled(); }); + it('should save note when ctrl+enter is pressed', () => { spyOn(vm, 'handleUpdate').and.callThrough(); vm.$el.querySelector('textarea').value = 'Foo'; diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index faeedae40e9..7bfbca83c77 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -538,7 +538,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; mockNotesPost(); $('.js-comment-button').click(); - expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true); + expect($notesContainer.find('.note.being-posted').length).toBeGreaterThan(0); }); it('should remove placeholder note when new comment is done posting', done => { @@ -582,7 +582,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $('.js-comment-button').click(); setTimeout(() => { - expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); + expect($notesContainer.find(`#note_${note.id}`).length).toBeGreaterThan(0); done(); }); @@ -776,7 +776,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; $form.find('textarea.js-note-text').val(sampleComment); const { formData, formContent, formAction } = this.notes.getFormData($form); - expect(formData.indexOf(sampleComment) > -1).toBe(true); + expect(formData.indexOf(sampleComment)).toBeGreaterThan(-1); expect(formContent).toEqual(sampleComment); expect(formAction).toEqual($form.attr('action')); }); diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js index 6f6eb161d14..bf11dbea386 100644 --- a/spec/javascripts/reports/components/report_section_spec.js +++ b/spec/javascripts/reports/components/report_section_spec.js @@ -86,6 +86,7 @@ describe('Report section', () => { }); }); }); + describe('when it is loading', () => { it('should render loading indicator', () => { vm = mountComponent(ReportSection, { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index c7190ea9960..936e8f16c52 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-return-assign, vars-on-top, jasmine/no-unsafe-spy, max-len */ +/* eslint-disable no-var, one-var, no-return-assign, vars-on-top, jasmine/no-unsafe-spy */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; @@ -60,10 +60,12 @@ import Sidebar from '~/right_sidebar'; $toggle.click(); assertSidebarState('expanded'); }); + it('should float over the page and when sidebar icons clicked', function() { $labelsIcon.click(); return assertSidebarState('expanded'); }); + it('should collapse when the icon arrow clicked while it is floating on page', function() { $labelsIcon.click(); assertSidebarState('expanded'); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 646d843162c..bc1bb50dc5c 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, object-shorthand, prefer-template, vars-on-top, max-len */ +/* eslint-disable no-var, one-var, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, object-shorthand, prefer-template, vars-on-top */ import $ from 'jquery'; import '~/gl_dropdown'; @@ -140,6 +140,7 @@ describe('Search autocomplete dropdown', () => { removeBodyAttributes(); window.gon = {}; }); + it('should show Dashboard specific dropdown menu', function() { var list; addBodyAttributes(); @@ -148,6 +149,7 @@ describe('Search autocomplete dropdown', () => { list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); }); + it('should show Group specific dropdown menu', function() { var list; addBodyAttributes('group'); @@ -156,6 +158,7 @@ describe('Search autocomplete dropdown', () => { list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, groupIssuesPath, groupMRsPath); }); + it('should show Project specific dropdown menu', function() { var list; addBodyAttributes('project'); @@ -164,6 +167,7 @@ describe('Search autocomplete dropdown', () => { list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, projectIssuesPath, projectMRsPath); }); + it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() { addBodyAttributes('project'); disableProjectIssues(); @@ -172,6 +176,7 @@ describe('Search autocomplete dropdown', () => { const list = widget.wrap.find('.dropdown-menu').find('ul'); assertLinks(list, null, projectMRsPath); }); + it('should not show category related menu if there is text in the input', function() { var link, list; addBodyAttributes('project'); @@ -182,6 +187,7 @@ describe('Search autocomplete dropdown', () => { link = "a[href='" + projectIssuesPath + '/?assignee_id=' + userId + "']"; return expect(list.find(link).length).toBe(0); }); + it('should not submit the search form when selecting an autocomplete row with the keyboard', function() { var ENTER = 13; var DOWN = 40; diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 1c3dac3584e..512be88c24c 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, no-return-assign, quotes */ +/* eslint-disable no-var, no-return-assign */ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; @@ -25,6 +25,7 @@ describe('Syntax Highlighter', function() { beforeEach(function() { return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); }); + it('applies highlighting to all applicable children', function() { stubUserColorScheme('monokai'); syntaxHighlight($('.parent')); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 012a1cefbbf..a8692be3546 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,4 +1,4 @@ -/* eslint-disable wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign */ +/* eslint-disable no-unused-expressions, no-return-assign, no-param-reassign */ export default class MockU2FDevice { constructor() { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index 91e81a0675a..305cee94f57 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -137,7 +137,7 @@ describe('MemoryUsage', () => { } = vm; expect(hasMetrics).toBeTruthy(); - expect(memoryMetrics.length > 0).toBeTruthy(); + expect(memoryMetrics.length).toBeGreaterThan(0); expect(deploymentTime).toEqual(deployment_time); expect(memoryFrom).toEqual('9.13'); expect(memoryTo).toEqual('4.28'); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index efa5c878678..033cb694249 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -157,6 +157,16 @@ describe('MRWidgetMerged', () => { expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.mergeCommitSha); }); + it('hides button to copy commit SHA if SHA does not exist', (done) => { + vm.mr.mergeCommitSha = null; + + Vue.nextTick(() => { + expect(selectors.copyMergeShaButton).not.toExist(); + expect(vm.$el.querySelector('.mr-info-list').innerText).not.toContain('with'); + done(); + }); + }); + it('shows merge commit SHA link', () => { expect(selectors.mergeCommitShaLink).toExist(); expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha); diff --git a/spec/javascripts/vue_shared/components/memory_graph_spec.js b/spec/javascripts/vue_shared/components/memory_graph_spec.js index 65d8ed39ade..0982b3e1f38 100644 --- a/spec/javascripts/vue_shared/components/memory_graph_spec.js +++ b/spec/javascripts/vue_shared/components/memory_graph_spec.js @@ -52,8 +52,8 @@ describe('MemoryGraph', () => { it('should show human readable median value based on provided median timestamp', () => { vm.deploymentTime = mockMedian; const formattedMedian = vm.getFormattedMedian; - expect(formattedMedian.indexOf('Deployed') > -1).toBeTruthy(); - expect(formattedMedian.indexOf('ago') > -1).toBeTruthy(); + expect(formattedMedian.indexOf('Deployed')).toBeGreaterThan(-1); + expect(formattedMedian.indexOf('ago')).toBeGreaterThan(-1); }); }); }); diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 18658588a40..4d5081b0a75 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -49,8 +49,6 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end it 'only connects to redis twice' do - # Stub circuitbreaker so it doesn't count the redis connections in there - stub_circuit_breaker(project_without_status) expect(Gitlab::Redis::Cache).to receive(:with).exactly(2).and_call_original described_class.load_in_batch_for_projects([project_without_status]) @@ -302,13 +300,4 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end end end - - def stub_circuit_breaker(project) - fake_circuitbreaker = double - allow(fake_circuitbreaker).to receive(:perform).and_yield - allow(project.repository.raw_repository) - .to receive(:circuit_breaker).and_return(fake_circuitbreaker) - allow(project.repository) - .to receive(:circuit_breaker).and_return(fake_circuitbreaker) - end end diff --git a/spec/lib/gitlab/git/storage/checker_spec.rb b/spec/lib/gitlab/git/storage/checker_spec.rb deleted file mode 100644 index d74c3bcb04c..00000000000 --- a/spec/lib/gitlab/git/storage/checker_spec.rb +++ /dev/null @@ -1,132 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Storage::Checker, :clean_gitlab_redis_shared_state do - let(:storage_name) { 'default' } - let(:hostname) { Gitlab::Environment.hostname } - let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } - - subject(:checker) { described_class.new(storage_name) } - - def value_from_redis(name) - Gitlab::Git::Storage.redis.with do |redis| - redis.hmget(cache_key, name) - end.first - end - - def set_in_redis(name, value) - Gitlab::Git::Storage.redis.with do |redis| - redis.hmset(cache_key, name, value) - end.first - end - - describe '.check_all' do - it 'calls a check for each storage' do - fake_checker_default = double - fake_checker_broken = double - fake_logger = fake_logger - - expect(described_class).to receive(:new).with('default', fake_logger) { fake_checker_default } - expect(described_class).to receive(:new).with('broken', fake_logger) { fake_checker_broken } - expect(fake_checker_default).to receive(:check_with_lease) - expect(fake_checker_broken).to receive(:check_with_lease) - - described_class.check_all(fake_logger) - end - - context 'with broken storage', :broken_storage do - it 'returns the results' do - expected_result = [ - { storage: 'default', success: true }, - { storage: 'broken', success: false } - ] - - expect(described_class.check_all).to eq(expected_result) - end - end - end - - describe '#initialize' do - it 'assigns the settings' do - expect(checker.hostname).to eq(hostname) - expect(checker.storage).to eq('default') - expect(checker.storage_path).to eq(TestEnv.repos_path) - end - end - - describe '#check_with_lease' do - it 'only allows one check at a time' do - expect(checker).to receive(:check).once { sleep 1 } - - thread = Thread.new { checker.check_with_lease } - checker.check_with_lease - thread.join - end - - it 'returns a result hash' do - expect(checker.check_with_lease).to eq(storage: 'default', success: true) - end - end - - describe '#check' do - it 'tracks that the storage was accessible' do - set_in_redis(:failure_count, 10) - set_in_redis(:last_failure, Time.now.to_f) - - checker.check - - expect(value_from_redis(:failure_count).to_i).to eq(0) - expect(value_from_redis(:last_failure)).to be_empty - expect(value_from_redis(:first_failure)).to be_empty - end - - it 'calls the check with the correct arguments' do - stub_application_setting(circuitbreaker_storage_timeout: 30, - circuitbreaker_access_retries: 3) - - expect(Gitlab::Git::Storage::ForkedStorageCheck) - .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3) - .and_call_original - - checker.check - end - - it 'returns `true`' do - expect(checker.check).to eq(true) - end - - it 'maintains known storage keys' do - Timecop.freeze do - # Insert an old key to expire - old_entry = Time.now.to_i - 3.days.to_i - Gitlab::Git::Storage.redis.with do |redis| - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, old_entry, 'to_be_removed') - end - - checker.check - - known_keys = Gitlab::Git::Storage.redis.with do |redis| - redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) - end - - expect(known_keys).to contain_exactly(cache_key) - end - end - - context 'the storage is not available', :broken_storage do - let(:storage_name) { 'broken' } - - it 'tracks that the storage was inaccessible' do - Timecop.freeze do - expect { checker.check }.to change { value_from_redis(:failure_count).to_i }.by(1) - - expect(value_from_redis(:last_failure)).not_to be_empty - expect(value_from_redis(:first_failure)).not_to be_empty - end - end - - it 'returns `false`' do - expect(checker.check).to eq(false) - end - end - end -end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb deleted file mode 100644 index 210b90bfba9..00000000000 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Storage::CircuitBreaker, :broken_storage do - let(:storage_name) { 'default' } - let(:circuit_breaker) { described_class.new(storage_name, hostname) } - let(:hostname) { Gitlab::Environment.hostname } - let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } - - def set_in_redis(name, value) - Gitlab::Git::Storage.redis.with do |redis| - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) - redis.hmset(cache_key, name, value) - end.first - end - - before do - # Override test-settings for the circuitbreaker with something more realistic - # for these specs. - stub_storage_settings('default' => { - 'path' => TestEnv.repos_path - }, - 'broken' => { - 'path' => 'tmp/tests/non-existent-repositories' - }, - 'nopath' => { 'path' => nil } - ) - end - - describe '.for_storage', :request_store do - it 'only builds a single circuitbreaker per storage' do - expect(described_class).to receive(:new).once.and_call_original - - breaker = described_class.for_storage('default') - - expect(breaker).to be_a(described_class) - expect(described_class.for_storage('default')).to eq(breaker) - end - - it 'returns a broken circuit breaker for an unknown storage' do - expect(described_class.for_storage('unknown').circuit_broken?).to be_truthy - end - - it 'returns a broken circuit breaker when the path is not set' do - expect(described_class.for_storage('nopath').circuit_broken?).to be_truthy - end - end - - describe '#initialize' do - it 'assigns the settings' do - expect(circuit_breaker.hostname).to eq(hostname) - expect(circuit_breaker.storage).to eq('default') - end - end - - context 'circuitbreaker settings' do - before do - stub_application_setting(circuitbreaker_failure_count_threshold: 0, - circuitbreaker_failure_wait_time: 1, - circuitbreaker_failure_reset_time: 2, - circuitbreaker_storage_timeout: 3, - circuitbreaker_access_retries: 4, - circuitbreaker_backoff_threshold: 5) - end - - describe '#failure_count_threshold' do - it 'reads the value from settings' do - expect(circuit_breaker.failure_count_threshold).to eq(0) - end - end - - describe '#check_interval' do - it 'reads the value from settings' do - expect(circuit_breaker.check_interval).to eq(1) - end - end - - describe '#failure_reset_time' do - it 'reads the value from settings' do - expect(circuit_breaker.failure_reset_time).to eq(2) - end - end - - describe '#storage_timeout' do - it 'reads the value from settings' do - expect(circuit_breaker.storage_timeout).to eq(3) - end - end - - describe '#access_retries' do - it 'reads the value from settings' do - expect(circuit_breaker.access_retries).to eq(4) - end - end - end - - describe '#perform' do - it 'raises the correct exception when the circuit is open' do - set_in_redis(:last_failure, 1.day.ago.to_f) - set_in_redis(:failure_count, 999) - - expect { |b| circuit_breaker.perform(&b) } - .to raise_error do |exception| - expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) - expect(exception.retry_after).to eq(1800) - end - end - - it 'yields the block' do - expect { |b| circuit_breaker.perform(&b) } - .to yield_control - end - - it 'checks if the storage is available' do - expect(circuit_breaker).to receive(:check_storage_accessible!) - .and_call_original - - circuit_breaker.perform { 'hello world' } - end - - it 'returns the value of the block' do - result = circuit_breaker.perform { 'return value' } - - expect(result).to eq('return value') - end - - it 'raises possible errors' do - expect { circuit_breaker.perform { raise Rugged::OSError.new('Broken') } } - .to raise_error(Rugged::OSError) - end - - context 'with the feature disabled' do - before do - stub_feature_flags(git_storage_circuit_breaker: false) - end - - it 'returns the block without checking accessibility' do - expect(circuit_breaker).not_to receive(:check_storage_accessible!) - - result = circuit_breaker.perform { 'hello' } - - expect(result).to eq('hello') - end - - it 'allows enabling the feature using an ENV var' do - stub_env('GIT_STORAGE_CIRCUIT_BREAKER', 'true') - expect(circuit_breaker).to receive(:check_storage_accessible!) - - result = circuit_breaker.perform { 'hello' } - - expect(result).to eq('hello') - end - end - end - - describe '#circuit_broken?' do - it 'is working when there is no last failure' do - set_in_redis(:last_failure, nil) - set_in_redis(:failure_count, 0) - - expect(circuit_breaker.circuit_broken?).to be_falsey - end - - it 'is broken when there are too many failures' do - set_in_redis(:last_failure, 1.day.ago.to_f) - set_in_redis(:failure_count, 200) - - expect(circuit_breaker.circuit_broken?).to be_truthy - end - end - - describe '#last_failure' do - it 'returns the last failure time' do - time = Time.parse("2017-05-26 17:52:30") - set_in_redis(:last_failure, time.to_i) - - expect(circuit_breaker.last_failure).to eq(time) - end - end - - describe '#failure_count' do - it 'returns the failure count' do - set_in_redis(:failure_count, 7) - - expect(circuit_breaker.failure_count).to eq(7) - end - end -end diff --git a/spec/lib/gitlab/git/storage/failure_info_spec.rb b/spec/lib/gitlab/git/storage/failure_info_spec.rb deleted file mode 100644 index bae88fdda86..00000000000 --- a/spec/lib/gitlab/git/storage/failure_info_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Storage::FailureInfo, :broken_storage do - let(:storage_name) { 'default' } - let(:hostname) { Gitlab::Environment.hostname } - let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } - - def value_from_redis(name) - Gitlab::Git::Storage.redis.with do |redis| - redis.hmget(cache_key, name) - end.first - end - - def set_in_redis(name, value) - Gitlab::Git::Storage.redis.with do |redis| - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) - redis.hmset(cache_key, name, value) - end.first - end - - describe '.reset_all!' do - it 'clears all entries form redis' do - set_in_redis(:failure_count, 10) - - described_class.reset_all! - - key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) } - - expect(key_exists).to be_falsey - end - - it 'does not break when there are no keys in redis' do - expect { described_class.reset_all! }.not_to raise_error - end - end - - describe '.load' do - it 'loads failure information for a storage on a host' do - first_failure = Time.parse("2017-11-14 17:52:30") - last_failure = Time.parse("2017-11-14 18:54:37") - failure_count = 11 - - set_in_redis(:first_failure, first_failure.to_i) - set_in_redis(:last_failure, last_failure.to_i) - set_in_redis(:failure_count, failure_count.to_i) - - info = described_class.load(cache_key) - - expect(info.first_failure).to eq(first_failure) - expect(info.last_failure).to eq(last_failure) - expect(info.failure_count).to eq(failure_count) - end - end - - describe '#no_failures?' do - it 'is true when there are no failures' do - info = described_class.new(nil, nil, 0) - - expect(info.no_failures?).to be_truthy - end - - it 'is false when there are failures' do - info = described_class.new(Time.parse("2017-11-14 17:52:30"), - Time.parse("2017-11-14 18:54:37"), - 20) - - expect(info.no_failures?).to be_falsy - end - end -end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb deleted file mode 100644 index 39a5d020bb4..00000000000 --- a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Storage::ForkedStorageCheck, broken_storage: true, skip_database_cleaner: true do - let(:existing_path) do - existing_path = TestEnv.repos_path - FileUtils.mkdir_p(existing_path) - existing_path - end - - describe '.storage_accessible?' do - it 'detects when a storage is not available' do - expect(described_class.storage_available?('/non/existant/path')).to be_falsey - end - - it 'detects when a storage is available' do - expect(described_class.storage_available?(existing_path)).to be_truthy - end - - it 'returns false when the check takes to long' do - # We're forking a process here that takes too long - # It will be killed it's parent process will be killed by it's parent - # and waited for inside `Gitlab::Git::Storage::ForkedStorageCheck.timeout_check` - allow(described_class).to receive(:check_filesystem_in_process) do - Process.spawn("sleep 10") - end - result = true - - runtime = Benchmark.realtime do - result = described_class.storage_available?(existing_path, 0.5) - end - - expect(result).to be_falsey - expect(runtime).to be < 1.0 - end - - it 'will try the specified amount of times before failing' do - allow(described_class).to receive(:check_filesystem_in_process) do - Process.spawn("sleep 10") - end - - expect(Process).to receive(:spawn).with('sleep 10').twice - .and_call_original - - runtime = Benchmark.realtime do - described_class.storage_available?(existing_path, 0.5, 2) - end - - expect(runtime).to be < 1.0 - end - - describe 'when using paths with spaces' do - let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') } - let(:path_with_spaces) { File.join(test_dir, 'path with spaces') } - - around do |example| - FileUtils.mkdir_p(path_with_spaces) - example.run - FileUtils.rm_r(test_dir) - end - - it 'works for paths with spaces' do - expect(described_class.storage_available?(path_with_spaces)).to be_truthy - end - - it 'works for a realpath with spaces' do - symlink_location = File.join(test_dir, 'a symlink') - FileUtils.ln_s(path_with_spaces, symlink_location) - - expect(described_class.storage_available?(symlink_location)).to be_truthy - end - end - end -end diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb deleted file mode 100644 index bb670fc5d94..00000000000 --- a/spec/lib/gitlab/git/storage/health_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Storage::Health, broken_storage: true do - let(:host1_key) { 'storage_accessible:broken:web01' } - let(:host2_key) { 'storage_accessible:default:kiq01' } - - def set_in_redis(cache_key, value) - Gitlab::Git::Storage.redis.with do |redis| - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) - redis.hmset(cache_key, :failure_count, value) - end.first - end - - describe '.for_failing_storages' do - it 'only includes health status for failures' do - set_in_redis(host1_key, 10) - set_in_redis(host2_key, 0) - - expect(described_class.for_failing_storages.map(&:storage_name)) - .to contain_exactly('broken') - end - end - - describe '.for_all_storages' do - it 'loads health status for all configured storages' do - healths = described_class.for_all_storages - - expect(healths.map(&:storage_name)).to contain_exactly('default', 'broken') - end - end - - describe '#failing_info' do - it 'only contains storages that have failures' do - health = described_class.new('broken', [{ name: host1_key, failure_count: 0 }, - { name: host2_key, failure_count: 3 }]) - - expect(health.failing_info).to contain_exactly({ name: host2_key, failure_count: 3 }) - end - end - - describe '#total_failures' do - it 'sums up all the failures' do - health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, - { name: host2_key, failure_count: 3 }]) - - expect(health.total_failures).to eq(5) - end - end - - describe '#failing_on_hosts' do - it 'collects only the failing hostnames' do - health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, - { name: host2_key, failure_count: 0 }]) - - expect(health.failing_on_hosts).to contain_exactly('web01') - end - end -end diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb deleted file mode 100644 index 93ad20011de..00000000000 --- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Storage::NullCircuitBreaker do - let(:storage) { 'default' } - let(:hostname) { 'localhost' } - let(:error) { nil } - - subject(:breaker) { described_class.new(storage, hostname, error: error) } - - context 'with an error' do - let(:error) { Gitlab::Git::Storage::Misconfiguration.new('error') } - - describe '#perform' do - it { expect { breaker.perform { 'ok' } }.to raise_error(error) } - end - - describe '#circuit_broken?' do - it { expect(breaker.circuit_broken?).to be_truthy } - end - - describe '#last_failure' do - it { Timecop.freeze { expect(breaker.last_failure).to eq(Time.now) } } - end - - describe '#failure_count' do - it { expect(breaker.failure_count).to eq(breaker.failure_count_threshold) } - end - - describe '#failure_info' do - it { expect(breaker.failure_info.no_failures?).to be_falsy } - end - end - - context 'not broken' do - describe '#perform' do - it { expect(breaker.perform { 'ok' }).to eq('ok') } - end - - describe '#circuit_broken?' do - it { expect(breaker.circuit_broken?).to be_falsy } - end - - describe '#last_failure' do - it { expect(breaker.last_failure).to be_nil } - end - - describe '#failure_count' do - it { expect(breaker.failure_count).to eq(0) } - end - - describe '#failure_info' do - it { expect(breaker.failure_info.no_failures?).to be_truthy } - end - end - - describe '#failure_count_threshold' do - before do - stub_application_setting(circuitbreaker_failure_count_threshold: 1) - end - - it { expect(breaker.failure_count_threshold).to eq(1) } - end - - it 'implements the CircuitBreaker interface' do - ours = described_class.public_instance_methods - theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods - - expect(theirs - ours).to be_empty - end -end diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb new file mode 100644 index 00000000000..c6f09ca2112 --- /dev/null +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::GonHelper do + let(:helper) do + Class.new do + include Gitlab::GonHelper + end.new + end + + describe '#push_frontend_feature_flag' do + it 'pushes a feature flag to the frontend' do + gon = instance_double('gon') + + allow(helper) + .to receive(:gon) + .and_return(gon) + + expect(Feature) + .to receive(:enabled?) + .with(:my_feature_flag, 10) + .and_return(true) + + expect(gon) + .to receive(:push) + .with({ features: { 'myFeatureFlag' => true } }, true) + + helper.push_frontend_feature_flag(:my_feature_flag, 10) + end + end +end diff --git a/spec/lib/gitlab/storage_check/cli_spec.rb b/spec/lib/gitlab/storage_check/cli_spec.rb deleted file mode 100644 index 6db0925899c..00000000000 --- a/spec/lib/gitlab/storage_check/cli_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe Gitlab::StorageCheck::CLI do - let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, 1, false) } - subject(:runner) { described_class.new(options) } - - describe '#update_settings' do - it 'updates the interval when changed in a valid response and logs the change' do - fake_response = double - expect(fake_response).to receive(:valid?).and_return(true) - expect(fake_response).to receive(:check_interval).and_return(42) - expect(runner.logger).to receive(:info) - - runner.update_settings(fake_response) - - expect(options.interval).to eq(42) - end - end -end diff --git a/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb deleted file mode 100644 index d869022fd31..00000000000 --- a/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' - -describe Gitlab::StorageCheck::GitlabCaller do - let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, nil, false) } - subject(:gitlab_caller) { described_class.new(options) } - - describe '#call!' do - context 'when a socket is given' do - it 'calls a socket' do - fake_connection = double - expect(fake_connection).to receive(:post) - expect(Excon).to receive(:new).with('unix://tmp/socket.sock', socket: "tmp/socket.sock") { fake_connection } - - gitlab_caller.call! - end - end - - context 'when a host is given' do - let(:options) { Gitlab::StorageCheck::Options.new('http://localhost:8080', nil, nil, false) } - - it 'it calls a http response' do - fake_connection = double - expect(Excon).to receive(:new).with('http://localhost:8080', socket: nil) { fake_connection } - expect(fake_connection).to receive(:post) - - gitlab_caller.call! - end - end - end - - describe '#headers' do - it 'Adds the JSON header' do - headers = gitlab_caller.headers - - expect(headers['Content-Type']).to eq('application/json') - end - - context 'when a token was provided' do - let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', 'atoken', nil, false) } - - it 'adds it to the headers' do - expect(gitlab_caller.headers['TOKEN']).to eq('atoken') - end - end - end -end diff --git a/spec/lib/gitlab/storage_check/option_parser_spec.rb b/spec/lib/gitlab/storage_check/option_parser_spec.rb deleted file mode 100644 index cad4dfbefcf..00000000000 --- a/spec/lib/gitlab/storage_check/option_parser_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -describe Gitlab::StorageCheck::OptionParser do - describe '.parse!' do - it 'assigns all options' do - args = %w(--target unix://tmp/hello/world.sock --token thetoken --interval 42) - - options = described_class.parse!(args) - - expect(options.token).to eq('thetoken') - expect(options.interval).to eq(42) - expect(options.target).to eq('unix://tmp/hello/world.sock') - end - - it 'requires the interval to be a number' do - args = %w(--target unix://tmp/hello/world.sock --interval fortytwo) - - expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument) - end - - it 'raises an error if the scheme is not included' do - args = %w(--target tmp/hello/world.sock) - - expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument) - end - - it 'raises an error if both socket and host are missing' do - expect { described_class.parse!([]) }.to raise_error(OptionParser::InvalidArgument) - end - end -end diff --git a/spec/lib/gitlab/storage_check/response_spec.rb b/spec/lib/gitlab/storage_check/response_spec.rb deleted file mode 100644 index 0ff2963e443..00000000000 --- a/spec/lib/gitlab/storage_check/response_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'spec_helper' - -describe Gitlab::StorageCheck::Response do - let(:fake_json) do - { - check_interval: 42, - results: [ - { storage: 'working', success: true }, - { storage: 'skipped', success: nil }, - { storage: 'failing', success: false } - ] - }.to_json - end - - let(:fake_http_response) do - fake_response = instance_double("Excon::Response - Status check") - allow(fake_response).to receive(:status).and_return(200) - allow(fake_response).to receive(:body).and_return(fake_json) - allow(fake_response).to receive(:headers).and_return('Content-Type' => 'application/json') - - fake_response - end - let(:response) { described_class.new(fake_http_response) } - - describe '#valid?' do - it 'is valid for a success response with parseable JSON' do - expect(response).to be_valid - end - end - - describe '#check_interval' do - it 'returns the result from the JSON' do - expect(response.check_interval).to eq(42) - end - end - - describe '#responsive_shards' do - it 'contains the names of working shards' do - expect(response.responsive_shards).to contain_exactly('working') - end - end - - describe '#skipped_shards' do - it 'contains the names of skipped shards' do - expect(response.skipped_shards).to contain_exactly('skipped') - end - end - - describe '#failing_shards' do - it 'contains the name of failing shards' do - expect(response.failing_shards).to contain_exactly('failing') - end - end -end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index dcfc80daa57..87b91286168 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -141,19 +141,6 @@ describe ApplicationSetting do end end - context 'circuitbreaker settings' do - [:circuitbreaker_failure_count_threshold, - :circuitbreaker_check_interval, - :circuitbreaker_failure_reset_time, - :circuitbreaker_storage_timeout].each do |field| - it "Validates #{field} as number" do - is_expected.to validate_numericality_of(field) - .only_integer - .is_greater_than_or_equal_to(0) - end - end - end - context 'repository storages' do before do storages = { diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index b335e0fbeb3..182070781dd 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -39,6 +39,29 @@ describe Deployment do end end + describe 'scopes' do + describe 'last_for_environment' do + let(:production) { create(:environment) } + let(:staging) { create(:environment) } + let(:testing) { create(:environment) } + + let!(:deployments) do + [ + create(:deployment, environment: production), + create(:deployment, environment: staging), + create(:deployment, environment: production) + ] + end + + it 'retrieves last deployments for environments' do + last_deployments = described_class.last_for_environment([staging, production, testing]) + + expect(last_deployments.size).to eq(2) + expect(last_deployments).to eq(deployments.last(2)) + end + end + end + describe '#includes_commit?' do let(:project) { create(:project, :repository) } let(:environment) { create(:environment, project: project) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 77e549d9528..aed8e02cc23 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -30,7 +30,7 @@ describe Repository do def expect_to_raise_storage_error expect { yield }.to raise_error do |exception| - storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable] + storage_exceptions = [Gitlab::Git::CommandError, GRPC::Unavailable] known_exception = storage_exceptions.select { |e| exception.is_a?(e) } expect(known_exception).not_to be_nil diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb index fe76f057115..6c7cb151c74 100644 --- a/spec/requests/api/circuit_breakers_spec.rb +++ b/spec/requests/api/circuit_breakers_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe API::CircuitBreakers do - let(:user) { create(:user) } - let(:admin) { create(:admin) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } describe 'GET circuit_breakers/repository_storage' do it 'returns a 401 for anonymous users' do @@ -18,37 +18,26 @@ describe API::CircuitBreakers do end it 'returns an Array of storages' do - expect(Gitlab::Git::Storage::Health).to receive(:for_all_storages) do - [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])] - end - get api('/circuit_breakers/repository_storage', admin) expect(response).to have_gitlab_http_status(200) expect(json_response).to be_kind_of(Array) - expect(json_response.first['storage_name']).to eq('broken') - expect(json_response.first['failing_on_hosts']).to eq(['web01']) - expect(json_response.first['total_failures']).to eq(4) + expect(json_response).to be_empty end describe 'GET circuit_breakers/repository_storage/failing' do it 'returns an array of failing storages' do - expect(Gitlab::Git::Storage::Health).to receive(:for_failing_storages) do - [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])] - end - get api('/circuit_breakers/repository_storage/failing', admin) expect(response).to have_gitlab_http_status(200) expect(json_response).to be_kind_of(Array) + expect(json_response).to be_empty end end end describe 'DELETE circuit_breakers/repository_storage' do it 'clears all circuit_breakers' do - expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!) - delete api('/circuit_breakers/repository_storage', admin) expect(response).to have_gitlab_http_status(204) diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index e379bd9785a..fb1be16a111 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' describe API::Settings, 'Settings' do let(:user) { create(:user) } - let(:admin) { create(:admin) } + set(:admin) { create(:admin) } describe "GET /application/settings" do it "returns application settings" do get api("/application/settings", admin) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Hash expect(json_response['default_projects_limit']).to eq(42) @@ -23,7 +24,6 @@ describe API::Settings, 'Settings' do expect(json_response['dsa_key_restriction']).to eq(0) expect(json_response['ecdsa_key_restriction']).to eq(0) expect(json_response['ed25519_key_restriction']).to eq(0) - expect(json_response['circuitbreaker_failure_count_threshold']).not_to be_nil expect(json_response['performance_bar_allowed_group_id']).to be_nil expect(json_response['instance_statistics_visibility_private']).to be(false) expect(json_response).not_to have_key('performance_bar_allowed_group_path') @@ -62,7 +62,6 @@ describe API::Settings, 'Settings' do dsa_key_restriction: 2048, ecdsa_key_restriction: 384, ed25519_key_restriction: 256, - circuitbreaker_check_interval: 2, enforce_terms: true, terms: 'Hello world!', performance_bar_allowed_group_path: group.full_path, @@ -88,7 +87,6 @@ describe API::Settings, 'Settings' do expect(json_response['dsa_key_restriction']).to eq(2048) expect(json_response['ecdsa_key_restriction']).to eq(384) expect(json_response['ed25519_key_restriction']).to eq(256) - expect(json_response['circuitbreaker_check_interval']).to eq(2) expect(json_response['enforce_terms']).to be(true) expect(json_response['terms']).to eq('Hello world!') expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) diff --git a/spec/serializers/deployment_serializer_spec.rb b/spec/serializers/deployment_serializer_spec.rb new file mode 100644 index 00000000000..4834f5ede3c --- /dev/null +++ b/spec/serializers/deployment_serializer_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DeploymentSerializer do + set(:project) { create(:project, :repository) } + set(:user) { create(:user, email: project.commit.author_email) } + + let(:resource) { create(:deployment, project: project, sha: project.commit.id) } + let(:serializer) { described_class.new(request) } + + shared_examples 'json schema' do + let(:json_entity) { subject.as_json } + + it 'matches deployment entity schema' do + expect(json_entity).to match_schema('deployment') + end + end + + describe '#represent' do + subject { serializer.represent(resource) } + + let(:request) { { project: project, current_user: user } } + + it_behaves_like 'json schema' + end + + describe '#represent_concise' do + subject { serializer.represent_concise(resource) } + + let(:request) { { project: project } } + + it_behaves_like 'json schema' + end +end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index beff499f2be..1d31d26f418 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -65,10 +65,12 @@ describe Projects::DestroyService do context 'Sidekiq inline' do before do - # Run sidekiq immediatly to check that renamed repository will be removed + # Run sidekiq immediately to check that renamed repository will be removed perform_enqueued_jobs { destroy_project(project, user, {}) } end + it_behaves_like 'deleting the project' + context 'when has remote mirrors' do let!(:project) do create(:project, :repository, namespace: user.namespace).tap do |project| @@ -82,13 +84,28 @@ describe Projects::DestroyService do end end - it_behaves_like 'deleting the project' - it 'invalidates personal_project_count cache' do expect(user).to receive(:invalidate_personal_projects_count) destroy_project(project, user) end + + context 'when project has exports' do + let!(:project_with_export) do + create(:project, :repository, namespace: user.namespace).tap do |project| + create(:import_export_upload, + project: project, + export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz')) + end + end + let!(:async) { true } + + it 'destroys project and export' do + expect { destroy_project(project_with_export, user) }.to change(ImportExportUpload, :count).by(-1) + + expect(Project.all).not_to include(project_with_export) + end + end end context 'Sidekiq fake' do diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index 26f823cb6ef..6a9ad43941d 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -7,24 +7,5 @@ RSpec.configure do |config| allow(Gitlab::GitalyClient).to receive(:call) do raise GRPC::Unavailable.new('Gitaly broken in this spec') end - - # Track the maximum number of failures - first_failure = Time.parse("2017-11-14 17:52:30") - last_failure = Time.parse("2017-11-14 18:54:37") - failure_count = Gitlab::CurrentSettings.circuitbreaker_failure_count_threshold + 1 - cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}" - - Gitlab::Git::Storage.redis.with do |redis| - redis.pipelined do - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) - redis.hset(cache_key, :first_failure, first_failure.to_i) - redis.hset(cache_key, :last_failure, last_failure.to_i) - redis.hset(cache_key, :failure_count, failure_count.to_i) - end - end - end - - config.after(:each, :broken_storage) do - Gitlab::Git::Storage.redis.with(&:flushall) end end diff --git a/yarn.lock b/yarn.lock index 7967ad0eb34..5879ccb9267 100644 --- a/yarn.lock +++ b/yarn.lock @@ -616,10 +616,10 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0": - version "1.31.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.31.0.tgz#495b074669f93af40e34f9978ce887773dea470a" - integrity sha512-tJbf99XX/ddFkXCXxQr9a0GJD9rPVoW3qMbU14dkxwG4WBmPEoVg+e7sLvm9OWTD1uUqiVW3qWKp++SGhhcRlw== +"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.32.0": + version "1.32.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3" + integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA== "@gitlab-org/gitlab-ui@^1.8.0": version "1.8.0" @@ -3012,6 +3012,13 @@ eslint-config-airbnb-base@^13.1.0: object.assign "^4.1.0" object.entries "^1.0.4" +eslint-config-prettier@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-3.1.0.tgz#2c26d2cdcfa3a05f0642cd7e6e4ef3316cdabfa2" + integrity sha512-QYGfmzuc4q4J6XIhlp8vRKdI/fI0tQfQPy1dME3UOLprE+v4ssH/3W9LM2Q7h5qBcy5m0ehCrBDU2YF8q6OY8w== + dependencies: + get-stdin "^6.0.0" + eslint-import-resolver-node@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" @@ -3708,6 +3715,11 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" integrity sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U= +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + get-stream@3.0.0, get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -6271,16 +6283,16 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" - integrity sha1-wa0g6APndJ+vkFpAnSNn4Gu+cyU= - prettier@1.13.7: version "1.13.7" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== +prettier@1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" + integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== + prismjs@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365" |