diff options
author | Douglas Barbosa Alexandre <dbalexandre@gmail.com> | 2019-07-09 14:45:46 -0300 |
---|---|---|
committer | Douglas Barbosa Alexandre <dbalexandre@gmail.com> | 2019-07-09 14:45:46 -0300 |
commit | 2615265ef82e07ea36df67f6d1a11ad4a731ad63 (patch) | |
tree | 92c1d3ab69f88e1dceed11ae886e1acb6ae65f1e /app | |
parent | 203ef336392befd907c335a4a851ec0794d5e91a (diff) | |
parent | 48c19d9e296b2bec3663f46b8d936da2082171b1 (diff) | |
download | gitlab-ce-2615265ef82e07ea36df67f6d1a11ad4a731ad63.tar.gz |
Merge branch 'master' into sathieu/gitlab-ce-project_api
Diffstat (limited to 'app')
185 files changed, 1613 insertions, 729 deletions
diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png Binary files differnew file mode 100644 index 00000000000..c8a86a0c515 --- /dev/null +++ b/app/assets/images/auth_buttons/salesforce_64.png diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 4f66a5d080c..a649c521405 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -24,6 +24,7 @@ const Api = { issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', projectTemplatesPath: '/api/:version/projects/:id/templates/:type', + userCountsPath: '/api/:version/user_counts', usersPath: '/api/:version/users.json', userPath: '/api/:version/users/:id', userStatusPath: '/api/:version/users/:id/status', @@ -312,6 +313,11 @@ const Api = { }); }, + userCounts() { + const url = Api.buildUrl(this.userCountsPath); + return axios.get(url); + }, + userStatus(id, options) { const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id)); return axios.get(url, { diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index f58149c9f7b..d8b0b60c183 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -61,7 +61,7 @@ export default { <div class="board-blank-state p-3"> <p> {{ - __('BoardBlankState|Add the following default lists to your Issue Board with one click:') + s__('BoardBlankState|Add the following default lists to your Issue Board with one click:') }} </p> <ul class="list-unstyled board-blank-state-list"> @@ -76,7 +76,7 @@ export default { </ul> <p> {{ - __( + s__( 'BoardBlankState|Starting out with the default set of lists will get you right on the way to making the most of your board.', ) }} @@ -86,10 +86,10 @@ export default { type="button" @click.stop="addDefaultLists" > - {{ __('BoardBlankState|Add default lists') }} + {{ s__('BoardBlankState|Add default lists') }} </button> <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState"> - {{ __("BoardBlankState|Nevermind, I'll use my own") }} + {{ s__("BoardBlankState|Nevermind, I'll use my own") }} </button> </div> </template> diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 6b54e8baefb..b1b4b1c5508 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -2,7 +2,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchManager from '../filtered_search/filtered_search_manager'; import boardsStore from './stores/boards_store'; -import { isEE } from '~/lib/utils/common_utils'; export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { @@ -10,7 +9,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { page: 'boards', isGroupDecendent: true, stateFiltersSelector: '.issues-state-filters', - isGroup: isEE(), + isGroup: IS_EE, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index a020765f335..23b107abefa 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import Vue from 'vue'; +import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; @@ -20,7 +21,7 @@ import modalMixin from './mixins/modal_mixins'; import './filters/due_date_filters'; import Board from './components/board'; import BoardSidebar from './components/board_sidebar'; -import initNewListDropdown from './components/new_list_dropdown'; +import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; import { @@ -78,13 +79,14 @@ export default () => { }, }, created() { - gl.boardService = new BoardService({ + boardsStore.setEndpoints({ boardsEndpoint: this.boardsEndpoint, recentBoardsEndpoint: this.recentBoardsEndpoint, listsEndpoint: this.listsEndpoint, bulkUpdatePath: this.bulkUpdatePath, boardId: this.boardId, }); + gl.boardService = new BoardService(); boardsStore.rootPath = this.boardsEndpoint; eventHub.$on('updateTokens', this.updateTokens); @@ -278,4 +280,6 @@ export default () => { `, }); } + + mountMultipleBoardsSwitcher(); }; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index f858b162c6b..9069b35db9a 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -5,7 +5,7 @@ import Vue from 'vue'; import './label'; -import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import IssueProject from './project'; import boardsStore from '../stores/boards_store'; @@ -91,13 +91,13 @@ class ListIssue { addMilestone(milestone) { const miletoneId = this.milestone ? this.milestone.id : null; - if (isEE && milestone.id !== miletoneId) { + if (IS_EE && milestone.id !== miletoneId) { this.milestone = new ListMilestone(milestone); } } removeMilestone(removeMilestone) { - if (isEE && removeMilestone && removeMilestone.id === this.milestone.id) { + if (IS_EE && removeMilestone && removeMilestone.id === this.milestone.id) { this.milestone = {}; } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index cd553d0c4af..7e0ccb9bd2a 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -4,7 +4,7 @@ import { __ } from '~/locale'; import ListLabel from './label'; import ListAssignee from './assignee'; -import { isEE, urlParamsToObject } from '~/lib/utils/common_utils'; +import { urlParamsToObject } from '~/lib/utils/common_utils'; import boardsStore from '../stores/boards_store'; import ListMilestone from './milestone'; @@ -58,7 +58,7 @@ class List { } else if (obj.user) { this.assignee = new ListAssignee(obj.user); this.title = this.assignee.name; - } else if (isEE && obj.milestone) { + } else if (IS_EE && obj.milestone) { this.milestone = new ListMilestone(obj.milestone); this.title = this.milestone.title; } @@ -85,7 +85,7 @@ class List { entityType = 'label_id'; } else if (this.assignee) { entityType = 'assignee_id'; - } else if (isEE && this.milestone) { + } else if (IS_EE && this.milestone) { entityType = 'milestone_id'; } @@ -205,7 +205,7 @@ class List { issue.addAssignee(this.assignee); } - if (isEE && this.milestone) { + if (IS_EE && this.milestone) { if (listFrom && listFrom.type === 'milestone') { issue.removeMilestone(listFrom.milestone); } diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js index 6f81d6bc6f8..7201b6e91f5 100644 --- a/app/assets/javascripts/boards/models/milestone.js +++ b/app/assets/javascripts/boards/models/milestone.js @@ -1,11 +1,9 @@ -import { isEE } from '~/lib/utils/common_utils'; - export default class ListMilestone { constructor(obj) { this.id = obj.id; this.title = obj.title; - if (isEE) { + if (IS_EE) { this.path = obj.path; this.state = obj.state; this.webUrl = obj.web_url || obj.webUrl; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js new file mode 100644 index 00000000000..bdb14a7f2f2 --- /dev/null +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -0,0 +1,2 @@ +// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 +export default () => {}; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 7d463f17ab1..580d04a3649 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -1,106 +1,66 @@ -import axios from '../../lib/utils/axios_utils'; -import { mergeUrlParams } from '../../lib/utils/url_utility'; +/* eslint-disable class-methods-use-this */ -export default class BoardService { - constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { - this.boardsEndpoint = boardsEndpoint; - this.boardId = boardId; - this.listsEndpoint = listsEndpoint; - this.listsEndpointGenerate = `${listsEndpoint}/generate.json`; - this.bulkUpdatePath = bulkUpdatePath; - this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`; - } +import boardsStore from '~/boards/stores/boards_store'; +export default class BoardService { generateBoardsPath(id) { - return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`; + return boardsStore.generateBoardsPath(id); } generateIssuesPath(id) { - return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`; + return boardsStore.generateIssuesPath(id); } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ - id ? `/${id}` : '' - }`; + return boardsStore.generateIssuePath(boardId, id); } all() { - return axios.get(this.listsEndpoint); + return boardsStore.all(); } generateDefaultLists() { - return axios.post(this.listsEndpointGenerate, {}); + return boardsStore.generateDefaultLists(); } createList(entityId, entityType) { - const list = { - [entityType]: entityId, - }; - - return axios.post(this.listsEndpoint, { - list, - }); + return boardsStore.createList(entityId, entityType); } updateList(id, position) { - return axios.put(`${this.listsEndpoint}/${id}`, { - list: { - position, - }, - }); + return boardsStore.updateList(id, position); } destroyList(id) { - return axios.delete(`${this.listsEndpoint}/${id}`); + return boardsStore.destroyList(id); } getIssuesForList(id, filter = {}) { - const data = { id }; - Object.keys(filter).forEach(key => { - data[key] = filter[key]; - }); - - return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); + return boardsStore.getIssuesForList(id, filter); } moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { - return axios.put(BoardService.generateIssuePath(this.boardId, id), { - from_list_id: fromListId, - to_list_id: toListId, - move_before_id: moveBeforeId, - move_after_id: moveAfterId, - }); + return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId); } newIssue(id, issue) { - return axios.post(this.generateIssuesPath(id), { - issue, - }); + return boardsStore.newIssue(id, issue); } getBacklog(data) { - return axios.get( - mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`), - ); + return boardsStore.getBacklog(data); } bulkUpdate(issueIds, extraData = {}) { - const data = { - update: Object.assign(extraData, { - issuable_ids: issueIds.join(','), - }), - }; - - return axios.post(this.bulkUpdatePath, data); + return boardsStore.bulkUpdate(issueIds, extraData); } static getIssueInfo(endpoint) { - return axios.get(endpoint); + return boardsStore.getIssueInfo(endpoint); } static toggleIssueSubscription(endpoint) { - return axios.post(endpoint); + return boardsStore.toggleIssueSubscription(endpoint); } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 4ba4cde6bae..b9cd4a143ef 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,8 @@ import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../eventhub'; const boardsStore = { @@ -28,6 +30,7 @@ const boardsStore = { }, currentPage: '', reload: false, + endpoints: {}, }, detail: { issue: {}, @@ -36,6 +39,19 @@ const boardsStore = { issue: {}, list: {}, }, + + setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { + const listsEndpointGenerate = `${listsEndpoint}/generate.json`; + this.state.endpoints = { + boardsEndpoint, + boardId, + listsEndpoint, + listsEndpointGenerate, + bulkUpdatePath, + recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, + }; + }, + create() { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); @@ -229,6 +245,101 @@ const boardsStore = { setTimeTrackingLimitToHours(limitToHours) { this.timeTracking.limitToHours = parseBoolean(limitToHours); }, + + generateBoardsPath(id) { + return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`; + }, + + generateIssuesPath(id) { + return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`; + }, + + generateIssuePath(boardId, id) { + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ + id ? `/${id}` : '' + }`; + }, + + all() { + return axios.get(this.state.endpoints.listsEndpoint); + }, + + generateDefaultLists() { + return axios.post(this.state.endpoints.listsEndpointGenerate, {}); + }, + + createList(entityId, entityType) { + const list = { + [entityType]: entityId, + }; + + return axios.post(this.state.endpoints.listsEndpoint, { + list, + }); + }, + + updateList(id, position) { + return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, { + list: { + position, + }, + }); + }, + + destroyList(id) { + return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`); + }, + + getIssuesForList(id, filter = {}) { + const data = { id }; + Object.keys(filter).forEach(key => { + data[key] = filter[key]; + }); + + return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); + }, + + moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { + return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), { + from_list_id: fromListId, + to_list_id: toListId, + move_before_id: moveBeforeId, + move_after_id: moveAfterId, + }); + }, + + newIssue(id, issue) { + return axios.post(this.generateIssuesPath(id), { + issue, + }); + }, + + getBacklog(data) { + return axios.get( + mergeUrlParams( + data, + `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`, + ), + ); + }, + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return axios.post(this.state.endpoints.bulkUpdatePath, data); + }, + + getIssueInfo(endpoint) { + return axios.get(endpoint); + }, + + toggleIssueSubscription(endpoint) { + return axios.post(endpoint); + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index 96bc6a5f8e8..7dbaf984acf 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -36,7 +36,9 @@ export default endpoint => { }, 100); Object.entries(data).forEach(([branchName, val]) => { - const el = document.querySelector(`.js-branch-${branchName} .js-branch-divergence-graph`); + const el = document.querySelector( + `[data-name="${branchName}"] .js-branch-divergence-graph`, + ); if (!el) return; diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 0d2fe2925d8..ad0f6cc1496 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -4,3 +4,6 @@ import './jquery'; import './bootstrap'; import './vue'; import '../lib/utils/axios_utils'; +import { openUserCountsBroadcast } from './nav/user_merge_requests'; + +openUserCountsBroadcast(); diff --git a/app/assets/javascripts/commons/nav/user_merge_requests.js b/app/assets/javascripts/commons/nav/user_merge_requests.js new file mode 100644 index 00000000000..8e694cca6a1 --- /dev/null +++ b/app/assets/javascripts/commons/nav/user_merge_requests.js @@ -0,0 +1,67 @@ +import Api from '~/api'; + +let channel; + +function broadcastCount(newCount) { + if (!channel) { + return; + } + + channel.postMessage(newCount); +} + +function updateUserMergeRequestCounts(newCount) { + const mergeRequestsCountEl = document.querySelector('.merge-requests-count'); + mergeRequestsCountEl.textContent = newCount.toLocaleString(); + mergeRequestsCountEl.classList.toggle('hidden', Number(newCount) === 0); +} + +/** + * Refresh user counts (and broadcast if open) + */ +export function refreshUserMergeRequestCounts() { + return Api.userCounts() + .then(({ data }) => { + const count = data.merge_requests; + + updateUserMergeRequestCounts(count); + broadcastCount(count); + }) + .catch(ex => { + console.error(ex); // eslint-disable-line no-console + }); +} + +/** + * Close the broadcast channel for user counts + */ +export function closeUserCountsBroadcast() { + if (!channel) { + return; + } + + channel.close(); + channel = null; +} + +/** + * Open the broadcast channel for user counts, adds user id so we only update + * + * **Please note:** + * Not supported in all browsers, but not polyfilling for now + * to keep bundle size small and + * no special functionality lost except cross tab notifications + */ +export function openUserCountsBroadcast() { + closeUserCountsBroadcast(); + + if (window.BroadcastChannel) { + const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id; + if (currentUserId) { + channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`); + channel.onmessage = ev => { + updateUserMergeRequestCounts(ev.data); + }; + } + } +} diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue new file mode 100644 index 00000000000..444640980af --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue @@ -0,0 +1,58 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + Icon, + }, + props: { + projects: { + type: Array, + required: true, + }, + selectedProject: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + dropdownText() { + if (Object.keys(this.selectedProject).length) { + return this.selectedProject.name; + } + + return __('Select private project'); + }, + }, + methods: { + selectProject(project) { + this.$emit('click', project); + }, + }, +}; +</script> + +<template> + <gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100"> + <template slot="button-content"> + <span class="str-truncated-100 mr-2"> + <icon name="lock" /> + {{ dropdownText }} + </span> + <icon name="chevron-down" class="ml-auto" /> + </template> + <gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)"> + <icon + name="mobile-issue-close" + :class="{ icon: project.id !== selectedProject.id }" + class="js-active-project-check" + /> + <span class="ml-1">{{ project.name }}</span> + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue new file mode 100644 index 00000000000..99d77a75c23 --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -0,0 +1,132 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { __, sprintf } from '../../locale'; +import createFlash from '../../flash'; +import Api from '../../api'; +import state from '../state'; +import Dropdown from './dropdown.vue'; + +export default { + components: { + GlLink, + Dropdown, + }, + props: { + namespacePath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + newForkPath: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + data() { + return { + projects: [], + }; + }, + computed: { + selectedProject() { + return state.selectedProject; + }, + noForkText() { + return sprintf( + __( + "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.", + ), + { link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' }, + false, + ); + }, + }, + mounted() { + this.fetchProjects(); + this.createBtn = document.querySelector('.js-create-target'); + this.warningText = document.querySelector('.js-exposed-info-warning'); + }, + methods: { + selectProject(project) { + if (project) { + Object.assign(state, { + selectedProject: project, + }); + + if (project.namespaceFullPath !== this.namespacePath) { + this.showWarning(); + } + } else if (this.createBtn) { + this.createBtn.setAttribute('disabled', 'disabled'); + } + }, + normalizeProjectData(data) { + return data.map(p => ({ + id: p.id, + name: p.name_with_namespace, + pathWithNamespace: p.path_with_namespace, + namespaceFullpath: p.namespace.full_path, + })); + }, + fetchProjects() { + Api.projectForks(this.projectPath, { + with_merge_requests_enabled: true, + min_access_level: 30, + visibility: 'private', + }) + .then(({ data }) => { + this.projects = this.normalizeProjectData(data); + this.selectProject(this.projects[0]); + }) + .catch(e => { + createFlash(__('Error fetching forked projects. Please try again.')); + throw e; + }); + }, + showWarning() { + if (this.warningText) { + this.warningText.classList.remove('hidden'); + } + + if (this.createBtn) { + this.createBtn.classList.add('btn-warning'); + this.createBtn.classList.remove('btn-success'); + } + }, + }, +}; +</script> + +<template> + <div class="form-group"> + <label>{{ __('Project') }}</label> + <div> + <dropdown + v-if="projects.length" + :projects="projects" + :selected-project="selectedProject" + @click="selectProject" + /> + <p class="text-muted mt-1 mb-0"> + <template v-if="projects.length"> + {{ + __( + "To protect this issue's confidentiality, a private fork of this project was selected.", + ) + }} + </template> + <template v-else> + {{ __('No forks available to you.') }}<br /> + <span v-html="noForkText"></span> + </template> + </p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/confidential_merge_request/index.js b/app/assets/javascripts/confidential_merge_request/index.js new file mode 100644 index 00000000000..9672821d30e --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import { parseBoolean } from '../lib/utils/common_utils'; +import ProjectFormGroup from './components/project_form_group.vue'; +import state from './state'; + +export function isConfidentialIssue() { + return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential); +} + +export function canCreateConfidentialMergeRequest() { + return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0; +} + +export function init() { + const el = document.getElementById('js-forked-project'); + + return new Vue({ + el, + render(h) { + return h(ProjectFormGroup, { + props: { + namespacePath: el.dataset.namespacePath, + projectPath: el.dataset.projectPath, + newForkPath: el.dataset.newForkPath, + helpPagePath: el.dataset.helpPagePath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/confidential_merge_request/state.js b/app/assets/javascripts/confidential_merge_request/state.js new file mode 100644 index 00000000000..95b0580f4b9 --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/state.js @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +export default Vue.observable({ + selectedProject: {}, +}); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 8f5cece0788..052168bb21c 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -5,6 +5,12 @@ import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; import { __, sprintf } from './locale'; +import { + init as initConfidentialMergeRequest, + isConfidentialIssue, + canCreateConfidentialMergeRequest, +} from './confidential_merge_request'; +import confidentialMergeRequestState from './confidential_merge_request/state'; // Todo: Remove this when fixing issue in input_setter plugin const InputSetter = Object.assign({}, ISetter); @@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter); const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_BRANCH = 'create-branch'; +function createEndpoint(projectPath, endpoint) { + if (canCreateConfidentialMergeRequest()) { + return endpoint.replace( + projectPath, + confidentialMergeRequestState.selectedProject.pathWithNamespace, + ); + } + + return endpoint; +} + export default class CreateMergeRequestDropdown { constructor(wrapperEl) { this.wrapperEl = wrapperEl; @@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown { this.refIsValid = true; this.refsPath = this.wrapperEl.dataset.refsPath; this.suggestedRef = this.refInput.value; + this.projectPath = this.wrapperEl.dataset.projectPath; + this.projectId = this.wrapperEl.dataset.projectId; // These regexps are used to replace // a backend generated new branch name and its source (ref) @@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown { }; this.init(); + + if (isConfidentialIssue()) { + this.createMergeRequestButton.setAttribute( + 'data-dropdown-trigger', + '#create-merge-request-dropdown', + ); + initConfidentialMergeRequest(); + } } available() { @@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown { this.isCreatingBranch = true; return axios - .post(this.createBranchPath) + .post(createEndpoint(this.projectPath, this.createBranchPath), { + confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null, + }) .then(({ data }) => { this.branchCreated = true; window.location.href = data.url; @@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown { this.isCreatingMergeRequest = true; return axios - .post(this.createMrPath) + .post(this.createMrPath, { + target_project_id: canCreateConfidentialMergeRequest() + ? confidentialMergeRequestState.selectedProject.id + : null, + }) .then(({ data }) => { this.mergeRequestCreated = true; window.location.href = data.url; @@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown { } enable() { + if (!canCreateConfidentialMergeRequest()) return; + this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.removeAttribute('disabled'); @@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown { if (!ref) return false; return axios - .get(`${this.refsPath}${encodeURIComponent(ref)}`) + .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown { let xhr = null; event.preventDefault(); + if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { + this.droplab.hooks.forEach(hook => hook.list.toggle()); + + return; + } + if (this.isBusy()) { return; } diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index ca3285e9afd..a06dbd70ac5 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -24,6 +24,11 @@ export default { required: false, default: '', }, + hasDraft: { + type: Boolean, + required: false, + default: false, + }, }, computed: { className() { @@ -55,6 +60,7 @@ export default { :help-page-path="helpPagePath" /> <diff-discussion-reply + v-if="!hasDraft" :has-form="line.hasForm" :render-reply-placeholder="Boolean(line.discussions.length)" @showNewDiscussionForm=" diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 8c76a555b62..b2bc3d9914f 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -57,6 +57,7 @@ export default { :diff-file-hash="diffFile.file_hash" :line="line" :help-page-path="helpPagePath" + :has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false" /> <inline-draft-comment-row v-if="shouldRenderDraftRow(diffFile.file_hash, line)" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index c00b0e010ff..65b41b0e456 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -28,6 +28,16 @@ export default { required: false, default: '', }, + hasDraftLeft: { + type: Boolean, + required: false, + default: false, + }, + hasDraftRight: { + type: Boolean, + required: false, + default: false, + }, }, computed: { hasExpandedDiscussionOnLeft() { @@ -121,6 +131,7 @@ export default { /> </div> <diff-discussion-reply + v-if="!hasDraftLeft" :has-form="showLeftSideCommentForm" :render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft" @showNewDiscussionForm="showNewDiscussionForm" @@ -145,6 +156,7 @@ export default { /> </div> <diff-discussion-reply + v-if="!hasDraftRight" :has-form="showRightSideCommentForm" :render-reply-placeholder="shouldRenderReplyPlaceholderOnRight" @showNewDiscussionForm="showNewDiscussionForm" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 41a80d99850..c477e68c33c 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -58,6 +58,8 @@ export default { :diff-file-hash="diffFile.file_hash" :line-index="index" :help-page-path="helpPagePath" + :has-draft-left="hasParallelDraftLeft(diffFile.file_hash, line) || false" + :has-draft-right="hasParallelDraftRight(diffFile.file_hash, line) || false" /> <parallel-draft-comment-row v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" diff --git a/app/assets/javascripts/diffs/mixins/draft_comments.js b/app/assets/javascripts/diffs/mixins/draft_comments.js index dfb71bf38ce..b6c9b132aeb 100644 --- a/app/assets/javascripts/diffs/mixins/draft_comments.js +++ b/app/assets/javascripts/diffs/mixins/draft_comments.js @@ -6,5 +6,7 @@ export default { imageDiscussions() { return this.diffFile.discussions; }, + hasParallelDraftLeft: () => () => false, + hasParallelDraftRight: () => () => false, }, }; diff --git a/app/assets/javascripts/event_tracking/notes.js b/app/assets/javascripts/event_tracking/notes.js index 2d1ec238274..1f70290c397 100644 --- a/app/assets/javascripts/event_tracking/notes.js +++ b/app/assets/javascripts/event_tracking/notes.js @@ -1 +1,2 @@ +// Noop function which has a EE counter-part export default () => {}; diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 5fcb11a232e..03756a634d5 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -144,7 +144,9 @@ export default { 'triggerFilesChange', ]), initEditor() { - if (this.shouldHideEditor) return; + if (this.shouldHideEditor && (this.file.content || this.file.raw)) { + return; + } this.editor.clearEditor(); diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index b8abaa41f23..51278640b5b 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -77,6 +77,7 @@ export const decorateFiles = ({ const fileFolder = parent && insertParent(parent); if (name) { + const previewMode = viewerInformationForPath(name); parentPath = fileFolder && fileFolder.path; file = decorateData({ @@ -92,9 +93,9 @@ export const decorateFiles = ({ changed: tempFile, content, base64, - binary, + binary: (previewMode && previewMode.binary) || binary, rawPath, - previewMode: viewerInformationForPath(name), + previewMode, parentPath, }); diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index c88244492e0..a52f1e235ed 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,6 +1,7 @@ import * as types from '../mutation_types'; import { sortTree } from '../utils'; import { diffModes } from '../../constants'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; export default { [types.SET_FILE_ACTIVE](state, { path, active }) { @@ -35,19 +36,18 @@ export default { } }, [types.SET_FILE_DATA](state, { data, file }) { - Object.assign(state.entries[file.path], { - id: data.id, - blamePath: data.blame_path, - commitsPath: data.commits_path, - permalink: data.permalink, - rawPath: data.raw_path, - binary: data.binary, - renderError: data.render_error, - raw: (state.entries[file.path] && state.entries[file.path].raw) || null, - baseRaw: null, - html: data.html, - size: data.size, - lastCommitSha: data.last_commit_sha, + const stateEntry = state.entries[file.path]; + const stagedFile = state.stagedFiles.find(f => f.path === file.path); + const openFile = state.openFiles.find(f => f.path === file.path); + const changedFile = state.changedFiles.find(f => f.path === file.path); + + [stateEntry, stagedFile, openFile, changedFile].forEach(f => { + if (f) { + Object.assign(f, convertObjectPropsToCamelCase(data, { dropKeys: ['raw', 'baseRaw'] }), { + raw: (stateEntry && stateEntry.raw) || null, + baseRaw: null, + }); + } }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index bc9d7fcf30d..c855f3973b0 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,4 +1,4 @@ -/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback, no-unused-vars */ +/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback */ import $ from 'jquery'; import _ from 'underscore'; @@ -7,7 +7,7 @@ import Flash from './flash'; import { __ } from './locale'; export default { - init({ container, form, issues, prefixId } = {}) { + init({ form, issues, prefixId } = {}) { this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); this.$labelDropdown = this.form.find('.js-label-select'); diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 16f88cddce3..f3f8b6ec715 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -2,26 +2,13 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; import { s__, __ } from './locale'; -import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; -import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar'; export default class IssuableIndex { constructor(pagePrefix) { - this.initBulkUpdate(pagePrefix); + issuableInitBulkUpdateSidebar.init(pagePrefix); IssuableIndex.resetIncomingEmailToken(); } - initBulkUpdate(pagePrefix) { - const userCanBulkUpdate = $('.issues-bulk-update').length > 0; - const alreadyInitialized = Boolean(this.bulkUpdateSidebar); - - if (userCanBulkUpdate && !alreadyInitialized) { - IssuableBulkUpdateActions.init({ - prefixId: pagePrefix, - }); - - this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); - } - } static resetIncomingEmailToken() { const $resetToken = $('.incoming-email-token-reset'); diff --git a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js new file mode 100644 index 00000000000..da8969c80f3 --- /dev/null +++ b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js @@ -0,0 +1,19 @@ +import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; +import issuableBulkUpdateActions from './issuable_bulk_update_actions'; + +export default { + bulkUpdateSidebar: null, + + init(prefixId) { + const bulkUpdateEl = document.querySelector('.issues-bulk-update'); + const alreadyInitialized = Boolean(this.bulkUpdateSidebar); + + if (bulkUpdateEl && !alreadyInitialized) { + issuableBulkUpdateActions.init({ prefixId }); + + this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); + } + + return this.bulkUpdateSidebar; + }, +}; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 3f954b43ee3..bea43430edc 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -11,7 +11,7 @@ import CreateLabelDropdown from './create_label'; import flash from './flash'; import ModalStore from './boards/stores/modal_store'; import boardsStore from './boards/stores/boards_store'; -import { isEE, isScopedLabel } from '~/lib/utils/common_utils'; +import { isScopedLabel } from '~/lib/utils/common_utils'; export default class LabelsSelect { constructor(els, options = {}) { @@ -140,7 +140,7 @@ export default class LabelsSelect { labelCount = data.labels.length; // EE Specific - if (isEE) { + if (IS_EE) { /** * For Scoped labels, the last label selected with the * same key will be applied to the current issueable. diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index cc5e12aa467..5e90893b684 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -727,14 +727,6 @@ export const NavigationType = { }; /** - * Returns the value of `gon.ee` - * Used to check if it's the EE codebase or the CE one. - * - * @returns Boolean - */ -export const isEE = () => window.gon && window.gon.ee; - -/** * Checks if the given Label has a special syntax `::` in * it's title. * diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9f30a989295..9e97f345717 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,6 +33,8 @@ import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import { __ } from './locale'; +import 'ee_else_ce/main_ee'; + // expose jQuery as global (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; @@ -119,11 +121,15 @@ function deferredInitialisation() { .catch(() => {}); } + const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); + const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0; + // Initialize tooltips $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', boundary: 'viewport', + delay, }); // Initialize popovers diff --git a/app/assets/javascripts/main_ee.js b/app/assets/javascripts/main_ee.js new file mode 100644 index 00000000000..84d74775163 --- /dev/null +++ b/app/assets/javascripts/main_ee.js @@ -0,0 +1 @@ +// This is an empty file to satisfy ee_else_ce import for the EE main entry point diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 2cbda8ea05d..ba79a697df2 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -4,7 +4,6 @@ import _ from 'underscore'; import { mapActions, mapState } from 'vuex'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import '~/vue_shared/mixins/is_ee'; import { getParameterValues } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import MonitorAreaChart from './charts/area.vue'; @@ -124,6 +123,11 @@ export default { required: false, default: '', }, + smallEmptyState: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -155,6 +159,12 @@ export default { selectedDashboardText() { return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); }, + addingMetricsAvailable() { + return IS_EE && this.canAddMetrics && !this.showEmptyState; + }, + alertWidgetAvailable() { + return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint; + }, }, created() { this.setEndpoints({ @@ -308,7 +318,7 @@ export default { </div> </div> <div class="d-flex"> - <div v-if="isEE && canAddMetrics && !showEmptyState"> + <div v-if="addingMetricsAvailable"> <gl-button v-gl-modal-directive="$options.addMetric.modalId" class="js-add-metric-button text-success border-success" @@ -367,7 +377,7 @@ export default { group-id="monitor-area-chart" > <alert-widget - v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData" + v-if="alertWidgetAvailable && graphData" :alerts-endpoint="alertsEndpoint" :relevant-queries="graphData.queries" :alerts-to-manage="getGraphAlerts(graphData.queries)" @@ -386,6 +396,7 @@ export default { :empty-loading-svg-path="emptyLoadingSvgPath" :empty-no-data-svg-path="emptyNoDataSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" + :compact="smallEmptyState" /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a3c6de14aa4..1bb40447a3e 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -1,7 +1,11 @@ <script> import { __ } from '~/locale'; +import { GlEmptyState } from '@gitlab/ui'; export default { + components: { + GlEmptyState, + }, props: { documentationPath: { type: String, @@ -37,6 +41,11 @@ export default { type: String, required: true, }, + compact: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -58,6 +67,8 @@ export default { If this takes a long time, ensure that data is available.`), buttonText: __('View documentation'), buttonPath: this.documentationPath, + secondaryButtonText: '', + secondaryButtonPath: '', }, noData: { svgUrl: this.emptyNoDataSvgPath, @@ -66,13 +77,19 @@ export default { no data to display.`), buttonText: __('Configure Prometheus'), buttonPath: this.settingsPath, + secondaryButtonText: '', + secondaryButtonPath: '', }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: __('Unable to connect to Prometheus server'), - description: 'Ensure connectivity is available from the GitLab server to the ', + description: __( + 'Ensure connectivity is available from the GitLab server to the Prometheus server', + ), buttonText: __('View documentation'), buttonPath: this.documentationPath, + secondaryButtonText: __('Configure Prometheus'), + secondaryButtonPath: this.settingsPath, }, }, }; @@ -81,45 +98,19 @@ export default { currentState() { return this.states[this.selectedState]; }, - showButtonDescription() { - if (this.selectedState === 'unableToConnect') return true; - return false; - }, }, }; </script> <template> - <div class="row empty-state js-empty-state"> - <div class="col-12"> - <div class="state-svg svg-content"> - <img :src="currentState.svgUrl" /> - </div> - </div> - - <div class="col-12"> - <div class="text-content"> - <h4 class="state-title text-center">{{ currentState.title }}</h4> - <p class="state-description"> - {{ currentState.description }} - <a v-if="showButtonDescription" :href="settingsPath">{{ __('Prometheus server') }}</a> - </p> - - <div class="text-center"> - <a - v-if="currentState.buttonPath" - :href="currentState.buttonPath" - class="btn btn-success" - >{{ currentState.buttonText }}</a - > - <a - v-if="currentState.secondaryButtonPath" - :href="currentState.secondaryButtonPath" - class="btn" - >{{ currentState.secondaryButtonText }}</a - > - </div> - </div> - </div> - </div> + <gl-empty-state + :title="currentState.title" + :description="currentState.description" + :primary-button-text="currentState.buttonText" + :primary-button-link="currentState.buttonPath" + :secondary-button-text="currentState.secondaryButtonText" + :secondary-button-link="currentState.secondaryButtonPath" + :svg-path="currentState.svgUrl" + :compact="compact" + /> </template> diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 84e1f1c4c20..721942f9d3b 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -36,15 +36,26 @@ function removeTimeSeriesNoData(queries) { // { metricId: 2, ...query2Attrs }] }, // { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]} // ] -function groupQueriesByChartInfo(metrics) { +export function groupQueriesByChartInfo(metrics) { const metricsByChart = metrics.reduce((accumulator, metric) => { const { queries, ...chart } = metric; - const metricId = chart.id ? chart.id.toString() : null; const chartKey = `${chart.title}|${chart.y_label}`; accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] }; - queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs })); + queries.forEach(queryAttrs => { + let metricId; + + if (chart.id) { + metricId = chart.id.toString(); + } else if (queryAttrs.metric_id) { + metricId = queryAttrs.metric_id.toString(); + } else { + metricId = null; + } + + accumulator[chartKey].queries.push({ metricId, ...queryAttrs }); + }); return accumulator; }, {}); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 5a4b5f9398b..fda494fec07 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -13,6 +13,7 @@ import { splitCamelCase, slugifyWithUnderscore, } from '../../lib/utils/text_utility'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; @@ -65,14 +66,12 @@ export default { return this.getUserData.id; }, commentButtonTitle() { - return this.noteType === constants.COMMENT ? 'Comment' : 'Start thread'; + return this.noteType === constants.COMMENT ? __('Comment') : __('Start thread'); }, startDiscussionDescription() { - let text = 'Discuss a specific suggestion or question'; - if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) { - text += ' that needs to be resolved'; - } - return `${text}.`; + return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE + ? __('Discuss a specific suggestion or question that needs to be resolved.') + : __('Discuss a specific suggestion or question.'); }, isOpen() { return this.openState === constants.OPENED || this.openState === constants.REOPENED; @@ -127,8 +126,8 @@ export default { }, issuableTypeTitle() { return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE - ? 'merge request' - : 'issue'; + ? __('merge request') + : __('issue'); }, trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); @@ -203,7 +202,7 @@ export default { this.discard(); } else { Flash( - 'Something went wrong while adding your comment. Please try again.', + __('Something went wrong while adding your comment. Please try again.'), 'alert', this.$refs.commentForm, ); @@ -219,8 +218,9 @@ export default { .catch(() => { this.enableButton(); this.discard(false); - const msg = `Your comment could not be submitted! -Please check your network connection and try again.`; + const msg = __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ); Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); @@ -235,7 +235,10 @@ Please check your network connection and try again.`; toggleIssueState() { if (this.isOpen) { this.closeIssue() - .then(() => this.enableButton()) + .then(() => { + this.enableButton(); + refreshUserMergeRequestCounts(); + }) .catch(() => { this.enableButton(); this.toggleStateButtonLoading(false); @@ -248,7 +251,10 @@ Please check your network connection and try again.`; }); } else { this.reopenIssue() - .then(() => this.enableButton()) + .then(() => { + this.enableButton(); + refreshUserMergeRequestCounts(); + }) .catch(({ data }) => { this.enableButton(); this.toggleStateButtonLoading(false); @@ -298,7 +304,7 @@ Please check your network connection and try again.`; const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); this.autosave = new Autosave($(this.$refs.textarea), [ - 'Note', + __('Note'), noteableType, this.getNoteableData.id, ]); @@ -359,8 +365,8 @@ Please check your network connection and try again.`; class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" - aria-label="Description" - placeholder="Write a comment or drag your files here…" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()" @@ -381,7 +387,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" data-track-event="click_button" @click.prevent="handleSave()" > - {{ __(commentButtonTitle) }} + {{ commentButtonTitle }} </button> <button :disabled="isSubmitButtonDisabled" @@ -390,7 +396,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" data-display="static" data-toggle="dropdown" - aria-label="Open comment type dropdown" + :aria-label="__('Open comment type dropdown')" > <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i> </button> @@ -404,8 +410,14 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> - <strong>Comment</strong> - <p>Add a general comment to this {{ noteableDisplayName }}.</p> + <strong>{{ __('Comment') }}</strong> + <p> + {{ + sprintf(__('Add a general comment to this %{noteableDisplayName}.'), { + noteableDisplayName, + }) + }} + </p> </div> </button> </li> @@ -418,7 +430,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> - <strong>Start thread</strong> + <strong>{{ __('Start thread') }}</strong> <p>{{ startDiscussionDescription }}</p> </div> </button> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 54c242b2fda..164e79c6294 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -100,7 +100,7 @@ export default { class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button" @click="fetchDiff" > - Try again + {{ __('Try again') }} </button> </td> <td v-else class="line_content js-success-lazy-load"> diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 2ff0fee62f3..0b136549c14 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -8,12 +8,14 @@ import SystemNote from '~/vue_shared/components/notes/system_note.vue'; import NoteableNote from './noteable_note.vue'; import ToggleRepliesWidget from './toggle_replies_widget.vue'; import NoteEditedText from './note_edited_text.vue'; +import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue'; export default { name: 'DiscussionNotes', components: { ToggleRepliesWidget, NoteEditedText, + DiscussionNotesRepliesWrapper, }, props: { discussion: { @@ -119,9 +121,7 @@ export default { /> <slot slot="avatar-badge" name="avatar-badge"></slot> </component> - <div - :class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''" - > + <discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion"> <toggle-replies-widget v-if="hasReplies" :collapsed="!isExpanded" @@ -141,7 +141,7 @@ export default { /> </template> <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> - </div> + </discussion-notes-replies-wrapper> </template> <template v-else> <component diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue new file mode 100644 index 00000000000..2ddca56ddd5 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue @@ -0,0 +1,27 @@ +<script> +/** + * Wrapper for discussion notes replies section. + * + * This is a functional component using the render method because in some cases + * the wrapper is not needed and we want to simply render along the children. + */ +export default { + functional: true, + props: { + isDiffDiscussion: { + type: Boolean, + required: false, + default: false, + }, + }, + render(h, { props, children }) { + if (props.isDiffDiscussion) { + return h('li', { class: 'discussion-collapsible bordered-box clearfix' }, [ + h('ul', { class: 'notes' }, children), + ]); + } + + return children; + }, +}; +</script> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 941b6d5cab3..d4a57d5d58d 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -4,6 +4,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; import { glEmojiTag } from '../../emoji'; +import { __, sprintf } from '~/locale'; export default { components: { @@ -108,23 +109,26 @@ export default { // Add myself to the beginning of the list so title will start with You. if (hasReactionByCurrentUser) { - namesToShow.unshift('You'); + namesToShow.unshift(__('You')); } let title = ''; // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; + title = sprintf(__(`%{listToShow}, and %{awardsListLength} more.`), { + listToShow: namesToShow.join(', '), + awardsListLength: remainingAwardList.length, + }); } else if (namesToShow.length > 1) { // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); // If we have more than 2 users we need an extra comma before and text. title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; // Append and text + title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }); // Append and text } else { // We have only 2 users so join them with and. - title = namesToShow.join(' and '); + title = namesToShow.join(__(' and ')); } return title; @@ -155,7 +159,7 @@ export default { awardName: parsedName, }; - this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.')); + this.toggleAwardRequest(data).catch(() => Flash(__('Something went wrong on our end.'))); }, }, }; @@ -184,7 +188,7 @@ export default { :class="{ 'js-user-authored': isAuthoredByMe }" class="award-control btn js-add-award" title="Add reaction" - aria-label="Add reaction" + :aria-label="__('Add reaction')" data-boundary="viewport" type="button" > diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 01be4f2b094..3823861c0b9 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,14 +1,14 @@ <script> import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mapGetters, mapActions } from 'vuex'; +import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import { getDraft, updateDraft } from '~/lib/utils/autosave'; -import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; export default { name: 'NoteForm', @@ -174,6 +174,18 @@ export default { (this.line && this.line.can_receive_suggestion) ); }, + changedCommentText() { + return sprintf( + __( + 'This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost.', + ), + { + startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`, + endTag: '</a>', + }, + false, + ); + }, }, watch: { noteBody() { @@ -228,11 +240,11 @@ export default { <template> <div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form"> - <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> - This comment has changed since you started editing, please review the - <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure - information is not lost. - </div> + <div + v-if="conflictWhileEditing" + class="js-conflict-edit-warning alert alert-danger" + v-html="changedCommentText" + ></div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> <issue-warning @@ -264,8 +276,8 @@ export default { name="note[note]" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" dir="auto" - aria-label="Description" - placeholder="Write a comment or drag your files here…" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" @keydown.meta.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()" @keydown.exact.up="editMyLastNote()" @@ -339,7 +351,7 @@ export default { type="button" @click="cancelHandler()" > - Cancel + {{ __('Cancel') }} </button> </template> </div> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 6466ab3acbe..3158e086f6c 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -103,7 +103,7 @@ export default { </template> <i class="fa fa-spinner fa-spin editing-spinner" - aria-label="Comment is being updated" + :aria-label="__('Comment is being updated')" aria-hidden="true" ></i> </span> diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue index e3eb92956b1..ccfe84ab098 100644 --- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue @@ -1,5 +1,6 @@ <script> import { mapGetters } from 'vuex'; +import { __, sprintf } from '~/locale'; export default { computed: { @@ -10,12 +11,24 @@ export default { signInLink() { return this.getNotesDataByProp('newSessionPath'); }, + signedOutText() { + return sprintf( + __( + 'Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply', + ), + { + startTagRegister: `<a href="${this.registerLink}">`, + startTagSignIn: `<a href="${this.signInLink}">`, + endRegisterTag: '</a>', + endSignInTag: '</a>', + }, + false, + ); + }, }, }; </script> <template> - <div class="disabled-comment text-center"> - Please <a :href="registerLink">register</a> or <a :href="signInLink">sign in</a> to reply - </div> + <div class="disabled-comment text-center" v-html="signedOutText"></div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index a71a89cfffc..ac743d9f4b8 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -144,15 +144,6 @@ export default { return {}; }, - componentClassName() { - if (this.shouldRenderDiffs) { - if (!this.lastUpdatedAt && !this.discussion.resolved) { - return 'unresolved'; - } - } - - return ''; - }, isExpanded() { return this.discussion.expanded || this.alwaysExpanded; }, @@ -283,8 +274,9 @@ export default { this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { - const msg = `Your comment could not be submitted! -Please check your network connection and try again.`; + const msg = __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ); Flash(msg, 'alert', this.$el); this.$refs.noteForm.note = noteText; callback(err); @@ -312,11 +304,11 @@ Please check your network connection and try again.`; </script> <template> - <timeline-entry-item class="note note-discussion" :class="componentClassName"> + <timeline-entry-item class="note note-discussion"> <div class="timeline-content"> <div :data-discussion-id="discussion.id" class="discussion js-discussion-container"> <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> - <div v-once class="timeline-icon"> + <div v-once class="timeline-icon align-self-start flex-shrink-0"> <user-avatar-link v-if="author" :link-href="author.path" @@ -325,7 +317,7 @@ Please check your network connection and try again.`; :img-size="40" /> </div> - <div class="timeline-content"> + <div class="timeline-content w-100"> <note-header :author="author" :created-at="firstNote.created_at" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index aa80e25a3e0..2f201839d45 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -5,7 +5,7 @@ import { escape } from 'underscore'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import draftMixin from 'ee_else_ce/notes/mixins/draft'; -import { s__, sprintf } from '../../locale'; +import { __, s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; @@ -128,9 +128,13 @@ export default { this.$emit('handleEdit'); }, deleteHandler() { - const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment'; - // eslint-disable-next-line no-alert - if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) { + const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment'); + if ( + // eslint-disable-next-line no-alert + window.confirm( + sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { typeOfComment }), + ) + ) { this.isDeleting = true; this.$emit('handleDeleteNote', this.note); @@ -141,7 +145,7 @@ export default { this.isDeleting = false; }) .catch(() => { - Flash('Something went wrong while deleting your note. Please try again.'); + Flash(__('Something went wrong while deleting your note. Please try again.')); this.isDeleting = false; }); } @@ -185,7 +189,7 @@ export default { this.isRequesting = false; this.isEditing = true; this.$nextTick(() => { - const msg = 'Something went wrong while editing your comment. Please try again.'; + const msg = __('Something went wrong while editing your comment. Please try again.'); Flash(msg, 'alert', this.$el); this.recoverNoteContent(noteText); callback(); @@ -195,7 +199,7 @@ export default { formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert - if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; + if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return; } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 4d00e957973..a0695f9e191 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; import { mapGetters, mapActions } from 'vuex'; import { getLocationHash } from '../../lib/utils/url_utility'; import Flash from '../../flash'; @@ -170,7 +171,7 @@ export default { .catch(() => { this.setLoadingState(false); this.setNotesFetchedState(true); - Flash('Something went wrong while fetching comments. Please try again.'); + Flash(__('Something went wrong while fetching comments. Please try again.')); }); }, initPolling() { diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 57dd1c5cab2..c70c0e4095c 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import { isEE } from '~/lib/utils/common_utils'; import initNoteStats from 'ee_else_ce/event_tracking/notes'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; @@ -41,9 +40,7 @@ document.addEventListener('DOMContentLoaded', () => { }; }, mounted() { - if (isEE) { - initNoteStats(); - } + initNoteStats(); }, render(createElement) { return createElement('notes-app', { diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index 237e70c0a4c..47a6f07cce2 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Api from '~/api'; import VueResource from 'vue-resource'; import * as constants from '../constants'; @@ -45,7 +44,4 @@ export default { toggleIssueState(endpoint, data) { return Vue.http.put(endpoint, data); }, - applySuggestion(id) { - return Api.applySuggestion(id); - }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 9054b4779aa..fef962f008e 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -14,6 +14,7 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import { __ } from '~/locale'; +import Api from '~/api'; let eTagPoll; @@ -449,8 +450,7 @@ export const submitSuggestion = ( { commit, dispatch }, { discussionId, noteId, suggestionId, flashContainer }, ) => - service - .applySuggestion(suggestionId) + Api.applySuggestion(suggestionId) .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId })) .then(() => dispatch('resolveDiscussion', { discussionId }).catch(() => {})) .catch(err => { diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 1b56b97f751..d51d411f3c6 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -82,7 +82,7 @@ export default class Todos { }) .catch(() => { this.updateRowState(target, true); - return flash(__('Error updating todo status.')); + return flash(__('Error updating status of to-do item.')); }); } @@ -124,7 +124,7 @@ export default class Todos { this.updateAllState(target, data); this.updateBadges(data); }) - .catch(() => flash(__('Error updating status for all todos.'))); + .catch(() => flash(__('Error updating status for all to-do items.'))); } updateAllState(target, data) { diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 23fb5656008..dcdee77a8ab 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,11 +1,15 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; import { FILTERED_SEARCH } from '~/pages/constants'; import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import initManualOrdering from '~/manual_ordering'; +const ISSUE_BULK_UPDATE_PREFIX = 'issue_'; + document.addEventListener('DOMContentLoaded', () => { IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); + issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX); initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 941c4552579..2205a7bafe3 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -17,7 +17,5 @@ export default () => { new MilestoneSelect(); new IssuableTemplateSelectors(); - if (gon.features.graphql) { - initSuggestions(); - } + initSuggestions(); }; diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 1e66ccbfa29..0d9e992e596 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -76,7 +76,7 @@ export default { variables: { projectPath: this.projectPath, ref: this.ref, - path: this.path, + path: this.path || '/', nextPageCursor: this.nextPageCursor, pageSize: PAGE_SIZE, }, diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 0ad2b3a73a2..fa6b6bfaef1 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,4 +1,6 @@ <script> +import { n__ } from '~/locale'; + export default { name: 'AssigneeTitle', props: { @@ -24,7 +26,7 @@ export default { computed: { assigneeTitle() { const assignees = this.numberOfAssignees; - return assignees > 1 ? `${assignees} Assignees` : 'Assignee'; + return n__('Assignee', `%d Assignees`, assignees); }, }, }; @@ -32,18 +34,18 @@ export default { <template> <div class="title hide-collapsed"> {{ assigneeTitle }} - <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"> </i> + <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i> <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#"> {{ __('Edit') }} </a> <a v-if="showToggle" - aria-label="Toggle sidebar" + :aria-label="__('Toggle sidebar')" class="gutter-toggle float-right js-sidebar-toggle" href="#" role="button" > - <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"> </i> + <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> </a> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 0074d7099dc..805c21d0965 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -1,5 +1,5 @@ <script> -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -62,7 +62,8 @@ export default { return this.numberOfHiddenAssignees > 0; }, hiddenAssigneesLabel() { - return `+ ${this.numberOfHiddenAssignees} more`; + const { numberOfHiddenAssignees } = this; + return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees }); }, collapsedTooltipTitle() { const maxRender = Math.min(this.defaultRenderCount, this.users.length); @@ -103,12 +104,15 @@ export default { // Everyone can merge return null; } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) { - return 'No one can merge'; + return __('No one can merge'); } else if (assigneesCount === 1) { - return 'Cannot merge'; + return __('Cannot merge'); } - return `${canMergeCount}/${assigneesCount} can merge`; + return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), { + canMergeCount, + assigneesCount, + }); }, }, methods: { @@ -128,7 +132,7 @@ export default { return `${this.rootPath}${user.username}`; }, assigneeAlt(user) { - return `${user.name}'s avatar`; + return sprintf(__("%{userName}'s avatar"), { userName: user.name }); }, assigneeUsername(user) { return `@${user.username}`; @@ -153,7 +157,7 @@ export default { data-placement="left" data-boundary="viewport" > - <i v-if="hasNoUsers" aria-label="None" class="fa fa-user"> </i> + <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i> <button v-for="(user, index) in users" v-if="shouldRenderCollapsedAssignee(index)" @@ -185,9 +189,12 @@ export default { </span> <template v-if="hasNoUsers"> <span class="assign-yourself no-value qa-assign-yourself"> - None + {{ __('None') }} <template v-if="editable"> - - <button type="button" class="btn-link" @click="assignSelf">assign yourself</button> + - + <button type="button" class="btn-link" @click="assignSelf"> + {{ __('assign yourself') }} + </button> </template> </span> </template> @@ -232,9 +239,7 @@ export default { <template v-if="showLess"> {{ hiddenAssigneesLabel }} </template> - <template v-else> - - show less - </template> + <template v-else>{{ __('- show less') }}</template> </button> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index cfa7029b388..be1e4811856 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -2,8 +2,10 @@ import Flash from '~/flash'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import AssigneeTitle from './assignee_title.vue'; import Assignees from './assignees.vue'; +import { __ } from '~/locale'; export default { name: 'SidebarAssignees', @@ -72,9 +74,12 @@ export default { this.mediator .saveAssignees(this.field) .then(setLoadingFalse.bind(this)) + .then(() => { + refreshUserMergeRequestCounts(); + }) .catch(() => { setLoadingFalse(); - return new Flash('Error occurred when saving assignees'); + return new Flash(__('Error occurred when saving assignees')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 4b9bb5c7b0e..5d0e39e8195 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -1,6 +1,7 @@ <script> import $ from 'jquery'; import eventHub from '../../event_hub'; +import { __ } from '~/locale'; export default { props: { @@ -15,7 +16,7 @@ export default { }, computed: { toggleButtonText() { - return this.isConfidential ? 'Turn Off' : 'Turn On'; + return this.isConfidential ? __('Turn Off') : __('Turn On'); }, updateConfidentialBool() { return !this.isConfidential; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 657ac837baf..24d5b14ded9 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -79,7 +79,7 @@ export default { } else if (this.showSpentOnlyState) { return `${this.timeSpent} / --`; } else if (this.showNoTimeTrackingState) { - return 'None'; + return __('None'); } return ''; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index bc263bc36e4..06aca547183 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -2,6 +2,7 @@ import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; import { GlProgressBar } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; export default { name: 'TimeTrackingComparisonPane', @@ -43,8 +44,14 @@ export default { return stringifyTime(this.parsedTimeRemaining); }, timeRemainingTooltip() { - const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; - return `${prefix} ${this.timeRemainingHumanReadable}`; + const { timeRemainingHumanReadable, timeRemainingMinutes } = this; + return timeRemainingMinutes < 0 + ? sprintf(s__('TimeTracking|Over by %{timeRemainingHumanReadable}'), { + timeRemainingHumanReadable, + }) + : sprintf(s__('TimeTracking|Time remaining: %{timeRemainingHumanReadable}'), { + timeRemainingHumanReadable, + }); }, /* Diff values for comparison meter */ timeRemainingMinutes() { @@ -74,12 +81,12 @@ export default { <gl-progress-bar :value="timeRemainingPercent" :variant="progressBarVariant" /> <div class="compare-display-container"> <div class="compare-display float-left"> - <span class="compare-label"> {{ s__('TimeTracking|Spent') }} </span> - <span class="compare-value spent"> {{ timeSpentHumanReadable }} </span> + <span class="compare-label">{{ s__('TimeTracking|Spent') }}</span> + <span class="compare-value spent">{{ timeSpentHumanReadable }}</span> </div> <div class="compare-display estimated float-right"> - <span class="compare-label"> {{ s__('TimeTrackingEstimated|Est') }} </span> - <span class="compare-value"> {{ timeEstimateHumanReadable }} </span> + <span class="compare-label">{{ s__('TimeTrackingEstimated|Est') }}</span> + <span class="compare-value">{{ timeEstimateHumanReadable }}</span> </div> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue index 7c7356e2afa..c2f30310e2e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue @@ -1,4 +1,6 @@ <script> +import { sprintf, s__ } from '~/locale'; + export default { name: 'TimeTrackingSpentOnlyPane', props: { @@ -7,11 +9,22 @@ export default { required: true, }, }, + computed: { + timeSpent() { + return sprintf( + s__('TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}'), + { + startTag: '<span class="bold">', + endTag: '</span>', + timeSpentHumanReadable: this.timeSpentHumanReadable, + }, + false, + ); + }, + }, }; </script> <template> - <div class="time-tracking-spend-only-pane"> - <span class="bold">Spent:</span> {{ timeSpentHumanReadable }} - </div> + <div class="time-tracking-spend-only-pane" v-html="timeSpent"></div> </template> diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index 57125c78cf6..e6f2fe2b5fc 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -5,8 +5,8 @@ import { GlLoadingIcon } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -const MARK_TEXT = __('Mark todo as done'); -const TODO_TEXT = __('Add todo'); +const MARK_TEXT = __('Mark as done'); +const TODO_TEXT = __('Add a To Do'); export default { directives: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 34cdb70ce14..5c7859828d8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -125,7 +125,9 @@ export default { this.isStopping = false; }) .catch(() => { - createFlash('Something went wrong while stopping this environment. Please try again.'); + createFlash( + __('Something went wrong while stopping this environment. Please try again.'), + ); this.isStopping = false; }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index e20a16900d4..fb826be19f5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -139,7 +139,7 @@ export default { type="button" class="btn dropdown-toggle qa-dropdown-toggle" data-toggle="dropdown" - aria-label="Download as" + :aria-label="__('Download as')" aria-haspopup="true" aria-expanded="false" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 5958c2cf87e..8e8e67228ed 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -6,6 +6,7 @@ import statusIcon from '../mr_widget_status_icon.vue'; import MrWidgetAuthor from '../../components/mr_widget_author.vue'; import eventHub from '../../event_hub'; import { AUTO_MERGE_STRATEGIES } from '../../constants'; +import { __ } from '~/locale'; export default { name: 'MRWidgetAutoMergeEnabled', @@ -55,7 +56,7 @@ export default { }) .catch(() => { this.isCancellingAutoMerge = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); }); }, removeSourceBranch() { @@ -76,7 +77,7 @@ export default { }) .catch(() => { this.isRemovingSourceBranch = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); }); }, }, @@ -107,15 +108,15 @@ export default { <section class="mr-info-list"> <p> {{ s__('mrWidget|The changes will be merged into') }} - <a :href="mr.targetBranchPath" class="label-branch"> {{ mr.targetBranch }} </a> + <a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a> </p> <p v-if="mr.shouldRemoveSourceBranch"> {{ s__('mrWidget|The source branch will be deleted') }} </p> <p v-else class="d-flex align-items-start"> - <span class="append-right-10"> - {{ s__('mrWidget|The source branch will not be deleted') }} - </span> + <span class="append-right-10">{{ + s__('mrWidget|The source branch will not be deleted') + }}</span> <a v-if="canRemoveSourceBranch" :disabled="isRemovingSourceBranch" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 0bcccc50eb2..c7b064b8506 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -4,6 +4,7 @@ import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; import Flash from '../../../flash'; +import { __, sprintf } from '~/locale'; export default { name: 'MRWidgetRebase', @@ -40,6 +41,17 @@ export default { showDisabledButton() { return ['failed', 'loading'].includes(this.status); }, + fastForwardMergeText() { + return sprintf( + __( + `Fast-forward merge is not possible. Rebase the source branch onto %{startTag}${this.mr.targetBranch}%{endTag} to allow this merge request to be merged.`, + ), + { + startTag: '<span class="label-branch">', + endTag: '</span>', + }, + ); + }, }, methods: { rebase() { @@ -54,7 +66,7 @@ export default { .catch(error => { this.rebasingError = error.merge_error; this.isMakingRequest = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); }); }, checkRebaseStatus(continuePolling, stopPolling) { @@ -69,7 +81,7 @@ export default { if (res.merge_error && res.merge_error.length) { this.rebasingError = res.merge_error; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); } eventHub.$emit('MRWidgetRebaseSuccess'); @@ -78,7 +90,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); stopPolling(); }); }, @@ -91,19 +103,14 @@ export default { <div class="rebase-state-find-class-convention media media-body space-children"> <template v-if="mr.rebaseInProgress || isMakingRequest"> - <span class="bold"> Rebase in progress </span> + <span class="bold">{{ __('Rebase in progress') }}</span> </template> <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch"> - <span class="bold"> - Fast-forward merge is not possible. Rebase the source branch onto - <span class="label-branch">{{ mr.targetBranch }}</span> to allow this merge request to be - merged. - </span> + <span class="bold" v-html="fastForwardMergeText"></span> </template> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <div - class="accept-merge-holder clearfix -js-toggle-container accept-action media space-children" + class="accept-merge-holder clearfix js-toggle-container accept-action media space-children" > <button :disabled="isMakingRequest" @@ -111,14 +118,14 @@ js-toggle-container accept-action media space-children" class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" @click="rebase" > - <gl-loading-icon v-if="isMakingRequest" /> - Rebase + <gl-loading-icon v-if="isMakingRequest" />{{ __('Rebase') }} </button> - <span v-if="!rebasingError" class="bold"> - Fast-forward merge is not possible. Rebase the source branch onto the target branch or - merge target branch into source branch to allow this merge request to be merged. - </span> - <span v-else class="bold danger"> {{ rebasingError }} </span> + <span v-if="!rebasingError" class="bold">{{ + __( + 'Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged.', + ) + }}</span> + <span v-else class="bold danger">{{ rebasingError }}</span> </div> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index a38495bb4cc..7312b31c01c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -22,19 +22,29 @@ export default { <span v-html="emptyStateSVG"></span> </div> <div class="text col-md-7 order-md-first col-12"> - <span> - Merge requests are a place to propose changes you have made to a project and discuss those - changes with others. - </span> - <p>Interested parties can even contribute by pushing commits if they want to.</p> + <span>{{ + s__( + 'mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others.', + ) + }}</span> <p> - Currently there are no changes in this merge request's source branch. Please push new - commits or use a different branch. + {{ + s__( + 'mrWidgetNothingToMerge|Interested parties can even contribute by pushing commits if they want to.', + ) + }} + </p> + <p> + {{ + s__( + "mrWidgetNothingToMerge|Currently there are no changes in this merge request's source branch. Please push new commits or use a different branch.", + ) + }} </p> <div> - <a v-if="mr.newBlobPath" :href="mr.newBlobPath" class="btn btn-inverted btn-success"> - Create file - </a> + <a v-if="mr.newBlobPath" :href="mr.newBlobPath" class="btn btn-inverted btn-success">{{ + __('Create file') + }}</a> </div> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index ca1b4a57717..d4514767912 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -6,6 +6,7 @@ import simplePoll from '~/lib/utils/simple_poll'; import { __ } from '~/locale'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; import MergeRequest from '../../../merge_request'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -174,6 +175,8 @@ export default { MergeRequest.decreaseCounter(); stopPolling(); + refreshUserMergeRequestCounts(); + // If user checked remove source branch and we didn't remove the branch yet // we should start another polling for source branch remove process if (this.removeSourceBranch && data.source_branch_exists) { @@ -248,7 +251,7 @@ export default { type="button" class="btn btn-sm btn-info dropdown-toggle js-merge-moment" data-toggle="dropdown" - aria-label="Select merge moment" + :aria-label="__('Select merge moment')" > <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 7c322388d30..91c0b40a0b5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -46,14 +46,20 @@ export default { <status-icon :show-disabled-button="Boolean(mr.removeWIPPath)" status="warning" /> <div class="media-body space-children"> <span class="bold"> - This is a Work in Progress + {{ __('This is a Work in Progress') }} <i v-tooltip class="fa fa-question-circle" - title="When this merge request is ready, - remove the WIP: prefix from the title to allow it to be merged" - aria-label="When this merge request is ready, - remove the WIP: prefix from the title to allow it to be merged" + :title=" + s__( + 'mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged', + ) + " + :aria-label=" + s__( + 'mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged', + ) + " > </i> </span> @@ -64,8 +70,8 @@ export default { class="btn btn-default btn-sm js-remove-wip" @click="removeWIP" > - <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> Resolve WIP - status + <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> + {{ s__('mrWidget|Resolve WIP status') }} </button> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index a79da476890..8d415c1bbea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -263,8 +263,11 @@ export default { if (!data.pipeline) return; const { label } = data.pipeline.details.status; - const title = `Pipeline ${label}`; - const message = `Pipeline ${label} for "${data.title}"`; + const title = sprintf(__('Pipeline %{label}'), { label }); + const message = sprintf(__('Pipeline %{label} for "%{dataTitle}"'), { + dataTitle: data.title, + label, + }); notify.notifyMe(title, message, this.mr.gitlabLogo); }, diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index e9ab6f5ba7a..15cb0bd9792 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -1,7 +1,6 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; import { getCommitIconMap } from '~/ide/utils'; @@ -69,7 +68,7 @@ export default { }); } else if (this.file.changed && this.file.staged) { return sprintf(__('Unstaged and staged %{type}'), { - type: pluralize(type), + type, }); } diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index a1168fa0f1e..ae9b013d980 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,6 +1,7 @@ <script> import _ from 'underscore'; import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from './user_avatar/user_avatar_link.vue'; import Icon from '../../vue_shared/components/icon.vue'; @@ -129,7 +130,9 @@ export default { * @returns {String} */ userImageAltDescription() { - return this.author && this.author.username ? `${this.author.username}'s avatar` : null; + return this.author && this.author.username + ? sprintf(__("%{username}'s avatar"), { username: this.author.username }) + : null; }, }, }; @@ -180,7 +183,7 @@ export default { {{ title }} </gl-link> </tooltip-on-truncate> - <span v-else> Can't find HEAD commit for this branch </span> + <span v-else>{{ __("Can't find HEAD commit for this branch") }}</span> </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index ba63683f5c0..da0b45110e2 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -3,6 +3,7 @@ import { __ } from '~/locale'; const viewers = { image: { id: 'image', + binary: true, }, markdown: { id: 'markdown', diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index 2ca933a37d2..fc6a45b957e 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -91,7 +91,9 @@ export default { | </template> <template v-if="hasDimensions"> - <strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }} + <strong>{{ s__('ImageViewerDimensions|W') }}</strong + >: {{ width }} | <strong>{{ s__('ImageViewerDimensions|H') }}</strong + >: {{ height }} </template> </p> </div> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 5fdc915fffb..655f0054887 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -40,7 +40,7 @@ export default { this.fetchMarkdownPreview(); }, destroyed() { - if (this.isLoading) axiosSource.cancel('Cancelling Preview'); + if (this.isLoading) axiosSource.cancel(__('Cancelling Preview')); }, methods: { fetchMarkdownPreview() { diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 36b3ee05456..d5558d93219 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -1,5 +1,7 @@ <script> /* eslint-disable vue/require-default-prop */ +import { __ } from '~/locale'; + export default { name: 'DeprecatedModal', // use GlModal instead @@ -39,7 +41,7 @@ export default { closeButtonLabel: { type: String, required: false, - default: 'Cancel', + default: __('Cancel'), }, primaryButtonLabel: { type: String, @@ -94,7 +96,7 @@ export default { type="button" class="close float-right" data-dismiss="modal" - aria-label="Close" + :aria-label="__('Close')" @click="emitCancel($event)" > <span aria-hidden="true">×</span> diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue index 7d49c87271d..c35fee84771 100644 --- a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue @@ -69,7 +69,7 @@ export default { data-display="static" data-toggle="dropdown" > - <icon name="arrow-down" aria-label="toggle dropdown" /> + <icon name="arrow-down" :aria-label="__('toggle dropdown')" /> </button> <ul :class="dropdownClass" class="dropdown-menu dropdown-open-top"> <template v-for="(action, index) in actions"> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue index 4e5dfbf3bf8..20bcceeb477 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue @@ -115,7 +115,7 @@ export default { data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" - aria-label="Expand dropdown" + :aria-label="__('Expand dropdown')" > <icon name="angle-down" :size="12" /> </button> @@ -125,7 +125,7 @@ export default { ref="searchInput" v-model="filter" type="search" - placeholder="Filter" + :placeholder="__('Filter')" class="js-filtered-dropdown-input dropdown-input-field" /> <icon class="dropdown-input-search" name="search" /> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 3f45dc7853b..c652a684d7c 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,5 +1,6 @@ <script> import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; import CiIconBadge from './ci_badge_link.vue'; import TimeagoTooltip from './time_ago_tooltip.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue'; @@ -65,7 +66,7 @@ export default { computed: { userAvatarAltText() { - return `${this.user.name}'s avatar`; + return sprintf(__(`%{username}'s avatar`), { username: this.user.name }); }, }, @@ -87,16 +88,12 @@ export default { <strong> {{ itemName }} #{{ itemId }} </strong> - <template v-if="shouldRenderTriggeredLabel"> - triggered - </template> - <template v-else> - created - </template> + <template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template> + <template v-else>{{ __('created') }}</template> <timeago-tooltip :time="time" /> - by + {{ __('by') }} <template v-if="user"> <gl-link diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index e438ff16a41..47f0851f650 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,7 +1,7 @@ <script> import { GlLink } from '@gitlab/ui'; import _ from 'underscore'; -import { sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import icon from '../../../vue_shared/components/icon.vue'; function buildDocsLinkStart(path) { @@ -47,7 +47,9 @@ export default { }, confidentialAndLockedDiscussionText() { return sprintf( - 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.', + __( + 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.', + ), { confidentialLinkStart: buildDocsLinkStart(this.confidentialIssueDocsPath), lockedLinkStart: buildDocsLinkStart(this.lockedIssueDocsPath), @@ -66,7 +68,7 @@ export default { <span v-if="isLockedAndConfidential"> <span v-html="confidentialAndLockedDiscussionText"></span> {{ - __(`People without permission will never get a notification and won't be able to comment.`) + __("People without permission will never get a notification and won't be able to comment.") }} </span> diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index eb0f666422f..b76679960ca 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -160,8 +160,8 @@ export default { :disabled="removeDisabled" type="button" class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button mr-xl-0 align-self-xl-center" - title="Remove" - aria-label="Remove" + :title="__('Remove')" + :aria-label="__('Remove')" @click="onRemoveRequest" > <icon :size="16" class="btn-item-remove-icon" name="close" /> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 3bdc0bb8ebd..b520d302407 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,7 +1,7 @@ <script> import $ from 'jquery'; import _ from 'underscore'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import Flash from '../../../flash'; import GLForm from '../../../gl_form'; @@ -118,6 +118,18 @@ export default { lineType() { return this.line ? this.line.type : ''; }, + addMultipleToDiscussionWarning() { + return sprintf( + __( + '%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution.', + ), + { + icon: '<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>', + usersTag: `<strong><span class="js-referenced-users-count">${this.referencedUsers.length}</span></strong>`, + }, + false, + ); + }, }, mounted() { /* @@ -172,7 +184,7 @@ export default { renderMarkdown(data = {}) { this.markdownPreviewLoading = false; - this.markdownPreview = data.body || 'Nothing to preview.'; + this.markdownPreview = data.body || __('Nothing to preview.'); if (data.references) { this.referencedCommands = data.references.commands; @@ -207,7 +219,11 @@ export default { <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> <slot name="textarea"></slot> - <a class="zen-control zen-control-leave js-zen-leave" href="#" aria-label="Enter zen mode"> + <a + class="zen-control zen-control-leave js-zen-leave" + href="#" + :aria-label="__('Enter zen mode')" + > <icon :size="32" name="screen-normal" /> </a> <markdown-toolbar @@ -246,13 +262,7 @@ export default { <template v-if="previewMarkdown && !markdownPreviewLoading"> <div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div> <div v-if="shouldShowReferencedUsers" class="referenced-users"> - <span> - <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> You are about to add - <strong> - <span class="js-referenced-users-count">{{ referencedUsers.length }}</span> - </strong> - people to the discussion. Proceed with caution. - </span> + <span v-html="addMultipleToDiscussionWarning"></span> </div> </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 8d3705e1e4a..7f0fcfac071 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,5 +1,6 @@ <script> import Vue from 'vue'; +import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; import Flash from '~/flash'; @@ -56,7 +57,7 @@ export default { const suggestionElements = container.querySelectorAll('.js-render-suggestion'); if (this.lineType === 'old') { - Flash('Unable to apply suggestions to a deleted line.', 'alert', this.$el); + Flash(__('Unable to apply suggestions to a deleted line.'), 'alert', this.$el); } suggestionElements.forEach((suggestionEl, i) => { diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index d6c398c8946..8ce5b615795 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -33,13 +33,18 @@ export default { <div class="comment-toolbar clearfix"> <div class="toolbar-text"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1" - >Markdown is supported</gl-link - > + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{ + __('Markdown is supported') + }}</gl-link> </template> <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">Markdown</gl-link> and - <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">quick actions</gl-link> + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{ + __('Markdown') + }}</gl-link> + and + <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">{{ + __('quick actions') + }}</gl-link> are supported </template> </div> @@ -57,15 +62,17 @@ export default { <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i> </span> <span class="uploading-error-message"></span> - <button class="retry-uploading-link" type="button">Try again</button> or - <button class="attach-new-file markdown-selector" type="button">attach a new file</button> + <button class="retry-uploading-link" type="button">{{ __('Try again') }}</button> or + <button class="attach-new-file markdown-selector" type="button"> + {{ __('attach a new file') }} + </button> </span> <button class="markdown-selector button-attach-file btn-link" tabindex="-1" type="button"> <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i - ><span class="text-attach-file">Attach a file</span> + ><span class="text-attach-file">{{ __('Attach a file') }}</span> </button> <button class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button"> - Cancel + {{ __('Cancel') }} </button> </span> </div> diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue index 16f4ff068f6..26d7d8e8866 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.vue +++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue @@ -1,4 +1,5 @@ <script> +import { __, sprintf } from '~/locale'; import { getTimeago } from '../../lib/utils/datetime_utility'; export default { @@ -20,7 +21,7 @@ export default { computed: { getFormattedMedian() { const deployedSince = getTimeago().format(this.deploymentTime * 1000); - return `Deployed ${deployedSince}`; + return sprintf(__('Deployed %{deployedSince}'), { deployedSince }); }, }, mounted() { diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 3c86b7e4c61..d6dfe9eded8 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -103,7 +103,7 @@ export default { <div v-if="hasMoreCommits" class="flex-list"> <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> <icon :name="toggleIcon" :size="8" class="append-right-5" /> - <span>Toggle commit list</span> + <span>{{ __('Toggle commit list') }}</span> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index b9311d65360..43bbb756805 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -14,7 +14,7 @@ /> */ - +import { __ } from '~/locale'; import defaultAvatarUrl from 'images/no_avatar.png'; import { placeholderImage } from '../../../lazy_loader'; @@ -39,7 +39,7 @@ export default { imgAlt: { type: String, required: false, - default: 'project avatar', + default: __('project avatar'), }, size: { type: Number, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index b5e43da401e..4dcc121496c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -85,7 +85,7 @@ export default { @click="toggleSidebar" > <span class="sidebar-collapsed-value"> - <span v-if="showFromText">From</span> <span>{{ dateText('min') }}</span> + <span v-if="showFromText">{{ __('From') }}</span> <span>{{ dateText('min') }}</span> </span> </collapsed-calendar-icon> <div v-if="hasMinAndMaxDates" class="text-center sidebar-collapsed-divider">-</div> @@ -96,7 +96,7 @@ export default { @click="toggleSidebar" > <span class="sidebar-collapsed-value"> - <span v-if="!minDate">Until</span> <span>{{ dateText('max') }}</span> + <span v-if="!minDate">{{ __('Until') }}</span> <span>{{ dateText('max') }}</span> </span> </collapsed-calendar-icon> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 45f01a6fced..6caf8bc92c2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -74,7 +74,7 @@ export default { return dateInWords(this.selectedDate, true); }, collapsedText() { - return this.selectedDateWords ? this.selectedDateWords : 'None'; + return this.selectedDateWords ? this.selectedDateWords : __('None'); }, }, methods: { @@ -112,7 +112,7 @@ export default { class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action" @click="toggleDatePicker" > - Edit + {{ __('Edit') }} </button> <toggle-sidebar v-if="showToggleSidebar" :collapsed="collapsed" @toggle="toggleSidebar" /> </div> @@ -137,11 +137,11 @@ export default { class="btn-blank btn-link btn-secondary-hover-link" @click="newDateSelected(null)" > - remove + {{ __('remove') }} </button> </span> </template> - <span v-else class="no-value"> None </span> + <span v-else class="no-value">{{ __('None') }}</span> </span> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 3b5ce0e9910..913c971a512 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -48,7 +48,7 @@ export default { 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed, }" - aria-label="toggle collapse" + :aria-label="__('toggle collapse')" class="fa" > </i> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index a6c1737dcab..ea483416c46 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -17,6 +17,7 @@ import { GlTooltip } from '@gitlab/ui'; import defaultAvatarUrl from 'images/no_avatar.png'; +import { __ } from '~/locale'; import { placeholderImage } from '../../../lazy_loader'; export default { @@ -43,7 +44,7 @@ export default { imgAlt: { type: String, required: false, - default: 'user avatar', + default: __('user avatar'), }, size: { type: Number, diff --git a/app/assets/javascripts/vue_shared/mixins/is_ee.js b/app/assets/javascripts/vue_shared/mixins/is_ee.js deleted file mode 100644 index 8e00d93ef18..00000000000 --- a/app/assets/javascripts/vue_shared/mixins/is_ee.js +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from 'vue'; -import { isEE } from '~/lib/utils/common_utils'; - -Vue.mixin({ - computed: { - isEE() { - return isEE(); - }, - }, -}); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cd951f67293..e75c1379dfb 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -288,7 +288,7 @@ padding: 0 1px; a, - button, + button:not(.dropdown-toggle,.ci-action-icon-container), .menu-item { @include dropdown-link; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index f75e5b55506..975dca168d5 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -35,7 +35,8 @@ background-color: $modal-body-bg; line-height: $line-height-base; position: relative; - padding: #{3 * $grid-size} #{2 * $grid-size}; + min-height: $modal-body-height; + padding: #{2 * $grid-size} #{6 * $grid-size} #{2 * $grid-size} #{2 * $grid-size}; text-align: left; white-space: normal; @@ -85,9 +86,9 @@ body.modal-open { .modal { background-color: $black-transparent; - @include media-breakpoint-up(md) { + @include media-breakpoint-up(sm) { .modal-dialog { - margin: 30px auto; + margin: 64px auto; } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b6a24247d40..406bcda418e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -805,7 +805,7 @@ $border-color-settings: #e1e1e1; /* Modals */ -$modal-body-height: 134px; +$modal-body-height: 80px; $modal-border-color: #e9ecef; $priority-label-empty-state-width: 114px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 623c44e062f..3ffe8ae304d 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1095,6 +1095,10 @@ table.code { .discussion-collapsible { margin: 0 $gl-padding $gl-padding 71px; + + .notes { + border-radius: $border-radius-default; + } } .parallel { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7bd1a4138e4..b9b8eabf909 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -139,7 +139,6 @@ $note-form-margin-left: 72px; border-radius: 4px 4px 0 0; &.collapsed { - border: 0; border-radius: 4px; } } @@ -406,7 +405,7 @@ $note-form-margin-left: 72px; border-radius: 0; @media (min-width: map-get($grid-breakpoints, md)) { - top: 91px; + top: $mr-tabs-height + $header-height; .with-performance-bar & { top: 126px; @@ -598,7 +597,8 @@ $note-form-margin-left: 72px; } .discussion-header { - min-height: 74px; + min-height: $line-height-base * 2em; + box-sizing: content-box; .note-header-info { padding-bottom: 0; @@ -608,13 +608,10 @@ $note-form-margin-left: 72px; overflow-x: auto; overflow-y: hidden; } -} -.unresolved { - .discussion-header { - .note-header-info { - margin-top: $gl-padding-8; - } + &.note-wrapper { + display: flex; + align-items: center; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 10120a472d3..60400f10ca5 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -168,6 +168,10 @@ } ul.wiki-pages-list.content-list { + a { + color: $blue-600; + } + ul { list-style: none; margin-left: 0; diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 065d2d3a4ec..6fa2f75be33 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -92,7 +92,7 @@ module IssuableActions end def bulk_update - result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name) + result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name) quantity = result[:count] render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } @@ -181,7 +181,7 @@ module IssuableActions end def authorize_admin_issuable! - unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables + unless can?(current_user, :"admin_#{resource_name}", parent) return access_denied! end end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index d43f5393ecc..daeb8fda417 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -7,8 +7,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } before_action :set_non_archived_param - before_action :projects, only: [:index] before_action :default_sorting + before_action :projects, only: [:index] skip_cross_project_access_check :index, :starred def index diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 27980466a42..8f6fcb362d2 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -22,7 +22,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController format.html do redirect_to dashboard_todos_path, status: 302, - notice: _('Todo was successfully marked as done.') + notice: _('To-do item successfully marked as done.') end format.js { head :ok } format.json { render json: todos_counts } @@ -33,7 +33,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user) respond_to do |format| - format.html { redirect_to dashboard_todos_path, status: 302, notice: _('All todos were marked as done.') } + format.html { redirect_to dashboard_todos_path, status: 302, notice: _('Everything on your to-do list is marked as done.') } format.js { head :ok } format.json { render json: todos_counts.merge(updated_ids: updated_ids) } end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 1ce0afac83b..9fbbe373b0d 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -11,7 +11,6 @@ class GraphqlController < ApplicationController # around in GraphiQL. protect_from_forgery with: :null_session, only: :execute - before_action :check_graphql_feature_flag! before_action :authorize_access_api! before_action(only: [:execute]) { authenticate_sessionless_user!(:api) } @@ -86,8 +85,4 @@ class GraphqlController < ApplicationController render json: error, status: status end - - def check_graphql_feature_flag! - render_404 unless Gitlab::Graphql.enabled? - end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d77f64a84f5..141a7dfb923 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -169,7 +169,7 @@ class Projects::BranchesController < Projects::ApplicationController end def confidential_issue_project - return unless Feature.enabled?(:create_confidential_merge_request, @project) + return unless helpers.create_confidential_merge_request_enabled? return if params[:confidential_issue_project_id].blank? confidential_issue_project = Project.find(params[:confidential_issue_project_id]) diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index 0a009477d61..32111b07a0b 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -15,24 +15,22 @@ class Projects::DeploymentsController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord def metrics - return render_404 unless deployment.has_metrics? + return render_404 unless deployment_metrics.has_metrics? - @metrics = deployment.metrics + @metrics = deployment_metrics.metrics if @metrics&.any? render json: @metrics, status: :ok else head :no_content end - rescue NotImplementedError - render_404 end def additional_metrics - return render_404 unless deployment.has_metrics? + return render_404 unless deployment_metrics.has_metrics? respond_to do |format| format.json do - metrics = deployment.additional_metrics + metrics = deployment_metrics.additional_metrics if metrics.any? render json: metrics @@ -45,6 +43,10 @@ class Projects::DeploymentsController < Projects::ApplicationController private + def deployment_metrics + @deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment) + end + # rubocop: disable CodeReuse/ActiveRecord def deployment @deployment ||= environment.deployments.find_by(iid: params[:id]) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e275b417784..228de8bc6f3 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -45,8 +45,6 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_import_issues!, only: [:import_csv] before_action :authorize_download_code!, only: [:related_branches] - before_action :set_suggested_issues_feature_flags, only: [:new] - respond_to :html def index @@ -172,7 +170,7 @@ class Projects::IssuesController < Projects::ApplicationController def create_merge_request create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid) - create_params[:target_project_id] = params[:target_project_id] if Feature.enabled?(:create_confidential_merge_request, @project) + create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled? result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute if result[:status] == :success @@ -285,8 +283,4 @@ class Projects::IssuesController < Projects::ApplicationController # 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42422') end - - def set_suggested_issues_feature_flags - push_frontend_feature_flag(:graphql, default_enabled: true) - end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 7ee8e0ea8f8..2aa2508be16 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -201,7 +201,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def rebase - RebaseWorker.perform_async(@merge_request.id, current_user.id) + @merge_request.rebase_async(current_user.id) head :ok end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 330e2d0f8a5..feefc7f8137 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -182,7 +182,7 @@ class ProjectsController < Projects::ApplicationController end def housekeeping - ::Projects::HousekeepingService.new(@project).execute + ::Projects::HousekeepingService.new(@project, :gc).execute redirect_to( project_path(@project), diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3592505a977..f4fbeacfaba 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -429,7 +429,7 @@ class IssuableFinder items = klass.with(cte.to_arel).from(klass.table_name) end - items.full_search(search, matched_columns: params[:in]) + items.full_search(search, matched_columns: params[:in], use_minimum_char_limit: !use_cte_for_search?) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index 4fca4ec94f3..ef90817416a 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -3,6 +3,8 @@ class RunnerJobsFinder attr_reader :runner, :params + ALLOWED_INDEXED_COLUMNS = %w[id].freeze + def initialize(runner, params = {}) @runner = runner @params = params @@ -11,7 +13,7 @@ class RunnerJobsFinder def execute items = @runner.builds items = by_status(items) - items + sort_items(items) end private @@ -23,4 +25,19 @@ class RunnerJobsFinder items.where(status: params[:status]) end # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def sort_items(items) + return items unless ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) + + order_by = params[:order_by] + sort = if /\A(ASC|DESC)\z/i.match?(params[:sort]) + params[:sort] + else + :desc + end + + items.order(order_by => sort) + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 5615909c4ec..152ebb930e2 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -13,6 +13,7 @@ class GitlabSchema < GraphQL::Schema use BatchLoader::GraphQL use Gitlab::Graphql::Authorize use Gitlab::Graphql::Present + use Gitlab::Graphql::CallsGitaly use Gitlab::Graphql::Connections use Gitlab::Graphql::GenericTracing diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index dd0d9105df6..efeee4a7a4d 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -7,18 +7,34 @@ module Types DEFAULT_COMPLEXITY = 1 def initialize(*args, **kwargs, &block) + @calls_gitaly = !!kwargs.delete(:calls_gitaly) + @constant_complexity = !!kwargs[:complexity] kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class]) super(*args, **kwargs, &block) end + def base_complexity + complexity = DEFAULT_COMPLEXITY + complexity += 1 if calls_gitaly? + complexity + end + + def calls_gitaly? + @calls_gitaly + end + + def constant_complexity? + @constant_complexity + end + private def field_complexity(resolver_class) if resolver_class field_resolver_complexity else - DEFAULT_COMPLEXITY + base_complexity end end @@ -31,6 +47,7 @@ module Types proc do |ctx, args, child_complexity| # Resolvers may add extra complexity depending on used arguments complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i + complexity += 1 if calls_gitaly? field_defn = to_graphql diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 577ccd48ef8..6734d4761c2 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -43,7 +43,7 @@ module Types field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true - field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false + field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage" field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 6ef1d816b7c..bc5fb709522 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,6 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle - mount_mutation Mutations::MergeRequests::SetWip + mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true end end diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index 13995d3ea8f..d877fc177d2 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -10,8 +10,8 @@ module Types abilities :read_merge_request, :admin_merge_request, :update_merge_request, :create_note - permission_field :push_to_source_branch, method: :can_push_to_source_branch? - permission_field :remove_source_branch, method: :can_remove_source_branch? + permission_field :push_to_source_branch, method: :can_push_to_source_branch?, calls_gitaly: true + permission_field :remove_source_branch, method: :can_remove_source_branch?, calls_gitaly: true permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request? permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request? end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index c25688ab043..13be71c26ee 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -26,7 +26,7 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true field :star_count, GraphQL::INT_TYPE, null: false - field :forks_count, GraphQL::INT_TYPE, null: false + field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true # 4 times field :created_at, Types::TimeType, null: true field :last_activity_at, Types::TimeType, null: true @@ -40,7 +40,7 @@ module Types field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true - field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do + field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true, resolve: -> (project, args, ctx) do project.avatar_url(only_path: false) end diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index 5987467e1ea..b024eca61fc 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -6,9 +6,9 @@ module Types authorize :download_code - field :root_ref, GraphQL::STRING_TYPE, null: true - field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty? + field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true + field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists? - field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver + field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true end end diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb index b947713074e..fbdc1597461 100644 --- a/app/graphql/types/tree/tree_type.rb +++ b/app/graphql/types/tree/tree_type.rb @@ -7,7 +7,7 @@ module Types graphql_name 'Tree' # Complexity 10 as it triggers a Gitaly call on each render - field :last_commit, Types::CommitType, null: true, complexity: 10, resolve: -> (tree, args, ctx) do + field :last_commit, Types::CommitType, null: true, complexity: 10, calls_gitaly: true, resolve: -> (tree, args, ctx) do tree.repository.last_commit_for_path(tree.sha, tree.path) end @@ -15,9 +15,9 @@ module Types Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository) end - field :submodules, Types::Tree::SubmoduleType.connection_type, null: false + field :submodules, Types::Tree::SubmoduleType.connection_type, null: false, calls_gitaly: true - field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do + field :blobs, Types::Tree::BlobType.connection_type, null: false, calls_gitaly: true, resolve: -> (obj, args, ctx) do Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository) end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index a7a4e945a99..4bf9b708401 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -187,6 +187,8 @@ module ApplicationSettingsHelper :gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast, + :grafana_enabled, + :grafana_url, :gravatar_enabled, :hashed_storage_enabled, :help_page_hide_commercial_content, diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 076976175a9..31c4b27273b 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce).freeze LDAP_PROVIDER = /\Aldap/.freeze def ldap_enabled? diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index cd2669ef6ad..67685ba4e1d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -390,8 +390,8 @@ module IssuablesHelper def issuable_todo_button_data(issuable, is_collapsed) { - todo_text: _('Add todo'), - mark_text: _('Mark todo as done'), + todo_text: _('Add a To Do'), + mark_text: _('Mark as done'), todo_icon: sprite_icon('todo-add'), mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'), issuable_id: issuable[:id], diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index dfadcfc33b2..5476a7cdff6 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -137,7 +137,7 @@ module IssuesHelper end def create_confidential_merge_request_enabled? - Feature.enabled?(:create_confidential_merge_request, @project) + Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true) end def show_new_branch_button? diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 766508b6609..3672d8b1b03 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -16,7 +16,7 @@ module PreferencesHelper project_activity: _("Your Projects' Activity"), starred_project_activity: _("Starred Projects' Activity"), groups: _("Your Groups"), - todos: _("Your Todos"), + todos: _("Your To-Do List"), issues: _("Assigned Issues"), merge_requests: _("Assigned Merge Requests"), operations: _("Operations Dashboard") diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8dee842a22d..8d0079a4dd3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -662,6 +662,6 @@ module ProjectsHelper end def vue_file_list_enabled? - Gitlab::Graphql.enabled? && Feature.enabled?(:vue_file_list, @project) + Feature.enabled?(:vue_file_list, @project) end end diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index ecf37bae6b3..ce810433a3a 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -17,6 +17,6 @@ module StorageHelper counter_lfs_objects: storage_counter(statistics.lfs_objects_size) } - _("%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters + _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects}") % counters end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fd5aa216174..20ca4a9ab24 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -790,6 +790,10 @@ module Ci stages.find_by!(name: name) end + def error_messages + errors ? errors.full_messages.to_sentence : "" + end + private def ci_yaml_from_repo diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb new file mode 100644 index 00000000000..dab034b7234 --- /dev/null +++ b/app/models/clusters/clusters_hierarchy.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Clusters + class ClustersHierarchy + DEPTH_COLUMN = :depth + + def initialize(clusterable) + @clusterable = clusterable + end + + # Returns clusters in order from deepest to highest group + def base_and_ancestors + cte = recursive_cte + cte_alias = cte.table.alias(model.table_name) + + model + .unscoped + .where('clusters.id IS NOT NULL') + .with + .recursive(cte.to_arel) + .from(cte_alias) + .order(DEPTH_COLUMN => :asc) + end + + private + + attr_reader :clusterable + + def recursive_cte + cte = Gitlab::SQL::RecursiveCTE.new(:clusters_cte) + + base_query = case clusterable + when ::Group + group_clusters_base_query + when ::Project + project_clusters_base_query + else + raise ArgumentError, "unknown type for #{clusterable}" + end + + cte << base_query + cte << parent_query(cte) + + cte + end + + def group_clusters_base_query + group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id') + join_sources = ::Group.left_joins(:clusters).join_sources + + model + .unscoped + .select([clusters_star, group_parent_id_alias, "1 AS #{DEPTH_COLUMN}"]) + .where(groups[:id].eq(clusterable.id)) + .from(groups) + .joins(join_sources) + end + + def project_clusters_base_query + projects = ::Project.arel_table + project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id') + join_sources = ::Project.left_joins(:clusters).join_sources + + model + .unscoped + .select([clusters_star, project_parent_id_alias, "1 AS #{DEPTH_COLUMN}"]) + .where(projects[:id].eq(clusterable.id)) + .from(projects) + .joins(join_sources) + end + + def parent_query(cte) + group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id') + + model + .unscoped + .select([clusters_star, group_parent_id_alias, cte.table[DEPTH_COLUMN] + 1]) + .from([cte.table, groups]) + .joins('LEFT OUTER JOIN cluster_groups ON cluster_groups.group_id = namespaces.id') + .joins('LEFT OUTER JOIN clusters ON cluster_groups.cluster_id = clusters.id') + .where(groups[:id].eq(cte.table[:group_parent_id])) + end + + def model + Clusters::Cluster + end + + def clusters + @clusters ||= model.arel_table + end + + def groups + @groups ||= ::Group.arel_table + end + + def clusters_star + @clusters_star ||= clusters[Arel.star] + end + + def alias_as_column(value, alias_to) + Arel::Nodes::As.new(value, Arel::Nodes::SqlLiteral.new(alias_to)) + end + end +end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 5a358ae2ef6..8f28c897eb6 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -12,11 +12,26 @@ module DeploymentPlatform private def find_deployment_platform(environment) - find_cluster_platform_kubernetes(environment: environment) || - find_group_cluster_platform_kubernetes(environment: environment) || + find_platform_kubernetes(environment) || find_instance_cluster_platform_kubernetes(environment: environment) end + def find_platform_kubernetes(environment) + if Feature.enabled?(:clusters_cte) + find_platform_kubernetes_with_cte(environment) + else + find_cluster_platform_kubernetes(environment: environment) || + find_group_cluster_platform_kubernetes(environment: environment) + end + end + + # EE would override this and utilize environment argument + def find_platform_kubernetes_with_cte(_environment) + Clusters::ClustersHierarchy.new(self).base_and_ancestors + .enabled.default_environment + .first&.platform_kubernetes + end + # EE would override this and utilize environment argument def find_cluster_platform_kubernetes(environment: nil) clusters.enabled.default_environment diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 299e413321d..952de92cae1 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -168,7 +168,7 @@ module Issuable # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma. # # Returns an ActiveRecord::Relation. - def full_search(query, matched_columns: 'title,description') + def full_search(query, matched_columns: 'title,description', use_minimum_char_limit: true) allowed_columns = [:title, :description] matched_columns = matched_columns.to_s.split(',').map(&:to_sym) matched_columns &= allowed_columns @@ -176,7 +176,7 @@ module Issuable # Matching title or description if the matched_columns did not contain any allowed columns. matched_columns = [:title, :description] if matched_columns.empty? - fuzzy_search(query, matched_columns) + fuzzy_search(query, matched_columns, use_minimum_char_limit: use_minimum_char_limit) end def simple_sorts diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 6c3962b4c4f..d91be73d6f0 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -173,12 +173,16 @@ module ReactiveCaching end def within_reactive_cache_lifetime?(*args) - !!Rails.cache.read(alive_reactive_cache_key(*args)) + if Feature.enabled?(:reactive_caching_check_key_exists, default_enabled: true) + Rails.cache.exist?(alive_reactive_cache_key(*args)) + else + !!Rails.cache.read(alive_reactive_cache_key(*args)) + end end def enqueuing_update(*args) yield - ensure + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index a8f5642f726..b69cda4f2f9 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -85,11 +85,6 @@ class Deployment < ApplicationRecord Commit.truncate_sha(sha) end - # Deprecated - will be replaced by a persisted cluster_id - def deployment_platform_cluster - environment.deployment_platform&.cluster - end - def execute_hooks deployment_data = Gitlab::DataBuilder::Deployment.build(self) project.execute_services(deployment_data, :deployment_hooks) @@ -176,45 +171,8 @@ class Deployment < ApplicationRecord deployed_at&.to_time&.in_time_zone&.to_s(:medium) end - def has_metrics? - success? && prometheus_adapter&.can_query? - end - - def metrics - return {} unless has_metrics? - - metrics = prometheus_adapter.query(:deployment, self) - metrics&.merge(deployment_time: finished_at.to_i) || {} - end - - def additional_metrics - return {} unless has_metrics? - - metrics = prometheus_adapter.query(:additional_metrics_deployment, self) - metrics&.merge(deployment_time: finished_at.to_i) || {} - end - private - def prometheus_adapter - service = project.find_or_initialize_service('prometheus') - - if service.can_query? - service - else - cluster_prometheus - end - end - - # TODO remove fallback case to deployment_platform_cluster. - # Otherwise we will continue to pay the performance penalty described in - # https://gitlab.com/gitlab-org/gitlab-ce/issues/63475 - def cluster_prometheus - cluster_with_fallback = cluster || deployment_platform_cluster - - cluster_with_fallback.application_prometheus if cluster_with_fallback&.application_prometheus_available? - end - def ref_path File.join(environment.ref_path, 'deployments', iid.to_s) end diff --git a/app/models/deployment_metrics.rb b/app/models/deployment_metrics.rb new file mode 100644 index 00000000000..cfe762ca25e --- /dev/null +++ b/app/models/deployment_metrics.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class DeploymentMetrics + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :deployment + + delegate :cluster, to: :deployment + + def initialize(project, deployment) + @project = project + @deployment = deployment + end + + def has_metrics? + deployment.success? && prometheus_adapter&.can_query? + end + + def metrics + return {} unless has_metrics? + + metrics = prometheus_adapter.query(:deployment, deployment) + metrics&.merge(deployment_time: deployment.finished_at.to_i) || {} + end + + def additional_metrics + return {} unless has_metrics? + + metrics = prometheus_adapter.query(:additional_metrics_deployment, deployment) + metrics&.merge(deployment_time: deployment.finished_at.to_i) || {} + end + + private + + def prometheus_adapter + strong_memoize(:prometheus_adapter) do + service = project.find_or_initialize_service('prometheus') + + if service.can_query? + service + else + cluster_prometheus + end + end + end + + # TODO remove fallback case to deployment_platform_cluster. + # Otherwise we will continue to pay the performance penalty described in + # https://gitlab.com/gitlab-org/gitlab-ce/issues/63475 + # + # Removal issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/64105 + def cluster_prometheus + cluster_with_fallback = cluster || deployment_platform_cluster + + cluster_with_fallback.application_prometheus if cluster_with_fallback&.application_prometheus_available? + end + + def deployment_platform_cluster + deployment.environment.deployment_platform&.cluster + end +end diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 2fb6cadc8cd..465a42759df 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -3,11 +3,10 @@ class EnvironmentStatus include Gitlab::Utils::StrongMemoize - attr_reader :environment, :merge_request, :sha + attr_reader :project, :environment, :merge_request, :sha delegate :id, to: :environment delegate :name, to: :environment - delegate :project, to: :environment delegate :status, to: :deployment, allow_nil: true delegate :deployed_at, to: :deployment, allow_nil: true @@ -21,7 +20,8 @@ class EnvironmentStatus build_environments_status(mr, user, mr.merge_pipeline) end - def initialize(environment, merge_request, sha) + def initialize(project, environment, merge_request, sha) + @project = project @environment = environment @merge_request = merge_request @sha = sha @@ -33,6 +33,12 @@ class EnvironmentStatus end end + def has_metrics? + strong_memoize(:has_metrics) do + deployment_metrics.has_metrics? + end + end + def changes return [] if project.route_map_for(sha).nil? @@ -48,6 +54,10 @@ class EnvironmentStatus PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze + def deployment_metrics + @deployment_metrics ||= DeploymentMetrics.new(project, deployment) + end + def build_change(file) public_path = project.public_path_for_source_path(file.new_path, sha) return if public_path.nil? @@ -67,7 +77,7 @@ class EnvironmentStatus pipeline.environments.available.map do |environment| next unless Ability.allowed?(user, :read_environment, environment) - EnvironmentStatus.new(environment, mr, pipeline.sha) + EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha) end.compact end private_class_method :build_environments_status diff --git a/app/models/group.rb b/app/models/group.rb index 8e89c7ecfb1..9520db1bc0a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -63,6 +63,8 @@ class Group < Namespace after_save :update_two_factor_requirement after_update :path_changed_hook, if: :saved_change_to_path? + scope :with_users, -> { includes(:users) } + class << self def sort_by_attribute(method) if method == 'storage_size_desc' diff --git a/app/models/label_note.rb b/app/models/label_note.rb index d6814f4a948..ba5f1f82a81 100644 --- a/app/models/label_note.rb +++ b/app/models/label_note.rb @@ -62,19 +62,27 @@ class LabelNote < Note end def note_text(html: false) - added = labels_str('added', label_refs_by_action('add', html)) - removed = labels_str('removed', label_refs_by_action('remove', html)) + added = labels_str(label_refs_by_action('add', html), prefix: 'added', suffix: added_suffix) + removed = labels_str(label_refs_by_action('remove', html), prefix: removed_prefix) [added, removed].compact.join(' and ') end + def removed_prefix + 'removed' + end + + def added_suffix + '' + end + # returns string containing added/removed labels including # count of deleted labels: # # added ~1 ~2 + 1 deleted label # added 3 deleted labels # added ~1 ~2 labels - def labels_str(prefix, label_refs) + def labels_str(label_refs, prefix: '', suffix: '') existing_refs = label_refs.select { |ref| ref.present? }.sort refs_str = existing_refs.empty? ? nil : existing_refs.join(' ') @@ -84,9 +92,9 @@ class LabelNote < Note return unless refs_str || deleted_str label_list_str = [refs_str, deleted_str].compact.join(' + ') - suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count) + suffix += ' label'.pluralize(deleted > 0 ? deleted : existing_refs.count) - "#{prefix} #{label_list_str} #{suffix}" + "#{prefix} #{label_list_str} #{suffix.squish}" end def label_refs_by_action(action, html) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8391d526d18..53977748c30 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -223,7 +223,13 @@ class MergeRequest < ApplicationRecord end def rebase_in_progress? - strong_memoize(:rebase_in_progress) do + (rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) || + gitaly_rebase_in_progress? + end + + # TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable + def gitaly_rebase_in_progress? + strong_memoize(:gitaly_rebase_in_progress) do # The source project can be deleted next false unless source_project @@ -389,6 +395,26 @@ class MergeRequest < ApplicationRecord update_column(:merge_jid, jid) end + # Set off a rebase asynchronously, atomically updating the `rebase_jid` of + # the MR so that the status of the operation can be tracked. + def rebase_async(user_id) + transaction do + lock! + + raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress? + + # Although there is a race between setting rebase_jid here and clearing it + # in the RebaseWorker, it can't do any harm since we check both that the + # attribute is set *and* that the sidekiq job is still running. So a JID + # for a completed RebaseWorker is equivalent to a nil JID. + jid = Sidekiq::Worker.skipping_transaction_check do + RebaseWorker.perform_async(id, user_id) + end + + update_column(:rebase_jid, jid) + end + end + def merge_participants participants = [author] @@ -1101,6 +1127,19 @@ class MergeRequest < ApplicationRecord "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/merge" end + def train_ref_path + "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/train" + end + + def cleanup_refs(only: :all) + target_refs = [] + target_refs << ref_path if %i[all head].include?(only) + target_refs << merge_ref_path if %i[all merge].include?(only) + target_refs << train_ref_path if %i[all train].include?(only) + + project.repository.delete_refs(*target_refs) + end + def self.merge_request_ref?(ref) ref.start_with?("refs/#{Repository::REF_MERGE_REQUEST}/") end diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb index 355593597c6..0bef352cf24 100644 --- a/app/models/namespace/aggregation_schedule.rb +++ b/app/models/namespace/aggregation_schedule.rb @@ -44,4 +44,12 @@ class Namespace::AggregationSchedule < ApplicationRecord def lease_key "namespace:namespaces_root_statistics:#{namespace_id}" end + + # Used by ExclusiveLeaseGuard + # Overriding value as we never release the lease + # before the timeout in order to prevent multiple + # RootStatisticsWorker to start in a short span of time + def lease_release? + false + end end diff --git a/app/models/project.rb b/app/models/project.rb index 822def0f936..bfc35b77b8f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2145,7 +2145,7 @@ class Project < ApplicationRecord public? && repository_exists? && Gitlab::CurrentSettings.hashed_storage_enabled && - Feature.enabled?(:object_pools, self) + Feature.enabled?(:object_pools, self, default_enabled: true) end def leave_pool_repository diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index dbdc8345c93..d2660051343 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -46,7 +46,7 @@ class DroneCiService < CiService end def commit_status(sha, ref) - with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + with_reactive_cache(sha, ref) { |cached| cached[:commit_status] } end def calculate_reactive_cache(sha, ref) @@ -68,7 +68,7 @@ class DroneCiService < CiService end { commit_status: status } - rescue Errno::ECONNREFUSED + rescue *Gitlab::HTTP::HTTP_ERRORS { commit_status: :error } end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 27b7827d55e..9f5c226f4c9 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,18 +1,8 @@ # frozen_string_literal: true -## -# NOTE: -# We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. -# After we've migrated data, we'll remove KubernetesService. This would happen in a few months. -# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes. class KubernetesService < Service - include Gitlab::Kubernetes - include ReactiveCaching - default_value_for :category, 'deployment' - self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } - # Namespace defaults to the project path, but can be overridden in case that # is an invalid or inappropriate name prop_accessor :namespace @@ -47,8 +37,6 @@ class KubernetesService < Service message: Gitlab::Regex.kubernetes_namespace_regex_message } - after_save :clear_reactive_cache! - def self.supported_events %w() end @@ -94,72 +82,6 @@ class KubernetesService < Service ] end - def kubernetes_namespace_for(project) - if namespace.present? - namespace - else - default_namespace - end - end - - # Check we can connect to the Kubernetes API - def test(*args) - kubeclient = build_kube_client! - - kubeclient.core_client.discover - { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" } - rescue => err - { success: false, result: err } - end - - # Project param was added on - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011, - # as a way to keep this service compatible with - # Clusters::Platforms::Kubernetes, it won't be used on this method - # as it's only needed for Clusters::Cluster. - def predefined_variables(project:) - Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables - .append(key: 'KUBE_URL', value: api_url) - .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) - .append(key: 'KUBE_NAMESPACE', value: kubernetes_namespace_for(project)) - .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) - - if ca_pem.present? - variables - .append(key: 'KUBE_CA_PEM', value: ca_pem) - .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) - end - end - end - - # Constructs a list of terminals from the reactive cache - # - # Returns nil if the cache is empty, in which case you should try again a - # short time later - def terminals(environment) - with_reactive_cache do |data| - project = environment.project - - pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug) - terminals = pods.flat_map { |pod| terminals_for_pod(api_url, kubernetes_namespace_for(project), pod) }.compact - terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } - end - end - - # Caches resources in the namespace so other calls don't need to block on - # network access - def calculate_reactive_cache - return unless active? && project && !project.pending_delete? - - # We may want to cache extra things in the future - { pods: read_pods } - end - - def kubeclient - @kubeclient ||= build_kube_client! - end - def deprecated? true end @@ -186,14 +108,6 @@ class KubernetesService < Service private - def kubeconfig - to_kubeconfig( - url: api_url, - namespace: kubernetes_namespace_for(project), - token: token, - ca_pem: ca_pem) - end - def namespace_placeholder default_namespace || TEMPLATE_PLACEHOLDER end @@ -205,49 +119,6 @@ class KubernetesService < Service slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kube_client! - raise "Incomplete settings" unless api_url && kubernetes_namespace_for(project) && token - - Gitlab::Kubernetes::KubeClient.new( - api_url, - auth_options: kubeclient_auth_options, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - # Returns a hash of all pods in the namespace - def read_pods - kubeclient = build_kube_client! - - kubeclient.get_pods(namespace: kubernetes_namespace_for(project)).as_json - rescue Kubeclient::ResourceNotFoundError - [] - end - - def kubeclient_ssl_options - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - - def kubeclient_auth_options - { bearer_token: token } - end - - def terminal_auth - { - token: token, - ca_pem: ca_pem, - max_session_time: Gitlab::CurrentSettings.terminal_max_session_time - } - end - def enforce_namespace_to_lower_case self.namespace = self.namespace&.downcase end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 8a179b4d56d..3802d258664 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProjectStatistics < ApplicationRecord + include AfterCommitQueue + belongs_to :project belongs_to :namespace @@ -15,6 +17,7 @@ class ProjectStatistics < ApplicationRecord COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count].freeze INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze + NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } @@ -22,13 +25,17 @@ class ProjectStatistics < ApplicationRecord repository_size + lfs_objects_size end - def refresh!(only: nil) + def refresh!(only: []) COLUMNS_TO_REFRESH.each do |column, generator| - if only.blank? || only.include?(column) + if only.empty? || only.include?(column) public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend end end + if only.empty? || only.any? { |column| NAMESPACE_RELATABLE_COLUMNS.include?(column) } + schedule_namespace_aggregation_worker + end + save! end @@ -81,4 +88,18 @@ class ProjectStatistics < ApplicationRecord update_all(updates.join(', ')) end + + private + + def schedule_namespace_aggregation_worker + run_after_commit do + next unless schedule_aggregation_worker? + + Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) + end + end + + def schedule_aggregation_worker? + Feature.enabled?(:update_statistics_namespace, project&.root_ancestor) + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index d087a5a7bbd..a25d5abfa64 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -839,10 +839,14 @@ class Repository end end - def merge_to_ref(user, source_sha, merge_request, target_ref, message) + def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref) branch = merge_request.target_branch - raw.merge_to_ref(user, source_sha, branch, target_ref, message) + raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) + end + + def delete_refs(*ref_names) + raw.delete_refs(*ref_names) end def ff_merge(user, source, target_branch, merge_request: nil) diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index f2c7cb6a65d..ad08f4763ae 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -36,10 +36,9 @@ class ResourceLabelEvent < ApplicationRecord issue || merge_request end - # create same discussion id for all actions with the same user and time def discussion_id(resource = nil) strong_memoize(:discussion_id) do - Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-")) + Digest::SHA1.hexdigest(discussion_id_key.join("-")) end end @@ -121,4 +120,8 @@ class ResourceLabelEvent < ApplicationRecord def resource_parent issuable.project || issuable.group end + + def discussion_id_key + [self.class.name, created_at, user_id] + end end diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb index f6321b9e520..811cc2ad5af 100644 --- a/app/serializers/environment_status_entity.rb +++ b/app/serializers/environment_status_entity.rb @@ -11,7 +11,7 @@ class EnvironmentStatusEntity < Grape::Entity project_environment_path(es.project, es.environment) end - expose :metrics_url, if: ->(*) { can_read_environment? && deployment.has_metrics? } do |es| + expose :metrics_url, if: ->(*) { can_read_environment? && has_metrics? } do |es| metrics_project_environment_deployment_path(es.project, es.environment, es.deployment) end @@ -45,8 +45,8 @@ class EnvironmentStatusEntity < Grape::Entity object.environment end - def deployment - object.deployment + def has_metrics? + object.has_metrics? end def project diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index 9fa3a897ebe..d402a4d5718 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class TestSuiteComparerEntity < Grape::Entity + DEFAULT_MAX_TESTS = 100 + DEFAULT_MIN_TESTS = 10 + expose :name expose :total_status, as: :status @@ -10,7 +13,27 @@ class TestSuiteComparerEntity < Grape::Entity expose :failed_count, as: :failed end - expose :new_failures, using: TestCaseEntity - expose :resolved_failures, using: TestCaseEntity - expose :existing_failures, using: TestCaseEntity + # rubocop: disable CodeReuse/ActiveRecord + expose :new_failures, using: TestCaseEntity do |suite| + suite.new_failures.take(max_tests) + end + + expose :existing_failures, using: TestCaseEntity do |suite| + suite.existing_failures.take( + max_tests(suite.new_failures)) + end + + expose :resolved_failures, using: TestCaseEntity do |suite| + suite.resolved_failures.take( + max_tests(suite.new_failures, suite.existing_failures)) + end + + private + + def max_tests(*used) + return Integer::MAX unless Feature.enabled?(:ci_limit_test_reports_size, default_enabled: true) + + [DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index d726085b89a..e06659a39cd 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -29,7 +29,7 @@ module AutoMerge end def cancel(merge_request) - if cancel_auto_merge(merge_request) + if clear_auto_merge_parameters(merge_request) yield if block_given? success @@ -38,6 +38,16 @@ module AutoMerge end end + def abort(merge_request, reason) + if clear_auto_merge_parameters(merge_request) + yield if block_given? + + success + else + error("Can't abort the automatic merge", 406) + end + end + private def strategy @@ -46,7 +56,7 @@ module AutoMerge end end - def cancel_auto_merge(merge_request) + def clear_auto_merge_parameters(merge_request) merge_request.auto_merge_enabled = false merge_request.merge_user = nil diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index c41073a73e9..6a33ec071db 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -5,7 +5,7 @@ module AutoMerge def execute(merge_request) super do if merge_request.saved_change_to_auto_merge_enabled? - SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.diff_head_commit) + SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.actual_head_pipeline.sha) end end end @@ -19,7 +19,13 @@ module AutoMerge def cancel(merge_request) super do - SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user) + SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, project, current_user) + end + end + + def abort(merge_request, reason) + super do + SystemNoteService.abort_merge_when_pipeline_succeeds(merge_request, project, current_user, reason) end end diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb index 926d2f5fc66..95bf2db2018 100644 --- a/app/services/auto_merge_service.rb +++ b/app/services/auto_merge_service.rb @@ -42,6 +42,12 @@ class AutoMergeService < BaseService get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request) end + def abort(merge_request, reason) + return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled? + + get_service_instance(merge_request.auto_merge_strategy).abort(merge_request, reason) + end + def available_strategies(merge_request) self.class.all_strategies.select do |strategy| get_service_instance(strategy).available_for?(merge_request) diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb index d5625857599..6c2d80d8f45 100644 --- a/app/services/ci/compare_reports_base_service.rb +++ b/app/services/ci/compare_reports_base_service.rb @@ -8,7 +8,7 @@ module Ci status: :parsed, key: key(base_pipeline, head_pipeline), data: serializer_class - .new(project: project) + .new(**serializer_params) .represent(comparer).as_json } rescue Gitlab::Ci::Parsers::ParserError => e @@ -40,6 +40,10 @@ module Ci raise NotImplementedError end + def serializer_params + { project: project } + end + def get_report(pipeline) raise NotImplementedError end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index c17712355af..cdcc4b15bea 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -65,7 +65,7 @@ module Ci def execute!(*args, &block) execute(*args, &block).tap do |pipeline| unless pipeline.persisted? - raise CreateError, pipeline.errors.full_messages.join(',') + raise CreateError, pipeline.error_messages end end end diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb index c61437fb2e3..7bdf7711155 100644 --- a/app/services/discussions/update_diff_position_service.rb +++ b/app/services/discussions/update_diff_position_service.rb @@ -3,7 +3,8 @@ module Discussions class UpdateDiffPositionService < BaseService def execute(discussion) - result = tracer.trace(discussion.position) + old_position = discussion.position + result = tracer.trace(old_position) return unless result position = result[:position] diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index c4beddf2294..6d215d7a3b9 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true module Issuable - class BulkUpdateService < IssuableBaseService + class BulkUpdateService + include Gitlab::Allowable + + attr_accessor :current_user, :params + + def initialize(user = nil, params = {}) + @current_user, @params = user, params.dup + end + # rubocop: disable CodeReuse/ActiveRecord def execute(type) model_class = type.classify.constantize diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 02de080e0ba..db673cace81 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -182,7 +182,7 @@ class IssuableBaseService < BaseService # To be overridden by subclasses end - def before_update(issuable) + def before_update(issuable, skip_spam_check: false) # To be overridden by subclasses end @@ -257,7 +257,7 @@ class IssuableBaseService < BaseService last_edited_at: Time.now, last_edited_by: current_user)) - before_update(issuable) + before_update(issuable, skip_spam_check: true) if issuable.with_transaction_returning_status { issuable.save } # We do not touch as it will affect a update on updated_at field diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 6b9f23f24cd..7cd825aa967 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -17,8 +17,8 @@ module Issues super end - def before_update(issue) - spam_check(issue, current_user) + def before_update(issue, skip_spam_check: false) + spam_check(issue, current_user) unless skip_spam_check end def handle_changes(issue, options) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index c34fbeb2adb..067510a8a0a 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -68,8 +68,8 @@ module MergeRequests !merge_request.for_fork? end - def cancel_auto_merge(merge_request) - AutoMergeService.new(project, current_user).cancel(merge_request) + def abort_auto_merge(merge_request, reason) + AutoMergeService.new(project, current_user).abort(merge_request, reason) end # Returns all origin and fork merge requests from `@project` satisfying passed arguments. diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index b81a4dd81d2..c2174d2a130 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -18,7 +18,7 @@ module MergeRequests invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches cleanup_environments(merge_request) - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'merge request was closed') end merge_request diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index efe4dcd6255..0ea50a5dbf5 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module MergeRequests - # Performs the merge between source SHA and the target branch. Instead + # Performs the merge between source SHA and the target branch or the specified first parent ref. Instead # of writing the result to the MR target branch, it targets the `target_ref`. # # Ideally this should leave the `target_ref` state with the same state the @@ -56,12 +56,22 @@ module MergeRequests raise_error(error) if error end + ## + # The parameter `target_ref` is where the merge result will be written. + # Default is the merge ref i.e. `refs/merge-requests/:iid/merge`. def target_ref - merge_request.merge_ref_path + params[:target_ref] || merge_request.merge_ref_path + end + + ## + # The parameter `first_parent_ref` is the main line of the merge commit. + # Default is the target branch ref of the merge request. + def first_parent_ref + params[:first_parent_ref] || merge_request.target_branch_ref end def commit - repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message) + repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message, first_parent_ref) rescue Gitlab::Git::PreReceiveError => error raise MergeError, error.message end diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb index 4b9921c28ba..8d3b9b05819 100644 --- a/app/services/merge_requests/rebase_service.rb +++ b/app/services/merge_requests/rebase_service.rb @@ -15,7 +15,7 @@ module MergeRequests end def rebase - if merge_request.rebase_in_progress? + if merge_request.gitaly_rebase_in_progress? log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true) return false end @@ -27,6 +27,8 @@ module MergeRequests log_error(REBASE_ERROR, save_message_on_model: true) log_error(e.message) false + ensure + merge_request.update_column(:rebase_jid, nil) end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 4b199bd8fa8..8961d2e1023 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -24,7 +24,7 @@ module MergeRequests reload_merge_requests outdate_suggestions refresh_pipelines_on_merge_requests - cancel_auto_merges + abort_auto_merges mark_pending_todos_done cache_merge_requests_closing_issues @@ -142,9 +142,9 @@ module MergeRequests end end - def cancel_auto_merges + def abort_auto_merges merge_requests_for_source_branch.each do |merge_request| - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'source branch was updated') end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 0066cd0491f..d361e96babf 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -44,7 +44,7 @@ module MergeRequests merge_request.previous_changes['target_branch'].first, merge_request.target_branch) - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'target branch was changed') end if merge_request.assignees != old_assignees diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb index 3be958e1613..399f4c35d66 100644 --- a/app/services/prometheus/adapter_service.rb +++ b/app/services/prometheus/adapter_service.rb @@ -27,12 +27,9 @@ module Prometheus end def cluster_prometheus_adapter - return unless deployment_platform.respond_to?(:cluster) + application = deployment_platform&.cluster&.application_prometheus - cluster = deployment_platform.cluster - return unless cluster.application_prometheus&.available? - - cluster.application_prometheus + application if application&.available? end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 237ddbcf2c2..e4564bc9b00 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -221,8 +221,8 @@ module SystemNoteService end # Called when 'merge when pipeline succeeds' is executed - def merge_when_pipeline_succeeds(noteable, project, author, last_commit) - body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" + def merge_when_pipeline_succeeds(noteable, project, author, sha) + body = "enabled an automatic merge when the pipeline for #{sha} succeeds" create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end @@ -234,6 +234,16 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end + # Called when 'merge when pipeline succeeds' is aborted + def abort_merge_when_pipeline_succeeds(noteable, project, author, reason) + body = "aborted the automatic merge because #{reason}" + + ## + # TODO: Abort message should be sent by the system, not a particular user. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63187. + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) + end + def handle_merge_request_wip(noteable, project, author) prefix = noteable.work_in_progress? ? "marked" : "unmarked" @@ -268,11 +278,13 @@ module SystemNoteService merge_request = discussion.noteable diff_refs = change_position.diff_refs version_index = merge_request.merge_request_diffs.viewable.count + position_on_text = change_position.on_text? + text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"] - text_parts = ["changed this line in"] if version_params = merge_request.version_params_for(diff_refs) - line_code = change_position.line_code(project.repository) - url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: line_code)) + repository = project.repository + anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash + url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor)) text_parts << "[version #{version_index} of the diff](#{url})" else diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 1c7582533ad..b326b266017 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -203,6 +203,6 @@ class FileUploader < GitlabUploader end def secure_url - File.join('/uploads', @secret, file.filename) + File.join('/uploads', @secret, filename) end end diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index b43162f0935..1ac69601d18 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -93,6 +93,6 @@ class PersonalFileUploader < FileUploader end def secure_url - File.join('/', base_dir, secret, file.filename) + File.join('/', base_dir, secret, filename) end end diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml new file mode 100644 index 00000000000..b6e02bde895 --- /dev/null +++ b/app/views/admin/application_settings/_grafana.html.haml @@ -0,0 +1,17 @@ += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + %p + = _("Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab.") + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/grafana_configuration.md') + .form-group + .form-check + = f.check_box :grafana_enabled, class: 'form-check-input' + = f.label :grafana_enabled, class: 'form-check-label' do + = _('Enable access to Grafana') + .form-group + = f.label :grafana_url, _('Grafana URL'), class: 'label-bold' + = f.text_field :grafana_url, class: 'form-control', placeholder: '/-/grafana' + + = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml index 01d61beaf53..55a48da8342 100644 --- a/app/views/admin/application_settings/metrics_and_profiling.html.haml +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -24,6 +24,17 @@ .settings-content = render 'prometheus' +%section.settings.as-grafana.no-animate#js-grafana-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Metrics - Grafana') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable and configure Grafana.') + .settings-content + = render 'grafana' + %section.settings.qa-performance-bar-settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index f524d35d79e..98230684d56 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -43,11 +43,7 @@ = render_if_exists 'admin/namespace_plan_info', namespace: @group %li - %span.light= _('Storage:') - %strong= storage_counter(@group.storage_size) - ( - = storage_counters_details(@group) - ) + = render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group %li %span.light= _('Group Git LFS status:') diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index e23accc1ea9..0fae8060b32 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -73,11 +73,7 @@ = @project.repository.relative_path %li - %span.light= _('Storage:') - %strong= storage_counter(@project.statistics&.storage_size) - - if @project.statistics - = surround '(', ')' do - = storage_counters_details(@project.statistics) + = render 'shared/storage_counter_statistics', storage_size: @project.statistics&.storage_size, storage_details: @project.statistics %li %span.light last commit: diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 5c6131db37d..a988f746ced 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -139,7 +139,7 @@ %strong = link_to @user.created_by.name, [:admin, @user.created_by] - = render_if_exists partial: "namespaces/shared_runner_status", locals: { namespace: @user.namespace } + = render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace .col-md-6 - unless @user == current_user diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index db6e40a6fd0..8cdfc7369a0 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -49,5 +49,5 @@ - else .todo-actions = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do - Add todo + Add a To Do = icon('spinner spin') diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 8212fb8bb33..731e763f2be 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -1,11 +1,11 @@ - @hide_top_links = true -- page_title "Todos" -- header_title "Todos", dashboard_todos_path +- page_title "To-Do List" +- header_title "To-Do List", dashboard_todos_path = render_dashboard_gold_trial(current_user) .page-title-holder.d-flex.align-items-center - %h1.page-title= _('Todos') + %h1.page-title= _('To-Do List') - if current_user.todos.any? .top-area @@ -13,7 +13,7 @@ %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> = link_to todos_filter_path(state: 'pending') do %span - Todos + To Do %span.badge.badge-pill = number_with_delimiter(todos_pending_count) %li.todos-done{ class: active_when(params[:state] == 'done') }> @@ -102,24 +102,24 @@ %p Are you looking for things to do? Take a look at = succeed "," do - = link_to "the opened issues", issues_dashboard_path + = link_to "open issues", issues_dashboard_path contribute to - = link_to "merge requests", merge_requests_dashboard_path - or mention someone in a comment to assign a new todo automatically. + = link_to "a merge request\,", merge_requests_dashboard_path + or mention someone in a comment to automatically assign them a new to-do item. - else %h4.text-center - There are no todos to show. + Nothing is on your to-do list. Nice work! - else .todos-empty .todos-empty-hero.svg-content = image_tag 'illustrations/todos_empty.svg' .todos-empty-content %h4 - Todos let you see what you should do next + Your To-Do List shows what to work on next %p - When an issue or merge request is assigned to you, or when you + When an issue or merge request is assigned to you, or when you receive a %strong @mention - in a comment, this will trigger a new item in your todo list, automatically. + in a comment, this automatically triggers a new item in your To-Do List. %p - You will always know what to work on next. + It's how you always know what to work on next. diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 91d17cfd745..f05e269553a 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,3 +1,5 @@ +- @can_bulk_update = can?(current_user, :admin_issue, @group) + - page_title "Issues" = content_for :meta_tags do = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues") @@ -9,8 +11,15 @@ = render 'shared/issuable/nav', type: :issues .nav-controls = render 'shared/issuable/feed_buttons' + + - if @can_bulk_update + = render_if_exists 'shared/issuable/bulk_update_button' + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true = render 'shared/issuable/search_bar', type: :issues + - if @can_bulk_update + = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues + = render 'shared/issues' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 496ec3c78b0..a5f57f5893c 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,5 +1,5 @@ - if @group && @group.persisted? && @group.path - - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) } + - group_data_attrs = { group_path: j(@group.path), name: j(@group.name), issues_path: issues_group_path(@group), mr_path: merge_requests_group_path(@group) } - if @project && @project.persisted? - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? } .search.search-form{ data: { track_label: "navbar_search", track_event: "activate_form_input" } } diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index fa04b5be9f2..91a7777514c 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -3,6 +3,7 @@ = render "layouts/head" %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' + = header_message = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'shared/outdated_browser' .mobile-overlay @@ -12,3 +13,4 @@ = render "layouts/flash" .content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch mt-0" } = yield + = footer_message diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f8b7d0c530a..f9ee6f42e23 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -53,7 +53,7 @@ = number_with_delimiter(merge_requests_count) - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do - = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('todo-done', size: 16) %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 83fe871285a..87133c7ba22 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -81,6 +81,11 @@ = link_to admin_requests_profiles_path, title: _('Requests Profiles') do %span = _('Requests Profiles') + - if Gitlab::CurrentSettings.current_application_settings.grafana_enabled? + = nav_link do + = link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard') do + %span + = _('Metrics Dashboard') = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar' = nav_link(controller: :broadcast_messages) do diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 4ebfaff0860..d16e2dddbe0 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -92,21 +92,21 @@ .col-lg-8 .form-group %h5= s_('Preferences|Time format') - .checkbox-icon-inline-wrapper.form-check + .checkbox-icon-inline-wrapper - time_format_label = capture do = s_('Preferences|Display time in 24-hour format') - = f.check_box :time_format_in_24h, class: 'form-check-input' + = f.check_box :time_format_in_24h = f.label :time_format_in_24h do = time_format_label %h5= s_('Preferences|Time display') - .checkbox-icon-inline-wrapper.form-check + .checkbox-icon-inline-wrapper - time_display_label = capture do = s_('Preferences|Use relative times') - = f.check_box :time_display_relative, class: 'form-check-input' + = f.check_box :time_display_relative = f.label :time_display_relative do = time_display_label - .text-muted - = s_('Preferences|For example: 30 mins ago.') + .form-text.text-muted + = s_('Preferences|For example: 30 mins ago.') .col-lg-4.profile-settings-sidebar .col-lg-8 .form-group diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index b2dab0b5348..d95045c9cce 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -5,6 +5,7 @@ - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' = render 'shared/no_password' + = render_if_exists 'shared/shared_runners_minutes_limit', project: project - unless project.empty_repo? = render 'shared/auto_devops_implicitly_enabled_banner', project: project = render_if_exists 'projects/above_size_limit_warning', project: project diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 52bb797b5b3..8d3e54dc455 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -3,13 +3,14 @@ - data_action = can_create_merge_request ? 'create-mr' : 'create-branch' - value = can_create_merge_request ? 'Create merge request' : 'Create branch' - value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value + - create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request') - can_create_path = can_create_branch_project_issue_path(@project, @issue) - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch) - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid) - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '') - .create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } } + .create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } } .btn-group.btn-group-sm.unavailable %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' } = icon('spinner', class: 'fa-spin') @@ -26,7 +27,7 @@ .droplab-dropdown %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } } - if can_create_merge_request - %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } } + %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } } .menu-item = icon('check', class: 'icon') - if can_create_confidential_merge_request? @@ -41,6 +42,8 @@ %li.divider.droplab-item-ignore %li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16 + - if can_create_confidential_merge_request? + #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests') } } .form-group %label{ for: 'new-branch-name' } = _('Branch name') @@ -55,4 +58,8 @@ .form-group %button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } } - = _('Create merge request') + = create_mr_text + + - if can_create_confidential_merge_request? + %p.text-warning.js-exposed-info-warning.hidden + = _('This may expose confidential information as the selected fork is in another namespace that can have other members.') diff --git a/app/views/shared/_storage_counter_statistics.html.haml b/app/views/shared/_storage_counter_statistics.html.haml new file mode 100644 index 00000000000..99b2323ca82 --- /dev/null +++ b/app/views/shared/_storage_counter_statistics.html.haml @@ -0,0 +1,4 @@ +%span.light= _('Storage:') +%strong= storage_counter(storage_size) +- if storage_details + (#{storage_counters_details(storage_details)}) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 1bd56e064d5..214e87052da 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -17,8 +17,7 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) -- if Gitlab::Graphql.enabled? - #js-suggestions{ data: { project_path: @project.full_path } } +#js-suggestions{ data: { project_path: @project.full_path } } = render 'shared/form_elements/description', model: issuable, form: form, project: project diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index e87e560266f..b4f8377c008 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -10,7 +10,7 @@ .block.issuable-sidebar-header - if signed_in %span.issuable-header-text.hide-collapsed.float-left - = _('Todo') + = _('To Do') %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } = sidebar_gutter_toggle_icon - if signed_in diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index 25c3a945077..2b36ccb8304 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'sidekiq/api' + Sidekiq::Worker.extend ActiveSupport::Concern module ApplicationWorker @@ -44,6 +46,10 @@ module ApplicationWorker get_sidekiq_options['queue'].to_s end + def queue_size + Sidekiq::Queue.new(queue).size + end + def bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb index a6baebc1443..8d06adcd993 100644 --- a/app/workers/rebase_worker.rb +++ b/app/workers/rebase_worker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# The RebaseWorker must be wrapped in important concurrency code, so should only +# be scheduled via MergeRequest#rebase_async class RebaseWorker include ApplicationWorker |