diff options
author | jerasmus <jerasmus@gitlab.com> | 2018-10-11 09:25:07 +0200 |
---|---|---|
committer | jerasmus <jerasmus@gitlab.com> | 2018-10-11 09:25:07 +0200 |
commit | 0572da24c990fc01d88acfbd32728221e3e3a711 (patch) | |
tree | e38be4c8535c4785b24a4234efff0660843fa49f | |
parent | 5df448786650a48b331071f7f0afc466eee407df (diff) | |
parent | ee6d9e2843d1f4ff5ad35cd1922e839f82971dbd (diff) | |
download | gitlab-ce-0572da24c990fc01d88acfbd32728221e3e3a711.tar.gz |
Merge branch 'master' into 48746-fix-files-uploaded-in-base64
207 files changed, 2658 insertions, 4107 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index b7aec5b8b14..242e7615211 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 @@ -54,6 +56,13 @@ Style/FrozenStringLiteralComment: - 'scripts/**/*' - 'spec/**/*' +RSpec/FilePath: + Exclude: + - 'qa/**/*' + - 'spec/javascripts/fixtures/*' + - 'ee/spec/javascripts/fixtures/*' + - 'spec/requests/api/v3/*' + Naming/FileName: ExpectMatchingDefinition: true Exclude: diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 26aaba0e866..9084fa2f716 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.2.0 +1.1.0 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/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 1fc7a29f785..75477ebb3b3 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -4,20 +4,17 @@ 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'; +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'; -const Store = gl.issueBoards.BoardsStore; - -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - -gl.issueBoards.Board = Vue.extend({ +export default Vue.extend({ components: { - boardList, - 'board-delete': gl.issueBoards.BoardDelete, BoardBlankState, + BoardDelete, + BoardList, Icon, }, directives: { @@ -47,8 +44,8 @@ gl.issueBoards.Board = Vue.extend({ }, data () { return { - detailIssue: Store.detail, - filter: Store.filter, + detailIssue: boardsStore.detail, + filter: boardsStore.filter, }; }, computed: { @@ -70,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 240d0911a31..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 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 df7efd3fa5c..62666954de0 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -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 609659bdf93..52d04389b88 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -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 58e423fbd44..3161f1da8c9 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -5,6 +5,7 @@ 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 bd181807e1f..471955747fd 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -3,13 +3,11 @@ 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/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/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/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/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_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..35d40c6898e 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,69 +1,68 @@ <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"> 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..7f0f301d72a 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -1,112 +1,112 @@ <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']), + 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 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..34d47b3a3bb 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,50 +1,50 @@ <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'; +import { __ } from '~/locale'; - 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'), - }; + }, + data() { + return { + selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'), + }; + }, + computed: { + hasRef() { + return !_.isEmpty(this.pipeline.ref); }, - 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; + } }, - 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; - } - }, + }, + methods: { + onStageClick(stage) { + this.$emit('requestSidebarStageDropdown', stage); + this.selectedStage = stage.name; }, - methods: { - onStageClick(stage) { - this.$emit('requestSidebarStageDropdown', stage); - this.selectedStage = stage.name; - }, - }, - }; + }, +}; </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/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/labels_select.js b/app/assets/javascripts/labels_select.js index 68ae1ca6842..3c38d998b6c 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -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 f4eb652a41a..d93873e0214 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -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 df20785b178..e26a6b986be 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -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/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index c1832d034ef..425b806e9d6 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -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 2cd70247bc6..0333335de06 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -4,7 +4,7 @@ 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/milestone_select.js b/app/assets/javascripts/milestone_select.js index 67c2d7909a2..42fb5c7177a 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -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/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/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/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/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/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/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/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/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/_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/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/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/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/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/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/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 d47156c6da4..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: 20181008145359) 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: 20181008145359) 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: 20181008145359) 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 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/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/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/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/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/yaml/README.md b/doc/ci/yaml/README.md index 8b770495853..f0738252640 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -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/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/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/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/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/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/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/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/locale/gitlab.pot b/locale/gitlab.pot index 8b7d4b0f17e..8e0324eb194 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 "" @@ -5155,9 +5123,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 +5335,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 +5939,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 +5984,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 +6023,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 "" @@ -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' @@ -214,6 +216,10 @@ 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 @@ -260,6 +266,8 @@ 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' + module Issuable autoload :Common, 'qa/page/component/issuable/common' 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/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/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/menu.rb b/qa/qa/page/main/menu.rb index e18b95bde9f..a2ee696e1b3 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -68,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 dddda4f2bdf..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::Main::Menu.act { assert_has_personal_area } + raise "Failed to register and sign in" unless signed_in 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/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/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/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/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 785897f4a97..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 @@ -66,7 +66,7 @@ module QA expect(pipeline).to have_build('production', status: :success, wait: 1200) end - Page::Menu::Side.act { click_operations_environments } + Page::Project::Menu.act { click_operations_environments } Page::Project::Operations::Environments::Index.perform do |index| index.go_to_environment('production') end diff --git a/scripts/trigger-build b/scripts/trigger-build index 4534fcadebf..b76cd5dd6f0 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -68,11 +68,11 @@ module Trigger def base_variables { - 'TOP_UPSTREAM_TRIGGER_PROJECT' => ENV['TOP_UPSTREAM_TRIGGER_PROJECT'] || ENV['CI_PROJECT_PATH'], - 'UPSTREAM_TRIGGER_PROJECT' => ENV['CI_PROJECT_PATH'], - 'UPSTREAM_TRIGGER_SOURCE' => ENV['TRIGGER_SOURCE'], 'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'], - 'TRIGGER_SOURCE' => ENV['CI_JOB_URL'] + '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 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/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/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/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/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 ed43ce9029e..dfd3ea0db66 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -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 0beb5782283..e8387068831 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -6,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', () => { @@ -14,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 4232e0fc221..ba6e81a29a9 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -9,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', () => { @@ -22,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); }); @@ -58,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/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/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/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/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 |